gsd-pi 2.19.0 → 2.20.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 (249) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +3 -3
  3. package/dist/onboarding.d.ts +3 -1
  4. package/dist/onboarding.js +77 -3
  5. package/dist/remote-questions-config.d.ts +1 -1
  6. package/dist/resources/extensions/google-search/index.ts +164 -47
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  9. package/dist/resources/extensions/gsd/auto.ts +424 -30
  10. package/dist/resources/extensions/gsd/commands.ts +518 -36
  11. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  12. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  14. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  15. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  16. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  17. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  18. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  19. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  20. package/dist/resources/extensions/gsd/export.ts +81 -2
  21. package/dist/resources/extensions/gsd/files.ts +39 -9
  22. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  23. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  24. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  25. package/dist/resources/extensions/gsd/history.ts +0 -1
  26. package/dist/resources/extensions/gsd/index.ts +277 -1
  27. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  28. package/dist/resources/extensions/gsd/metrics.ts +39 -3
  29. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  30. package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  31. package/dist/resources/extensions/gsd/preferences.ts +125 -150
  32. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  33. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  34. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  35. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  36. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  37. package/dist/resources/extensions/gsd/quick.ts +156 -0
  38. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  39. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  40. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  41. package/dist/resources/extensions/gsd/state.ts +30 -0
  42. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  43. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  44. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  45. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  46. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  47. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  48. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  49. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  51. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  52. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  54. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  55. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  56. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  57. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  58. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  59. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  60. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  61. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  62. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  63. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  64. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  65. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  66. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  67. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  68. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  69. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  70. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  71. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  72. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  73. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  74. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  75. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  76. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  77. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  78. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  79. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  80. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  81. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  82. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  83. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  84. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  85. package/dist/resources/extensions/gsd/types.ts +29 -0
  86. package/dist/resources/extensions/gsd/undo.ts +0 -1
  87. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  88. package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
  89. package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  90. package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
  91. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  92. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  93. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  94. package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  95. package/dist/resources/extensions/remote-questions/format.ts +154 -8
  96. package/dist/resources/extensions/remote-questions/manager.ts +9 -7
  97. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  98. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  99. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  100. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  101. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  102. package/dist/resources/extensions/voice/index.ts +4 -3
  103. package/package.json +1 -1
  104. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  106. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  109. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  113. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  117. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  119. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  120. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  122. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  124. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  126. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  128. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  130. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  133. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  136. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  138. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  141. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  142. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  143. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  144. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  145. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  146. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  147. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  148. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  149. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  150. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  151. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  152. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  153. package/src/resources/extensions/google-search/index.ts +164 -47
  154. package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
  155. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  156. package/src/resources/extensions/gsd/auto.ts +424 -30
  157. package/src/resources/extensions/gsd/commands.ts +518 -36
  158. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  159. package/src/resources/extensions/gsd/context-store.ts +195 -0
  160. package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  161. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  162. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  163. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  164. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  165. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  166. package/src/resources/extensions/gsd/doctor.ts +283 -2
  167. package/src/resources/extensions/gsd/export.ts +81 -2
  168. package/src/resources/extensions/gsd/files.ts +39 -9
  169. package/src/resources/extensions/gsd/git-service.ts +6 -0
  170. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  171. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  172. package/src/resources/extensions/gsd/history.ts +0 -1
  173. package/src/resources/extensions/gsd/index.ts +277 -1
  174. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  175. package/src/resources/extensions/gsd/metrics.ts +39 -3
  176. package/src/resources/extensions/gsd/notifications.ts +0 -1
  177. package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  178. package/src/resources/extensions/gsd/preferences.ts +125 -150
  179. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  180. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  181. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  182. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  183. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  184. package/src/resources/extensions/gsd/quick.ts +156 -0
  185. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  186. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  187. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  188. package/src/resources/extensions/gsd/state.ts +30 -0
  189. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  190. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  191. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  193. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  194. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  195. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  196. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  197. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  198. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  199. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  200. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  201. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  202. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  203. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  204. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  205. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  206. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  207. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  208. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  209. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  210. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  211. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  212. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  213. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  214. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  215. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  216. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  217. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  218. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  219. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  220. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  221. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  222. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  223. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  224. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  225. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  226. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  227. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  228. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  229. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  230. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  231. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  232. package/src/resources/extensions/gsd/types.ts +29 -0
  233. package/src/resources/extensions/gsd/undo.ts +0 -1
  234. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  235. package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
  236. package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  237. package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
  238. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  239. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  240. package/src/resources/extensions/remote-questions/config.ts +4 -2
  241. package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  242. package/src/resources/extensions/remote-questions/format.ts +154 -8
  243. package/src/resources/extensions/remote-questions/manager.ts +9 -7
  244. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  245. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  246. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  247. package/src/resources/extensions/remote-questions/types.ts +2 -1
  248. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  249. package/src/resources/extensions/voice/index.ts +4 -3
@@ -64,8 +64,17 @@ import {
64
64
  formatValidationIssues,
65
65
  } from "./observability-validator.js";
66
66
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
67
- import { runGSDDoctor, rebuildState } from "./doctor.js";
67
+ import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
68
+ import {
69
+ preDispatchHealthGate,
70
+ recordHealthSnapshot,
71
+ checkHealEscalation,
72
+ resetProactiveHealing,
73
+ formatHealthSummary,
74
+ getConsecutiveErrorUnits,
75
+ } from "./doctor-proactive.js";
68
76
  import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
77
+ import { captureAvailableSkills, getAndClearSkills, resetSkillTelemetry } from "./skill-telemetry.js";
69
78
  import {
70
79
  initMetrics, resetMetrics, snapshotUnitMetrics, getLedger,
71
80
  getProjectTotals, formatCost, formatTokenCount,
@@ -100,6 +109,7 @@ import {
100
109
  } from "./auto-worktree.js";
101
110
  import { pruneQueueOrder } from "./queue-order.js";
102
111
  import { showNextAction } from "../shared/next-action-ui.js";
112
+ import { debugLog, debugTime, debugCount, debugPeak, enableDebug, isDebugEnabled, writeDebugSummary, getDebugLogPath } from "./debug-logger.js";
103
113
  import {
104
114
  resolveExpectedArtifactPath,
105
115
  verifyExpectedArtifact,
@@ -133,6 +143,7 @@ import {
133
143
  deregisterSigtermHandler as _deregisterSigtermHandler,
134
144
  detectWorkingTreeActivity,
135
145
  } from "./auto-supervisor.js";
146
+ import { isDbAvailable } from "./gsd-db.js";
136
147
  import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from "./captures.js";
137
148
 
138
149
  // ─── State ────────────────────────────────────────────────────────────────────
@@ -241,6 +252,15 @@ let currentUnit: { type: string; id: string; startedAt: number } | null = null;
241
252
  /** Track dynamic routing decision for the current unit (for metrics) */
242
253
  let currentUnitRouting: { tier: string; modelDowngraded: boolean } | null = null;
243
254
 
255
+ /**
256
+ * Model captured at auto-mode start. Used to prevent model bleed between
257
+ * concurrent GSD instances sharing the same global settings.json (#650).
258
+ * When preferences don't specify a model for a unit type, this ensures
259
+ * the session's original model is re-applied instead of reading from
260
+ * the shared global settings (which another instance may have overwritten).
261
+ */
262
+ let autoModeStartModel: { provider: string; id: string } | null = null;
263
+
244
264
  /** Track current milestone to detect transitions */
245
265
  let currentMilestoneId: string | null = null;
246
266
  let lastBudgetAlertLevel: BudgetAlertLevel = 0;
@@ -262,6 +282,10 @@ let idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
262
282
  let dispatchGapHandle: ReturnType<typeof setTimeout> | null = null;
263
283
  const DISPATCH_GAP_TIMEOUT_MS = 5_000; // 5 seconds
264
284
 
285
+ /** Prompt character measurement for token savings analysis (R051). */
286
+ let lastPromptCharCount: number | undefined;
287
+ let lastBaselineCharCount: number | undefined;
288
+
265
289
  /** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
266
290
  let _sigtermHandler: (() => void) | null = null;
267
291
 
@@ -475,6 +499,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
475
499
  clearUnitTimeout();
476
500
  if (lockBase()) clearLock(lockBase());
477
501
  clearSkillSnapshot();
502
+ resetSkillTelemetry();
478
503
  _dispatching = false;
479
504
  _skipDepth = 0;
480
505
 
@@ -482,12 +507,17 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
482
507
  deregisterSigtermHandler();
483
508
 
484
509
  // ── Auto-worktree: exit worktree and reset basePath on stop ──
510
+ // Preserve the milestone branch so the next /gsd auto can re-enter
511
+ // where it left off. The branch is only deleted during milestone
512
+ // completion (mergeMilestoneToMain) after the work has been squash-merged.
485
513
  if (currentMilestoneId && isInAutoWorktree(basePath)) {
486
514
  try {
487
- teardownAutoWorktree(originalBasePath, currentMilestoneId);
515
+ // Auto-commit any dirty state before leaving so work isn't lost
516
+ try { autoCommitCurrentBranch(basePath, "stop", currentMilestoneId); } catch { /* non-fatal */ }
517
+ teardownAutoWorktree(originalBasePath, currentMilestoneId, { preserveBranch: true });
488
518
  basePath = originalBasePath;
489
519
  gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
490
- ctx?.ui.notify("Exited auto-worktree.", "info");
520
+ ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
491
521
  } catch (err) {
492
522
  ctx?.ui.notify(
493
523
  `Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
@@ -496,6 +526,14 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
496
526
  }
497
527
  }
498
528
 
529
+ // ── DB cleanup: close the SQLite connection ──
530
+ if (isDbAvailable()) {
531
+ try {
532
+ const { closeDatabase } = await import("./gsd-db.js");
533
+ closeDatabase();
534
+ } catch { /* non-fatal */ }
535
+ }
536
+
499
537
  // Always restore cwd to project root on stop (#608).
500
538
  // Even if isInAutoWorktree returned false (e.g., module state was already
501
539
  // cleared by mergeMilestoneToMain), the process cwd may still be inside
@@ -521,6 +559,14 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
521
559
  try { await rebuildState(basePath); } catch { /* non-fatal */ }
522
560
  }
523
561
 
562
+ // Write debug summary before resetting state
563
+ if (isDebugEnabled()) {
564
+ const logPath = writeDebugSummary();
565
+ if (logPath) {
566
+ ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
567
+ }
568
+ }
569
+
524
570
  resetMetrics();
525
571
  resetRoutingHistory();
526
572
  resetHookState();
@@ -534,11 +580,13 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
534
580
  lastBudgetAlertLevel = 0;
535
581
  unitLifetimeDispatches.clear();
536
582
  currentUnit = null;
583
+ autoModeStartModel = null;
537
584
  currentMilestoneId = null;
538
585
  originalBasePath = "";
539
586
  completedUnits = [];
540
587
  clearSliceProgressCache();
541
588
  clearActivityLogState();
589
+ resetProactiveHealing();
542
590
  pendingCrashRecovery = null;
543
591
  _handlingAgentEnd = false;
544
592
  ctx?.ui.setStatus("gsd-auto", undefined);
@@ -725,27 +773,122 @@ export async function startAuto(
725
773
  clearLock(base);
726
774
  }
727
775
 
728
- const state = await deriveState(base);
776
+ // ── Debug mode: env-var activation ──────────────────────────────────────
777
+ if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
778
+ enableDebug(base);
779
+ }
780
+ if (isDebugEnabled()) {
781
+ const { isNativeParserAvailable } = await import("./native-parser-bridge.js");
782
+ debugLog("debug-start", {
783
+ platform: process.platform,
784
+ arch: process.arch,
785
+ node: process.version,
786
+ model: ctx.model?.id ?? "unknown",
787
+ provider: ctx.model?.provider ?? "unknown",
788
+ nativeParser: isNativeParserAvailable(),
789
+ cwd: base,
790
+ });
791
+ ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
792
+ }
729
793
 
730
- // No active work at all — start a new milestone via the discuss flow.
731
- if (!state.activeMilestone || state.phase === "complete") {
732
- const { showSmartEntry } = await import("./guided-flow.js");
733
- await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
734
- return;
794
+ let state = await deriveState(base);
795
+
796
+ // ── Milestone branch recovery (#601) ─────────────────────────────────────
797
+ // When auto-mode was previously stopped, the milestone branch is preserved
798
+ // but the worktree is removed. The project root (integration branch) may
799
+ // not have the roadmap/artifacts — they live on the milestone branch.
800
+ // If state looks like pre-planning but a milestone branch exists with prior
801
+ // work, skip the early-return checks and let worktree setup + dispatch
802
+ // handle it correctly from the branch's state.
803
+ let hasSurvivorBranch = false;
804
+ if (
805
+ state.activeMilestone &&
806
+ (state.phase === "pre-planning" || state.phase === "needs-discussion") &&
807
+ shouldUseWorktreeIsolation() &&
808
+ !detectWorktreeName(base) &&
809
+ !base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)
810
+ ) {
811
+ const milestoneBranch = `milestone/${state.activeMilestone.id}`;
812
+ const { nativeBranchExists } = await import("./native-git-bridge.js");
813
+ hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
814
+ if (hasSurvivorBranch) {
815
+ ctx.ui.notify(
816
+ `Found prior session branch ${milestoneBranch}. Resuming.`,
817
+ "info",
818
+ );
819
+ }
735
820
  }
736
821
 
737
- // Active milestone exists but has no roadmap — check if context exists.
738
- // If context was pre-written (multi-milestone planning), auto-mode can
739
- // research and plan it. If no context either, need user discussion.
740
- if (state.phase === "pre-planning") {
741
- const contextFile = resolveMilestoneFile(base, state.activeMilestone.id, "CONTEXT");
742
- const hasContext = !!(contextFile && await loadFile(contextFile));
743
- if (!hasContext) {
822
+ if (!hasSurvivorBranch) {
823
+ // No active work at all — start a new milestone via the discuss flow.
824
+ // After discussion completes, checkAutoStartAfterDiscuss() (fired from
825
+ // agent_end) will detect the new CONTEXT.md and restart auto mode.
826
+ // If the LLM didn't follow the discussion protocol (e.g. started editing
827
+ // files directly for a simple task), we re-derive state and either proceed
828
+ // with what was created or notify the user clearly (#609).
829
+ if (!state.activeMilestone || state.phase === "complete") {
744
830
  const { showSmartEntry } = await import("./guided-flow.js");
745
831
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
746
- return;
832
+
833
+ // Re-derive state after discussion — the LLM may have created artifacts
834
+ // even if it didn't follow the full protocol.
835
+ invalidateAllCaches();
836
+ const postState = await deriveState(base);
837
+ if (postState.activeMilestone && postState.phase !== "complete" && postState.phase !== "pre-planning") {
838
+ state = postState;
839
+ } else if (postState.activeMilestone && postState.phase === "pre-planning") {
840
+ const contextFile = resolveMilestoneFile(base, postState.activeMilestone.id, "CONTEXT");
841
+ const hasContext = !!(contextFile && await loadFile(contextFile));
842
+ if (hasContext) {
843
+ state = postState;
844
+ } else {
845
+ ctx.ui.notify(
846
+ "Discussion completed but no milestone context was written. Run /gsd to try the discussion again, or /gsd auto after creating the milestone manually.",
847
+ "warning",
848
+ );
849
+ return;
850
+ }
851
+ } else {
852
+ return;
853
+ }
747
854
  }
748
- // Has context, no roadmap — auto-mode will research + plan it
855
+
856
+ // Active milestone exists but has no roadmap — check if context exists.
857
+ // If context was pre-written (multi-milestone planning), auto-mode can
858
+ // research and plan it. If no context either, need user discussion.
859
+ if (state.phase === "pre-planning") {
860
+ const mid = state.activeMilestone!.id;
861
+ const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
862
+ const hasContext = !!(contextFile && await loadFile(contextFile));
863
+ if (!hasContext) {
864
+ const { showSmartEntry } = await import("./guided-flow.js");
865
+ await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
866
+
867
+ // Same re-derive pattern as above
868
+ invalidateAllCaches();
869
+ const postState = await deriveState(base);
870
+ if (postState.activeMilestone && postState.phase !== "pre-planning") {
871
+ state = postState;
872
+ } else {
873
+ ctx.ui.notify(
874
+ "Discussion completed but milestone context is still missing. Run /gsd to try again.",
875
+ "warning",
876
+ );
877
+ return;
878
+ }
879
+ }
880
+ // Has context, no roadmap — auto-mode will research + plan it
881
+ }
882
+ }
883
+
884
+ // At this point activeMilestone is guaranteed non-null: either
885
+ // hasSurvivorBranch is true (which requires activeMilestone) or
886
+ // the !activeMilestone early-return above would have fired.
887
+ if (!state.activeMilestone) {
888
+ // Unreachable — satisfies TypeScript's null check
889
+ const { showSmartEntry } = await import("./guided-flow.js");
890
+ await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
891
+ return;
749
892
  }
750
893
 
751
894
  active = true;
@@ -761,6 +904,7 @@ export async function startAuto(
761
904
  loadPersistedKeys(base, completedKeySet);
762
905
  resetHookState();
763
906
  restoreHookState(base);
907
+ resetProactiveHealing();
764
908
  autoStartTime = Date.now();
765
909
  resourceSyncedAtOnStart = readResourceSyncedAt();
766
910
  completedUnits = [];
@@ -825,12 +969,47 @@ export async function startAuto(
825
969
  }
826
970
  }
827
971
 
972
+ // ── DB lifecycle: auto-migrate or open existing database ──
973
+ const gsdDbPath = join(basePath, ".gsd", "gsd.db");
974
+ const gsdDirPath = join(basePath, ".gsd");
975
+ if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
976
+ const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
977
+ const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
978
+ const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
979
+ if (hasDecisions || hasRequirements || hasMilestones) {
980
+ try {
981
+ const { openDatabase: openDb } = await import("./gsd-db.js");
982
+ const { migrateFromMarkdown } = await import("./md-importer.js");
983
+ openDb(gsdDbPath);
984
+ migrateFromMarkdown(basePath);
985
+ } catch (err) {
986
+ process.stderr.write(`gsd-migrate: auto-migration failed: ${(err as Error).message}\n`);
987
+ }
988
+ }
989
+ }
990
+ if (existsSync(gsdDbPath) && !isDbAvailable()) {
991
+ try {
992
+ const { openDatabase: openDb } = await import("./gsd-db.js");
993
+ openDb(gsdDbPath);
994
+ } catch (err) {
995
+ process.stderr.write(`gsd-db: failed to open existing database: ${(err as Error).message}\n`);
996
+ }
997
+ }
998
+
828
999
  // Initialize metrics — loads existing ledger from disk
829
1000
  initMetrics(base);
830
1001
 
831
1002
  // Initialize routing history for adaptive learning
832
1003
  initRoutingHistory(base);
833
1004
 
1005
+ // Capture the session's current model at auto-mode start (#650).
1006
+ // This prevents model bleed when multiple GSD instances share the
1007
+ // same global settings.json — each instance remembers its own model.
1008
+ const currentModel = ctx.model;
1009
+ if (currentModel) {
1010
+ autoModeStartModel = { provider: currentModel.provider, id: currentModel.id };
1011
+ }
1012
+
834
1013
  // Snapshot installed skills so we can detect new ones after research
835
1014
  if (resolveSkillDiscoveryMode() !== "off") {
836
1015
  snapshotSkills();
@@ -846,7 +1025,7 @@ export async function startAuto(
846
1025
  ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
847
1026
 
848
1027
  // Secrets collection gate — collect pending secrets before first dispatch
849
- const mid = state.activeMilestone.id;
1028
+ const mid = state.activeMilestone!.id;
850
1029
  try {
851
1030
  const manifestStatus = await getManifestStatus(base, mid);
852
1031
  if (manifestStatus && manifestStatus.pending.length > 0) {
@@ -965,6 +1144,35 @@ export async function handleAgentEnd(
965
1144
  if (report.fixesApplied.length > 0) {
966
1145
  ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
967
1146
  }
1147
+
1148
+ // ── Proactive health tracking ──────────────────────────────────────
1149
+ // Record health snapshot for trend analysis and escalation logic.
1150
+ const summary = summarizeDoctorIssues(report.issues);
1151
+ recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
1152
+
1153
+ // Check if we should escalate to LLM-assisted heal
1154
+ if (summary.errors > 0) {
1155
+ const unresolvedErrors = report.issues
1156
+ .filter(i => i.severity === "error" && !i.fixable)
1157
+ .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
1158
+ const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
1159
+ if (escalation.shouldEscalate) {
1160
+ ctx.ui.notify(
1161
+ `Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`,
1162
+ "warning",
1163
+ );
1164
+ try {
1165
+ const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
1166
+ const { dispatchDoctorHeal } = await import("./commands.js");
1167
+ const actionable = report.issues.filter(i => i.severity === "error");
1168
+ const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
1169
+ const structuredIssues = formatDoctorIssuesForPrompt(actionable);
1170
+ dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
1171
+ } catch {
1172
+ // Non-fatal — escalation dispatch failure
1173
+ }
1174
+ }
1175
+ }
968
1176
  } catch {
969
1177
  // Non-fatal — doctor failure should never block dispatch
970
1178
  }
@@ -1025,6 +1233,16 @@ export async function handleAgentEnd(
1025
1233
  }
1026
1234
  }
1027
1235
 
1236
+ // ── DB dual-write: re-import changed markdown files so next unit's prompts use fresh data ──
1237
+ if (isDbAvailable()) {
1238
+ try {
1239
+ const { migrateFromMarkdown } = await import("./md-importer.js");
1240
+ migrateFromMarkdown(basePath);
1241
+ } catch (err) {
1242
+ process.stderr.write(`gsd-db: re-import failed: ${(err as Error).message}\n`);
1243
+ }
1244
+ }
1245
+
1028
1246
  // ── Post-unit hooks: check if a configured hook should run before normal dispatch ──
1029
1247
  if (currentUnit && !stepMode) {
1030
1248
  const hookUnit = checkPostUnitHooks(currentUnit.type, currentUnit.id, basePath);
@@ -1033,7 +1251,7 @@ export async function handleAgentEnd(
1033
1251
  const hookStartedAt = Date.now();
1034
1252
  if (currentUnit) {
1035
1253
  const modelId = ctx.model?.id ?? "unknown";
1036
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
1254
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1037
1255
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1038
1256
  }
1039
1257
  currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
@@ -1421,8 +1639,34 @@ async function dispatchNextUnit(
1421
1639
  // Parse cache is also cleared — doctor may have re-populated it with
1422
1640
  // stale data between handleAgentEnd and this dispatch call (Path B fix).
1423
1641
  invalidateAllCaches();
1642
+ lastPromptCharCount = undefined;
1643
+ lastBaselineCharCount = undefined;
1644
+
1645
+ // ── Pre-dispatch health gate ──────────────────────────────────────────
1646
+ // Lightweight check for critical issues that would cause the next unit
1647
+ // to fail or corrupt state. Auto-heals what it can, blocks on the rest.
1648
+ try {
1649
+ const healthGate = preDispatchHealthGate(basePath);
1650
+ if (healthGate.fixesApplied.length > 0) {
1651
+ ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
1652
+ }
1653
+ if (!healthGate.proceed) {
1654
+ ctx.ui.notify(healthGate.reason ?? "Pre-dispatch health check failed.", "error");
1655
+ await pauseAuto(ctx, pi);
1656
+ return;
1657
+ }
1658
+ } catch {
1659
+ // Non-fatal — health gate failure should never block dispatch
1660
+ }
1424
1661
 
1662
+ const stopDeriveTimer = debugTime("derive-state");
1425
1663
  let state = await deriveState(basePath);
1664
+ stopDeriveTimer({
1665
+ phase: state.phase,
1666
+ milestone: state.activeMilestone?.id,
1667
+ slice: state.activeSlice?.id,
1668
+ task: state.activeTask?.id,
1669
+ });
1426
1670
  let mid = state.activeMilestone?.id;
1427
1671
  let midTitle = state.activeMilestone?.title;
1428
1672
 
@@ -1527,7 +1771,7 @@ async function dispatchNextUnit(
1527
1771
  // Save final session before stopping
1528
1772
  if (currentUnit) {
1529
1773
  const modelId = ctx.model?.id ?? "unknown";
1530
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
1774
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1531
1775
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1532
1776
  }
1533
1777
  sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
@@ -1555,7 +1799,7 @@ async function dispatchNextUnit(
1555
1799
  if (!mid || !midTitle) {
1556
1800
  if (currentUnit) {
1557
1801
  const modelId = ctx.model?.id ?? "unknown";
1558
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
1802
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1559
1803
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1560
1804
  }
1561
1805
  await stopAuto(ctx, pi);
@@ -1570,7 +1814,7 @@ async function dispatchNextUnit(
1570
1814
  if (state.phase === "complete") {
1571
1815
  if (currentUnit) {
1572
1816
  const modelId = ctx.model?.id ?? "unknown";
1573
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
1817
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1574
1818
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1575
1819
  }
1576
1820
  // Clear completed-units.json for the finished milestone so it doesn't grow unbounded.
@@ -1640,7 +1884,7 @@ async function dispatchNextUnit(
1640
1884
  if (state.phase === "blocked") {
1641
1885
  if (currentUnit) {
1642
1886
  const modelId = ctx.model?.id ?? "unknown";
1643
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
1887
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1644
1888
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1645
1889
  }
1646
1890
  await stopAuto(ctx, pi);
@@ -1748,7 +1992,7 @@ async function dispatchNextUnit(
1748
1992
  if (dispatchResult.action === "stop") {
1749
1993
  if (currentUnit) {
1750
1994
  const modelId = ctx.model?.id ?? "unknown";
1751
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
1995
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1752
1996
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1753
1997
  }
1754
1998
  await stopAuto(ctx, pi);
@@ -1850,6 +2094,14 @@ async function dispatchNextUnit(
1850
2094
  const dispatchKey = `${unitType}/${unitId}`;
1851
2095
  const prevCount = unitDispatchCount.get(dispatchKey) ?? 0;
1852
2096
 
2097
+ debugLog("dispatch-unit", {
2098
+ type: unitType,
2099
+ id: unitId,
2100
+ cycle: prevCount + 1,
2101
+ lifetime: (unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1,
2102
+ });
2103
+ debugCount("dispatches");
2104
+
1853
2105
  // Hard lifetime cap — survives counter resets from loop-recovery/self-repair.
1854
2106
  // Catches the case where reconciliation "succeeds" (artifacts exist) but
1855
2107
  // deriveState keeps returning the same unit, creating an infinite cycle.
@@ -1858,7 +2110,7 @@ async function dispatchNextUnit(
1858
2110
  if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
1859
2111
  if (currentUnit) {
1860
2112
  const modelId = ctx.model?.id ?? "unknown";
1861
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
2113
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1862
2114
  }
1863
2115
  saveActivityLog(ctx, basePath, unitType, unitId);
1864
2116
  const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
@@ -1872,7 +2124,7 @@ async function dispatchNextUnit(
1872
2124
  if (prevCount >= MAX_UNIT_DISPATCHES) {
1873
2125
  if (currentUnit) {
1874
2126
  const modelId = ctx.model?.id ?? "unknown";
1875
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
2127
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1876
2128
  }
1877
2129
  saveActivityLog(ctx, basePath, unitType, unitId);
1878
2130
 
@@ -2030,7 +2282,7 @@ async function dispatchNextUnit(
2030
2282
  // The session still holds the previous unit's data (newSession hasn't fired yet).
2031
2283
  if (currentUnit) {
2032
2284
  const modelId = ctx.model?.id ?? "unknown";
2033
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
2285
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2034
2286
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
2035
2287
 
2036
2288
  // Record routing outcome for adaptive learning
@@ -2076,6 +2328,7 @@ async function dispatchNextUnit(
2076
2328
  }
2077
2329
  }
2078
2330
  currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
2331
+ captureAvailableSkills(); // Capture skill telemetry at dispatch time (#599)
2079
2332
  writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
2080
2333
  phase: "dispatched",
2081
2334
  wrapupWarningSent: false,
@@ -2140,6 +2393,26 @@ async function dispatchNextUnit(
2140
2393
  finalPrompt = `${finalPrompt}${repairBlock}`;
2141
2394
  }
2142
2395
 
2396
+ // ── Prompt char measurement (R051) ──
2397
+ lastPromptCharCount = finalPrompt.length;
2398
+ lastBaselineCharCount = undefined;
2399
+ if (isDbAvailable()) {
2400
+ try {
2401
+ const { inlineGsdRootFile } = await import("./auto-prompts.js");
2402
+ const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
2403
+ inlineGsdRootFile(basePath, "decisions.md", "Decisions"),
2404
+ inlineGsdRootFile(basePath, "requirements.md", "Requirements"),
2405
+ inlineGsdRootFile(basePath, "project.md", "Project"),
2406
+ ]);
2407
+ lastBaselineCharCount =
2408
+ (decisionsContent?.length ?? 0) +
2409
+ (requirementsContent?.length ?? 0) +
2410
+ (projectContent?.length ?? 0);
2411
+ } catch {
2412
+ // Non-fatal — baseline measurement is best-effort
2413
+ }
2414
+ }
2415
+
2143
2416
  // Switch model if preferences specify one for this unit type
2144
2417
  // Try primary model, then fallbacks in order if setting fails
2145
2418
  const modelConfig = resolveModelWithFallbacksForUnit(unitType);
@@ -2275,6 +2548,22 @@ async function dispatchNextUnit(
2275
2548
  }
2276
2549
 
2277
2550
  // modelSet=false is already handled by the "all fallbacks exhausted" warning above
2551
+ } else if (autoModeStartModel) {
2552
+ // No model preference for this unit type — re-apply the model captured
2553
+ // at auto-mode start to prevent bleed from the shared global settings.json
2554
+ // when multiple GSD instances run concurrently (#650).
2555
+ const availableModels = ctx.modelRegistry.getAvailable();
2556
+ const startModel = availableModels.find(
2557
+ m => m.provider === autoModeStartModel!.provider && m.id === autoModeStartModel!.id,
2558
+ );
2559
+ if (startModel) {
2560
+ const ok = await pi.setModel(startModel, { persist: false });
2561
+ if (!ok) {
2562
+ // Fallback: try matching just by ID across providers
2563
+ const byId = availableModels.find(m => m.id === autoModeStartModel!.id);
2564
+ if (byId) await pi.setModel(byId, { persist: false });
2565
+ }
2566
+ }
2278
2567
  }
2279
2568
 
2280
2569
  // Start progress-aware supervision: a soft warning, an idle watchdog, and
@@ -2340,7 +2629,7 @@ async function dispatchNextUnit(
2340
2629
 
2341
2630
  if (currentUnit) {
2342
2631
  const modelId = ctx.model?.id ?? "unknown";
2343
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
2632
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2344
2633
  }
2345
2634
  saveActivityLog(ctx, basePath, unitType, unitId);
2346
2635
 
@@ -2366,7 +2655,7 @@ async function dispatchNextUnit(
2366
2655
  timeoutAt: Date.now(),
2367
2656
  });
2368
2657
  const modelId = ctx.model?.id ?? "unknown";
2369
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, currentUnitRouting ?? undefined);
2658
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2370
2659
  }
2371
2660
  saveActivityLog(ctx, basePath, unitType, unitId);
2372
2661
 
@@ -2748,3 +3037,108 @@ export {
2748
3037
  skipExecuteTask,
2749
3038
  buildLoopRemediationSteps,
2750
3039
  } from "./auto-recovery.js";
3040
+
3041
+ /**
3042
+ * Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
3043
+ * Used for manual hook triggers via /gsd run-hook.
3044
+ */
3045
+ export async function dispatchHookUnit(
3046
+ ctx: ExtensionContext,
3047
+ pi: ExtensionAPI,
3048
+ hookName: string,
3049
+ triggerUnitType: string,
3050
+ triggerUnitId: string,
3051
+ hookPrompt: string,
3052
+ hookModel: string | undefined,
3053
+ targetBasePath: string,
3054
+ ): Promise<boolean> {
3055
+ // Ensure auto-mode is active
3056
+ if (!active) {
3057
+ // Initialize auto-mode state minimally
3058
+ active = true;
3059
+ stepMode = true;
3060
+ cmdCtx = ctx as ExtensionCommandContext;
3061
+ basePath = targetBasePath;
3062
+ autoStartTime = Date.now();
3063
+ currentUnit = null;
3064
+ completedUnits = [];
3065
+ }
3066
+
3067
+ const hookUnitType = `hook/${hookName}`;
3068
+ const hookStartedAt = Date.now();
3069
+
3070
+ // Set up the trigger unit as the "current" unit so post-unit hooks can reference it
3071
+ currentUnit = { type: triggerUnitType, id: triggerUnitId, startedAt: hookStartedAt };
3072
+
3073
+ // Create a new session for the hook
3074
+ const result = await cmdCtx!.newSession();
3075
+ if (result.cancelled) {
3076
+ await stopAuto(ctx, pi);
3077
+ return false;
3078
+ }
3079
+
3080
+ // Update current unit to the hook unit
3081
+ currentUnit = { type: hookUnitType, id: triggerUnitId, startedAt: hookStartedAt };
3082
+
3083
+ // Write runtime record
3084
+ writeUnitRuntimeRecord(basePath, hookUnitType, triggerUnitId, hookStartedAt, {
3085
+ phase: "dispatched",
3086
+ wrapupWarningSent: false,
3087
+ timeoutAt: null,
3088
+ lastProgressAt: hookStartedAt,
3089
+ progressCount: 0,
3090
+ lastProgressKind: "dispatch",
3091
+ });
3092
+
3093
+ // Switch model if specified
3094
+ if (hookModel) {
3095
+ const availableModels = ctx.modelRegistry.getAvailable();
3096
+ const match = availableModels.find(m =>
3097
+ m.id === hookModel || `${m.provider}/${m.id}` === hookModel,
3098
+ );
3099
+ if (match) {
3100
+ try {
3101
+ await pi.setModel(match);
3102
+ } catch { /* non-fatal — use current model */ }
3103
+ }
3104
+ }
3105
+
3106
+ // Write lock
3107
+ const sessionFile = ctx.sessionManager.getSessionFile();
3108
+ writeLock(lockBase(), hookUnitType, triggerUnitId, completedUnits.length, sessionFile);
3109
+
3110
+ // Set up timeout
3111
+ clearUnitTimeout();
3112
+ const supervisor = resolveAutoSupervisorConfig();
3113
+ const hookHardTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
3114
+ unitTimeoutHandle = setTimeout(async () => {
3115
+ unitTimeoutHandle = null;
3116
+ if (!active) return;
3117
+ if (currentUnit) {
3118
+ writeUnitRuntimeRecord(basePath, hookUnitType, triggerUnitId, hookStartedAt, {
3119
+ phase: "timeout",
3120
+ timeoutAt: Date.now(),
3121
+ });
3122
+ }
3123
+ ctx.ui.notify(
3124
+ `Hook ${hookName} exceeded ${supervisor.hard_timeout_minutes ?? 30}min timeout. Pausing auto-mode.`,
3125
+ "warning",
3126
+ );
3127
+ resetHookState();
3128
+ await pauseAuto(ctx, pi);
3129
+ }, hookHardTimeoutMs);
3130
+
3131
+ // Update status
3132
+ ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
3133
+ ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
3134
+
3135
+ // Send the hook prompt
3136
+ console.log(`[dispatchHookUnit] Sending prompt of length ${hookPrompt.length}`);
3137
+ console.log(`[dispatchHookUnit] Prompt preview: ${hookPrompt.substring(0, 200)}...`);
3138
+ pi.sendMessage(
3139
+ { customType: "gsd-auto", content: hookPrompt, display: true },
3140
+ { triggerTurn: true },
3141
+ );
3142
+
3143
+ return true;
3144
+ }