gsd-pi 2.24.0 → 2.26.0

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 (206) hide show
  1. package/README.md +13 -3
  2. package/dist/headless.js +24 -4
  3. package/dist/models-resolver.d.ts +0 -11
  4. package/dist/models-resolver.js +0 -15
  5. package/dist/resource-loader.d.ts +0 -1
  6. package/dist/resource-loader.js +0 -9
  7. package/dist/resources/GSD-WORKFLOW.md +12 -9
  8. package/dist/resources/extensions/async-jobs/index.ts +9 -1
  9. package/dist/resources/extensions/bg-shell/index.ts +3 -2
  10. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  11. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  12. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  13. package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
  14. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
  15. package/dist/resources/extensions/gsd/auto-worktree.ts +132 -3
  16. package/dist/resources/extensions/gsd/auto.ts +265 -48
  17. package/dist/resources/extensions/gsd/cache.ts +3 -1
  18. package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
  19. package/dist/resources/extensions/gsd/doctor.ts +26 -1
  20. package/dist/resources/extensions/gsd/files.ts +13 -2
  21. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  22. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  23. package/dist/resources/extensions/gsd/guided-flow.ts +54 -22
  24. package/dist/resources/extensions/gsd/index.ts +62 -8
  25. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  26. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  27. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  28. package/dist/resources/extensions/gsd/migrate/writer.ts +39 -0
  29. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  30. package/dist/resources/extensions/gsd/preferences.ts +2 -1
  31. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
  33. package/dist/resources/extensions/gsd/prompts/discuss.md +5 -5
  34. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/queue.md +3 -3
  39. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  40. package/dist/resources/extensions/gsd/roadmap-slices.ts +45 -1
  41. package/dist/resources/extensions/gsd/state.ts +17 -6
  42. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  43. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  44. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  45. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  46. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  47. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  48. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  49. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  50. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  51. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  52. package/dist/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  53. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  54. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  56. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  57. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  58. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  59. package/dist/resources/extensions/gsd/types.ts +2 -0
  60. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  61. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  62. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  63. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  64. package/dist/resources/extensions/search-the-web/native-search.ts +19 -5
  65. package/dist/resources/extensions/shared/path-display.ts +19 -0
  66. package/package.json +1 -6
  67. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  68. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  69. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  70. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  71. package/packages/pi-ai/dist/providers/anthropic.js +64 -0
  72. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  73. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  74. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  75. package/packages/pi-ai/dist/types.d.ts +23 -1
  76. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/types.js.map +1 -1
  78. package/packages/pi-ai/src/providers/anthropic.ts +65 -1
  79. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  80. package/packages/pi-ai/src/types.ts +19 -1
  81. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
  82. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/agent-session.js +32 -0
  84. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/keybindings.js +1 -1
  86. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/lsp/client.js +12 -1
  89. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/lsp/index.js +7 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/sdk.d.ts +2 -2
  94. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/sdk.js +8 -3
  96. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  98. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  100. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  103. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -1
  106. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/index.d.ts +2 -1
  108. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/index.js +5 -1
  110. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +41 -3
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +301 -62
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +5 -0
  119. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +135 -30
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts +8 -0
  123. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/tests/path-display.test.js +60 -0
  125. package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/utils/clipboard-image.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/utils/clipboard-image.js +32 -6
  128. package/packages/pi-coding-agent/dist/utils/clipboard-image.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/utils/path-display.d.ts +34 -0
  130. package/packages/pi-coding-agent/dist/utils/path-display.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/utils/path-display.js +36 -0
  132. package/packages/pi-coding-agent/dist/utils/path-display.js.map +1 -0
  133. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -0
  134. package/packages/pi-coding-agent/src/core/keybindings.ts +1 -1
  135. package/packages/pi-coding-agent/src/core/lsp/client.ts +11 -1
  136. package/packages/pi-coding-agent/src/core/lsp/index.ts +7 -0
  137. package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
  138. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  139. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  140. package/packages/pi-coding-agent/src/core/system-prompt.ts +2 -1
  141. package/packages/pi-coding-agent/src/index.ts +15 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +347 -62
  143. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  144. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +124 -4
  145. package/packages/pi-coding-agent/src/tests/path-display.test.ts +85 -0
  146. package/packages/pi-coding-agent/src/utils/clipboard-image.ts +33 -6
  147. package/packages/pi-coding-agent/src/utils/path-display.ts +36 -0
  148. package/src/resources/GSD-WORKFLOW.md +12 -9
  149. package/src/resources/extensions/async-jobs/index.ts +9 -1
  150. package/src/resources/extensions/bg-shell/index.ts +3 -2
  151. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  152. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  153. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  154. package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
  155. package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
  156. package/src/resources/extensions/gsd/auto-worktree.ts +132 -3
  157. package/src/resources/extensions/gsd/auto.ts +265 -48
  158. package/src/resources/extensions/gsd/cache.ts +3 -1
  159. package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
  160. package/src/resources/extensions/gsd/doctor.ts +26 -1
  161. package/src/resources/extensions/gsd/files.ts +13 -2
  162. package/src/resources/extensions/gsd/git-service.ts +74 -14
  163. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  164. package/src/resources/extensions/gsd/guided-flow.ts +54 -22
  165. package/src/resources/extensions/gsd/index.ts +62 -8
  166. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  167. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  168. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  169. package/src/resources/extensions/gsd/migrate/writer.ts +39 -0
  170. package/src/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  171. package/src/resources/extensions/gsd/preferences.ts +2 -1
  172. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
  174. package/src/resources/extensions/gsd/prompts/discuss.md +5 -5
  175. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  176. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  177. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  178. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  179. package/src/resources/extensions/gsd/prompts/queue.md +3 -3
  180. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  181. package/src/resources/extensions/gsd/roadmap-slices.ts +45 -1
  182. package/src/resources/extensions/gsd/state.ts +17 -6
  183. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  184. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  185. package/src/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  186. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  187. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  188. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  189. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  190. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  191. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  192. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  193. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  194. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  196. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  197. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  198. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  199. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  200. package/src/resources/extensions/gsd/types.ts +2 -0
  201. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  202. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  203. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  204. package/src/resources/extensions/gsd/worktree.ts +9 -2
  205. package/src/resources/extensions/search-the-web/native-search.ts +19 -5
  206. package/src/resources/extensions/shared/path-display.ts +19 -0
@@ -16,9 +16,9 @@ import type {
16
16
  ExtensionCommandContext,
17
17
  } from "@gsd/pi-coding-agent";
18
18
 
19
- import { deriveState, invalidateStateCache } from "./state.js";
19
+ import { deriveState } from "./state.js";
20
20
  import type { BudgetEnforcementMode, GSDState } from "./types.js";
21
- import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides } from "./files.js";
21
+ import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides, parseSummary } from "./files.js";
22
22
  import { loadPrompt } from "./prompt-loader.js";
23
23
  export { inlinePriorMilestoneSummary } from "./files.js";
24
24
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
@@ -94,7 +94,7 @@ import {
94
94
  parseSliceBranch,
95
95
  setActiveMilestoneId,
96
96
  } from "./worktree.js";
97
- import { GitServiceImpl } from "./git-service.js";
97
+ import { GitServiceImpl, type TaskCommitContext } from "./git-service.js";
98
98
  import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
99
99
  import { formatGitError } from "./git-self-heal.js";
100
100
  import {
@@ -166,6 +166,41 @@ import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from ".
166
166
  // auto-mode reads stale state from the project root and re-dispatches
167
167
  // already-completed units.
168
168
 
169
+ /**
170
+ * Sync milestone artifacts from project root INTO worktree before deriveState.
171
+ * Covers the case where the LLM wrote artifacts to the main repo filesystem
172
+ * (e.g. via absolute paths) but the worktree has stale data. Also deletes
173
+ * gsd.db in the worktree so it rebuilds from fresh disk state (#853).
174
+ * Non-fatal — sync failure should never block dispatch.
175
+ */
176
+ function syncProjectRootToWorktree(projectRoot: string, worktreePath: string, milestoneId: string | null): void {
177
+ if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
178
+ if (!milestoneId) return;
179
+
180
+ const prGsd = join(projectRoot, ".gsd");
181
+ const wtGsd = join(worktreePath, ".gsd");
182
+
183
+ // Copy milestone directory from project root to worktree if the project root
184
+ // has newer artifacts (e.g. slices that don't exist in the worktree yet)
185
+ try {
186
+ const srcMilestone = join(prGsd, "milestones", milestoneId);
187
+ const dstMilestone = join(wtGsd, "milestones", milestoneId);
188
+ if (existsSync(srcMilestone)) {
189
+ mkdirSync(dstMilestone, { recursive: true });
190
+ cpSync(srcMilestone, dstMilestone, { recursive: true, force: false });
191
+ }
192
+ } catch { /* non-fatal */ }
193
+
194
+ // Delete worktree gsd.db so it rebuilds from the freshly synced files.
195
+ // Stale DB rows are the root cause of the infinite skip loop (#853).
196
+ try {
197
+ const wtDb = join(wtGsd, "gsd.db");
198
+ if (existsSync(wtDb)) {
199
+ unlinkSync(wtDb);
200
+ }
201
+ } catch { /* non-fatal */ }
202
+ }
203
+
169
204
  /**
170
205
  * Sync dispatch-critical .gsd/ state files from worktree to project root.
171
206
  * Only runs when inside an auto-worktree (worktreePath differs from projectRoot).
@@ -261,28 +296,30 @@ const MAX_CONSECUTIVE_SKIPS = 3;
261
296
  /** Persisted completed-unit keys — survives restarts. Loaded from .gsd/completed-units.json. */
262
297
  const completedKeySet = new Set<string>();
263
298
 
264
- /** Resource sync timestamp captured at auto-mode start. If the managed-resources
265
- * manifest changes mid-session (e.g. /gsd:update or dev edit + copy-resources),
299
+ /** Resource version captured at auto-mode start. If the managed-resources
300
+ * manifest version changes mid-session (e.g. npm update -g gsd-pi),
266
301
  * templates on disk may expect variables the in-memory code doesn't provide.
267
- * Detect this and stop gracefully instead of crashing. */
268
- let resourceSyncedAtOnStart: number | null = null;
302
+ * Detect this and stop gracefully instead of crashing.
303
+ * Uses gsdVersion (semver) instead of syncedAt (timestamp) so that
304
+ * launching a second session doesn't falsely trigger staleness (#804). */
305
+ let resourceVersionOnStart: string | null = null;
269
306
 
270
- function readResourceSyncedAt(): number | null {
307
+ function readResourceVersion(): string | null {
271
308
  const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
272
309
  const manifestPath = join(agentDir, "managed-resources.json");
273
310
  try {
274
311
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
275
- return typeof manifest?.syncedAt === "number" ? manifest.syncedAt : null;
312
+ return typeof manifest?.gsdVersion === "string" ? manifest.gsdVersion : null;
276
313
  } catch {
277
314
  return null;
278
315
  }
279
316
  }
280
317
 
281
318
  function checkResourcesStale(): string | null {
282
- if (resourceSyncedAtOnStart === null) return null;
283
- const current = readResourceSyncedAt();
319
+ if (resourceVersionOnStart === null) return null;
320
+ const current = readResourceVersion();
284
321
  if (current === null) return null;
285
- if (current !== resourceSyncedAtOnStart) {
322
+ if (current !== resourceVersionOnStart) {
286
323
  return "GSD resources were updated since this session started. Restart gsd to load the new code.";
287
324
  }
288
325
  return null;
@@ -889,23 +926,37 @@ export async function startAuto(
889
926
  );
890
927
  return;
891
928
  }
892
- // Stale lock from a dead process — synthesize crash recovery context.
893
- const activityDir = join(gsdRoot(base), "activity");
894
- const recovery = synthesizeCrashRecovery(
895
- base, crashLock.unitType, crashLock.unitId,
896
- crashLock.sessionFile, activityDir,
897
- );
898
- if (recovery && recovery.trace.toolCallCount > 0) {
899
- pendingCrashRecovery = recovery.prompt;
929
+ // Stale lock from a dead process — validate before synthesizing recovery context.
930
+ // If the recovered unit belongs to a fully-completed milestone (SUMMARY exists),
931
+ // discard recovery context to prevent phantom skip loops (#790).
932
+ const recoveredMid = crashLock.unitId.split("/")[0];
933
+ const milestoneAlreadyComplete = recoveredMid
934
+ ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
935
+ : false;
936
+
937
+ if (milestoneAlreadyComplete) {
900
938
  ctx.ui.notify(
901
- `${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
902
- "warning",
939
+ `Crash recovery: discarding stale context for ${crashLock.unitId} milestone ${recoveredMid} is already complete.`,
940
+ "info",
903
941
  );
904
942
  } else {
905
- ctx.ui.notify(
906
- `${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
907
- "warning",
943
+ const activityDir = join(gsdRoot(base), "activity");
944
+ const recovery = synthesizeCrashRecovery(
945
+ base, crashLock.unitType, crashLock.unitId,
946
+ crashLock.sessionFile, activityDir,
908
947
  );
948
+ if (recovery && recovery.trace.toolCallCount > 0) {
949
+ pendingCrashRecovery = recovery.prompt;
950
+ ctx.ui.notify(
951
+ `${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
952
+ "warning",
953
+ );
954
+ } else {
955
+ ctx.ui.notify(
956
+ `${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
957
+ "warning",
958
+ );
959
+ }
909
960
  }
910
961
  clearLock(base);
911
962
  }
@@ -928,6 +979,11 @@ export async function startAuto(
928
979
  ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
929
980
  }
930
981
 
982
+ // Invalidate all caches before initial state derivation to ensure we read
983
+ // fresh disk state. Without this, a stale cache from a prior session (e.g.
984
+ // after a discussion that wrote new artifacts) may cause deriveState to
985
+ // return pre-planning when the roadmap already exists (#800).
986
+ invalidateAllCaches();
931
987
  let state = await deriveState(base);
932
988
 
933
989
  // ── Stale worktree state recovery (#654) ─────────────────────────────────
@@ -1061,7 +1117,7 @@ export async function startAuto(
1061
1117
  restoreHookState(base);
1062
1118
  resetProactiveHealing();
1063
1119
  autoStartTime = Date.now();
1064
- resourceSyncedAtOnStart = readResourceSyncedAt();
1120
+ resourceVersionOnStart = readResourceVersion();
1065
1121
  completedUnits = [];
1066
1122
  pendingQuickTasks = [];
1067
1123
  currentUnit = null;
@@ -1314,12 +1370,41 @@ export async function handleAgentEnd(
1314
1370
  // Small delay to let files settle (git commits, file writes)
1315
1371
  await new Promise(r => setTimeout(r, 500));
1316
1372
 
1317
- // Auto-commit any dirty files the LLM left behind on the current branch.
1373
+ // Commit any dirty files the LLM left behind on the current branch.
1374
+ // For execute-task units, build a meaningful commit message from the
1375
+ // task summary (one-liner, key_files, inferred type). For other unit
1376
+ // types, fall back to the generic chore() message.
1318
1377
  if (currentUnit) {
1319
1378
  try {
1320
- const commitMsg = autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id);
1379
+ let taskContext: TaskCommitContext | undefined;
1380
+
1381
+ if (currentUnit.type === "execute-task") {
1382
+ const parts = currentUnit.id.split("/");
1383
+ const [mid, sid, tid] = parts;
1384
+ if (mid && sid && tid) {
1385
+ const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
1386
+ if (summaryPath) {
1387
+ try {
1388
+ const summaryContent = await loadFile(summaryPath);
1389
+ if (summaryContent) {
1390
+ const summary = parseSummary(summaryContent);
1391
+ taskContext = {
1392
+ taskId: `${sid}/${tid}`,
1393
+ taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
1394
+ oneLiner: summary.oneLiner || undefined,
1395
+ keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
1396
+ };
1397
+ }
1398
+ } catch {
1399
+ // Non-fatal — fall back to generic message
1400
+ }
1401
+ }
1402
+ }
1403
+ }
1404
+
1405
+ const commitMsg = autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id, taskContext);
1321
1406
  if (commitMsg) {
1322
- ctx.ui.notify(`Auto-committed uncommitted changes.`, "info");
1407
+ ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
1323
1408
  }
1324
1409
  } catch {
1325
1410
  // Non-fatal
@@ -1331,10 +1416,13 @@ export async function handleAgentEnd(
1331
1416
  // fixLevel:"task" ensures doctor only fixes task-level issues (e.g. marking
1332
1417
  // checkboxes). Slice/milestone completion transitions (summary stubs,
1333
1418
  // roadmap [x] marking) are left for the complete-slice dispatch unit.
1419
+ // Exception: after complete-slice itself, use fixLevel:"all" so roadmap
1420
+ // checkboxes get fixed even if complete-slice crashed (#839).
1334
1421
  try {
1335
1422
  const scopeParts = currentUnit.id.split("/").slice(0, 2);
1336
1423
  const doctorScope = scopeParts.join("/");
1337
- const report = await runGSDDoctor(basePath, { fix: true, scope: doctorScope, fixLevel: "task" });
1424
+ const effectiveFixLevel = currentUnit.type === "complete-slice" ? "all" as const : "task" as const;
1425
+ const report = await runGSDDoctor(basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
1338
1426
  if (report.fixesApplied.length > 0) {
1339
1427
  ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
1340
1428
  }
@@ -1372,7 +1460,8 @@ export async function handleAgentEnd(
1372
1460
  }
1373
1461
  try {
1374
1462
  await rebuildState(basePath);
1375
- autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id);
1463
+ // State rebuild commit is bookkeeping — generic message is appropriate
1464
+ autoCommitCurrentBranch(basePath, "state-rebuild", currentUnit.id);
1376
1465
  } catch {
1377
1466
  // Non-fatal
1378
1467
  }
@@ -1465,7 +1554,7 @@ export async function handleAgentEnd(
1465
1554
  persistCompletedKey(basePath, completionKey);
1466
1555
  completedKeySet.add(completionKey);
1467
1556
  }
1468
- invalidateStateCache();
1557
+ invalidateAllCaches();
1469
1558
  }
1470
1559
  } catch {
1471
1560
  // Non-fatal — worst case we fall through to normal dispatch which has its own checks
@@ -1504,7 +1593,16 @@ export async function handleAgentEnd(
1504
1593
  if (currentUnit) {
1505
1594
  const modelId = ctx.model?.id ?? "unknown";
1506
1595
  snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1507
- saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1596
+ const hookActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1597
+ if (hookActivityFile) {
1598
+ try {
1599
+ const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
1600
+ const llmCallFn = buildMemoryLLMCall(ctx);
1601
+ if (llmCallFn) {
1602
+ extractMemoriesFromUnit(hookActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
1603
+ }
1604
+ } catch { /* non-fatal */ }
1605
+ }
1508
1606
  }
1509
1607
  currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
1510
1608
  writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
@@ -1646,7 +1744,16 @@ export async function handleAgentEnd(
1646
1744
  if (currentUnit) {
1647
1745
  const modelId = ctx.model?.id ?? "unknown";
1648
1746
  snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1649
- saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1747
+ const triageActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1748
+ if (triageActivityFile) {
1749
+ try {
1750
+ const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
1751
+ const llmCallFn = buildMemoryLLMCall(ctx);
1752
+ if (llmCallFn) {
1753
+ extractMemoriesFromUnit(triageActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
1754
+ }
1755
+ } catch { /* non-fatal */ }
1756
+ }
1650
1757
  }
1651
1758
 
1652
1759
  // Dispatch triage as a new unit (early-dispatch-and-return)
@@ -1724,7 +1831,16 @@ export async function handleAgentEnd(
1724
1831
  if (currentUnit) {
1725
1832
  const modelId = ctx.model?.id ?? "unknown";
1726
1833
  snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1727
- saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1834
+ const qtActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1835
+ if (qtActivityFile) {
1836
+ try {
1837
+ const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
1838
+ const llmCallFn = buildMemoryLLMCall(ctx);
1839
+ if (llmCallFn) {
1840
+ extractMemoriesFromUnit(qtActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
1841
+ }
1842
+ } catch { /* non-fatal */ }
1843
+ }
1728
1844
  }
1729
1845
 
1730
1846
  // Dispatch quick-task as a new unit
@@ -1940,6 +2056,7 @@ async function dispatchNextUnit(
1940
2056
  pi: ExtensionAPI,
1941
2057
  ): Promise<void> {
1942
2058
  if (!active || !cmdCtx) {
2059
+ debugLog(`dispatchNextUnit early return — active=${active}, cmdCtx=${!!cmdCtx}`);
1943
2060
  if (active && !cmdCtx) {
1944
2061
  ctx.ui.notify("Auto-mode session expired. Run /gsd auto to restart.", "info");
1945
2062
  }
@@ -1949,6 +2066,7 @@ async function dispatchNextUnit(
1949
2066
  // Reentrancy guard: allow recursive calls from skip paths (_skipDepth > 0)
1950
2067
  // but block concurrent external calls (watchdog, step wizard, etc.)
1951
2068
  if (_dispatching && _skipDepth === 0) {
2069
+ debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
1952
2070
  return; // Another dispatch is in progress — bail silently
1953
2071
  }
1954
2072
  _dispatching = true;
@@ -1984,7 +2102,7 @@ async function dispatchNextUnit(
1984
2102
  // Lightweight check for critical issues that would cause the next unit
1985
2103
  // to fail or corrupt state. Auto-heals what it can, blocks on the rest.
1986
2104
  try {
1987
- const healthGate = preDispatchHealthGate(basePath);
2105
+ const healthGate = await preDispatchHealthGate(basePath);
1988
2106
  if (healthGate.fixesApplied.length > 0) {
1989
2107
  ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
1990
2108
  }
@@ -1997,6 +2115,14 @@ async function dispatchNextUnit(
1997
2115
  // Non-fatal — health gate failure should never block dispatch
1998
2116
  }
1999
2117
 
2118
+ // ── Sync project root artifacts into worktree (#853) ─────────────────
2119
+ // When the LLM writes artifacts to the main repo filesystem instead of
2120
+ // the worktree, the worktree's gsd.db becomes stale. Sync before
2121
+ // deriveState to ensure the worktree has the latest artifacts.
2122
+ if (originalBasePath && basePath !== originalBasePath && currentMilestoneId) {
2123
+ syncProjectRootToWorktree(originalBasePath, basePath, currentMilestoneId);
2124
+ }
2125
+
2000
2126
  const stopDeriveTimer = debugTime("derive-state");
2001
2127
  let state = await deriveState(basePath);
2002
2128
  stopDeriveTimer({
@@ -2418,26 +2544,61 @@ async function dispatchNextUnit(
2418
2544
  const skipCount = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
2419
2545
  unitConsecutiveSkips.set(idempotencyKey, skipCount);
2420
2546
  if (skipCount > MAX_CONSECUTIVE_SKIPS) {
2547
+ // Cross-check: verify deriveState actually returns this unit (#790).
2548
+ // If the unit's milestone is already complete, this is a phantom skip
2549
+ // loop from stale crash recovery context — don't evict.
2550
+ const skippedMid = unitId.split("/")[0];
2551
+ const skippedMilestoneComplete = skippedMid
2552
+ ? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
2553
+ : false;
2554
+ if (skippedMilestoneComplete) {
2555
+ // Milestone is complete — evicting this key would fight self-heal.
2556
+ // Clear skip counter and re-dispatch from fresh state.
2557
+ unitConsecutiveSkips.delete(idempotencyKey);
2558
+ invalidateAllCaches();
2559
+ ctx.ui.notify(
2560
+ `Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid}. Re-dispatching from fresh state.`,
2561
+ "info",
2562
+ );
2563
+ _skipDepth++;
2564
+ await new Promise(r => setTimeout(r, 50));
2565
+ await dispatchNextUnit(ctx, pi);
2566
+ _skipDepth = Math.max(0, _skipDepth - 1);
2567
+ return;
2568
+ }
2421
2569
  unitConsecutiveSkips.delete(idempotencyKey);
2422
2570
  completedKeySet.delete(idempotencyKey);
2423
2571
  removePersistedKey(basePath, idempotencyKey);
2424
- invalidateStateCache();
2572
+ invalidateAllCaches();
2425
2573
  ctx.ui.notify(
2426
2574
  `Skip loop detected: ${unitType} ${unitId} skipped ${skipCount} times without advancing. Evicting completion record and forcing reconciliation.`,
2427
2575
  "warning",
2428
2576
  );
2577
+ if (!active) return;
2429
2578
  _skipDepth++;
2430
- await new Promise(r => setTimeout(r, 50));
2579
+ await new Promise(r => setTimeout(r, 150));
2431
2580
  await dispatchNextUnit(ctx, pi);
2432
2581
  _skipDepth = Math.max(0, _skipDepth - 1);
2433
2582
  return;
2434
2583
  }
2584
+ // Count toward lifetime cap so hard-stop fires during skip loops (#792)
2585
+ const lifeSkip = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
2586
+ unitLifetimeDispatches.set(idempotencyKey, lifeSkip);
2587
+ if (lifeSkip > MAX_LIFETIME_DISPATCHES) {
2588
+ await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
2589
+ ctx.ui.notify(
2590
+ `Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip} iterations).`,
2591
+ "error",
2592
+ );
2593
+ return;
2594
+ }
2435
2595
  ctx.ui.notify(
2436
2596
  `Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
2437
2597
  "info",
2438
2598
  );
2599
+ if (!active) return;
2439
2600
  _skipDepth++;
2440
- await new Promise(r => setTimeout(r, 50));
2601
+ await new Promise(r => setTimeout(r, 150));
2441
2602
  await dispatchNextUnit(ctx, pi);
2442
2603
  _skipDepth = Math.max(0, _skipDepth - 1);
2443
2604
  return;
@@ -2460,31 +2621,62 @@ async function dispatchNextUnit(
2460
2621
  if (verifyExpectedArtifact(unitType, unitId, basePath)) {
2461
2622
  persistCompletedKey(basePath, idempotencyKey);
2462
2623
  completedKeySet.add(idempotencyKey);
2463
- invalidateStateCache();
2624
+ invalidateAllCaches();
2464
2625
  // Same consecutive-skip guard as the idempotency path above.
2465
2626
  const skipCount2 = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
2466
2627
  unitConsecutiveSkips.set(idempotencyKey, skipCount2);
2467
2628
  if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
2629
+ // Cross-check: verify the unit's milestone is still active (#790).
2630
+ const skippedMid2 = unitId.split("/")[0];
2631
+ const skippedMilestoneComplete2 = skippedMid2
2632
+ ? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
2633
+ : false;
2634
+ if (skippedMilestoneComplete2) {
2635
+ unitConsecutiveSkips.delete(idempotencyKey);
2636
+ invalidateAllCaches();
2637
+ ctx.ui.notify(
2638
+ `Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid2}. Re-dispatching from fresh state.`,
2639
+ "info",
2640
+ );
2641
+ _skipDepth++;
2642
+ await new Promise(r => setTimeout(r, 50));
2643
+ await dispatchNextUnit(ctx, pi);
2644
+ _skipDepth = Math.max(0, _skipDepth - 1);
2645
+ return;
2646
+ }
2468
2647
  unitConsecutiveSkips.delete(idempotencyKey);
2469
2648
  completedKeySet.delete(idempotencyKey);
2470
2649
  removePersistedKey(basePath, idempotencyKey);
2471
- invalidateStateCache();
2650
+ invalidateAllCaches();
2472
2651
  ctx.ui.notify(
2473
2652
  `Skip loop detected: ${unitType} ${unitId} skipped ${skipCount2} times without advancing. Evicting completion record and forcing reconciliation.`,
2474
2653
  "warning",
2475
2654
  );
2655
+ if (!active) return;
2476
2656
  _skipDepth++;
2477
- await new Promise(r => setTimeout(r, 50));
2657
+ await new Promise(r => setTimeout(r, 150));
2478
2658
  await dispatchNextUnit(ctx, pi);
2479
2659
  _skipDepth = Math.max(0, _skipDepth - 1);
2480
2660
  return;
2481
2661
  }
2662
+ // Count toward lifetime cap so hard-stop fires during skip loops (#792)
2663
+ const lifeSkip2 = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
2664
+ unitLifetimeDispatches.set(idempotencyKey, lifeSkip2);
2665
+ if (lifeSkip2 > MAX_LIFETIME_DISPATCHES) {
2666
+ await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
2667
+ ctx.ui.notify(
2668
+ `Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip2} iterations).`,
2669
+ "error",
2670
+ );
2671
+ return;
2672
+ }
2482
2673
  ctx.ui.notify(
2483
2674
  `Skipping ${unitType} ${unitId} — artifact exists but completion key was missing. Repaired and advancing.`,
2484
2675
  "info",
2485
2676
  );
2677
+ if (!active) return;
2486
2678
  _skipDepth++;
2487
- await new Promise(r => setTimeout(r, 50));
2679
+ await new Promise(r => setTimeout(r, 150));
2488
2680
  await dispatchNextUnit(ctx, pi);
2489
2681
  _skipDepth = Math.max(0, _skipDepth - 1);
2490
2682
  return;
@@ -2554,7 +2746,7 @@ async function dispatchNextUnit(
2554
2746
  persistCompletedKey(basePath, reconciledKey);
2555
2747
  completedKeySet.add(reconciledKey);
2556
2748
  unitDispatchCount.delete(dispatchKey);
2557
- invalidateStateCache();
2749
+ invalidateAllCaches();
2558
2750
  await new Promise(r => setImmediate(r));
2559
2751
  await dispatchNextUnit(ctx, pi);
2560
2752
  return;
@@ -2581,7 +2773,7 @@ async function dispatchNextUnit(
2581
2773
  persistCompletedKey(basePath, dispatchKey);
2582
2774
  completedKeySet.add(dispatchKey);
2583
2775
  unitDispatchCount.delete(dispatchKey);
2584
- invalidateStateCache();
2776
+ invalidateAllCaches();
2585
2777
  await new Promise(r => setImmediate(r));
2586
2778
  await dispatchNextUnit(ctx, pi);
2587
2779
  return;
@@ -2601,7 +2793,7 @@ async function dispatchNextUnit(
2601
2793
  persistCompletedKey(basePath, dispatchKey);
2602
2794
  completedKeySet.add(dispatchKey);
2603
2795
  unitDispatchCount.delete(dispatchKey);
2604
- invalidateStateCache();
2796
+ invalidateAllCaches();
2605
2797
  await new Promise(r => setImmediate(r));
2606
2798
  await dispatchNextUnit(ctx, pi);
2607
2799
  return;
@@ -2642,7 +2834,7 @@ async function dispatchNextUnit(
2642
2834
  persistCompletedKey(basePath, repairedKey);
2643
2835
  completedKeySet.add(repairedKey);
2644
2836
  unitDispatchCount.delete(dispatchKey);
2645
- invalidateStateCache();
2837
+ invalidateAllCaches();
2646
2838
  await new Promise(r => setImmediate(r));
2647
2839
  await dispatchNextUnit(ctx, pi);
2648
2840
  return;
@@ -2686,7 +2878,18 @@ async function dispatchNextUnit(
2686
2878
  if (currentUnit) {
2687
2879
  const modelId = ctx.model?.id ?? "unknown";
2688
2880
  snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2689
- saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
2881
+ const activityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
2882
+
2883
+ // Fire-and-forget memory extraction from completed unit
2884
+ if (activityFile) {
2885
+ try {
2886
+ const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
2887
+ const llmCallFn = buildMemoryLLMCall(ctx);
2888
+ if (llmCallFn) {
2889
+ extractMemoriesFromUnit(activityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
2890
+ }
2891
+ } catch { /* non-fatal */ }
2892
+ }
2690
2893
 
2691
2894
  // Record routing outcome for adaptive learning
2692
2895
  if (currentUnitRouting) {
@@ -3603,6 +3806,20 @@ export async function dispatchDirectPhase(
3603
3806
  ctx.ui.notify("Cannot dispatch research-slice: no active slice.", "warning");
3604
3807
  return;
3605
3808
  }
3809
+
3810
+ // When require_slice_discussion is enabled, pause auto-mode before
3811
+ // each new slice so the user can discuss requirements first (#789).
3812
+ const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
3813
+ const requireDiscussion = loadEffectiveGSDPreferences()?.preferences?.phases?.require_slice_discussion;
3814
+ if (requireDiscussion && !sliceContextFile) {
3815
+ ctx.ui.notify(
3816
+ `Slice ${sid} requires discussion before planning. Run /gsd discuss to discuss this slice, then /gsd auto to resume.`,
3817
+ "info",
3818
+ );
3819
+ await pauseAuto(ctx, pi);
3820
+ return;
3821
+ }
3822
+
3606
3823
  unitType = "research-slice";
3607
3824
  unitId = `${mid}/${sid}`;
3608
3825
  prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
@@ -12,16 +12,18 @@
12
12
  import { invalidateStateCache } from './state.js';
13
13
  import { clearPathCache } from './paths.js';
14
14
  import { clearParseCache } from './files.js';
15
+ import { clearArtifacts } from './gsd-db.js';
15
16
 
16
17
  /**
17
18
  * Invalidate all GSD runtime caches in one call.
18
19
  *
19
20
  * Call this after file writes, milestone transitions, merge reconciliation,
20
21
  * or any operation that changes .gsd/ contents on disk. Forgetting to clear
21
- * any single cache causes stale reads (see #431).
22
+ * any single cache causes stale reads (see #431, #793).
22
23
  */
23
24
  export function invalidateAllCaches(): void {
24
25
  invalidateStateCache();
25
26
  clearPathCache();
26
27
  clearParseCache();
28
+ clearArtifacts();
27
29
  }
@@ -19,6 +19,7 @@ import { join } from "node:path";
19
19
  import { gsdRoot, resolveGsdRootFile } from "./paths.js";
20
20
  import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
21
21
  import { abortAndReset } from "./git-self-heal.js";
22
+ import { rebuildState } from "./doctor.js";
22
23
 
23
24
  // ── Health Score Tracking ──────────────────────────────────────────────────
24
25
 
@@ -131,7 +132,7 @@ export interface PreDispatchHealthResult {
131
132
  *
132
133
  * Returns { proceed: true } if dispatch should continue.
133
134
  */
134
- export function preDispatchHealthGate(basePath: string): PreDispatchHealthResult {
135
+ export async function preDispatchHealthGate(basePath: string): Promise<PreDispatchHealthResult> {
135
136
  const issues: string[] = [];
136
137
  const fixesApplied: string[] = [];
137
138
 
@@ -172,17 +173,17 @@ export function preDispatchHealthGate(basePath: string): PreDispatchHealthResult
172
173
  }
173
174
 
174
175
  // ── STATE.md existence check ──
175
- // If STATE.md is missing, deriveState will still work but the LLM
176
- // may get confused. Rebuild it silently.
176
+ // If STATE.md is missing, rebuild it now so the next unit has accurate
177
+ // context. Non-blocking if the rebuild throws, dispatch continues anyway.
177
178
  try {
178
179
  const stateFile = resolveGsdRootFile(basePath, "STATE");
179
180
  const milestonesDir = join(gsdRoot(basePath), "milestones");
180
181
  if (existsSync(milestonesDir) && !existsSync(stateFile)) {
181
- issues.push("STATE.md missing — will rebuild after this unit");
182
- // Don't block dispatch — rebuilding happens in post-hook
182
+ await rebuildState(basePath);
183
+ fixesApplied.push("rebuilt missing STATE.md before dispatch");
183
184
  }
184
185
  } catch {
185
- // Non-fatal
186
+ // Non-fatal — dispatch continues without STATE.md if rebuild fails
186
187
  }
187
188
 
188
189
  // If we had critical issues that couldn't be auto-healed, block dispatch
@@ -4,6 +4,7 @@ import { join, sep } from "node:path";
4
4
  import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
5
5
  import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
6
6
  import { deriveState, isMilestoneComplete } from "./state.js";
7
+ import { invalidateAllCaches } from "./cache.js";
7
8
  import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
8
9
  import { listWorktrees, resolveGitDir } from "./worktree-manager.js";
9
10
  import { abortAndReset } from "./git-self-heal.js";
@@ -200,6 +201,7 @@ async function updateStateFile(basePath: string, fixesApplied: string[]): Promis
200
201
 
201
202
  /** Rebuild STATE.md from current disk state. Exported for auto-mode post-hooks. */
202
203
  export async function rebuildState(basePath: string): Promise<void> {
204
+ invalidateAllCaches();
203
205
  const state = await deriveState(basePath);
204
206
  const path = resolveGsdRootFile(basePath, "STATE");
205
207
  await saveFile(path, buildStateMarkdown(state));
@@ -1142,8 +1144,31 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
1142
1144
  unitId: taskUnitId,
1143
1145
  message: `Task ${task.id} is marked done but summary is missing`,
1144
1146
  file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
1145
- fixable: false,
1147
+ fixable: true,
1146
1148
  });
1149
+ // Write a stub summary so validate-milestone can proceed.
1150
+ // This prevents infinite skip loops when tasks are marked done
1151
+ // without summaries (#820).
1152
+ if (shouldFix("task_done_missing_summary")) {
1153
+ const stubPath = join(
1154
+ basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks",
1155
+ `${task.id}-SUMMARY.md`,
1156
+ );
1157
+ const stubContent = [
1158
+ `---`,
1159
+ `status: done`,
1160
+ `result: unknown`,
1161
+ `doctor_generated: true`,
1162
+ `---`,
1163
+ ``,
1164
+ `# ${task.id}: ${task.title || "Unknown"}`,
1165
+ ``,
1166
+ `Summary stub generated by \`/gsd doctor\` — task was marked done but no summary existed.`,
1167
+ ``,
1168
+ ].join("\n");
1169
+ await saveFile(stubPath, stubContent);
1170
+ fixesApplied.push(`created stub summary for ${taskUnitId}`);
1171
+ }
1147
1172
  }
1148
1173
 
1149
1174
  if (!task.done && hasSummary) {