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
@@ -63,6 +63,8 @@ export class AutoSession {
63
63
  pendingVerificationRetry = null;
64
64
  verificationRetryCount = new Map();
65
65
  pausedSessionFile = null;
66
+ pausedUnitType = null;
67
+ pausedUnitId = null;
66
68
  resourceVersionOnStart = null;
67
69
  lastStateRebuildAt = 0;
68
70
  // ── Sidecar queue ─────────────────────────────────────────────────────
@@ -159,6 +161,8 @@ export class AutoSession {
159
161
  this.pendingVerificationRetry = null;
160
162
  this.verificationRetryCount.clear();
161
163
  this.pausedSessionFile = null;
164
+ this.pausedUnitType = null;
165
+ this.pausedUnitId = null;
162
166
  this.resourceVersionOnStart = null;
163
167
  this.lastStateRebuildAt = 0;
164
168
  // Metrics
@@ -104,7 +104,7 @@ export function isVerificationNotApplicable(value) {
104
104
  const v = (value ?? "").toLowerCase().trim().replace(/[.\s]+$/, "");
105
105
  if (!v || v === "none")
106
106
  return true;
107
- return /^(?:none[\s._-]*(?:required|needed|planned)?|n\/?a|not[\s._-]+(?:applicable|required|needed|provided)|no[\s._-]+operational[\s\S]*)$/i.test(v);
107
+ return /^(?:none(?:[\s._\u2014-]+[\s\S]*)?|n\/?a|not[\s._-]+(?:applicable|required|needed|provided)|no[\s._-]+operational[\s\S]*)$/i.test(v);
108
108
  }
109
109
  // ─── Rules ────────────────────────────────────────────────────────────────
110
110
  export const DISPATCH_RULES = [
@@ -16,8 +16,7 @@ import { migrateToExternalState, recoverFailedMigration } from "./migrate-extern
16
16
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
17
17
  import { gsdRoot, resolveMilestoneFile } from "./paths.js";
18
18
  import { invalidateAllCaches } from "./cache.js";
19
- import { synthesizeCrashRecovery } from "./session-forensics.js";
20
- import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive, } from "./crash-recovery.js";
19
+ import { writeLock, clearLock } from "./crash-recovery.js";
21
20
  import { acquireSessionLock, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
22
21
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
23
22
  import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit, nativeGetCurrentBranch, nativeDetectMainBranch, nativeCheckoutBranch, nativeBranchList, nativeBranchListMerged, nativeBranchDelete, nativeWorktreeRemove, } from "./native-git-bridge.js";
@@ -35,7 +34,6 @@ import { isDbAvailable, getMilestone, openDatabase } from "./gsd-db.js";
35
34
  import { hideFooter } from "./auto-dashboard.js";
36
35
  import { debugLog, enableDebug, isDebugEnabled, getDebugLogPath, } from "./debug-logger.js";
37
36
  import { logWarning, logError } from "./workflow-logger.js";
38
- import { parseUnitId } from "./unit-id.js";
39
37
  import { existsSync, mkdirSync, readdirSync, rmSync, statSync, unlinkSync, } from "node:fs";
40
38
  import { join } from "node:path";
41
39
  import { sep as pathSep } from "node:path";
@@ -174,7 +172,7 @@ export function auditOrphanedMilestoneBranches(basePath, isolationMode) {
174
172
  }
175
173
  return { recovered, warnings };
176
174
  }
177
- export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps) {
175
+ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps, interrupted) {
178
176
  const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase, buildResolver, } = deps;
179
177
  const lockResult = acquireSessionLock(base);
180
178
  if (!lockResult.acquired) {
@@ -264,33 +262,6 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
264
262
  }
265
263
  // Initialize GitServiceImpl
266
264
  s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
267
- // Check for crash from previous session. Skip our own fresh bootstrap lock.
268
- const crashLock = readCrashLock(base);
269
- if (crashLock && crashLock.pid !== process.pid) {
270
- if (isLockProcessAlive(crashLock)) {
271
- ctx.ui.notify(`Another auto-mode session (PID ${crashLock.pid}) appears to be running.\nStop it with \`kill ${crashLock.pid}\` before starting a new session.`, "error");
272
- return releaseLockAndReturn();
273
- }
274
- const recoveredMid = parseUnitId(crashLock.unitId).milestone;
275
- const milestoneAlreadyComplete = recoveredMid
276
- ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
277
- : false;
278
- if (milestoneAlreadyComplete) {
279
- ctx.ui.notify(`Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`, "info");
280
- }
281
- else {
282
- const activityDir = join(gsdRoot(base), "activity");
283
- const recovery = synthesizeCrashRecovery(base, crashLock.unitType, crashLock.unitId, crashLock.sessionFile, activityDir);
284
- if (recovery && recovery.trace.toolCallCount > 0) {
285
- s.pendingCrashRecovery = recovery.prompt;
286
- ctx.ui.notify(`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`, "warning");
287
- }
288
- else {
289
- ctx.ui.notify(`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`, "warning");
290
- }
291
- }
292
- clearLock(base);
293
- }
294
265
  // ── Debug mode ──
295
266
  if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
296
267
  enableDebug(base);
@@ -308,6 +279,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
308
279
  });
309
280
  ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
310
281
  }
282
+ if (interrupted.classification !== "recoverable") {
283
+ s.pendingCrashRecovery = null;
284
+ }
311
285
  // Invalidate caches before initial state derivation
312
286
  invalidateAllCaches();
313
287
  // Clean stale runtime unit files for completed milestones (#887)
@@ -983,6 +983,8 @@ function copyPlanningArtifacts(srcBase, wtPath) {
983
983
  const dstGsd = join(wtPath, ".gsd");
984
984
  if (!existsSync(srcGsd))
985
985
  return;
986
+ if (isSamePath(srcGsd, dstGsd))
987
+ return;
986
988
  // Copy milestones/ directory (planning files, roadmaps, plans, research)
987
989
  safeCopyRecursive(join(srcGsd, "milestones"), join(dstGsd, "milestones"), {
988
990
  force: true,
@@ -1209,8 +1211,32 @@ function autoCommitDirtyState(cwd) {
1209
1211
  export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapContent) {
1210
1212
  const worktreeCwd = process.cwd();
1211
1213
  const milestoneBranch = autoWorktreeBranch(milestoneId);
1212
- // 1. Auto-commit dirty state in worktree before leaving
1213
- autoCommitDirtyState(worktreeCwd);
1214
+ // 1. Auto-commit dirty state before leaving.
1215
+ // Guard: when we entered through an auto-worktree (originalBase is set),
1216
+ // only auto-commit when cwd is on the milestone branch. In parallel mode,
1217
+ // cwd may be on the integration branch after a prior merge's
1218
+ // MergeConflictError left cwd unrestored. Auto-committing on the
1219
+ // integration branch captures dirty files from OTHER milestones under a
1220
+ // misleading commit message, contaminating the main branch (#2929).
1221
+ //
1222
+ // When originalBase is null (branch mode, no worktree), autoCommitDirtyState
1223
+ // runs unconditionally — the caller is responsible for cwd placement.
1224
+ {
1225
+ let shouldAutoCommit = true;
1226
+ if (originalBase !== null) {
1227
+ try {
1228
+ const currentBranch = nativeGetCurrentBranch(worktreeCwd);
1229
+ shouldAutoCommit = currentBranch === milestoneBranch;
1230
+ }
1231
+ catch {
1232
+ // If we can't determine the branch, skip the auto-commit to be safe
1233
+ shouldAutoCommit = false;
1234
+ }
1235
+ }
1236
+ if (shouldAutoCommit) {
1237
+ autoCommitDirtyState(worktreeCwd);
1238
+ }
1239
+ }
1214
1240
  // Reconcile worktree DB into main DB before leaving worktree context.
1215
1241
  // Skip when both paths resolve to the same physical file (shared WAL /
1216
1242
  // symlink layout) — ATTACHing a WAL-mode file to itself corrupts the
@@ -1551,6 +1577,12 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1551
1577
  }
1552
1578
  }
1553
1579
  restoreShelter();
1580
+ // Restore cwd so the caller is not stranded on the integration branch.
1581
+ // Without this, the next mergeMilestoneToMain call in a parallel merge
1582
+ // sequence uses process.cwd() (now the project root) as worktreeCwd,
1583
+ // causing autoCommitDirtyState to commit unrelated milestone files to
1584
+ // the integration branch (#2929).
1585
+ process.chdir(previousCwd);
1554
1586
  throw new MergeConflictError(codeConflicts, "squash", milestoneBranch, mainBranch);
1555
1587
  }
1556
1588
  }
@@ -1725,25 +1757,40 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
1725
1757
  // changes (e.g. nativeHasChanges cache returned stale false, or auto-commit
1726
1758
  // silently failed), force one final commit so code is not destroyed by
1727
1759
  // `git worktree remove --force`.
1760
+ //
1761
+ // Guard: only run when worktreeCwd is on the milestone branch (#2929).
1762
+ // In parallel mode or branch-mode merges, worktreeCwd may be the project
1763
+ // root on the integration branch. Committing dirty state there would
1764
+ // capture unrelated files from other milestones.
1728
1765
  if (existsSync(worktreeCwd)) {
1766
+ let preTeardownBranch = null;
1729
1767
  try {
1730
- const dirtyCheck = nativeWorkingTreeStatus(worktreeCwd);
1731
- if (dirtyCheck) {
1768
+ preTeardownBranch = nativeGetCurrentBranch(worktreeCwd);
1769
+ }
1770
+ catch (err) {
1771
+ debugLog("mergeMilestoneToMain", { phase: "pre-teardown-branch-detect-failed", error: String(err) });
1772
+ }
1773
+ const isOnMilestoneBranch = preTeardownBranch === milestoneBranch;
1774
+ if (isOnMilestoneBranch) {
1775
+ try {
1776
+ const dirtyCheck = nativeWorkingTreeStatus(worktreeCwd);
1777
+ if (dirtyCheck) {
1778
+ debugLog("mergeMilestoneToMain", {
1779
+ phase: "pre-teardown-dirty",
1780
+ worktreeCwd,
1781
+ status: dirtyCheck.slice(0, 200),
1782
+ });
1783
+ nativeAddAllWithExclusions(worktreeCwd, RUNTIME_EXCLUSION_PATHS);
1784
+ nativeCommit(worktreeCwd, "chore: pre-teardown auto-commit of uncommitted worktree changes");
1785
+ }
1786
+ }
1787
+ catch (e) {
1732
1788
  debugLog("mergeMilestoneToMain", {
1733
- phase: "pre-teardown-dirty",
1734
- worktreeCwd,
1735
- status: dirtyCheck.slice(0, 200),
1789
+ phase: "pre-teardown-commit-error",
1790
+ error: String(e),
1736
1791
  });
1737
- nativeAddAllWithExclusions(worktreeCwd, RUNTIME_EXCLUSION_PATHS);
1738
- nativeCommit(worktreeCwd, "chore: pre-teardown auto-commit of uncommitted worktree changes");
1739
1792
  }
1740
1793
  }
1741
- catch (e) {
1742
- debugLog("mergeMilestoneToMain", {
1743
- phase: "pre-teardown-commit-error",
1744
- error: String(e),
1745
- });
1746
- }
1747
1794
  }
1748
1795
  // 12. Remove worktree directory first (must happen before branch deletion)
1749
1796
  try {
@@ -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
  /**
@@ -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",
@@ -701,6 +701,7 @@ let currentDb = null;
701
701
  let currentPath = null;
702
702
  let currentPid = 0;
703
703
  let _exitHandlerRegistered = false;
704
+ let _dbOpenAttempted = false;
704
705
  export function getDbProvider() {
705
706
  loadProvider();
706
707
  return providerName;
@@ -708,7 +709,17 @@ export function getDbProvider() {
708
709
  export function isDbAvailable() {
709
710
  return currentDb !== null;
710
711
  }
712
+ /**
713
+ * Returns true if openDatabase() has been called at least once this session.
714
+ * Used to distinguish "DB not yet initialized" from "DB genuinely unavailable"
715
+ * so that early callers (e.g. before_agent_start context injection) don't
716
+ * trigger a false degraded-mode warning.
717
+ */
718
+ export function wasDbOpenAttempted() {
719
+ return _dbOpenAttempted;
720
+ }
711
721
  export function openDatabase(path) {
722
+ _dbOpenAttempted = true;
712
723
  if (currentDb && currentPath !== path)
713
724
  closeDatabase();
714
725
  if (currentDb && currentPath === path)