gsd-pi 2.18.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 (289) 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-dashboard.ts +14 -2
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +148 -39
  9. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  10. package/dist/resources/extensions/gsd/auto.ts +690 -39
  11. package/dist/resources/extensions/gsd/captures.ts +384 -0
  12. package/dist/resources/extensions/gsd/commands.ts +654 -36
  13. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  14. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  15. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  16. package/dist/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  17. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  18. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  19. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  20. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  21. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  22. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  23. package/dist/resources/extensions/gsd/export.ts +81 -2
  24. package/dist/resources/extensions/gsd/files.ts +39 -9
  25. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  26. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  27. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  28. package/dist/resources/extensions/gsd/history.ts +0 -1
  29. package/dist/resources/extensions/gsd/index.ts +277 -1
  30. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  31. package/dist/resources/extensions/gsd/metrics.ts +84 -0
  32. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  33. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  34. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  35. package/dist/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  36. package/dist/resources/extensions/gsd/preferences.ts +198 -150
  37. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  39. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  40. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  41. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  42. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  43. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  44. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  45. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  46. package/dist/resources/extensions/gsd/quick.ts +156 -0
  47. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  48. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  49. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  50. package/dist/resources/extensions/gsd/state.ts +30 -0
  51. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  52. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  53. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  54. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  55. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  57. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  58. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  59. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  60. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  61. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  62. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  63. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  64. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  65. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  66. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  67. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  68. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  69. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  70. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  71. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  72. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  73. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  74. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  75. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  76. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  77. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  78. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  79. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  80. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  81. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  82. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  83. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  84. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  85. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  86. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  87. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  88. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  89. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  90. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  91. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  92. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  93. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  94. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  95. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  96. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  97. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  98. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  99. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  100. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  101. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  102. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  103. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  104. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  105. package/dist/resources/extensions/gsd/types.ts +29 -0
  106. package/dist/resources/extensions/gsd/undo.ts +0 -1
  107. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  108. package/dist/resources/extensions/gsd/visualizer-data.ts +505 -0
  109. package/dist/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  110. package/dist/resources/extensions/gsd/visualizer-views.ts +755 -0
  111. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  112. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  113. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  114. package/dist/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  115. package/dist/resources/extensions/remote-questions/format.ts +166 -14
  116. package/dist/resources/extensions/remote-questions/manager.ts +14 -4
  117. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  118. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  119. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  120. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  121. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  122. package/dist/resources/extensions/voice/index.ts +4 -3
  123. package/package.json +1 -1
  124. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  126. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  129. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  131. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  133. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  137. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  139. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  140. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  142. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  144. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  146. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  148. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  150. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  152. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  153. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  154. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  156. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  158. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  160. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  161. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  162. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  163. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  164. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  165. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  166. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  167. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  168. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  169. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  170. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  171. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  172. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  173. package/src/resources/extensions/google-search/index.ts +164 -47
  174. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  175. package/src/resources/extensions/gsd/auto-prompts.ts +148 -39
  176. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  177. package/src/resources/extensions/gsd/auto.ts +690 -39
  178. package/src/resources/extensions/gsd/captures.ts +384 -0
  179. package/src/resources/extensions/gsd/commands.ts +654 -36
  180. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  181. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  182. package/src/resources/extensions/gsd/context-store.ts +195 -0
  183. package/src/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  184. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  185. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  186. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  187. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  188. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  189. package/src/resources/extensions/gsd/doctor.ts +283 -2
  190. package/src/resources/extensions/gsd/export.ts +81 -2
  191. package/src/resources/extensions/gsd/files.ts +39 -9
  192. package/src/resources/extensions/gsd/git-service.ts +6 -0
  193. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  194. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  195. package/src/resources/extensions/gsd/history.ts +0 -1
  196. package/src/resources/extensions/gsd/index.ts +277 -1
  197. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  198. package/src/resources/extensions/gsd/metrics.ts +84 -0
  199. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  200. package/src/resources/extensions/gsd/model-router.ts +256 -0
  201. package/src/resources/extensions/gsd/notifications.ts +0 -1
  202. package/src/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  203. package/src/resources/extensions/gsd/preferences.ts +198 -150
  204. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  205. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  206. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  207. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  208. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  209. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  210. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  211. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  212. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  213. package/src/resources/extensions/gsd/quick.ts +156 -0
  214. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  215. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  216. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  217. package/src/resources/extensions/gsd/state.ts +30 -0
  218. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  219. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  220. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  221. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  222. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  223. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  224. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  225. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  226. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  227. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  228. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  229. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  230. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  231. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  232. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  233. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  234. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  235. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  236. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  237. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  238. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  239. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  240. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  241. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  242. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  243. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  244. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  245. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  246. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  247. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  248. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  249. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  250. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  251. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  252. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  253. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  254. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  255. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  256. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  257. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  258. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  259. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  260. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  261. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  262. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  263. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  264. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  265. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  266. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  267. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  268. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  269. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  270. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  271. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  272. package/src/resources/extensions/gsd/types.ts +29 -0
  273. package/src/resources/extensions/gsd/undo.ts +0 -1
  274. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  275. package/src/resources/extensions/gsd/visualizer-data.ts +505 -0
  276. package/src/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  277. package/src/resources/extensions/gsd/visualizer-views.ts +755 -0
  278. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  279. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  280. package/src/resources/extensions/remote-questions/config.ts +4 -2
  281. package/src/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  282. package/src/resources/extensions/remote-questions/format.ts +166 -14
  283. package/src/resources/extensions/remote-questions/manager.ts +14 -4
  284. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  285. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  286. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  287. package/src/resources/extensions/remote-questions/types.ts +2 -1
  288. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  289. package/src/resources/extensions/voice/index.ts +4 -3
@@ -19,6 +19,7 @@ import type {
19
19
  import { deriveState, invalidateStateCache } from "./state.js";
20
20
  import type { BudgetEnforcementMode, GSDState } from "./types.js";
21
21
  import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides } from "./files.js";
22
+ import { loadPrompt } from "./prompt-loader.js";
22
23
  export { inlinePriorMilestoneSummary } from "./files.js";
23
24
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
24
25
  import {
@@ -39,9 +40,12 @@ import {
39
40
  readUnitRuntimeRecord,
40
41
  writeUnitRuntimeRecord,
41
42
  } from "./unit-runtime.js";
42
- import { resolveAutoSupervisorConfig, resolveModelWithFallbacksForUnit, loadEffectiveGSDPreferences, resolveSkillDiscoveryMode } from "./preferences.js";
43
+ import { resolveAutoSupervisorConfig, resolveModelWithFallbacksForUnit, loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, resolveDynamicRoutingConfig } from "./preferences.js";
43
44
  import { sendDesktopNotification } from "./notifications.js";
44
45
  import type { GSDPreferences } from "./preferences.js";
46
+ import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js";
47
+ import { resolveModelForComplexity } from "./model-router.js";
48
+ import { initRoutingHistory, resetRoutingHistory, recordOutcome } from "./routing-history.js";
45
49
  import {
46
50
  checkPostUnitHooks,
47
51
  getActiveHook,
@@ -60,8 +64,17 @@ import {
60
64
  formatValidationIssues,
61
65
  } from "./observability-validator.js";
62
66
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
63
- 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";
64
76
  import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
77
+ import { captureAvailableSkills, getAndClearSkills, resetSkillTelemetry } from "./skill-telemetry.js";
65
78
  import {
66
79
  initMetrics, resetMetrics, snapshotUnitMetrics, getLedger,
67
80
  getProjectTotals, formatCost, formatTokenCount,
@@ -96,6 +109,7 @@ import {
96
109
  } from "./auto-worktree.js";
97
110
  import { pruneQueueOrder } from "./queue-order.js";
98
111
  import { showNextAction } from "../shared/next-action-ui.js";
112
+ import { debugLog, debugTime, debugCount, debugPeak, enableDebug, isDebugEnabled, writeDebugSummary, getDebugLogPath } from "./debug-logger.js";
99
113
  import {
100
114
  resolveExpectedArtifactPath,
101
115
  verifyExpectedArtifact,
@@ -129,6 +143,8 @@ import {
129
143
  deregisterSigtermHandler as _deregisterSigtermHandler,
130
144
  detectWorkingTreeActivity,
131
145
  } from "./auto-supervisor.js";
146
+ import { isDbAvailable } from "./gsd-db.js";
147
+ import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from "./captures.js";
132
148
 
133
149
  // ─── State ────────────────────────────────────────────────────────────────────
134
150
 
@@ -233,6 +249,18 @@ let autoStartTime: number = 0;
233
249
  let completedUnits: { type: string; id: string; startedAt: number; finishedAt: number }[] = [];
234
250
  let currentUnit: { type: string; id: string; startedAt: number } | null = null;
235
251
 
252
+ /** Track dynamic routing decision for the current unit (for metrics) */
253
+ let currentUnitRouting: { tier: string; modelDowngraded: boolean } | null = null;
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
+
236
264
  /** Track current milestone to detect transitions */
237
265
  let currentMilestoneId: string | null = null;
238
266
  let lastBudgetAlertLevel: BudgetAlertLevel = 0;
@@ -254,6 +282,10 @@ let idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
254
282
  let dispatchGapHandle: ReturnType<typeof setTimeout> | null = null;
255
283
  const DISPATCH_GAP_TIMEOUT_MS = 5_000; // 5 seconds
256
284
 
285
+ /** Prompt character measurement for token savings analysis (R051). */
286
+ let lastPromptCharCount: number | undefined;
287
+ let lastBaselineCharCount: number | undefined;
288
+
257
289
  /** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
258
290
  let _sigtermHandler: (() => void) | null = null;
259
291
 
@@ -301,6 +333,15 @@ export { type AutoDashboardData } from "./auto-dashboard.js";
301
333
  export function getAutoDashboardData(): AutoDashboardData {
302
334
  const ledger = getLedger();
303
335
  const totals = ledger ? getProjectTotals(ledger.units) : null;
336
+ // Pending capture count — lazy check, non-fatal
337
+ let pendingCaptureCount = 0;
338
+ try {
339
+ if (basePath) {
340
+ pendingCaptureCount = countPendingCaptures(basePath);
341
+ }
342
+ } catch {
343
+ // Non-fatal — captures module may not be loaded
344
+ }
304
345
  return {
305
346
  active,
306
347
  paused,
@@ -312,6 +353,7 @@ export function getAutoDashboardData(): AutoDashboardData {
312
353
  basePath,
313
354
  totalCost: totals?.cost ?? 0,
314
355
  totalTokens: totals?.tokens.total ?? 0,
356
+ pendingCaptureCount,
315
357
  };
316
358
  }
317
359
 
@@ -457,6 +499,7 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
457
499
  clearUnitTimeout();
458
500
  if (lockBase()) clearLock(lockBase());
459
501
  clearSkillSnapshot();
502
+ resetSkillTelemetry();
460
503
  _dispatching = false;
461
504
  _skipDepth = 0;
462
505
 
@@ -464,12 +507,17 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
464
507
  deregisterSigtermHandler();
465
508
 
466
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.
467
513
  if (currentMilestoneId && isInAutoWorktree(basePath)) {
468
514
  try {
469
- 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 });
470
518
  basePath = originalBasePath;
471
519
  gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
472
- ctx?.ui.notify("Exited auto-worktree.", "info");
520
+ ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
473
521
  } catch (err) {
474
522
  ctx?.ui.notify(
475
523
  `Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
@@ -478,6 +526,14 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
478
526
  }
479
527
  }
480
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
+
481
537
  // Always restore cwd to project root on stop (#608).
482
538
  // Even if isInAutoWorktree returned false (e.g., module state was already
483
539
  // cleared by mergeMilestoneToMain), the process cwd may still be inside
@@ -503,7 +559,16 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
503
559
  try { await rebuildState(basePath); } catch { /* non-fatal */ }
504
560
  }
505
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
+
506
570
  resetMetrics();
571
+ resetRoutingHistory();
507
572
  resetHookState();
508
573
  if (basePath) clearPersistedHookState(basePath);
509
574
  active = false;
@@ -515,11 +580,13 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
515
580
  lastBudgetAlertLevel = 0;
516
581
  unitLifetimeDispatches.clear();
517
582
  currentUnit = null;
583
+ autoModeStartModel = null;
518
584
  currentMilestoneId = null;
519
585
  originalBasePath = "";
520
586
  completedUnits = [];
521
587
  clearSliceProgressCache();
522
588
  clearActivityLogState();
589
+ resetProactiveHealing();
523
590
  pendingCrashRecovery = null;
524
591
  _handlingAgentEnd = false;
525
592
  ctx?.ui.setStatus("gsd-auto", undefined);
@@ -706,27 +773,122 @@ export async function startAuto(
706
773
  clearLock(base);
707
774
  }
708
775
 
709
- const state = await deriveState(base);
710
-
711
- // No active work at all — start a new milestone via the discuss flow.
712
- if (!state.activeMilestone || state.phase === "complete") {
713
- const { showSmartEntry } = await import("./guided-flow.js");
714
- await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
715
- return;
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
+ }
793
+
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
+ }
716
820
  }
717
821
 
718
- // Active milestone exists but has no roadmap — check if context exists.
719
- // If context was pre-written (multi-milestone planning), auto-mode can
720
- // research and plan it. If no context either, need user discussion.
721
- if (state.phase === "pre-planning") {
722
- const contextFile = resolveMilestoneFile(base, state.activeMilestone.id, "CONTEXT");
723
- const hasContext = !!(contextFile && await loadFile(contextFile));
724
- 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") {
725
830
  const { showSmartEntry } = await import("./guided-flow.js");
726
831
  await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
727
- 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
+ }
854
+ }
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
728
881
  }
729
- // Has context, no roadmap — auto-mode will research + plan it
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;
730
892
  }
731
893
 
732
894
  active = true;
@@ -742,6 +904,7 @@ export async function startAuto(
742
904
  loadPersistedKeys(base, completedKeySet);
743
905
  resetHookState();
744
906
  restoreHookState(base);
907
+ resetProactiveHealing();
745
908
  autoStartTime = Date.now();
746
909
  resourceSyncedAtOnStart = readResourceSyncedAt();
747
910
  completedUnits = [];
@@ -806,9 +969,47 @@ export async function startAuto(
806
969
  }
807
970
  }
808
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
+
809
999
  // Initialize metrics — loads existing ledger from disk
810
1000
  initMetrics(base);
811
1001
 
1002
+ // Initialize routing history for adaptive learning
1003
+ initRoutingHistory(base);
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
+
812
1013
  // Snapshot installed skills so we can detect new ones after research
813
1014
  if (resolveSkillDiscoveryMode() !== "off") {
814
1015
  snapshotSkills();
@@ -824,7 +1025,7 @@ export async function startAuto(
824
1025
  ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
825
1026
 
826
1027
  // Secrets collection gate — collect pending secrets before first dispatch
827
- const mid = state.activeMilestone.id;
1028
+ const mid = state.activeMilestone!.id;
828
1029
  try {
829
1030
  const manifestStatus = await getManifestStatus(base, mid);
830
1031
  if (manifestStatus && manifestStatus.pending.length > 0) {
@@ -943,6 +1144,35 @@ export async function handleAgentEnd(
943
1144
  if (report.fixesApplied.length > 0) {
944
1145
  ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
945
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
+ }
946
1176
  } catch {
947
1177
  // Non-fatal — doctor failure should never block dispatch
948
1178
  }
@@ -1003,6 +1233,16 @@ export async function handleAgentEnd(
1003
1233
  }
1004
1234
  }
1005
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
+
1006
1246
  // ── Post-unit hooks: check if a configured hook should run before normal dispatch ──
1007
1247
  if (currentUnit && !stepMode) {
1008
1248
  const hookUnit = checkPostUnitHooks(currentUnit.type, currentUnit.id, basePath);
@@ -1011,7 +1251,7 @@ export async function handleAgentEnd(
1011
1251
  const hookStartedAt = Date.now();
1012
1252
  if (currentUnit) {
1013
1253
  const modelId = ctx.model?.id ?? "unknown";
1014
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1254
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1015
1255
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1016
1256
  }
1017
1257
  currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
@@ -1106,6 +1346,108 @@ export async function handleAgentEnd(
1106
1346
  }
1107
1347
  }
1108
1348
 
1349
+ // ── Triage check: dispatch triage unit if pending captures exist ──────────
1350
+ // Fires after hooks complete, before normal dispatch. Follows the same
1351
+ // early-dispatch-and-return pattern as hooks and fix-merge.
1352
+ // Skip for: step mode (shows wizard instead), triage units (prevent triage-on-triage),
1353
+ // hook units (hooks run before triage conceptually).
1354
+ if (
1355
+ !stepMode &&
1356
+ currentUnit &&
1357
+ !currentUnit.type.startsWith("hook/") &&
1358
+ currentUnit.type !== "triage-captures" &&
1359
+ currentUnit.type !== "quick-task"
1360
+ ) {
1361
+ try {
1362
+ if (hasPendingCaptures(basePath)) {
1363
+ const pending = loadPendingCaptures(basePath);
1364
+ if (pending.length > 0) {
1365
+ const state = await deriveState(basePath);
1366
+ const mid = state.activeMilestone?.id;
1367
+ const sid = state.activeSlice?.id;
1368
+
1369
+ if (mid && sid) {
1370
+ // Build triage prompt with current context
1371
+ let currentPlan = "";
1372
+ let roadmapContext = "";
1373
+ const planFile = resolveSliceFile(basePath, mid, sid, "PLAN");
1374
+ if (planFile) currentPlan = (await loadFile(planFile)) ?? "";
1375
+ const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
1376
+ if (roadmapFile) roadmapContext = (await loadFile(roadmapFile)) ?? "";
1377
+
1378
+ const capturesList = pending.map(c =>
1379
+ `- **${c.id}**: "${c.text}" (captured: ${c.timestamp})`
1380
+ ).join("\n");
1381
+
1382
+ const prompt = loadPrompt("triage-captures", {
1383
+ pendingCaptures: capturesList,
1384
+ currentPlan: currentPlan || "(no active slice plan)",
1385
+ roadmapContext: roadmapContext || "(no active roadmap)",
1386
+ });
1387
+
1388
+ ctx.ui.notify(
1389
+ `Triaging ${pending.length} pending capture${pending.length === 1 ? "" : "s"}...`,
1390
+ "info",
1391
+ );
1392
+
1393
+ // Close out previous unit metrics
1394
+ if (currentUnit) {
1395
+ const modelId = ctx.model?.id ?? "unknown";
1396
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1397
+ saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1398
+ }
1399
+
1400
+ // Dispatch triage as a new unit (early-dispatch-and-return)
1401
+ const triageUnitType = "triage-captures";
1402
+ const triageUnitId = `${mid}/${sid}/triage`;
1403
+ const triageStartedAt = Date.now();
1404
+ currentUnit = { type: triageUnitType, id: triageUnitId, startedAt: triageStartedAt };
1405
+ writeUnitRuntimeRecord(basePath, triageUnitType, triageUnitId, triageStartedAt, {
1406
+ phase: "dispatched",
1407
+ wrapupWarningSent: false,
1408
+ timeoutAt: null,
1409
+ lastProgressAt: triageStartedAt,
1410
+ progressCount: 0,
1411
+ lastProgressKind: "dispatch",
1412
+ });
1413
+ updateProgressWidget(ctx, triageUnitType, triageUnitId, state);
1414
+
1415
+ const result = await cmdCtx!.newSession();
1416
+ if (result.cancelled) {
1417
+ await stopAuto(ctx, pi);
1418
+ return;
1419
+ }
1420
+ const sessionFile = ctx.sessionManager.getSessionFile();
1421
+ writeLock(basePath, triageUnitType, triageUnitId, completedUnits.length, sessionFile);
1422
+
1423
+ // Start unit timeout for triage (use same supervisor config as hooks)
1424
+ clearUnitTimeout();
1425
+ const supervisor = resolveAutoSupervisorConfig();
1426
+ const triageTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
1427
+ unitTimeoutHandle = setTimeout(async () => {
1428
+ unitTimeoutHandle = null;
1429
+ if (!active) return;
1430
+ ctx.ui.notify(
1431
+ `Triage unit exceeded timeout. Pausing auto-mode.`,
1432
+ "warning",
1433
+ );
1434
+ await pauseAuto(ctx, pi);
1435
+ }, triageTimeoutMs);
1436
+
1437
+ if (!active) return;
1438
+ pi.sendMessage(
1439
+ { customType: "gsd-auto", content: prompt, display: verbose },
1440
+ { triggerTurn: true },
1441
+ );
1442
+ return; // handleAgentEnd will fire again when triage session completes
1443
+ }
1444
+ }
1445
+ }
1446
+ } catch {
1447
+ // Triage check failure is non-fatal — proceed to normal dispatch
1448
+ }
1449
+ }
1450
+
1109
1451
  // In step mode, pause and show a wizard instead of immediately dispatching
1110
1452
  if (stepMode) {
1111
1453
  await showStepWizard(ctx, pi);
@@ -1227,7 +1569,10 @@ function updateProgressWidget(
1227
1569
  unitId: string,
1228
1570
  state: GSDState,
1229
1571
  ): void {
1230
- _updateProgressWidget(ctx, unitType, unitId, state, widgetStateAccessors);
1572
+ const badge = currentUnitRouting?.tier
1573
+ ? ({ light: "L", standard: "S", heavy: "H" }[currentUnitRouting.tier] ?? undefined)
1574
+ : undefined;
1575
+ _updateProgressWidget(ctx, unitType, unitId, state, widgetStateAccessors, badge);
1231
1576
  }
1232
1577
 
1233
1578
  /** State accessors for the widget — closures over module globals. */
@@ -1294,8 +1639,34 @@ async function dispatchNextUnit(
1294
1639
  // Parse cache is also cleared — doctor may have re-populated it with
1295
1640
  // stale data between handleAgentEnd and this dispatch call (Path B fix).
1296
1641
  invalidateAllCaches();
1642
+ lastPromptCharCount = undefined;
1643
+ lastBaselineCharCount = undefined;
1297
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
+ }
1661
+
1662
+ const stopDeriveTimer = debugTime("derive-state");
1298
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
+ });
1299
1670
  let mid = state.activeMilestone?.id;
1300
1671
  let midTitle = state.activeMilestone?.title;
1301
1672
 
@@ -1306,12 +1677,85 @@ async function dispatchNextUnit(
1306
1677
  "info",
1307
1678
  );
1308
1679
  sendDesktopNotification("GSD", `Milestone ${currentMilestoneId} complete!`, "success", "milestone");
1680
+ // Hint: visualizer available after milestone transition
1681
+ const vizPrefs = loadEffectiveGSDPreferences()?.preferences;
1682
+ if (vizPrefs?.auto_visualize) {
1683
+ ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
1684
+ }
1309
1685
  // Reset stuck detection for new milestone
1310
1686
  unitDispatchCount.clear();
1311
1687
  unitRecoveryCount.clear();
1312
1688
  unitLifetimeDispatches.clear();
1313
- // Capture integration branch for the new milestone and update git service
1314
- captureIntegrationBranch(originalBasePath || basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1689
+ // Clear completed-units.json for the finished milestone
1690
+ try {
1691
+ const file = completedKeysPath(basePath);
1692
+ if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
1693
+ completedKeySet.clear();
1694
+ } catch { /* non-fatal */ }
1695
+
1696
+ // ── Worktree lifecycle on milestone transition (#616) ──────────────
1697
+ // When transitioning from M_old to M_new inside a worktree, we must:
1698
+ // 1. Merge the completed milestone's worktree back to main
1699
+ // 2. Re-derive state from the project root
1700
+ // 3. Create a new worktree for the incoming milestone
1701
+ // Without this, M_new runs inside M_old's worktree on the wrong branch,
1702
+ // and artifact paths resolve against the wrong .gsd/ directory.
1703
+ if (isInAutoWorktree(basePath) && originalBasePath && shouldUseWorktreeIsolation()) {
1704
+ try {
1705
+ const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
1706
+ if (roadmapPath) {
1707
+ const roadmapContent = readFileSync(roadmapPath, "utf-8");
1708
+ const mergeResult = mergeMilestoneToMain(originalBasePath, currentMilestoneId, roadmapContent);
1709
+ ctx.ui.notify(
1710
+ `Milestone ${currentMilestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1711
+ "info",
1712
+ );
1713
+ } else {
1714
+ // No roadmap found — teardown worktree without merge
1715
+ teardownAutoWorktree(originalBasePath, currentMilestoneId);
1716
+ ctx.ui.notify(`Exited worktree for ${currentMilestoneId} (no roadmap for merge).`, "info");
1717
+ }
1718
+ } catch (err) {
1719
+ ctx.ui.notify(
1720
+ `Milestone merge failed during transition: ${err instanceof Error ? err.message : String(err)}`,
1721
+ "warning",
1722
+ );
1723
+ // Force cwd back to project root even if merge failed
1724
+ if (originalBasePath) {
1725
+ try { process.chdir(originalBasePath); } catch { /* best-effort */ }
1726
+ }
1727
+ }
1728
+
1729
+ // Update basePath to project root (mergeMilestoneToMain already chdir'd)
1730
+ basePath = originalBasePath;
1731
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1732
+ invalidateAllCaches();
1733
+
1734
+ // Re-derive state from project root before creating new worktree
1735
+ state = await deriveState(basePath);
1736
+ mid = state.activeMilestone?.id;
1737
+ midTitle = state.activeMilestone?.title;
1738
+
1739
+ // Create new worktree for the incoming milestone
1740
+ if (mid) {
1741
+ captureIntegrationBranch(basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1742
+ try {
1743
+ const wtPath = createAutoWorktree(basePath, mid);
1744
+ basePath = wtPath;
1745
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1746
+ ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
1747
+ } catch (err) {
1748
+ ctx.ui.notify(
1749
+ `Auto-worktree creation for ${mid} failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
1750
+ "warning",
1751
+ );
1752
+ }
1753
+ }
1754
+ } else {
1755
+ // Not in worktree — just capture integration branch for the new milestone
1756
+ captureIntegrationBranch(originalBasePath || basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1757
+ }
1758
+
1315
1759
  // Prune completed milestone from queue order file
1316
1760
  const pendingIds = state.registry
1317
1761
  .filter(m => m.status !== "complete")
@@ -1327,7 +1771,7 @@ async function dispatchNextUnit(
1327
1771
  // Save final session before stopping
1328
1772
  if (currentUnit) {
1329
1773
  const modelId = ctx.model?.id ?? "unknown";
1330
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1774
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1331
1775
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1332
1776
  }
1333
1777
  sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
@@ -1355,7 +1799,7 @@ async function dispatchNextUnit(
1355
1799
  if (!mid || !midTitle) {
1356
1800
  if (currentUnit) {
1357
1801
  const modelId = ctx.model?.id ?? "unknown";
1358
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1802
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1359
1803
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1360
1804
  }
1361
1805
  await stopAuto(ctx, pi);
@@ -1370,7 +1814,7 @@ async function dispatchNextUnit(
1370
1814
  if (state.phase === "complete") {
1371
1815
  if (currentUnit) {
1372
1816
  const modelId = ctx.model?.id ?? "unknown";
1373
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1817
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1374
1818
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1375
1819
  }
1376
1820
  // Clear completed-units.json for the finished milestone so it doesn't grow unbounded.
@@ -1440,7 +1884,7 @@ async function dispatchNextUnit(
1440
1884
  if (state.phase === "blocked") {
1441
1885
  if (currentUnit) {
1442
1886
  const modelId = ctx.model?.id ?? "unknown";
1443
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1887
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1444
1888
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1445
1889
  }
1446
1890
  await stopAuto(ctx, pi);
@@ -1548,7 +1992,7 @@ async function dispatchNextUnit(
1548
1992
  if (dispatchResult.action === "stop") {
1549
1993
  if (currentUnit) {
1550
1994
  const modelId = ctx.model?.id ?? "unknown";
1551
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1995
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1552
1996
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1553
1997
  }
1554
1998
  await stopAuto(ctx, pi);
@@ -1650,6 +2094,14 @@ async function dispatchNextUnit(
1650
2094
  const dispatchKey = `${unitType}/${unitId}`;
1651
2095
  const prevCount = unitDispatchCount.get(dispatchKey) ?? 0;
1652
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
+
1653
2105
  // Hard lifetime cap — survives counter resets from loop-recovery/self-repair.
1654
2106
  // Catches the case where reconciliation "succeeds" (artifacts exist) but
1655
2107
  // deriveState keeps returning the same unit, creating an infinite cycle.
@@ -1658,7 +2110,7 @@ async function dispatchNextUnit(
1658
2110
  if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
1659
2111
  if (currentUnit) {
1660
2112
  const modelId = ctx.model?.id ?? "unknown";
1661
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
2113
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1662
2114
  }
1663
2115
  saveActivityLog(ctx, basePath, unitType, unitId);
1664
2116
  const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
@@ -1672,7 +2124,7 @@ async function dispatchNextUnit(
1672
2124
  if (prevCount >= MAX_UNIT_DISPATCHES) {
1673
2125
  if (currentUnit) {
1674
2126
  const modelId = ctx.model?.id ?? "unknown";
1675
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
2127
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1676
2128
  }
1677
2129
  saveActivityLog(ctx, basePath, unitType, unitId);
1678
2130
 
@@ -1830,9 +2282,19 @@ async function dispatchNextUnit(
1830
2282
  // The session still holds the previous unit's data (newSession hasn't fired yet).
1831
2283
  if (currentUnit) {
1832
2284
  const modelId = ctx.model?.id ?? "unknown";
1833
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
2285
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1834
2286
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1835
2287
 
2288
+ // Record routing outcome for adaptive learning
2289
+ if (currentUnitRouting) {
2290
+ const isRetry = currentUnit.type === unitType && currentUnit.id === unitId;
2291
+ recordOutcome(
2292
+ currentUnit.type,
2293
+ currentUnitRouting.tier as "light" | "standard" | "heavy",
2294
+ !isRetry, // success = not being retried
2295
+ );
2296
+ }
2297
+
1836
2298
  // Only mark the previous unit as completed if:
1837
2299
  // 1. We're not about to re-dispatch the same unit (retry scenario)
1838
2300
  // 2. The expected artifact actually exists on disk
@@ -1866,6 +2328,7 @@ async function dispatchNextUnit(
1866
2328
  }
1867
2329
  }
1868
2330
  currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
2331
+ captureAvailableSkills(); // Capture skill telemetry at dispatch time (#599)
1869
2332
  writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
1870
2333
  phase: "dispatched",
1871
2334
  wrapupWarningSent: false,
@@ -1930,12 +2393,79 @@ async function dispatchNextUnit(
1930
2393
  finalPrompt = `${finalPrompt}${repairBlock}`;
1931
2394
  }
1932
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
+
1933
2416
  // Switch model if preferences specify one for this unit type
1934
2417
  // Try primary model, then fallbacks in order if setting fails
1935
2418
  const modelConfig = resolveModelWithFallbacksForUnit(unitType);
1936
2419
  if (modelConfig) {
1937
2420
  const availableModels = ctx.modelRegistry.getAvailable();
1938
- const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
2421
+
2422
+ // ─── Dynamic Model Routing ─────────────────────────────────────────
2423
+ // If enabled, classify unit complexity and potentially downgrade to a
2424
+ // cheaper model. The user's configured model is the ceiling.
2425
+ const routingConfig = resolveDynamicRoutingConfig();
2426
+ let effectiveModelConfig = modelConfig;
2427
+ let routingTierLabel = "";
2428
+ currentUnitRouting = null;
2429
+
2430
+ if (routingConfig.enabled) {
2431
+ // Compute budget pressure if budget ceiling is set
2432
+ let budgetPct: number | undefined;
2433
+ if (routingConfig.budget_pressure !== false) {
2434
+ const budgetCeiling = prefs?.budget_ceiling;
2435
+ if (budgetCeiling !== undefined && budgetCeiling > 0) {
2436
+ const currentLedger = getLedger();
2437
+ const totalCost = currentLedger ? getProjectTotals(currentLedger.units).cost : 0;
2438
+ budgetPct = totalCost / budgetCeiling;
2439
+ }
2440
+ }
2441
+
2442
+ // Classify complexity (hook routing controlled by config.hooks)
2443
+ const isHook = unitType.startsWith("hook/");
2444
+ const shouldClassify = !isHook || routingConfig.hooks !== false;
2445
+
2446
+ if (shouldClassify) {
2447
+ const classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
2448
+ const availableModelIds = availableModels.map(m => m.id);
2449
+ const routing = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds);
2450
+
2451
+ if (routing.wasDowngraded) {
2452
+ effectiveModelConfig = {
2453
+ primary: routing.modelId,
2454
+ fallbacks: routing.fallbacks,
2455
+ };
2456
+ if (verbose) {
2457
+ ctx.ui.notify(
2458
+ `Dynamic routing [${tierLabel(classification.tier)}]: ${routing.modelId} (${classification.reason})`,
2459
+ "info",
2460
+ );
2461
+ }
2462
+ }
2463
+ routingTierLabel = ` [${tierLabel(classification.tier)}]`;
2464
+ currentUnitRouting = { tier: classification.tier, modelDowngraded: routing.wasDowngraded };
2465
+ }
2466
+ }
2467
+
2468
+ const modelsToTry = [effectiveModelConfig.primary, ...effectiveModelConfig.fallbacks];
1939
2469
  let modelSet = false;
1940
2470
 
1941
2471
  for (const modelId of modelsToTry) {
@@ -2000,11 +2530,11 @@ async function dispatchNextUnit(
2000
2530
 
2001
2531
  const ok = await pi.setModel(model, { persist: false });
2002
2532
  if (ok) {
2003
- const fallbackNote = modelId === modelConfig.primary
2533
+ const fallbackNote = modelId === effectiveModelConfig.primary
2004
2534
  ? ""
2005
- : ` (fallback from ${modelConfig.primary})`;
2535
+ : ` (fallback from ${effectiveModelConfig.primary})`;
2006
2536
  const phase = unitPhaseLabel(unitType);
2007
- ctx.ui.notify(`Model [${phase}]: ${model.provider}/${model.id}${fallbackNote}`, "info");
2537
+ ctx.ui.notify(`Model [${phase}]${routingTierLabel}: ${model.provider}/${model.id}${fallbackNote}`, "info");
2008
2538
  modelSet = true;
2009
2539
  break;
2010
2540
  } else {
@@ -2018,6 +2548,22 @@ async function dispatchNextUnit(
2018
2548
  }
2019
2549
 
2020
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
+ }
2021
2567
  }
2022
2568
 
2023
2569
  // Start progress-aware supervision: a soft warning, an idle watchdog, and
@@ -2083,7 +2629,7 @@ async function dispatchNextUnit(
2083
2629
 
2084
2630
  if (currentUnit) {
2085
2631
  const modelId = ctx.model?.id ?? "unknown";
2086
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
2632
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2087
2633
  }
2088
2634
  saveActivityLog(ctx, basePath, unitType, unitId);
2089
2635
 
@@ -2109,7 +2655,7 @@ async function dispatchNextUnit(
2109
2655
  timeoutAt: Date.now(),
2110
2656
  });
2111
2657
  const modelId = ctx.model?.id ?? "unknown";
2112
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
2658
+ snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2113
2659
  }
2114
2660
  saveActivityLog(ctx, basePath, unitType, unitId);
2115
2661
 
@@ -2491,3 +3037,108 @@ export {
2491
3037
  skipExecuteTask,
2492
3038
  buildLoopRemediationSteps,
2493
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
+ }