gsd-pi 2.67.0-dev.fe39184 → 2.68.0-dev.4cf2433

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 (105) hide show
  1. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  2. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  3. package/dist/resources/extensions/gsd/auto-start.js +5 -31
  4. package/dist/resources/extensions/gsd/auto-worktree.js +62 -15
  5. package/dist/resources/extensions/gsd/auto.js +94 -59
  6. package/dist/resources/extensions/gsd/bootstrap/system-context.js +7 -2
  7. package/dist/resources/extensions/gsd/doctor.js +8 -4
  8. package/dist/resources/extensions/gsd/gsd-db.js +11 -0
  9. package/dist/resources/extensions/gsd/guided-flow.js +40 -31
  10. package/dist/resources/extensions/gsd/interrupted-session.js +146 -0
  11. package/dist/resources/extensions/gsd/state.js +7 -2
  12. package/dist/web/standalone/.next/BUILD_ID +1 -1
  13. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  14. package/dist/web/standalone/.next/build-manifest.json +3 -3
  15. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  16. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  17. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.html +1 -1
  36. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  37. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  38. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  39. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  44. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  45. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  47. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  48. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  49. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +9 -0
  50. package/dist/web/standalone/.next/static/chunks/app/{page-0c485498795110d6.js → page-f1e30ab6bb269149.js} +1 -1
  51. package/dist/web/standalone/.next/static/chunks/{webpack-42a66876b763aa26.js → webpack-6e4d7e9a4f57bed4.js} +1 -1
  52. package/package.json +1 -1
  53. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts +43 -0
  54. package/packages/pi-coding-agent/dist/core/contextual-tips.d.ts.map +1 -0
  55. package/packages/pi-coding-agent/dist/core/contextual-tips.js +208 -0
  56. package/packages/pi-coding-agent/dist/core/contextual-tips.js.map +1 -0
  57. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts +2 -0
  58. package/packages/pi-coding-agent/dist/core/contextual-tips.test.d.ts.map +1 -0
  59. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js +227 -0
  60. package/packages/pi-coding-agent/dist/core/contextual-tips.test.js.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  62. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  63. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  64. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +4 -0
  66. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +14 -0
  68. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +3 -0
  70. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -0
  72. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  73. package/packages/pi-coding-agent/package.json +1 -1
  74. package/packages/pi-coding-agent/src/core/contextual-tips.test.ts +259 -0
  75. package/packages/pi-coding-agent/src/core/contextual-tips.ts +232 -0
  76. package/packages/pi-coding-agent/src/core/index.ts +2 -0
  77. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +19 -0
  78. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +17 -0
  79. package/pkg/package.json +1 -1
  80. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  81. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  82. package/src/resources/extensions/gsd/auto-start.ts +8 -54
  83. package/src/resources/extensions/gsd/auto-worktree.ts +59 -15
  84. package/src/resources/extensions/gsd/auto.ts +104 -63
  85. package/src/resources/extensions/gsd/bootstrap/system-context.ts +8 -2
  86. package/src/resources/extensions/gsd/doctor.ts +9 -5
  87. package/src/resources/extensions/gsd/gsd-db.ts +12 -0
  88. package/src/resources/extensions/gsd/guided-flow.ts +42 -36
  89. package/src/resources/extensions/gsd/interrupted-session.ts +224 -0
  90. package/src/resources/extensions/gsd/state.ts +7 -1
  91. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +668 -2
  92. package/src/resources/extensions/gsd/tests/cold-resume-db-reopen.test.ts +14 -4
  93. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +21 -0
  94. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +380 -2
  95. package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +30 -0
  96. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +12 -0
  97. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +2 -2
  98. package/src/resources/extensions/gsd/tests/integration/doctor-fixlevel.test.ts +52 -1
  99. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +169 -0
  100. package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +146 -0
  101. package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +136 -0
  102. package/src/resources/extensions/gsd/tests/verification-operational-gate.test.ts +11 -0
  103. package/dist/web/standalone/.next/static/chunks/6502.5dcdcf1e1432e20d.js +0 -9
  104. /package/dist/web/standalone/.next/static/{gbSATDX4Jt2ufxzUr5nYm → gd7sngpqfUCltp8w_pCwF}/_buildManifest.js +0 -0
  105. /package/dist/web/standalone/.next/static/{gbSATDX4Jt2ufxzUr5nYm → gd7sngpqfUCltp8w_pCwF}/_ssgManifest.js +0 -0
@@ -15,6 +15,7 @@ import type {
15
15
  } from "@gsd/pi-coding-agent";
16
16
  import { deriveState } from "./state.js";
17
17
  import { loadFile, getManifestStatus } from "./files.js";
18
+ import type { InterruptedSessionAssessment } from "./interrupted-session.js";
18
19
  import {
19
20
  loadEffectiveGSDPreferences,
20
21
  resolveSkillDiscoveryMode,
@@ -23,16 +24,9 @@ import {
23
24
  import { ensureGsdSymlink, isInheritedRepo, validateProjectId } from "./repo-identity.js";
24
25
  import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
25
26
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
26
- import { gsdRoot, resolveMilestoneFile, milestonesDir } from "./paths.js";
27
+ import { gsdRoot, resolveMilestoneFile } from "./paths.js";
27
28
  import { invalidateAllCaches } from "./cache.js";
28
- import { synthesizeCrashRecovery } from "./session-forensics.js";
29
- import {
30
- writeLock,
31
- clearLock,
32
- readCrashLock,
33
- formatCrashInfo,
34
- isLockProcessAlive,
35
- } from "./crash-recovery.js";
29
+ import { writeLock, clearLock } from "./crash-recovery.js";
36
30
  import {
37
31
  acquireSessionLock,
38
32
  releaseSessionLock,
@@ -248,6 +242,7 @@ export async function bootstrapAutoSession(
248
242
  verboseMode: boolean,
249
243
  requestedStepMode: boolean,
250
244
  deps: BootstrapDeps,
245
+ interrupted: InterruptedSessionAssessment,
251
246
  ): Promise<boolean> {
252
247
  const {
253
248
  shouldUseWorktreeIsolation,
@@ -361,51 +356,6 @@ export async function bootstrapAutoSession(
361
356
  loadEffectiveGSDPreferences()?.preferences?.git ?? {},
362
357
  );
363
358
 
364
- // Check for crash from previous session. Skip our own fresh bootstrap lock.
365
- const crashLock = readCrashLock(base);
366
- if (crashLock && crashLock.pid !== process.pid) {
367
- if (isLockProcessAlive(crashLock)) {
368
- ctx.ui.notify(
369
- `Another auto-mode session (PID ${crashLock.pid}) appears to be running.\nStop it with \`kill ${crashLock.pid}\` before starting a new session.`,
370
- "error",
371
- );
372
- return releaseLockAndReturn();
373
- }
374
- const recoveredMid = parseUnitId(crashLock.unitId).milestone;
375
- const milestoneAlreadyComplete = recoveredMid
376
- ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
377
- : false;
378
-
379
- if (milestoneAlreadyComplete) {
380
- ctx.ui.notify(
381
- `Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`,
382
- "info",
383
- );
384
- } else {
385
- const activityDir = join(gsdRoot(base), "activity");
386
- const recovery = synthesizeCrashRecovery(
387
- base,
388
- crashLock.unitType,
389
- crashLock.unitId,
390
- crashLock.sessionFile,
391
- activityDir,
392
- );
393
- if (recovery && recovery.trace.toolCallCount > 0) {
394
- s.pendingCrashRecovery = recovery.prompt;
395
- ctx.ui.notify(
396
- `${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
397
- "warning",
398
- );
399
- } else {
400
- ctx.ui.notify(
401
- `${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
402
- "warning",
403
- );
404
- }
405
- }
406
- clearLock(base);
407
- }
408
-
409
359
  // ── Debug mode ──
410
360
  if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
411
361
  enableDebug(base);
@@ -425,6 +375,10 @@ export async function bootstrapAutoSession(
425
375
  ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
426
376
  }
427
377
 
378
+ if (interrupted.classification !== "recoverable") {
379
+ s.pendingCrashRecovery = null;
380
+ }
381
+
428
382
  // Invalidate caches before initial state derivation
429
383
  invalidateAllCaches();
430
384
 
@@ -1137,6 +1137,7 @@ function copyPlanningArtifacts(srcBase: string, wtPath: string): void {
1137
1137
  const srcGsd = join(srcBase, ".gsd");
1138
1138
  const dstGsd = join(wtPath, ".gsd");
1139
1139
  if (!existsSync(srcGsd)) return;
1140
+ if (isSamePath(srcGsd, dstGsd)) return;
1140
1141
 
1141
1142
  // Copy milestones/ directory (planning files, roadmaps, plans, research)
1142
1143
  safeCopyRecursive(join(srcGsd, "milestones"), join(dstGsd, "milestones"), {
@@ -1420,8 +1421,31 @@ export function mergeMilestoneToMain(
1420
1421
  const worktreeCwd = process.cwd();
1421
1422
  const milestoneBranch = autoWorktreeBranch(milestoneId);
1422
1423
 
1423
- // 1. Auto-commit dirty state in worktree before leaving
1424
- autoCommitDirtyState(worktreeCwd);
1424
+ // 1. Auto-commit dirty state before leaving.
1425
+ // Guard: when we entered through an auto-worktree (originalBase is set),
1426
+ // only auto-commit when cwd is on the milestone branch. In parallel mode,
1427
+ // cwd may be on the integration branch after a prior merge's
1428
+ // MergeConflictError left cwd unrestored. Auto-committing on the
1429
+ // integration branch captures dirty files from OTHER milestones under a
1430
+ // misleading commit message, contaminating the main branch (#2929).
1431
+ //
1432
+ // When originalBase is null (branch mode, no worktree), autoCommitDirtyState
1433
+ // runs unconditionally — the caller is responsible for cwd placement.
1434
+ {
1435
+ let shouldAutoCommit = true;
1436
+ if (originalBase !== null) {
1437
+ try {
1438
+ const currentBranch = nativeGetCurrentBranch(worktreeCwd);
1439
+ shouldAutoCommit = currentBranch === milestoneBranch;
1440
+ } catch {
1441
+ // If we can't determine the branch, skip the auto-commit to be safe
1442
+ shouldAutoCommit = false;
1443
+ }
1444
+ }
1445
+ if (shouldAutoCommit) {
1446
+ autoCommitDirtyState(worktreeCwd);
1447
+ }
1448
+ }
1425
1449
 
1426
1450
  // Reconcile worktree DB into main DB before leaving worktree context.
1427
1451
  // Skip when both paths resolve to the same physical file (shared WAL /
@@ -1778,6 +1802,12 @@ export function mergeMilestoneToMain(
1778
1802
  }
1779
1803
  }
1780
1804
  restoreShelter();
1805
+ // Restore cwd so the caller is not stranded on the integration branch.
1806
+ // Without this, the next mergeMilestoneToMain call in a parallel merge
1807
+ // sequence uses process.cwd() (now the project root) as worktreeCwd,
1808
+ // causing autoCommitDirtyState to commit unrelated milestone files to
1809
+ // the integration branch (#2929).
1810
+ process.chdir(previousCwd);
1781
1811
  throw new MergeConflictError(
1782
1812
  codeConflicts,
1783
1813
  "squash",
@@ -1975,23 +2005,38 @@ export function mergeMilestoneToMain(
1975
2005
  // changes (e.g. nativeHasChanges cache returned stale false, or auto-commit
1976
2006
  // silently failed), force one final commit so code is not destroyed by
1977
2007
  // `git worktree remove --force`.
2008
+ //
2009
+ // Guard: only run when worktreeCwd is on the milestone branch (#2929).
2010
+ // In parallel mode or branch-mode merges, worktreeCwd may be the project
2011
+ // root on the integration branch. Committing dirty state there would
2012
+ // capture unrelated files from other milestones.
1978
2013
  if (existsSync(worktreeCwd)) {
2014
+ let preTeardownBranch: string | null = null;
1979
2015
  try {
1980
- const dirtyCheck = nativeWorkingTreeStatus(worktreeCwd);
1981
- if (dirtyCheck) {
2016
+ preTeardownBranch = nativeGetCurrentBranch(worktreeCwd);
2017
+ } catch (err) {
2018
+ debugLog("mergeMilestoneToMain", { phase: "pre-teardown-branch-detect-failed", error: String(err) });
2019
+ }
2020
+ const isOnMilestoneBranch = preTeardownBranch === milestoneBranch;
2021
+
2022
+ if (isOnMilestoneBranch) {
2023
+ try {
2024
+ const dirtyCheck = nativeWorkingTreeStatus(worktreeCwd);
2025
+ if (dirtyCheck) {
2026
+ debugLog("mergeMilestoneToMain", {
2027
+ phase: "pre-teardown-dirty",
2028
+ worktreeCwd,
2029
+ status: dirtyCheck.slice(0, 200),
2030
+ });
2031
+ nativeAddAllWithExclusions(worktreeCwd, RUNTIME_EXCLUSION_PATHS);
2032
+ nativeCommit(worktreeCwd, "chore: pre-teardown auto-commit of uncommitted worktree changes");
2033
+ }
2034
+ } catch (e) {
1982
2035
  debugLog("mergeMilestoneToMain", {
1983
- phase: "pre-teardown-dirty",
1984
- worktreeCwd,
1985
- status: dirtyCheck.slice(0, 200),
2036
+ phase: "pre-teardown-commit-error",
2037
+ error: String(e),
1986
2038
  });
1987
- nativeAddAllWithExclusions(worktreeCwd, RUNTIME_EXCLUSION_PATHS);
1988
- nativeCommit(worktreeCwd, "chore: pre-teardown auto-commit of uncommitted worktree changes");
1989
2039
  }
1990
- } catch (e) {
1991
- debugLog("mergeMilestoneToMain", {
1992
- phase: "pre-teardown-commit-error",
1993
- error: String(e),
1994
- });
1995
2040
  }
1996
2041
  }
1997
2042
 
@@ -2020,4 +2065,3 @@ export function mergeMilestoneToMain(
2020
2065
 
2021
2066
  return { commitMessage, pushed, prCreated, codeFilesChanged };
2022
2067
  }
2023
-
@@ -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
 
@@ -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",
@@ -778,6 +778,7 @@ let currentDb: DbAdapter | null = null;
778
778
  let currentPath: string | null = null;
779
779
  let currentPid: number = 0;
780
780
  let _exitHandlerRegistered = false;
781
+ let _dbOpenAttempted = false;
781
782
 
782
783
  export function getDbProvider(): ProviderName | null {
783
784
  loadProvider();
@@ -788,7 +789,18 @@ export function isDbAvailable(): boolean {
788
789
  return currentDb !== null;
789
790
  }
790
791
 
792
+ /**
793
+ * Returns true if openDatabase() has been called at least once this session.
794
+ * Used to distinguish "DB not yet initialized" from "DB genuinely unavailable"
795
+ * so that early callers (e.g. before_agent_start context injection) don't
796
+ * trigger a false degraded-mode warning.
797
+ */
798
+ export function wasDbOpenAttempted(): boolean {
799
+ return _dbOpenAttempted;
800
+ }
801
+
791
802
  export function openDatabase(path: string): boolean {
803
+ _dbOpenAttempted = true;
792
804
  if (currentDb && currentPath !== path) closeDatabase();
793
805
  if (currentDb && currentPath === path) return true;
794
806