gsd-pi 2.26.1-next.1 → 2.28.0-dev.346ee62

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 (1310) hide show
  1. package/README.md +76 -12
  2. package/dist/cli.js +24 -23
  3. package/dist/extension-discovery.d.ts +16 -0
  4. package/dist/extension-discovery.js +66 -0
  5. package/dist/headless-answers.d.ts +51 -0
  6. package/dist/headless-answers.js +224 -0
  7. package/dist/headless-context.d.ts +18 -0
  8. package/dist/headless-context.js +44 -0
  9. package/dist/headless-events.d.ts +28 -0
  10. package/dist/headless-events.js +59 -0
  11. package/dist/headless-query.d.ts +40 -0
  12. package/dist/headless-query.js +77 -0
  13. package/dist/headless-ui.d.ts +23 -0
  14. package/dist/headless-ui.js +103 -0
  15. package/dist/headless.d.ts +4 -0
  16. package/dist/headless.js +128 -155
  17. package/dist/help-text.js +9 -0
  18. package/dist/loader.js +18 -59
  19. package/dist/onboarding.js +8 -7
  20. package/dist/remote-questions-config.js +8 -3
  21. package/dist/resource-loader.d.ts +1 -6
  22. package/dist/resource-loader.js +52 -95
  23. package/dist/resources/extensions/ask-user-questions.ts +3 -2
  24. package/dist/resources/extensions/bg-shell/bg-shell-command.ts +219 -0
  25. package/dist/resources/extensions/bg-shell/bg-shell-lifecycle.ts +400 -0
  26. package/dist/resources/extensions/bg-shell/bg-shell-tool.ts +985 -0
  27. package/dist/resources/extensions/bg-shell/index.ts +18 -1545
  28. package/dist/resources/extensions/bg-shell/overlay.ts +4 -0
  29. package/dist/resources/extensions/bg-shell/process-manager.ts +45 -0
  30. package/dist/resources/extensions/bg-shell/types.ts +21 -1
  31. package/dist/resources/extensions/bg-shell/utilities.ts +4 -16
  32. package/dist/resources/extensions/browser-tools/capture.ts +34 -2
  33. package/dist/resources/extensions/browser-tools/lifecycle.ts +5 -5
  34. package/dist/resources/extensions/browser-tools/settle.ts +1 -1
  35. package/dist/resources/extensions/browser-tools/state.ts +5 -5
  36. package/dist/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +1 -1
  37. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +3 -3
  38. package/dist/resources/extensions/browser-tools/tools/assertions.ts +1 -1
  39. package/dist/resources/extensions/browser-tools/tools/device.ts +1 -1
  40. package/dist/resources/extensions/browser-tools/tools/extract.ts +1 -1
  41. package/dist/resources/extensions/browser-tools/tools/navigation.ts +6 -6
  42. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +1 -1
  43. package/dist/resources/extensions/browser-tools/tools/pages.ts +1 -1
  44. package/dist/resources/extensions/browser-tools/tools/screenshot.ts +28 -10
  45. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +1 -1
  46. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +1 -1
  47. package/dist/resources/extensions/browser-tools/utils.ts +5 -5
  48. package/dist/resources/extensions/get-secrets-from-user.ts +1 -1
  49. package/dist/resources/extensions/google-search/index.ts +21 -8
  50. package/dist/resources/extensions/gsd/activity-log.ts +2 -1
  51. package/dist/resources/extensions/gsd/atomic-write.ts +35 -0
  52. package/dist/resources/extensions/gsd/auto/session.ts +236 -0
  53. package/dist/resources/extensions/gsd/auto-budget.ts +32 -0
  54. package/dist/resources/extensions/gsd/auto-dashboard.ts +147 -13
  55. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  56. package/dist/resources/extensions/gsd/auto-dispatch.ts +23 -10
  57. package/dist/resources/extensions/gsd/auto-idempotency.ts +150 -0
  58. package/dist/resources/extensions/gsd/auto-model-selection.ts +179 -0
  59. package/dist/resources/extensions/gsd/auto-observability.ts +74 -0
  60. package/dist/resources/extensions/gsd/auto-post-unit.ts +591 -0
  61. package/dist/resources/extensions/gsd/auto-prompts.ts +116 -23
  62. package/dist/resources/extensions/gsd/auto-recovery.ts +20 -9
  63. package/dist/resources/extensions/gsd/auto-start.ts +500 -0
  64. package/dist/resources/extensions/gsd/auto-stuck-detection.ts +220 -0
  65. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  66. package/dist/resources/extensions/gsd/auto-timers.ts +223 -0
  67. package/dist/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  68. package/dist/resources/extensions/gsd/auto-unit-closeout.ts +48 -0
  69. package/dist/resources/extensions/gsd/auto-verification.ts +195 -0
  70. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +184 -0
  71. package/dist/resources/extensions/gsd/auto-worktree.ts +83 -67
  72. package/dist/resources/extensions/gsd/auto.ts +825 -3069
  73. package/dist/resources/extensions/gsd/commands-config.ts +102 -0
  74. package/dist/resources/extensions/gsd/commands-handlers.ts +402 -0
  75. package/dist/resources/extensions/gsd/commands-inspect.ts +90 -0
  76. package/dist/resources/extensions/gsd/commands-logs.ts +537 -0
  77. package/dist/resources/extensions/gsd/commands-maintenance.ts +206 -0
  78. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +747 -0
  79. package/dist/resources/extensions/gsd/commands.ts +380 -1433
  80. package/dist/resources/extensions/gsd/constants.ts +21 -0
  81. package/dist/resources/extensions/gsd/context-budget.ts +25 -2
  82. package/dist/resources/extensions/gsd/crash-recovery.ts +4 -2
  83. package/dist/resources/extensions/gsd/dashboard-overlay.ts +58 -74
  84. package/dist/resources/extensions/gsd/db-writer.ts +21 -2
  85. package/dist/resources/extensions/gsd/detection.ts +469 -0
  86. package/dist/resources/extensions/gsd/diff-context.ts +2 -1
  87. package/dist/resources/extensions/gsd/dispatch-guard.ts +4 -0
  88. package/dist/resources/extensions/gsd/docs/preferences-reference.md +83 -0
  89. package/dist/resources/extensions/gsd/doctor-checks.ts +564 -0
  90. package/dist/resources/extensions/gsd/doctor-format.ts +78 -0
  91. package/dist/resources/extensions/gsd/doctor-proactive.ts +9 -4
  92. package/dist/resources/extensions/gsd/doctor-types.ts +72 -0
  93. package/dist/resources/extensions/gsd/doctor.ts +64 -701
  94. package/dist/resources/extensions/gsd/errors.ts +0 -2
  95. package/dist/resources/extensions/gsd/export-html.ts +1357 -0
  96. package/dist/resources/extensions/gsd/export.ts +141 -4
  97. package/dist/resources/extensions/gsd/files.ts +8 -126
  98. package/dist/resources/extensions/gsd/forensics.ts +2 -12
  99. package/dist/resources/extensions/gsd/git-constants.ts +11 -0
  100. package/dist/resources/extensions/gsd/git-service.ts +19 -9
  101. package/dist/resources/extensions/gsd/gitignore.ts +4 -1
  102. package/dist/resources/extensions/gsd/gsd-db.ts +26 -6
  103. package/dist/resources/extensions/gsd/guided-flow-queue.ts +451 -0
  104. package/dist/resources/extensions/gsd/guided-flow.ts +252 -516
  105. package/dist/resources/extensions/gsd/history.ts +2 -20
  106. package/dist/resources/extensions/gsd/index.ts +260 -46
  107. package/dist/resources/extensions/gsd/init-wizard.ts +615 -0
  108. package/dist/resources/extensions/gsd/jsonl-utils.ts +21 -0
  109. package/dist/resources/extensions/gsd/key-manager.ts +995 -0
  110. package/dist/resources/extensions/gsd/metrics.ts +32 -5
  111. package/dist/resources/extensions/gsd/migrate/command.ts +1 -1
  112. package/dist/resources/extensions/gsd/migrate/parsers.ts +10 -95
  113. package/dist/resources/extensions/gsd/milestone-actions.ts +126 -0
  114. package/dist/resources/extensions/gsd/milestone-ids.ts +95 -0
  115. package/dist/resources/extensions/gsd/native-git-bridge.ts +35 -12
  116. package/dist/resources/extensions/gsd/parallel-eligibility.ts +3 -3
  117. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  118. package/dist/resources/extensions/gsd/paths.ts +1 -3
  119. package/dist/resources/extensions/gsd/plugin-importer.ts +3 -2
  120. package/dist/resources/extensions/gsd/preferences-hooks.ts +10 -0
  121. package/dist/resources/extensions/gsd/preferences-models.ts +323 -0
  122. package/dist/resources/extensions/gsd/preferences-skills.ts +169 -0
  123. package/dist/resources/extensions/gsd/preferences-types.ts +223 -0
  124. package/dist/resources/extensions/gsd/preferences-validation.ts +597 -0
  125. package/dist/resources/extensions/gsd/preferences.ts +219 -1286
  126. package/dist/resources/extensions/gsd/prompt-cache-optimizer.ts +213 -0
  127. package/dist/resources/extensions/gsd/prompt-compressor.ts +508 -0
  128. package/dist/resources/extensions/gsd/prompt-loader.ts +4 -2
  129. package/dist/resources/extensions/gsd/prompt-ordering.ts +200 -0
  130. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
  131. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  132. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -4
  133. package/dist/resources/extensions/gsd/prompts/discuss.md +13 -5
  134. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -2
  135. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  136. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +0 -1
  137. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -2
  138. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -2
  139. package/dist/resources/extensions/gsd/prompts/queue.md +30 -0
  140. package/dist/resources/extensions/gsd/prompts/quick-task.md +0 -6
  141. package/dist/resources/extensions/gsd/prompts/replan-slice.md +0 -1
  142. package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +0 -1
  143. package/dist/resources/extensions/gsd/prompts/system.md +6 -3
  144. package/dist/resources/extensions/gsd/provider-error-pause.ts +59 -10
  145. package/dist/resources/extensions/gsd/queue-order.ts +1 -1
  146. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +15 -2
  147. package/dist/resources/extensions/gsd/quick.ts +18 -15
  148. package/dist/resources/extensions/gsd/reports.ts +504 -0
  149. package/dist/resources/extensions/gsd/roadmap-slices.ts +1 -1
  150. package/dist/resources/extensions/gsd/safe-fs.ts +47 -0
  151. package/dist/resources/extensions/gsd/semantic-chunker.ts +336 -0
  152. package/dist/resources/extensions/gsd/session-forensics.ts +8 -23
  153. package/dist/resources/extensions/gsd/session-lock.ts +284 -0
  154. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +242 -0
  155. package/dist/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +83 -0
  156. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +64 -0
  157. package/dist/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +176 -0
  158. package/dist/resources/extensions/gsd/state.ts +84 -2
  159. package/dist/resources/extensions/gsd/structured-data-formatter.ts +144 -0
  160. package/dist/resources/extensions/gsd/summary-distiller.ts +258 -0
  161. package/dist/resources/extensions/gsd/templates/preferences.md +34 -0
  162. package/dist/resources/extensions/gsd/tests/activity-log.test.ts +213 -0
  163. package/dist/resources/extensions/gsd/tests/agent-end-retry.test.ts +107 -0
  164. package/dist/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +197 -0
  165. package/dist/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  166. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +33 -39
  167. package/dist/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +108 -2
  168. package/dist/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +257 -0
  169. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -0
  170. package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  171. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +69 -0
  172. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  173. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  174. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  175. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  176. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  177. package/dist/resources/extensions/gsd/tests/detection.test.ts +398 -0
  178. package/dist/resources/extensions/gsd/tests/discuss-prompt.test.ts +12 -24
  179. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +118 -94
  180. package/dist/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  181. package/dist/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +126 -0
  182. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +7 -3
  183. package/dist/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +75 -0
  184. package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +17 -55
  185. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  186. package/dist/resources/extensions/gsd/tests/export-html-all.test.ts +105 -0
  187. package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +375 -0
  188. package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +122 -0
  189. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +3 -0
  190. package/dist/resources/extensions/gsd/tests/headless-answers.test.ts +340 -0
  191. package/dist/resources/extensions/gsd/tests/headless-query.test.ts +162 -0
  192. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +24 -82
  193. package/dist/resources/extensions/gsd/tests/init-wizard.test.ts +197 -0
  194. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  195. package/dist/resources/extensions/gsd/tests/key-manager.test.ts +414 -0
  196. package/dist/resources/extensions/gsd/tests/metrics.test.ts +173 -305
  197. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +4 -1
  198. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +59 -1
  199. package/dist/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  200. package/dist/resources/extensions/gsd/tests/next-milestone-id.test.ts +18 -61
  201. package/dist/resources/extensions/gsd/tests/none-mode-gates.test.ts +17 -8
  202. package/dist/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  203. package/dist/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  204. package/dist/resources/extensions/gsd/tests/parallel-merge.test.ts +468 -0
  205. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  206. package/dist/resources/extensions/gsd/tests/park-edge-cases.test.ts +276 -0
  207. package/dist/resources/extensions/gsd/tests/park-milestone.test.ts +401 -0
  208. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +47 -0
  209. package/dist/resources/extensions/gsd/tests/preferences.test.ts +284 -0
  210. package/dist/resources/extensions/gsd/tests/prompt-cache-optimizer.test.ts +314 -0
  211. package/dist/resources/extensions/gsd/tests/prompt-compressor.test.ts +529 -0
  212. package/dist/resources/extensions/gsd/tests/prompt-ordering.test.ts +296 -0
  213. package/dist/resources/extensions/gsd/tests/provider-errors.test.ts +338 -0
  214. package/dist/resources/extensions/gsd/tests/reassess-detection.test.ts +154 -0
  215. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +1 -1
  216. package/dist/resources/extensions/gsd/tests/remote-status.test.ts +2 -2
  217. package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  218. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -60
  219. package/dist/resources/extensions/gsd/tests/semantic-chunker.test.ts +426 -0
  220. package/dist/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  221. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +3 -0
  222. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +8 -5
  223. package/dist/resources/extensions/gsd/tests/structured-data-formatter.test.ts +365 -0
  224. package/dist/resources/extensions/gsd/tests/summary-distiller.test.ts +323 -0
  225. package/dist/resources/extensions/gsd/tests/token-counter.test.ts +129 -0
  226. package/dist/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +1272 -0
  227. package/dist/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +164 -0
  228. package/dist/resources/extensions/gsd/tests/token-profile.test.ts +8 -1
  229. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +75 -79
  230. package/dist/resources/extensions/gsd/tests/update-command.test.ts +67 -0
  231. package/dist/resources/extensions/gsd/tests/validate-directory.test.ts +222 -0
  232. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  233. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +115 -1
  234. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  235. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  236. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +2 -1
  237. package/dist/resources/extensions/gsd/tests/workspace-index.test.ts +24 -61
  238. package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +5 -2
  239. package/dist/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  240. package/dist/resources/extensions/gsd/tests/write-gate.test.ts +132 -43
  241. package/dist/resources/extensions/gsd/token-counter.ts +20 -0
  242. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  243. package/dist/resources/extensions/gsd/types.ts +4 -1
  244. package/dist/resources/extensions/gsd/validate-directory.ts +164 -0
  245. package/dist/resources/extensions/gsd/verification-evidence.ts +7 -4
  246. package/dist/resources/extensions/gsd/verification-gate.ts +70 -5
  247. package/dist/resources/extensions/gsd/visualizer-data.ts +26 -4
  248. package/dist/resources/extensions/gsd/visualizer-overlay.ts +35 -22
  249. package/dist/resources/extensions/gsd/visualizer-views.ts +14 -66
  250. package/dist/resources/extensions/gsd/worktree-command.ts +4 -51
  251. package/dist/resources/extensions/gsd/worktree-manager.ts +7 -9
  252. package/dist/resources/extensions/gsd/worktree.ts +41 -1
  253. package/dist/resources/extensions/mcporter/index.ts +27 -14
  254. package/dist/resources/extensions/remote-questions/manager.ts +6 -24
  255. package/dist/resources/extensions/remote-questions/mod.ts +16 -0
  256. package/dist/resources/extensions/remote-questions/notify.ts +91 -0
  257. package/dist/resources/extensions/remote-questions/remote-command.ts +1 -1
  258. package/dist/resources/extensions/remote-questions/store.ts +5 -1
  259. package/dist/resources/extensions/remote-questions/types.ts +26 -3
  260. package/dist/resources/extensions/search-the-web/native-search.ts +7 -0
  261. package/dist/resources/extensions/search-the-web/provider.ts +15 -3
  262. package/dist/resources/extensions/search-the-web/tool-llm-context.ts +1 -13
  263. package/dist/resources/extensions/search-the-web/tool-search.ts +27 -13
  264. package/dist/resources/extensions/shared/format-utils.ts +138 -0
  265. package/dist/resources/extensions/shared/frontmatter.ts +117 -0
  266. package/dist/resources/extensions/shared/mod.ts +30 -0
  267. package/dist/resources/extensions/shared/sanitize.ts +19 -0
  268. package/dist/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  269. package/dist/resources/extensions/slash-commands/create-extension.ts +1 -1
  270. package/dist/resources/extensions/slash-commands/create-slash-command.ts +1 -1
  271. package/dist/resources/extensions/subagent/index.ts +47 -3
  272. package/dist/resources/extensions/subagent/isolation.ts +9 -6
  273. package/dist/resources/extensions/ttsr/index.ts +5 -0
  274. package/dist/resources/extensions/ttsr/rule-loader.ts +4 -51
  275. package/dist/resources/extensions/universal-config/discovery.ts +37 -15
  276. package/dist/resources/extensions/voice/index.ts +1 -1
  277. package/dist/resources/skills/accessibility/SKILL.md +522 -0
  278. package/dist/resources/skills/accessibility/references/WCAG.md +162 -0
  279. package/dist/resources/skills/agent-browser/SKILL.md +517 -0
  280. package/dist/resources/skills/agent-browser/references/authentication.md +202 -0
  281. package/dist/resources/skills/agent-browser/references/commands.md +263 -0
  282. package/dist/resources/skills/agent-browser/references/profiling.md +120 -0
  283. package/dist/resources/skills/agent-browser/references/proxy-support.md +194 -0
  284. package/dist/resources/skills/agent-browser/references/session-management.md +193 -0
  285. package/dist/resources/skills/agent-browser/references/snapshot-refs.md +194 -0
  286. package/dist/resources/skills/agent-browser/references/video-recording.md +173 -0
  287. package/dist/resources/skills/agent-browser/templates/authenticated-session.sh +105 -0
  288. package/dist/resources/skills/agent-browser/templates/capture-workflow.sh +69 -0
  289. package/dist/resources/skills/agent-browser/templates/form-automation.sh +62 -0
  290. package/dist/resources/skills/best-practices/SKILL.md +583 -0
  291. package/dist/resources/skills/code-optimizer/SKILL.md +160 -0
  292. package/dist/resources/skills/code-optimizer/references/algorithmic-complexity.md +66 -0
  293. package/dist/resources/skills/code-optimizer/references/build-compilation.md +90 -0
  294. package/dist/resources/skills/code-optimizer/references/bundle-dependencies.md +82 -0
  295. package/dist/resources/skills/code-optimizer/references/caching-memoization.md +76 -0
  296. package/dist/resources/skills/code-optimizer/references/concurrency-async.md +80 -0
  297. package/dist/resources/skills/code-optimizer/references/config-infra.md +71 -0
  298. package/dist/resources/skills/code-optimizer/references/data-structures.md +80 -0
  299. package/dist/resources/skills/code-optimizer/references/database-queries.md +76 -0
  300. package/dist/resources/skills/code-optimizer/references/dead-code-redundancy.md +84 -0
  301. package/dist/resources/skills/code-optimizer/references/error-resilience.md +80 -0
  302. package/dist/resources/skills/code-optimizer/references/io-network.md +89 -0
  303. package/dist/resources/skills/code-optimizer/references/logging-observability.md +64 -0
  304. package/dist/resources/skills/code-optimizer/references/memory-resources.md +66 -0
  305. package/dist/resources/skills/code-optimizer/references/rendering-ui.md +90 -0
  306. package/dist/resources/skills/code-optimizer/references/security-performance.md +68 -0
  307. package/dist/resources/skills/core-web-vitals/SKILL.md +441 -0
  308. package/dist/resources/skills/core-web-vitals/references/LCP.md +208 -0
  309. package/dist/resources/skills/make-interfaces-feel-better/SKILL.md +122 -0
  310. package/dist/resources/skills/make-interfaces-feel-better/animations.md +379 -0
  311. package/dist/resources/skills/make-interfaces-feel-better/performance.md +88 -0
  312. package/dist/resources/skills/make-interfaces-feel-better/surfaces.md +247 -0
  313. package/dist/resources/skills/make-interfaces-feel-better/typography.md +123 -0
  314. package/dist/resources/skills/react-best-practices/README.md +123 -0
  315. package/dist/resources/skills/react-best-practices/SKILL.md +136 -0
  316. package/dist/resources/skills/react-best-practices/metadata.json +15 -0
  317. package/dist/resources/skills/react-best-practices/rules/_sections.md +46 -0
  318. package/dist/resources/skills/react-best-practices/rules/_template.md +28 -0
  319. package/dist/resources/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  320. package/dist/resources/skills/react-best-practices/rules/advanced-init-once.md +42 -0
  321. package/dist/resources/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
  322. package/dist/resources/skills/react-best-practices/rules/async-api-routes.md +38 -0
  323. package/dist/resources/skills/react-best-practices/rules/async-defer-await.md +80 -0
  324. package/dist/resources/skills/react-best-practices/rules/async-dependencies.md +51 -0
  325. package/dist/resources/skills/react-best-practices/rules/async-parallel.md +28 -0
  326. package/dist/resources/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
  327. package/dist/resources/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
  328. package/dist/resources/skills/react-best-practices/rules/bundle-conditional.md +31 -0
  329. package/dist/resources/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
  330. package/dist/resources/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  331. package/dist/resources/skills/react-best-practices/rules/bundle-preload.md +50 -0
  332. package/dist/resources/skills/react-best-practices/rules/client-event-listeners.md +74 -0
  333. package/dist/resources/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
  334. package/dist/resources/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
  335. package/dist/resources/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
  336. package/dist/resources/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
  337. package/dist/resources/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
  338. package/dist/resources/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
  339. package/dist/resources/skills/react-best-practices/rules/js-cache-storage.md +70 -0
  340. package/dist/resources/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
  341. package/dist/resources/skills/react-best-practices/rules/js-early-exit.md +50 -0
  342. package/dist/resources/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
  343. package/dist/resources/skills/react-best-practices/rules/js-index-maps.md +37 -0
  344. package/dist/resources/skills/react-best-practices/rules/js-length-check-first.md +49 -0
  345. package/dist/resources/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
  346. package/dist/resources/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
  347. package/dist/resources/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
  348. package/dist/resources/skills/react-best-practices/rules/rendering-activity.md +26 -0
  349. package/dist/resources/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  350. package/dist/resources/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
  351. package/dist/resources/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
  352. package/dist/resources/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  353. package/dist/resources/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  354. package/dist/resources/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  355. package/dist/resources/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
  356. package/dist/resources/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  357. package/dist/resources/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
  358. package/dist/resources/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
  359. package/dist/resources/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  360. package/dist/resources/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
  361. package/dist/resources/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
  362. package/dist/resources/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  363. package/dist/resources/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  364. package/dist/resources/skills/react-best-practices/rules/rerender-memo.md +44 -0
  365. package/dist/resources/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  366. package/dist/resources/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  367. package/dist/resources/skills/react-best-practices/rules/rerender-transitions.md +40 -0
  368. package/dist/resources/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  369. package/dist/resources/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
  370. package/dist/resources/skills/react-best-practices/rules/server-auth-actions.md +96 -0
  371. package/dist/resources/skills/react-best-practices/rules/server-cache-lru.md +41 -0
  372. package/dist/resources/skills/react-best-practices/rules/server-cache-react.md +76 -0
  373. package/dist/resources/skills/react-best-practices/rules/server-dedup-props.md +65 -0
  374. package/dist/resources/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
  375. package/dist/resources/skills/react-best-practices/rules/server-serialization.md +38 -0
  376. package/dist/resources/skills/userinterface-wiki/SKILL.md +253 -0
  377. package/dist/resources/skills/userinterface-wiki/rules/_sections.md +66 -0
  378. package/dist/resources/skills/userinterface-wiki/rules/_template.md +24 -0
  379. package/dist/resources/skills/userinterface-wiki/rules/a11y-reduced-motion-check.md +30 -0
  380. package/dist/resources/skills/userinterface-wiki/rules/a11y-toggle-setting.md +30 -0
  381. package/dist/resources/skills/userinterface-wiki/rules/a11y-visual-equivalent.md +36 -0
  382. package/dist/resources/skills/userinterface-wiki/rules/a11y-volume-control.md +28 -0
  383. package/dist/resources/skills/userinterface-wiki/rules/appropriate-confirmations-only.md +19 -0
  384. package/dist/resources/skills/userinterface-wiki/rules/appropriate-errors-warnings.md +18 -0
  385. package/dist/resources/skills/userinterface-wiki/rules/appropriate-no-decorative.md +21 -0
  386. package/dist/resources/skills/userinterface-wiki/rules/appropriate-no-high-frequency.md +28 -0
  387. package/dist/resources/skills/userinterface-wiki/rules/appropriate-no-punishing.md +27 -0
  388. package/dist/resources/skills/userinterface-wiki/rules/container-callback-ref.md +31 -0
  389. package/dist/resources/skills/userinterface-wiki/rules/container-guard-initial-zero.md +25 -0
  390. package/dist/resources/skills/userinterface-wiki/rules/container-no-excessive-use.md +13 -0
  391. package/dist/resources/skills/userinterface-wiki/rules/container-overflow-hidden.md +25 -0
  392. package/dist/resources/skills/userinterface-wiki/rules/container-transition-delay.md +21 -0
  393. package/dist/resources/skills/userinterface-wiki/rules/container-two-div-pattern.md +35 -0
  394. package/dist/resources/skills/userinterface-wiki/rules/container-use-resize-observer.md +48 -0
  395. package/dist/resources/skills/userinterface-wiki/rules/context-cleanup-nodes.md +25 -0
  396. package/dist/resources/skills/userinterface-wiki/rules/context-resume-suspended.md +28 -0
  397. package/dist/resources/skills/userinterface-wiki/rules/context-reuse-single.md +30 -0
  398. package/dist/resources/skills/userinterface-wiki/rules/design-filter-for-character.md +25 -0
  399. package/dist/resources/skills/userinterface-wiki/rules/design-noise-for-percussion.md +26 -0
  400. package/dist/resources/skills/userinterface-wiki/rules/design-oscillator-for-tonal.md +22 -0
  401. package/dist/resources/skills/userinterface-wiki/rules/duration-max-300ms.md +21 -0
  402. package/dist/resources/skills/userinterface-wiki/rules/duration-press-hover.md +21 -0
  403. package/dist/resources/skills/userinterface-wiki/rules/duration-shorten-before-curve.md +21 -0
  404. package/dist/resources/skills/userinterface-wiki/rules/duration-small-state.md +15 -0
  405. package/dist/resources/skills/userinterface-wiki/rules/easing-entrance-ease-out.md +21 -0
  406. package/dist/resources/skills/userinterface-wiki/rules/easing-exit-ease-in.md +21 -0
  407. package/dist/resources/skills/userinterface-wiki/rules/easing-for-state-change.md +27 -0
  408. package/dist/resources/skills/userinterface-wiki/rules/easing-linear-only-progress.md +21 -0
  409. package/dist/resources/skills/userinterface-wiki/rules/easing-natural-decay.md +22 -0
  410. package/dist/resources/skills/userinterface-wiki/rules/easing-no-linear-motion.md +22 -0
  411. package/dist/resources/skills/userinterface-wiki/rules/easing-transition-ease-in-out.md +15 -0
  412. package/dist/resources/skills/userinterface-wiki/rules/envelope-exponential-decay.md +21 -0
  413. package/dist/resources/skills/userinterface-wiki/rules/envelope-no-zero-target.md +21 -0
  414. package/dist/resources/skills/userinterface-wiki/rules/envelope-set-initial-value.md +22 -0
  415. package/dist/resources/skills/userinterface-wiki/rules/exit-key-required.md +29 -0
  416. package/dist/resources/skills/userinterface-wiki/rules/exit-matches-initial.md +29 -0
  417. package/dist/resources/skills/userinterface-wiki/rules/exit-prop-required.md +33 -0
  418. package/dist/resources/skills/userinterface-wiki/rules/exit-requires-wrapper.md +27 -0
  419. package/dist/resources/skills/userinterface-wiki/rules/impl-default-subtle.md +21 -0
  420. package/dist/resources/skills/userinterface-wiki/rules/impl-preload-audio.md +34 -0
  421. package/dist/resources/skills/userinterface-wiki/rules/impl-reset-current-time.md +26 -0
  422. package/dist/resources/skills/userinterface-wiki/rules/mode-pop-layout-for-lists.md +25 -0
  423. package/dist/resources/skills/userinterface-wiki/rules/mode-sync-layout-conflict.md +29 -0
  424. package/dist/resources/skills/userinterface-wiki/rules/mode-wait-doubles-duration.md +25 -0
  425. package/dist/resources/skills/userinterface-wiki/rules/morphing-aria-hidden.md +21 -0
  426. package/dist/resources/skills/userinterface-wiki/rules/morphing-consistent-viewbox.md +23 -0
  427. package/dist/resources/skills/userinterface-wiki/rules/morphing-group-variants.md +33 -0
  428. package/dist/resources/skills/userinterface-wiki/rules/morphing-jump-non-grouped.md +29 -0
  429. package/dist/resources/skills/userinterface-wiki/rules/morphing-reduced-motion.md +28 -0
  430. package/dist/resources/skills/userinterface-wiki/rules/morphing-spring-rotation.md +23 -0
  431. package/dist/resources/skills/userinterface-wiki/rules/morphing-strokelinecap-round.md +21 -0
  432. package/dist/resources/skills/userinterface-wiki/rules/morphing-three-lines.md +32 -0
  433. package/dist/resources/skills/userinterface-wiki/rules/morphing-use-collapsed.md +33 -0
  434. package/dist/resources/skills/userinterface-wiki/rules/native-backdrop-styling.md +27 -0
  435. package/dist/resources/skills/userinterface-wiki/rules/native-placeholder-styling.md +27 -0
  436. package/dist/resources/skills/userinterface-wiki/rules/native-selection-styling.md +18 -0
  437. package/dist/resources/skills/userinterface-wiki/rules/nested-consistent-timing.md +25 -0
  438. package/dist/resources/skills/userinterface-wiki/rules/nested-propagate-required.md +41 -0
  439. package/dist/resources/skills/userinterface-wiki/rules/none-context-menu-entrance.md +25 -0
  440. package/dist/resources/skills/userinterface-wiki/rules/none-high-frequency.md +29 -0
  441. package/dist/resources/skills/userinterface-wiki/rules/none-keyboard-navigation.md +32 -0
  442. package/dist/resources/skills/userinterface-wiki/rules/param-click-duration.md +21 -0
  443. package/dist/resources/skills/userinterface-wiki/rules/param-filter-frequency-range.md +21 -0
  444. package/dist/resources/skills/userinterface-wiki/rules/param-q-value-range.md +21 -0
  445. package/dist/resources/skills/userinterface-wiki/rules/param-reasonable-gain.md +21 -0
  446. package/dist/resources/skills/userinterface-wiki/rules/physics-active-state.md +23 -0
  447. package/dist/resources/skills/userinterface-wiki/rules/physics-no-excessive-stagger.md +22 -0
  448. package/dist/resources/skills/userinterface-wiki/rules/physics-spring-for-overshoot.md +23 -0
  449. package/dist/resources/skills/userinterface-wiki/rules/physics-subtle-deformation.md +22 -0
  450. package/dist/resources/skills/userinterface-wiki/rules/prefetch-hit-slop.md +27 -0
  451. package/dist/resources/skills/userinterface-wiki/rules/prefetch-keyboard-tab.md +19 -0
  452. package/dist/resources/skills/userinterface-wiki/rules/prefetch-not-everything.md +22 -0
  453. package/dist/resources/skills/userinterface-wiki/rules/prefetch-touch-fallback.md +34 -0
  454. package/dist/resources/skills/userinterface-wiki/rules/prefetch-trajectory-over-hover.md +32 -0
  455. package/dist/resources/skills/userinterface-wiki/rules/prefetch-use-selectively.md +13 -0
  456. package/dist/resources/skills/userinterface-wiki/rules/presence-disable-interactions.md +31 -0
  457. package/dist/resources/skills/userinterface-wiki/rules/presence-hook-in-child.md +31 -0
  458. package/dist/resources/skills/userinterface-wiki/rules/presence-safe-to-remove.md +37 -0
  459. package/dist/resources/skills/userinterface-wiki/rules/pseudo-content-required.md +28 -0
  460. package/dist/resources/skills/userinterface-wiki/rules/pseudo-first-line-styling.md +27 -0
  461. package/dist/resources/skills/userinterface-wiki/rules/pseudo-hit-target-expansion.md +31 -0
  462. package/dist/resources/skills/userinterface-wiki/rules/pseudo-marker-styling.md +28 -0
  463. package/dist/resources/skills/userinterface-wiki/rules/pseudo-over-dom-node.md +32 -0
  464. package/dist/resources/skills/userinterface-wiki/rules/pseudo-position-relative-parent.md +33 -0
  465. package/dist/resources/skills/userinterface-wiki/rules/pseudo-z-index-layering.md +37 -0
  466. package/dist/resources/skills/userinterface-wiki/rules/spring-for-gestures.md +27 -0
  467. package/dist/resources/skills/userinterface-wiki/rules/spring-for-interruptible.md +27 -0
  468. package/dist/resources/skills/userinterface-wiki/rules/spring-params-balanced.md +29 -0
  469. package/dist/resources/skills/userinterface-wiki/rules/spring-preserves-velocity.md +28 -0
  470. package/dist/resources/skills/userinterface-wiki/rules/staging-dim-background.md +22 -0
  471. package/dist/resources/skills/userinterface-wiki/rules/staging-one-focal-point.md +24 -0
  472. package/dist/resources/skills/userinterface-wiki/rules/staging-z-index-hierarchy.md +22 -0
  473. package/dist/resources/skills/userinterface-wiki/rules/timing-consistent.md +24 -0
  474. package/dist/resources/skills/userinterface-wiki/rules/timing-no-entrance-context-menu.md +22 -0
  475. package/dist/resources/skills/userinterface-wiki/rules/timing-under-300ms.md +22 -0
  476. package/dist/resources/skills/userinterface-wiki/rules/transition-name-cleanup.md +28 -0
  477. package/dist/resources/skills/userinterface-wiki/rules/transition-name-required.md +27 -0
  478. package/dist/resources/skills/userinterface-wiki/rules/transition-name-unique.md +24 -0
  479. package/dist/resources/skills/userinterface-wiki/rules/transition-over-js-library.md +32 -0
  480. package/dist/resources/skills/userinterface-wiki/rules/transition-style-pseudo-elements.md +24 -0
  481. package/dist/resources/skills/userinterface-wiki/rules/type-antialiased-on-retina.md +18 -0
  482. package/dist/resources/skills/userinterface-wiki/rules/type-disambiguation-stylistic-set.md +15 -0
  483. package/dist/resources/skills/userinterface-wiki/rules/type-font-display-swap.md +28 -0
  484. package/dist/resources/skills/userinterface-wiki/rules/type-justify-with-hyphens.md +24 -0
  485. package/dist/resources/skills/userinterface-wiki/rules/type-letter-spacing-uppercase.md +28 -0
  486. package/dist/resources/skills/userinterface-wiki/rules/type-no-font-synthesis.md +18 -0
  487. package/dist/resources/skills/userinterface-wiki/rules/type-oldstyle-nums-for-prose.md +21 -0
  488. package/dist/resources/skills/userinterface-wiki/rules/type-opentype-contextual-alternates.md +15 -0
  489. package/dist/resources/skills/userinterface-wiki/rules/type-optical-sizing-auto.md +25 -0
  490. package/dist/resources/skills/userinterface-wiki/rules/type-proper-fractions.md +15 -0
  491. package/dist/resources/skills/userinterface-wiki/rules/type-slashed-zero.md +17 -0
  492. package/dist/resources/skills/userinterface-wiki/rules/type-tabular-nums-for-data.md +21 -0
  493. package/dist/resources/skills/userinterface-wiki/rules/type-text-wrap-balance-headings.md +21 -0
  494. package/dist/resources/skills/userinterface-wiki/rules/type-text-wrap-pretty.md +16 -0
  495. package/dist/resources/skills/userinterface-wiki/rules/type-underline-offset.md +25 -0
  496. package/dist/resources/skills/userinterface-wiki/rules/type-variable-weight-continuous.md +23 -0
  497. package/dist/resources/skills/userinterface-wiki/rules/ux-aesthetic-usability.md +32 -0
  498. package/dist/resources/skills/userinterface-wiki/rules/ux-cognitive-load-reduce.md +49 -0
  499. package/dist/resources/skills/userinterface-wiki/rules/ux-common-region-boundaries.md +50 -0
  500. package/dist/resources/skills/userinterface-wiki/rules/ux-doherty-perceived-speed.md +29 -0
  501. package/dist/resources/skills/userinterface-wiki/rules/ux-doherty-under-400ms.md +30 -0
  502. package/dist/resources/skills/userinterface-wiki/rules/ux-fitts-hit-area.md +32 -0
  503. package/dist/resources/skills/userinterface-wiki/rules/ux-fitts-target-size.md +31 -0
  504. package/dist/resources/skills/userinterface-wiki/rules/ux-goal-gradient-progress.md +33 -0
  505. package/dist/resources/skills/userinterface-wiki/rules/ux-hicks-minimize-choices.md +45 -0
  506. package/dist/resources/skills/userinterface-wiki/rules/ux-jakobs-familiar-patterns.md +37 -0
  507. package/dist/resources/skills/userinterface-wiki/rules/ux-millers-chunking.md +23 -0
  508. package/dist/resources/skills/userinterface-wiki/rules/ux-pareto-prioritize-features.md +36 -0
  509. package/dist/resources/skills/userinterface-wiki/rules/ux-peak-end-finish-strong.md +35 -0
  510. package/dist/resources/skills/userinterface-wiki/rules/ux-postels-accept-messy-input.md +45 -0
  511. package/dist/resources/skills/userinterface-wiki/rules/ux-pragnanz-simplify.md +33 -0
  512. package/dist/resources/skills/userinterface-wiki/rules/ux-progressive-disclosure.md +41 -0
  513. package/dist/resources/skills/userinterface-wiki/rules/ux-proximity-grouping.md +38 -0
  514. package/dist/resources/skills/userinterface-wiki/rules/ux-serial-position.md +31 -0
  515. package/dist/resources/skills/userinterface-wiki/rules/ux-similarity-consistency.md +35 -0
  516. package/dist/resources/skills/userinterface-wiki/rules/ux-teslers-complexity.md +28 -0
  517. package/dist/resources/skills/userinterface-wiki/rules/ux-uniform-connectedness.md +43 -0
  518. package/dist/resources/skills/userinterface-wiki/rules/ux-von-restorff-emphasis.md +29 -0
  519. package/dist/resources/skills/userinterface-wiki/rules/ux-zeigarnik-show-incomplete.md +36 -0
  520. package/dist/resources/skills/userinterface-wiki/rules/visual-animate-shadow-pseudo.md +49 -0
  521. package/dist/resources/skills/userinterface-wiki/rules/visual-border-alpha-colors.md +25 -0
  522. package/dist/resources/skills/userinterface-wiki/rules/visual-button-shadow-anatomy.md +49 -0
  523. package/dist/resources/skills/userinterface-wiki/rules/visual-concentric-radius.md +40 -0
  524. package/dist/resources/skills/userinterface-wiki/rules/visual-consistent-spacing-scale.md +35 -0
  525. package/dist/resources/skills/userinterface-wiki/rules/visual-layered-shadows.md +30 -0
  526. package/dist/resources/skills/userinterface-wiki/rules/visual-no-pure-black-shadow.md +25 -0
  527. package/dist/resources/skills/userinterface-wiki/rules/visual-shadow-direction.md +25 -0
  528. package/dist/resources/skills/userinterface-wiki/rules/visual-shadow-matches-elevation.md +23 -0
  529. package/dist/resources/skills/userinterface-wiki/rules/weight-duration-matches-action.md +29 -0
  530. package/dist/resources/skills/userinterface-wiki/rules/weight-match-action.md +32 -0
  531. package/dist/resources/skills/web-design-guidelines/SKILL.md +39 -0
  532. package/dist/resources/skills/web-quality-audit/SKILL.md +170 -0
  533. package/dist/resources/skills/web-quality-audit/scripts/analyze.sh +91 -0
  534. package/dist/update-check.js +1 -1
  535. package/package.json +20 -8
  536. package/packages/native/package.json +28 -0
  537. package/packages/pi-agent-core/package.json +6 -0
  538. package/packages/pi-ai/dist/models.generated.d.ts +43 -11
  539. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  540. package/packages/pi-ai/dist/models.generated.js +34 -26
  541. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  542. package/packages/pi-ai/dist/providers/anthropic.js +3 -2
  543. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  544. package/packages/pi-ai/dist/providers/openai-codex-responses.js +14 -4
  545. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  546. package/packages/pi-ai/dist/providers/openai-completions.js +7 -4
  547. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  548. package/packages/pi-ai/dist/utils/oauth/anthropic.d.ts.map +1 -1
  549. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -0
  550. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  551. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  552. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +5 -1
  553. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  554. package/packages/pi-ai/dist/utils/oauth/google-antigravity.d.ts.map +1 -1
  555. package/packages/pi-ai/dist/utils/oauth/google-antigravity.js +4 -0
  556. package/packages/pi-ai/dist/utils/oauth/google-antigravity.js.map +1 -1
  557. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.d.ts.map +1 -1
  558. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js +6 -0
  559. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js.map +1 -1
  560. package/packages/pi-ai/dist/utils/oauth/index.js +2 -2
  561. package/packages/pi-ai/dist/utils/oauth/index.js.map +1 -1
  562. package/packages/pi-ai/dist/utils/oauth/openai-codex.d.ts.map +1 -1
  563. package/packages/pi-ai/dist/utils/oauth/openai-codex.js +2 -0
  564. package/packages/pi-ai/dist/utils/oauth/openai-codex.js.map +1 -1
  565. package/packages/pi-ai/oauth.d.ts +1 -0
  566. package/packages/pi-ai/oauth.js +1 -0
  567. package/packages/pi-ai/package.json +2 -2
  568. package/packages/pi-ai/src/models.generated.ts +42 -34
  569. package/packages/pi-ai/src/providers/anthropic.ts +3 -2
  570. package/packages/pi-ai/src/providers/openai-codex-responses.ts +15 -4
  571. package/packages/pi-ai/src/providers/openai-completions.ts +7 -4
  572. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -0
  573. package/packages/pi-ai/src/utils/oauth/github-copilot.ts +5 -1
  574. package/packages/pi-ai/src/utils/oauth/google-antigravity.ts +4 -0
  575. package/packages/pi-ai/src/utils/oauth/google-gemini-cli.ts +6 -0
  576. package/packages/pi-ai/src/utils/oauth/index.ts +2 -2
  577. package/packages/pi-ai/src/utils/oauth/openai-codex.ts +2 -0
  578. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  579. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  580. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  581. package/packages/pi-coding-agent/dist/core/auth-storage.js +2 -1
  582. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  583. package/packages/pi-coding-agent/dist/core/bash-executor.d.ts.map +1 -1
  584. package/packages/pi-coding-agent/dist/core/bash-executor.js +23 -1
  585. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  586. package/packages/pi-coding-agent/dist/core/blob-store.d.ts +8 -0
  587. package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -1
  588. package/packages/pi-coding-agent/dist/core/blob-store.js +50 -1
  589. package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -1
  590. package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  591. package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.js +2 -1
  592. package/packages/pi-coding-agent/dist/core/compaction/branch-summarization.js.map +1 -1
  593. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  594. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +3 -2
  595. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  596. package/packages/pi-coding-agent/dist/core/compaction/utils.d.ts.map +1 -1
  597. package/packages/pi-coding-agent/dist/core/compaction/utils.js +2 -2
  598. package/packages/pi-coding-agent/dist/core/compaction/utils.js.map +1 -1
  599. package/packages/pi-coding-agent/dist/core/constants.d.ts +29 -0
  600. package/packages/pi-coding-agent/dist/core/constants.d.ts.map +1 -0
  601. package/packages/pi-coding-agent/dist/core/constants.js +44 -0
  602. package/packages/pi-coding-agent/dist/core/constants.js.map +1 -0
  603. package/packages/pi-coding-agent/dist/core/extensions/loader.js +1 -1
  604. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  605. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  606. package/packages/pi-coding-agent/dist/core/extensions/runner.js +32 -10
  607. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  608. package/packages/pi-coding-agent/dist/core/extensions/runner.test.d.ts +2 -0
  609. package/packages/pi-coding-agent/dist/core/extensions/runner.test.d.ts.map +1 -0
  610. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +77 -0
  611. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -0
  612. package/packages/pi-coding-agent/dist/core/fs-utils.d.ts +7 -0
  613. package/packages/pi-coding-agent/dist/core/fs-utils.d.ts.map +1 -0
  614. package/packages/pi-coding-agent/dist/core/fs-utils.js +12 -0
  615. package/packages/pi-coding-agent/dist/core/fs-utils.js.map +1 -0
  616. package/packages/pi-coding-agent/dist/core/fs-utils.test.d.ts +2 -0
  617. package/packages/pi-coding-agent/dist/core/fs-utils.test.d.ts.map +1 -0
  618. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +67 -0
  619. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -0
  620. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  621. package/packages/pi-coding-agent/dist/core/lsp/client.js +29 -2
  622. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  623. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  624. package/packages/pi-coding-agent/dist/core/lsp/config.js +9 -2
  625. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  626. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  627. package/packages/pi-coding-agent/dist/core/lsp/index.js +30 -4
  628. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  629. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -1
  630. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +13 -5
  631. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -1
  632. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  633. package/packages/pi-coding-agent/dist/core/model-resolver.js +14 -0
  634. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  635. package/packages/pi-coding-agent/dist/core/resolve-config-value.d.ts.map +1 -1
  636. package/packages/pi-coding-agent/dist/core/resolve-config-value.js +12 -4
  637. package/packages/pi-coding-agent/dist/core/resolve-config-value.js.map +1 -1
  638. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +49 -0
  639. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  640. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +7 -0
  641. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  642. package/packages/pi-coding-agent/dist/core/session-manager.js +108 -14
  643. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  644. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  645. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  646. package/packages/pi-coding-agent/dist/core/settings-manager.js +14 -5
  647. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  648. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +2 -2
  649. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  650. package/packages/pi-coding-agent/dist/core/tools/find.d.ts.map +1 -1
  651. package/packages/pi-coding-agent/dist/core/tools/find.js +2 -1
  652. package/packages/pi-coding-agent/dist/core/tools/find.js.map +1 -1
  653. package/packages/pi-coding-agent/dist/core/tools/hashline-edit.js +4 -4
  654. package/packages/pi-coding-agent/dist/core/tools/hashline-edit.js.map +1 -1
  655. package/packages/pi-coding-agent/dist/core/tools/hashline.d.ts +1 -1
  656. package/packages/pi-coding-agent/dist/core/tools/hashline.js +1 -1
  657. package/packages/pi-coding-agent/dist/core/tools/hashline.js.map +1 -1
  658. package/packages/pi-coding-agent/dist/core/tools/hashline.test.js +8 -8
  659. package/packages/pi-coding-agent/dist/core/tools/hashline.test.js.map +1 -1
  660. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  661. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  662. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  663. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -0
  664. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  665. package/packages/pi-coding-agent/dist/core/tools/path-utils.js +1 -1
  666. package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  667. package/packages/pi-coding-agent/dist/core/tools/truncate.d.ts.map +1 -1
  668. package/packages/pi-coding-agent/dist/core/tools/truncate.js +2 -1
  669. package/packages/pi-coding-agent/dist/core/tools/truncate.js.map +1 -1
  670. package/packages/pi-coding-agent/dist/index.d.ts +2 -1
  671. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  672. package/packages/pi-coding-agent/dist/index.js +3 -1
  673. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  674. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  675. package/packages/pi-coding-agent/dist/main.js +6 -0
  676. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  677. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.d.ts +9 -0
  678. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  679. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js +47 -5
  680. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-selector.js.map +1 -1
  681. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -1
  682. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  683. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -1
  684. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  685. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +4 -4
  686. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
  687. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  688. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  689. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
  690. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  691. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  692. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +30 -2
  693. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  694. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  695. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js +11 -0
  696. package/packages/pi-coding-agent/dist/modes/rpc/rpc-client.js.map +1 -1
  697. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  698. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +12 -2
  699. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  700. package/packages/pi-coding-agent/dist/resources/extensions/memory/pipeline.d.ts.map +1 -1
  701. package/packages/pi-coding-agent/dist/resources/extensions/memory/pipeline.js +25 -3
  702. package/packages/pi-coding-agent/dist/resources/extensions/memory/pipeline.js.map +1 -1
  703. package/packages/pi-coding-agent/package.json +0 -4
  704. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  705. package/packages/pi-coding-agent/src/core/auth-storage.ts +2 -1
  706. package/packages/pi-coding-agent/src/core/bash-executor.ts +23 -1
  707. package/packages/pi-coding-agent/src/core/blob-store.ts +46 -1
  708. package/packages/pi-coding-agent/src/core/compaction/branch-summarization.ts +2 -1
  709. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +3 -2
  710. package/packages/pi-coding-agent/src/core/compaction/utils.ts +2 -2
  711. package/packages/pi-coding-agent/src/core/constants.ts +59 -0
  712. package/packages/pi-coding-agent/src/core/extensions/loader.ts +1 -1
  713. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +84 -0
  714. package/packages/pi-coding-agent/src/core/extensions/runner.ts +31 -10
  715. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +66 -0
  716. package/packages/pi-coding-agent/src/core/fs-utils.ts +12 -0
  717. package/packages/pi-coding-agent/src/core/lsp/client.ts +30 -2
  718. package/packages/pi-coding-agent/src/core/lsp/config.ts +9 -2
  719. package/packages/pi-coding-agent/src/core/lsp/index.ts +31 -5
  720. package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +13 -5
  721. package/packages/pi-coding-agent/src/core/model-resolver.ts +14 -0
  722. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +58 -0
  723. package/packages/pi-coding-agent/src/core/resolve-config-value.ts +14 -4
  724. package/packages/pi-coding-agent/src/core/session-manager.ts +113 -18
  725. package/packages/pi-coding-agent/src/core/settings-manager.ts +22 -5
  726. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +2 -2
  727. package/packages/pi-coding-agent/src/core/tools/find.ts +2 -1
  728. package/packages/pi-coding-agent/src/core/tools/hashline-edit.ts +4 -4
  729. package/packages/pi-coding-agent/src/core/tools/hashline.test.ts +8 -8
  730. package/packages/pi-coding-agent/src/core/tools/hashline.ts +1 -1
  731. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -1
  732. package/packages/pi-coding-agent/src/core/tools/path-utils.ts +1 -1
  733. package/packages/pi-coding-agent/src/core/tools/truncate.ts +3 -1
  734. package/packages/pi-coding-agent/src/index.ts +9 -0
  735. package/packages/pi-coding-agent/src/main.ts +7 -0
  736. package/packages/pi-coding-agent/src/modes/interactive/components/extension-selector.ts +49 -5
  737. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -1
  738. package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +4 -4
  739. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
  740. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +32 -1
  741. package/packages/pi-coding-agent/src/modes/rpc/rpc-client.ts +12 -0
  742. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +11 -2
  743. package/packages/pi-coding-agent/src/resources/extensions/memory/pipeline.ts +23 -3
  744. package/packages/pi-tui/dist/__tests__/autocomplete.test.d.ts +2 -0
  745. package/packages/pi-tui/dist/__tests__/autocomplete.test.d.ts.map +1 -0
  746. package/packages/pi-tui/dist/__tests__/autocomplete.test.js +149 -0
  747. package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -0
  748. package/packages/pi-tui/dist/__tests__/fuzzy.test.d.ts +2 -0
  749. package/packages/pi-tui/dist/__tests__/fuzzy.test.d.ts.map +1 -0
  750. package/packages/pi-tui/dist/__tests__/fuzzy.test.js +94 -0
  751. package/packages/pi-tui/dist/__tests__/fuzzy.test.js.map +1 -0
  752. package/packages/pi-tui/dist/autocomplete.d.ts +5 -1
  753. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  754. package/packages/pi-tui/dist/autocomplete.js +6 -2
  755. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  756. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  757. package/packages/pi-tui/dist/components/editor.js +1 -1
  758. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  759. package/packages/pi-tui/package.json +8 -2
  760. package/packages/pi-tui/src/__tests__/autocomplete.test.ts +186 -0
  761. package/packages/pi-tui/src/__tests__/fuzzy.test.ts +112 -0
  762. package/packages/pi-tui/src/autocomplete.ts +8 -1
  763. package/packages/pi-tui/src/components/editor.ts +3 -1
  764. package/src/resources/extensions/ask-user-questions.ts +3 -2
  765. package/src/resources/extensions/bg-shell/bg-shell-command.ts +219 -0
  766. package/src/resources/extensions/bg-shell/bg-shell-lifecycle.ts +400 -0
  767. package/src/resources/extensions/bg-shell/bg-shell-tool.ts +985 -0
  768. package/src/resources/extensions/bg-shell/index.ts +18 -1545
  769. package/src/resources/extensions/bg-shell/overlay.ts +4 -0
  770. package/src/resources/extensions/bg-shell/process-manager.ts +45 -0
  771. package/src/resources/extensions/bg-shell/types.ts +21 -1
  772. package/src/resources/extensions/bg-shell/utilities.ts +4 -16
  773. package/src/resources/extensions/browser-tools/capture.ts +34 -2
  774. package/src/resources/extensions/browser-tools/lifecycle.ts +5 -5
  775. package/src/resources/extensions/browser-tools/settle.ts +1 -1
  776. package/src/resources/extensions/browser-tools/state.ts +5 -5
  777. package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +1 -1
  778. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +3 -3
  779. package/src/resources/extensions/browser-tools/tools/assertions.ts +1 -1
  780. package/src/resources/extensions/browser-tools/tools/device.ts +1 -1
  781. package/src/resources/extensions/browser-tools/tools/extract.ts +1 -1
  782. package/src/resources/extensions/browser-tools/tools/navigation.ts +6 -6
  783. package/src/resources/extensions/browser-tools/tools/network-mock.ts +1 -1
  784. package/src/resources/extensions/browser-tools/tools/pages.ts +1 -1
  785. package/src/resources/extensions/browser-tools/tools/screenshot.ts +28 -10
  786. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +1 -1
  787. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +1 -1
  788. package/src/resources/extensions/browser-tools/utils.ts +5 -5
  789. package/src/resources/extensions/get-secrets-from-user.ts +1 -1
  790. package/src/resources/extensions/google-search/index.ts +21 -8
  791. package/src/resources/extensions/gsd/activity-log.ts +2 -1
  792. package/src/resources/extensions/gsd/atomic-write.ts +35 -0
  793. package/src/resources/extensions/gsd/auto/session.ts +236 -0
  794. package/src/resources/extensions/gsd/auto-budget.ts +32 -0
  795. package/src/resources/extensions/gsd/auto-dashboard.ts +147 -13
  796. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +229 -0
  797. package/src/resources/extensions/gsd/auto-dispatch.ts +23 -10
  798. package/src/resources/extensions/gsd/auto-idempotency.ts +150 -0
  799. package/src/resources/extensions/gsd/auto-model-selection.ts +179 -0
  800. package/src/resources/extensions/gsd/auto-observability.ts +74 -0
  801. package/src/resources/extensions/gsd/auto-post-unit.ts +591 -0
  802. package/src/resources/extensions/gsd/auto-prompts.ts +116 -23
  803. package/src/resources/extensions/gsd/auto-recovery.ts +20 -9
  804. package/src/resources/extensions/gsd/auto-start.ts +500 -0
  805. package/src/resources/extensions/gsd/auto-stuck-detection.ts +220 -0
  806. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +262 -0
  807. package/src/resources/extensions/gsd/auto-timers.ts +223 -0
  808. package/src/resources/extensions/gsd/auto-tool-tracking.ts +54 -0
  809. package/src/resources/extensions/gsd/auto-unit-closeout.ts +48 -0
  810. package/src/resources/extensions/gsd/auto-verification.ts +195 -0
  811. package/src/resources/extensions/gsd/auto-worktree-sync.ts +184 -0
  812. package/src/resources/extensions/gsd/auto-worktree.ts +83 -67
  813. package/src/resources/extensions/gsd/auto.ts +825 -3069
  814. package/src/resources/extensions/gsd/commands-config.ts +102 -0
  815. package/src/resources/extensions/gsd/commands-handlers.ts +402 -0
  816. package/src/resources/extensions/gsd/commands-inspect.ts +90 -0
  817. package/src/resources/extensions/gsd/commands-logs.ts +537 -0
  818. package/src/resources/extensions/gsd/commands-maintenance.ts +206 -0
  819. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +747 -0
  820. package/src/resources/extensions/gsd/commands.ts +380 -1433
  821. package/src/resources/extensions/gsd/constants.ts +21 -0
  822. package/src/resources/extensions/gsd/context-budget.ts +25 -2
  823. package/src/resources/extensions/gsd/crash-recovery.ts +4 -2
  824. package/src/resources/extensions/gsd/dashboard-overlay.ts +58 -74
  825. package/src/resources/extensions/gsd/db-writer.ts +21 -2
  826. package/src/resources/extensions/gsd/detection.ts +469 -0
  827. package/src/resources/extensions/gsd/diff-context.ts +2 -1
  828. package/src/resources/extensions/gsd/dispatch-guard.ts +4 -0
  829. package/src/resources/extensions/gsd/docs/preferences-reference.md +83 -0
  830. package/src/resources/extensions/gsd/doctor-checks.ts +564 -0
  831. package/src/resources/extensions/gsd/doctor-format.ts +78 -0
  832. package/src/resources/extensions/gsd/doctor-proactive.ts +9 -4
  833. package/src/resources/extensions/gsd/doctor-types.ts +72 -0
  834. package/src/resources/extensions/gsd/doctor.ts +64 -701
  835. package/src/resources/extensions/gsd/errors.ts +0 -2
  836. package/src/resources/extensions/gsd/export-html.ts +1357 -0
  837. package/src/resources/extensions/gsd/export.ts +141 -4
  838. package/src/resources/extensions/gsd/files.ts +8 -126
  839. package/src/resources/extensions/gsd/forensics.ts +2 -12
  840. package/src/resources/extensions/gsd/git-constants.ts +11 -0
  841. package/src/resources/extensions/gsd/git-service.ts +19 -9
  842. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  843. package/src/resources/extensions/gsd/gsd-db.ts +26 -6
  844. package/src/resources/extensions/gsd/guided-flow-queue.ts +451 -0
  845. package/src/resources/extensions/gsd/guided-flow.ts +252 -516
  846. package/src/resources/extensions/gsd/history.ts +2 -20
  847. package/src/resources/extensions/gsd/index.ts +260 -46
  848. package/src/resources/extensions/gsd/init-wizard.ts +615 -0
  849. package/src/resources/extensions/gsd/jsonl-utils.ts +21 -0
  850. package/src/resources/extensions/gsd/key-manager.ts +995 -0
  851. package/src/resources/extensions/gsd/metrics.ts +32 -5
  852. package/src/resources/extensions/gsd/migrate/command.ts +1 -1
  853. package/src/resources/extensions/gsd/migrate/parsers.ts +10 -95
  854. package/src/resources/extensions/gsd/milestone-actions.ts +126 -0
  855. package/src/resources/extensions/gsd/milestone-ids.ts +95 -0
  856. package/src/resources/extensions/gsd/native-git-bridge.ts +35 -12
  857. package/src/resources/extensions/gsd/parallel-eligibility.ts +3 -3
  858. package/src/resources/extensions/gsd/parallel-orchestrator.ts +231 -20
  859. package/src/resources/extensions/gsd/paths.ts +1 -3
  860. package/src/resources/extensions/gsd/plugin-importer.ts +3 -2
  861. package/src/resources/extensions/gsd/preferences-hooks.ts +10 -0
  862. package/src/resources/extensions/gsd/preferences-models.ts +323 -0
  863. package/src/resources/extensions/gsd/preferences-skills.ts +169 -0
  864. package/src/resources/extensions/gsd/preferences-types.ts +223 -0
  865. package/src/resources/extensions/gsd/preferences-validation.ts +597 -0
  866. package/src/resources/extensions/gsd/preferences.ts +219 -1286
  867. package/src/resources/extensions/gsd/prompt-cache-optimizer.ts +213 -0
  868. package/src/resources/extensions/gsd/prompt-compressor.ts +508 -0
  869. package/src/resources/extensions/gsd/prompt-loader.ts +4 -2
  870. package/src/resources/extensions/gsd/prompt-ordering.ts +200 -0
  871. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
  872. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  873. package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -4
  874. package/src/resources/extensions/gsd/prompts/discuss.md +13 -5
  875. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -2
  876. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  877. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +0 -1
  878. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -2
  879. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -2
  880. package/src/resources/extensions/gsd/prompts/queue.md +30 -0
  881. package/src/resources/extensions/gsd/prompts/quick-task.md +0 -6
  882. package/src/resources/extensions/gsd/prompts/replan-slice.md +0 -1
  883. package/src/resources/extensions/gsd/prompts/rewrite-docs.md +0 -1
  884. package/src/resources/extensions/gsd/prompts/system.md +6 -3
  885. package/src/resources/extensions/gsd/provider-error-pause.ts +59 -10
  886. package/src/resources/extensions/gsd/queue-order.ts +1 -1
  887. package/src/resources/extensions/gsd/queue-reorder-ui.ts +15 -2
  888. package/src/resources/extensions/gsd/quick.ts +18 -15
  889. package/src/resources/extensions/gsd/reports.ts +504 -0
  890. package/src/resources/extensions/gsd/roadmap-slices.ts +1 -1
  891. package/src/resources/extensions/gsd/safe-fs.ts +47 -0
  892. package/src/resources/extensions/gsd/semantic-chunker.ts +336 -0
  893. package/src/resources/extensions/gsd/session-forensics.ts +8 -23
  894. package/src/resources/extensions/gsd/session-lock.ts +284 -0
  895. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +242 -0
  896. package/src/resources/extensions/gsd/skills/gsd-headless/references/answer-injection.md +83 -0
  897. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +64 -0
  898. package/src/resources/extensions/gsd/skills/gsd-headless/references/multi-session.md +176 -0
  899. package/src/resources/extensions/gsd/state.ts +84 -2
  900. package/src/resources/extensions/gsd/structured-data-formatter.ts +144 -0
  901. package/src/resources/extensions/gsd/summary-distiller.ts +258 -0
  902. package/src/resources/extensions/gsd/templates/preferences.md +34 -0
  903. package/src/resources/extensions/gsd/tests/activity-log.test.ts +213 -0
  904. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +107 -0
  905. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +197 -0
  906. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +13 -0
  907. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +33 -39
  908. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +108 -2
  909. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +257 -0
  910. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -0
  911. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  912. package/src/resources/extensions/gsd/tests/context-budget.test.ts +69 -0
  913. package/src/resources/extensions/gsd/tests/continue-here.test.ts +81 -0
  914. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +5 -0
  915. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +1 -0
  916. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +1 -0
  917. package/src/resources/extensions/gsd/tests/derive-state.test.ts +10 -1
  918. package/src/resources/extensions/gsd/tests/detection.test.ts +398 -0
  919. package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +12 -24
  920. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +118 -94
  921. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +132 -0
  922. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +126 -0
  923. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +7 -3
  924. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +75 -0
  925. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +17 -55
  926. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +14 -0
  927. package/src/resources/extensions/gsd/tests/export-html-all.test.ts +105 -0
  928. package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +375 -0
  929. package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +122 -0
  930. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +3 -0
  931. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +340 -0
  932. package/src/resources/extensions/gsd/tests/headless-query.test.ts +162 -0
  933. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +24 -82
  934. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +197 -0
  935. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +1 -0
  936. package/src/resources/extensions/gsd/tests/key-manager.test.ts +414 -0
  937. package/src/resources/extensions/gsd/tests/metrics.test.ts +173 -305
  938. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +4 -1
  939. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +59 -1
  940. package/src/resources/extensions/gsd/tests/native-has-changes-cache.test.ts +61 -0
  941. package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +18 -61
  942. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +17 -8
  943. package/src/resources/extensions/gsd/tests/parallel-budget-atomicity.test.ts +331 -0
  944. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +298 -0
  945. package/src/resources/extensions/gsd/tests/parallel-merge.test.ts +468 -0
  946. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +39 -10
  947. package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +276 -0
  948. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +401 -0
  949. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +47 -0
  950. package/src/resources/extensions/gsd/tests/preferences.test.ts +284 -0
  951. package/src/resources/extensions/gsd/tests/prompt-cache-optimizer.test.ts +314 -0
  952. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +529 -0
  953. package/src/resources/extensions/gsd/tests/prompt-ordering.test.ts +296 -0
  954. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +338 -0
  955. package/src/resources/extensions/gsd/tests/reassess-detection.test.ts +154 -0
  956. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +1 -1
  957. package/src/resources/extensions/gsd/tests/remote-status.test.ts +2 -2
  958. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +42 -0
  959. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -60
  960. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +426 -0
  961. package/src/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  962. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +3 -0
  963. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +8 -5
  964. package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +365 -0
  965. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +323 -0
  966. package/src/resources/extensions/gsd/tests/token-counter.test.ts +129 -0
  967. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +1272 -0
  968. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +164 -0
  969. package/src/resources/extensions/gsd/tests/token-profile.test.ts +8 -1
  970. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +75 -79
  971. package/src/resources/extensions/gsd/tests/update-command.test.ts +67 -0
  972. package/src/resources/extensions/gsd/tests/validate-directory.test.ts +222 -0
  973. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  974. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +115 -1
  975. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +1 -1
  976. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +44 -10
  977. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +2 -1
  978. package/src/resources/extensions/gsd/tests/workspace-index.test.ts +24 -61
  979. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +5 -2
  980. package/src/resources/extensions/gsd/tests/worktree.test.ts +3 -1
  981. package/src/resources/extensions/gsd/tests/write-gate.test.ts +132 -43
  982. package/src/resources/extensions/gsd/token-counter.ts +20 -0
  983. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  984. package/src/resources/extensions/gsd/types.ts +4 -1
  985. package/src/resources/extensions/gsd/validate-directory.ts +164 -0
  986. package/src/resources/extensions/gsd/verification-evidence.ts +7 -4
  987. package/src/resources/extensions/gsd/verification-gate.ts +70 -5
  988. package/src/resources/extensions/gsd/visualizer-data.ts +26 -4
  989. package/src/resources/extensions/gsd/visualizer-overlay.ts +35 -22
  990. package/src/resources/extensions/gsd/visualizer-views.ts +14 -66
  991. package/src/resources/extensions/gsd/worktree-command.ts +4 -51
  992. package/src/resources/extensions/gsd/worktree-manager.ts +7 -9
  993. package/src/resources/extensions/gsd/worktree.ts +41 -1
  994. package/src/resources/extensions/mcporter/index.ts +27 -14
  995. package/src/resources/extensions/remote-questions/manager.ts +6 -24
  996. package/src/resources/extensions/remote-questions/mod.ts +16 -0
  997. package/src/resources/extensions/remote-questions/notify.ts +91 -0
  998. package/src/resources/extensions/remote-questions/remote-command.ts +1 -1
  999. package/src/resources/extensions/remote-questions/store.ts +5 -1
  1000. package/src/resources/extensions/remote-questions/types.ts +26 -3
  1001. package/src/resources/extensions/search-the-web/native-search.ts +7 -0
  1002. package/src/resources/extensions/search-the-web/provider.ts +15 -3
  1003. package/src/resources/extensions/search-the-web/tool-llm-context.ts +1 -13
  1004. package/src/resources/extensions/search-the-web/tool-search.ts +27 -13
  1005. package/src/resources/extensions/shared/format-utils.ts +138 -0
  1006. package/src/resources/extensions/shared/frontmatter.ts +117 -0
  1007. package/src/resources/extensions/shared/mod.ts +30 -0
  1008. package/src/resources/extensions/shared/sanitize.ts +19 -0
  1009. package/src/resources/extensions/shared/tests/format-utils.test.ts +153 -0
  1010. package/src/resources/extensions/slash-commands/create-extension.ts +1 -1
  1011. package/src/resources/extensions/slash-commands/create-slash-command.ts +1 -1
  1012. package/src/resources/extensions/subagent/index.ts +47 -3
  1013. package/src/resources/extensions/subagent/isolation.ts +9 -6
  1014. package/src/resources/extensions/ttsr/index.ts +5 -0
  1015. package/src/resources/extensions/ttsr/rule-loader.ts +4 -51
  1016. package/src/resources/extensions/universal-config/discovery.ts +37 -15
  1017. package/src/resources/extensions/voice/index.ts +1 -1
  1018. package/src/resources/skills/accessibility/SKILL.md +522 -0
  1019. package/src/resources/skills/accessibility/references/WCAG.md +162 -0
  1020. package/src/resources/skills/agent-browser/SKILL.md +517 -0
  1021. package/src/resources/skills/agent-browser/references/authentication.md +202 -0
  1022. package/src/resources/skills/agent-browser/references/commands.md +263 -0
  1023. package/src/resources/skills/agent-browser/references/profiling.md +120 -0
  1024. package/src/resources/skills/agent-browser/references/proxy-support.md +194 -0
  1025. package/src/resources/skills/agent-browser/references/session-management.md +193 -0
  1026. package/src/resources/skills/agent-browser/references/snapshot-refs.md +194 -0
  1027. package/src/resources/skills/agent-browser/references/video-recording.md +173 -0
  1028. package/src/resources/skills/agent-browser/templates/authenticated-session.sh +105 -0
  1029. package/src/resources/skills/agent-browser/templates/capture-workflow.sh +69 -0
  1030. package/src/resources/skills/agent-browser/templates/form-automation.sh +62 -0
  1031. package/src/resources/skills/best-practices/SKILL.md +583 -0
  1032. package/src/resources/skills/code-optimizer/SKILL.md +160 -0
  1033. package/src/resources/skills/code-optimizer/references/algorithmic-complexity.md +66 -0
  1034. package/src/resources/skills/code-optimizer/references/build-compilation.md +90 -0
  1035. package/src/resources/skills/code-optimizer/references/bundle-dependencies.md +82 -0
  1036. package/src/resources/skills/code-optimizer/references/caching-memoization.md +76 -0
  1037. package/src/resources/skills/code-optimizer/references/concurrency-async.md +80 -0
  1038. package/src/resources/skills/code-optimizer/references/config-infra.md +71 -0
  1039. package/src/resources/skills/code-optimizer/references/data-structures.md +80 -0
  1040. package/src/resources/skills/code-optimizer/references/database-queries.md +76 -0
  1041. package/src/resources/skills/code-optimizer/references/dead-code-redundancy.md +84 -0
  1042. package/src/resources/skills/code-optimizer/references/error-resilience.md +80 -0
  1043. package/src/resources/skills/code-optimizer/references/io-network.md +89 -0
  1044. package/src/resources/skills/code-optimizer/references/logging-observability.md +64 -0
  1045. package/src/resources/skills/code-optimizer/references/memory-resources.md +66 -0
  1046. package/src/resources/skills/code-optimizer/references/rendering-ui.md +90 -0
  1047. package/src/resources/skills/code-optimizer/references/security-performance.md +68 -0
  1048. package/src/resources/skills/core-web-vitals/SKILL.md +441 -0
  1049. package/src/resources/skills/core-web-vitals/references/LCP.md +208 -0
  1050. package/src/resources/skills/make-interfaces-feel-better/SKILL.md +122 -0
  1051. package/src/resources/skills/make-interfaces-feel-better/animations.md +379 -0
  1052. package/src/resources/skills/make-interfaces-feel-better/performance.md +88 -0
  1053. package/src/resources/skills/make-interfaces-feel-better/surfaces.md +247 -0
  1054. package/src/resources/skills/make-interfaces-feel-better/typography.md +123 -0
  1055. package/src/resources/skills/react-best-practices/README.md +123 -0
  1056. package/src/resources/skills/react-best-practices/SKILL.md +136 -0
  1057. package/src/resources/skills/react-best-practices/metadata.json +15 -0
  1058. package/src/resources/skills/react-best-practices/rules/_sections.md +46 -0
  1059. package/src/resources/skills/react-best-practices/rules/_template.md +28 -0
  1060. package/src/resources/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  1061. package/src/resources/skills/react-best-practices/rules/advanced-init-once.md +42 -0
  1062. package/src/resources/skills/react-best-practices/rules/advanced-use-latest.md +39 -0
  1063. package/src/resources/skills/react-best-practices/rules/async-api-routes.md +38 -0
  1064. package/src/resources/skills/react-best-practices/rules/async-defer-await.md +80 -0
  1065. package/src/resources/skills/react-best-practices/rules/async-dependencies.md +51 -0
  1066. package/src/resources/skills/react-best-practices/rules/async-parallel.md +28 -0
  1067. package/src/resources/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
  1068. package/src/resources/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
  1069. package/src/resources/skills/react-best-practices/rules/bundle-conditional.md +31 -0
  1070. package/src/resources/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
  1071. package/src/resources/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  1072. package/src/resources/skills/react-best-practices/rules/bundle-preload.md +50 -0
  1073. package/src/resources/skills/react-best-practices/rules/client-event-listeners.md +74 -0
  1074. package/src/resources/skills/react-best-practices/rules/client-localstorage-schema.md +71 -0
  1075. package/src/resources/skills/react-best-practices/rules/client-passive-event-listeners.md +48 -0
  1076. package/src/resources/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
  1077. package/src/resources/skills/react-best-practices/rules/js-batch-dom-css.md +107 -0
  1078. package/src/resources/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
  1079. package/src/resources/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
  1080. package/src/resources/skills/react-best-practices/rules/js-cache-storage.md +70 -0
  1081. package/src/resources/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
  1082. package/src/resources/skills/react-best-practices/rules/js-early-exit.md +50 -0
  1083. package/src/resources/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
  1084. package/src/resources/skills/react-best-practices/rules/js-index-maps.md +37 -0
  1085. package/src/resources/skills/react-best-practices/rules/js-length-check-first.md +49 -0
  1086. package/src/resources/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
  1087. package/src/resources/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
  1088. package/src/resources/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
  1089. package/src/resources/skills/react-best-practices/rules/rendering-activity.md +26 -0
  1090. package/src/resources/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  1091. package/src/resources/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
  1092. package/src/resources/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
  1093. package/src/resources/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  1094. package/src/resources/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  1095. package/src/resources/skills/react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  1096. package/src/resources/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
  1097. package/src/resources/skills/react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  1098. package/src/resources/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
  1099. package/src/resources/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
  1100. package/src/resources/skills/react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  1101. package/src/resources/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
  1102. package/src/resources/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
  1103. package/src/resources/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  1104. package/src/resources/skills/react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  1105. package/src/resources/skills/react-best-practices/rules/rerender-memo.md +44 -0
  1106. package/src/resources/skills/react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  1107. package/src/resources/skills/react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  1108. package/src/resources/skills/react-best-practices/rules/rerender-transitions.md +40 -0
  1109. package/src/resources/skills/react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  1110. package/src/resources/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
  1111. package/src/resources/skills/react-best-practices/rules/server-auth-actions.md +96 -0
  1112. package/src/resources/skills/react-best-practices/rules/server-cache-lru.md +41 -0
  1113. package/src/resources/skills/react-best-practices/rules/server-cache-react.md +76 -0
  1114. package/src/resources/skills/react-best-practices/rules/server-dedup-props.md +65 -0
  1115. package/src/resources/skills/react-best-practices/rules/server-parallel-fetching.md +83 -0
  1116. package/src/resources/skills/react-best-practices/rules/server-serialization.md +38 -0
  1117. package/src/resources/skills/userinterface-wiki/SKILL.md +253 -0
  1118. package/src/resources/skills/userinterface-wiki/rules/_sections.md +66 -0
  1119. package/src/resources/skills/userinterface-wiki/rules/_template.md +24 -0
  1120. package/src/resources/skills/userinterface-wiki/rules/a11y-reduced-motion-check.md +30 -0
  1121. package/src/resources/skills/userinterface-wiki/rules/a11y-toggle-setting.md +30 -0
  1122. package/src/resources/skills/userinterface-wiki/rules/a11y-visual-equivalent.md +36 -0
  1123. package/src/resources/skills/userinterface-wiki/rules/a11y-volume-control.md +28 -0
  1124. package/src/resources/skills/userinterface-wiki/rules/appropriate-confirmations-only.md +19 -0
  1125. package/src/resources/skills/userinterface-wiki/rules/appropriate-errors-warnings.md +18 -0
  1126. package/src/resources/skills/userinterface-wiki/rules/appropriate-no-decorative.md +21 -0
  1127. package/src/resources/skills/userinterface-wiki/rules/appropriate-no-high-frequency.md +28 -0
  1128. package/src/resources/skills/userinterface-wiki/rules/appropriate-no-punishing.md +27 -0
  1129. package/src/resources/skills/userinterface-wiki/rules/container-callback-ref.md +31 -0
  1130. package/src/resources/skills/userinterface-wiki/rules/container-guard-initial-zero.md +25 -0
  1131. package/src/resources/skills/userinterface-wiki/rules/container-no-excessive-use.md +13 -0
  1132. package/src/resources/skills/userinterface-wiki/rules/container-overflow-hidden.md +25 -0
  1133. package/src/resources/skills/userinterface-wiki/rules/container-transition-delay.md +21 -0
  1134. package/src/resources/skills/userinterface-wiki/rules/container-two-div-pattern.md +35 -0
  1135. package/src/resources/skills/userinterface-wiki/rules/container-use-resize-observer.md +48 -0
  1136. package/src/resources/skills/userinterface-wiki/rules/context-cleanup-nodes.md +25 -0
  1137. package/src/resources/skills/userinterface-wiki/rules/context-resume-suspended.md +28 -0
  1138. package/src/resources/skills/userinterface-wiki/rules/context-reuse-single.md +30 -0
  1139. package/src/resources/skills/userinterface-wiki/rules/design-filter-for-character.md +25 -0
  1140. package/src/resources/skills/userinterface-wiki/rules/design-noise-for-percussion.md +26 -0
  1141. package/src/resources/skills/userinterface-wiki/rules/design-oscillator-for-tonal.md +22 -0
  1142. package/src/resources/skills/userinterface-wiki/rules/duration-max-300ms.md +21 -0
  1143. package/src/resources/skills/userinterface-wiki/rules/duration-press-hover.md +21 -0
  1144. package/src/resources/skills/userinterface-wiki/rules/duration-shorten-before-curve.md +21 -0
  1145. package/src/resources/skills/userinterface-wiki/rules/duration-small-state.md +15 -0
  1146. package/src/resources/skills/userinterface-wiki/rules/easing-entrance-ease-out.md +21 -0
  1147. package/src/resources/skills/userinterface-wiki/rules/easing-exit-ease-in.md +21 -0
  1148. package/src/resources/skills/userinterface-wiki/rules/easing-for-state-change.md +27 -0
  1149. package/src/resources/skills/userinterface-wiki/rules/easing-linear-only-progress.md +21 -0
  1150. package/src/resources/skills/userinterface-wiki/rules/easing-natural-decay.md +22 -0
  1151. package/src/resources/skills/userinterface-wiki/rules/easing-no-linear-motion.md +22 -0
  1152. package/src/resources/skills/userinterface-wiki/rules/easing-transition-ease-in-out.md +15 -0
  1153. package/src/resources/skills/userinterface-wiki/rules/envelope-exponential-decay.md +21 -0
  1154. package/src/resources/skills/userinterface-wiki/rules/envelope-no-zero-target.md +21 -0
  1155. package/src/resources/skills/userinterface-wiki/rules/envelope-set-initial-value.md +22 -0
  1156. package/src/resources/skills/userinterface-wiki/rules/exit-key-required.md +29 -0
  1157. package/src/resources/skills/userinterface-wiki/rules/exit-matches-initial.md +29 -0
  1158. package/src/resources/skills/userinterface-wiki/rules/exit-prop-required.md +33 -0
  1159. package/src/resources/skills/userinterface-wiki/rules/exit-requires-wrapper.md +27 -0
  1160. package/src/resources/skills/userinterface-wiki/rules/impl-default-subtle.md +21 -0
  1161. package/src/resources/skills/userinterface-wiki/rules/impl-preload-audio.md +34 -0
  1162. package/src/resources/skills/userinterface-wiki/rules/impl-reset-current-time.md +26 -0
  1163. package/src/resources/skills/userinterface-wiki/rules/mode-pop-layout-for-lists.md +25 -0
  1164. package/src/resources/skills/userinterface-wiki/rules/mode-sync-layout-conflict.md +29 -0
  1165. package/src/resources/skills/userinterface-wiki/rules/mode-wait-doubles-duration.md +25 -0
  1166. package/src/resources/skills/userinterface-wiki/rules/morphing-aria-hidden.md +21 -0
  1167. package/src/resources/skills/userinterface-wiki/rules/morphing-consistent-viewbox.md +23 -0
  1168. package/src/resources/skills/userinterface-wiki/rules/morphing-group-variants.md +33 -0
  1169. package/src/resources/skills/userinterface-wiki/rules/morphing-jump-non-grouped.md +29 -0
  1170. package/src/resources/skills/userinterface-wiki/rules/morphing-reduced-motion.md +28 -0
  1171. package/src/resources/skills/userinterface-wiki/rules/morphing-spring-rotation.md +23 -0
  1172. package/src/resources/skills/userinterface-wiki/rules/morphing-strokelinecap-round.md +21 -0
  1173. package/src/resources/skills/userinterface-wiki/rules/morphing-three-lines.md +32 -0
  1174. package/src/resources/skills/userinterface-wiki/rules/morphing-use-collapsed.md +33 -0
  1175. package/src/resources/skills/userinterface-wiki/rules/native-backdrop-styling.md +27 -0
  1176. package/src/resources/skills/userinterface-wiki/rules/native-placeholder-styling.md +27 -0
  1177. package/src/resources/skills/userinterface-wiki/rules/native-selection-styling.md +18 -0
  1178. package/src/resources/skills/userinterface-wiki/rules/nested-consistent-timing.md +25 -0
  1179. package/src/resources/skills/userinterface-wiki/rules/nested-propagate-required.md +41 -0
  1180. package/src/resources/skills/userinterface-wiki/rules/none-context-menu-entrance.md +25 -0
  1181. package/src/resources/skills/userinterface-wiki/rules/none-high-frequency.md +29 -0
  1182. package/src/resources/skills/userinterface-wiki/rules/none-keyboard-navigation.md +32 -0
  1183. package/src/resources/skills/userinterface-wiki/rules/param-click-duration.md +21 -0
  1184. package/src/resources/skills/userinterface-wiki/rules/param-filter-frequency-range.md +21 -0
  1185. package/src/resources/skills/userinterface-wiki/rules/param-q-value-range.md +21 -0
  1186. package/src/resources/skills/userinterface-wiki/rules/param-reasonable-gain.md +21 -0
  1187. package/src/resources/skills/userinterface-wiki/rules/physics-active-state.md +23 -0
  1188. package/src/resources/skills/userinterface-wiki/rules/physics-no-excessive-stagger.md +22 -0
  1189. package/src/resources/skills/userinterface-wiki/rules/physics-spring-for-overshoot.md +23 -0
  1190. package/src/resources/skills/userinterface-wiki/rules/physics-subtle-deformation.md +22 -0
  1191. package/src/resources/skills/userinterface-wiki/rules/prefetch-hit-slop.md +27 -0
  1192. package/src/resources/skills/userinterface-wiki/rules/prefetch-keyboard-tab.md +19 -0
  1193. package/src/resources/skills/userinterface-wiki/rules/prefetch-not-everything.md +22 -0
  1194. package/src/resources/skills/userinterface-wiki/rules/prefetch-touch-fallback.md +34 -0
  1195. package/src/resources/skills/userinterface-wiki/rules/prefetch-trajectory-over-hover.md +32 -0
  1196. package/src/resources/skills/userinterface-wiki/rules/prefetch-use-selectively.md +13 -0
  1197. package/src/resources/skills/userinterface-wiki/rules/presence-disable-interactions.md +31 -0
  1198. package/src/resources/skills/userinterface-wiki/rules/presence-hook-in-child.md +31 -0
  1199. package/src/resources/skills/userinterface-wiki/rules/presence-safe-to-remove.md +37 -0
  1200. package/src/resources/skills/userinterface-wiki/rules/pseudo-content-required.md +28 -0
  1201. package/src/resources/skills/userinterface-wiki/rules/pseudo-first-line-styling.md +27 -0
  1202. package/src/resources/skills/userinterface-wiki/rules/pseudo-hit-target-expansion.md +31 -0
  1203. package/src/resources/skills/userinterface-wiki/rules/pseudo-marker-styling.md +28 -0
  1204. package/src/resources/skills/userinterface-wiki/rules/pseudo-over-dom-node.md +32 -0
  1205. package/src/resources/skills/userinterface-wiki/rules/pseudo-position-relative-parent.md +33 -0
  1206. package/src/resources/skills/userinterface-wiki/rules/pseudo-z-index-layering.md +37 -0
  1207. package/src/resources/skills/userinterface-wiki/rules/spring-for-gestures.md +27 -0
  1208. package/src/resources/skills/userinterface-wiki/rules/spring-for-interruptible.md +27 -0
  1209. package/src/resources/skills/userinterface-wiki/rules/spring-params-balanced.md +29 -0
  1210. package/src/resources/skills/userinterface-wiki/rules/spring-preserves-velocity.md +28 -0
  1211. package/src/resources/skills/userinterface-wiki/rules/staging-dim-background.md +22 -0
  1212. package/src/resources/skills/userinterface-wiki/rules/staging-one-focal-point.md +24 -0
  1213. package/src/resources/skills/userinterface-wiki/rules/staging-z-index-hierarchy.md +22 -0
  1214. package/src/resources/skills/userinterface-wiki/rules/timing-consistent.md +24 -0
  1215. package/src/resources/skills/userinterface-wiki/rules/timing-no-entrance-context-menu.md +22 -0
  1216. package/src/resources/skills/userinterface-wiki/rules/timing-under-300ms.md +22 -0
  1217. package/src/resources/skills/userinterface-wiki/rules/transition-name-cleanup.md +28 -0
  1218. package/src/resources/skills/userinterface-wiki/rules/transition-name-required.md +27 -0
  1219. package/src/resources/skills/userinterface-wiki/rules/transition-name-unique.md +24 -0
  1220. package/src/resources/skills/userinterface-wiki/rules/transition-over-js-library.md +32 -0
  1221. package/src/resources/skills/userinterface-wiki/rules/transition-style-pseudo-elements.md +24 -0
  1222. package/src/resources/skills/userinterface-wiki/rules/type-antialiased-on-retina.md +18 -0
  1223. package/src/resources/skills/userinterface-wiki/rules/type-disambiguation-stylistic-set.md +15 -0
  1224. package/src/resources/skills/userinterface-wiki/rules/type-font-display-swap.md +28 -0
  1225. package/src/resources/skills/userinterface-wiki/rules/type-justify-with-hyphens.md +24 -0
  1226. package/src/resources/skills/userinterface-wiki/rules/type-letter-spacing-uppercase.md +28 -0
  1227. package/src/resources/skills/userinterface-wiki/rules/type-no-font-synthesis.md +18 -0
  1228. package/src/resources/skills/userinterface-wiki/rules/type-oldstyle-nums-for-prose.md +21 -0
  1229. package/src/resources/skills/userinterface-wiki/rules/type-opentype-contextual-alternates.md +15 -0
  1230. package/src/resources/skills/userinterface-wiki/rules/type-optical-sizing-auto.md +25 -0
  1231. package/src/resources/skills/userinterface-wiki/rules/type-proper-fractions.md +15 -0
  1232. package/src/resources/skills/userinterface-wiki/rules/type-slashed-zero.md +17 -0
  1233. package/src/resources/skills/userinterface-wiki/rules/type-tabular-nums-for-data.md +21 -0
  1234. package/src/resources/skills/userinterface-wiki/rules/type-text-wrap-balance-headings.md +21 -0
  1235. package/src/resources/skills/userinterface-wiki/rules/type-text-wrap-pretty.md +16 -0
  1236. package/src/resources/skills/userinterface-wiki/rules/type-underline-offset.md +25 -0
  1237. package/src/resources/skills/userinterface-wiki/rules/type-variable-weight-continuous.md +23 -0
  1238. package/src/resources/skills/userinterface-wiki/rules/ux-aesthetic-usability.md +32 -0
  1239. package/src/resources/skills/userinterface-wiki/rules/ux-cognitive-load-reduce.md +49 -0
  1240. package/src/resources/skills/userinterface-wiki/rules/ux-common-region-boundaries.md +50 -0
  1241. package/src/resources/skills/userinterface-wiki/rules/ux-doherty-perceived-speed.md +29 -0
  1242. package/src/resources/skills/userinterface-wiki/rules/ux-doherty-under-400ms.md +30 -0
  1243. package/src/resources/skills/userinterface-wiki/rules/ux-fitts-hit-area.md +32 -0
  1244. package/src/resources/skills/userinterface-wiki/rules/ux-fitts-target-size.md +31 -0
  1245. package/src/resources/skills/userinterface-wiki/rules/ux-goal-gradient-progress.md +33 -0
  1246. package/src/resources/skills/userinterface-wiki/rules/ux-hicks-minimize-choices.md +45 -0
  1247. package/src/resources/skills/userinterface-wiki/rules/ux-jakobs-familiar-patterns.md +37 -0
  1248. package/src/resources/skills/userinterface-wiki/rules/ux-millers-chunking.md +23 -0
  1249. package/src/resources/skills/userinterface-wiki/rules/ux-pareto-prioritize-features.md +36 -0
  1250. package/src/resources/skills/userinterface-wiki/rules/ux-peak-end-finish-strong.md +35 -0
  1251. package/src/resources/skills/userinterface-wiki/rules/ux-postels-accept-messy-input.md +45 -0
  1252. package/src/resources/skills/userinterface-wiki/rules/ux-pragnanz-simplify.md +33 -0
  1253. package/src/resources/skills/userinterface-wiki/rules/ux-progressive-disclosure.md +41 -0
  1254. package/src/resources/skills/userinterface-wiki/rules/ux-proximity-grouping.md +38 -0
  1255. package/src/resources/skills/userinterface-wiki/rules/ux-serial-position.md +31 -0
  1256. package/src/resources/skills/userinterface-wiki/rules/ux-similarity-consistency.md +35 -0
  1257. package/src/resources/skills/userinterface-wiki/rules/ux-teslers-complexity.md +28 -0
  1258. package/src/resources/skills/userinterface-wiki/rules/ux-uniform-connectedness.md +43 -0
  1259. package/src/resources/skills/userinterface-wiki/rules/ux-von-restorff-emphasis.md +29 -0
  1260. package/src/resources/skills/userinterface-wiki/rules/ux-zeigarnik-show-incomplete.md +36 -0
  1261. package/src/resources/skills/userinterface-wiki/rules/visual-animate-shadow-pseudo.md +49 -0
  1262. package/src/resources/skills/userinterface-wiki/rules/visual-border-alpha-colors.md +25 -0
  1263. package/src/resources/skills/userinterface-wiki/rules/visual-button-shadow-anatomy.md +49 -0
  1264. package/src/resources/skills/userinterface-wiki/rules/visual-concentric-radius.md +40 -0
  1265. package/src/resources/skills/userinterface-wiki/rules/visual-consistent-spacing-scale.md +35 -0
  1266. package/src/resources/skills/userinterface-wiki/rules/visual-layered-shadows.md +30 -0
  1267. package/src/resources/skills/userinterface-wiki/rules/visual-no-pure-black-shadow.md +25 -0
  1268. package/src/resources/skills/userinterface-wiki/rules/visual-shadow-direction.md +25 -0
  1269. package/src/resources/skills/userinterface-wiki/rules/visual-shadow-matches-elevation.md +23 -0
  1270. package/src/resources/skills/userinterface-wiki/rules/weight-duration-matches-action.md +29 -0
  1271. package/src/resources/skills/userinterface-wiki/rules/weight-match-action.md +32 -0
  1272. package/src/resources/skills/web-design-guidelines/SKILL.md +39 -0
  1273. package/src/resources/skills/web-quality-audit/SKILL.md +170 -0
  1274. package/src/resources/skills/web-quality-audit/scripts/analyze.sh +91 -0
  1275. package/dist/resources/extensions/gsd/complexity.ts +0 -237
  1276. package/dist/resources/extensions/gsd/github-client.ts +0 -235
  1277. package/dist/resources/extensions/gsd/mcp-server.ts +0 -108
  1278. package/dist/resources/extensions/gsd/tests/activity-log-prune.test.ts +0 -297
  1279. package/dist/resources/extensions/gsd/tests/activity-log-save.test.ts +0 -127
  1280. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +0 -110
  1281. package/dist/resources/extensions/gsd/tests/auto-draft-pause.test.ts +0 -115
  1282. package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +0 -294
  1283. package/dist/resources/extensions/gsd/tests/marketplace-discovery.test.ts +0 -202
  1284. package/dist/resources/extensions/gsd/tests/metrics-io.test.ts +0 -176
  1285. package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +0 -54
  1286. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -120
  1287. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -226
  1288. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +0 -110
  1289. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -207
  1290. package/dist/resources/extensions/gsd/tests/preferences-schema-validation.test.ts +0 -183
  1291. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +0 -168
  1292. package/dist/resources/extensions/shared/bundled-extension-paths.ts +0 -11
  1293. package/src/resources/extensions/gsd/complexity.ts +0 -237
  1294. package/src/resources/extensions/gsd/github-client.ts +0 -235
  1295. package/src/resources/extensions/gsd/mcp-server.ts +0 -108
  1296. package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +0 -297
  1297. package/src/resources/extensions/gsd/tests/activity-log-save.test.ts +0 -127
  1298. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +0 -110
  1299. package/src/resources/extensions/gsd/tests/auto-draft-pause.test.ts +0 -115
  1300. package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +0 -294
  1301. package/src/resources/extensions/gsd/tests/marketplace-discovery.test.ts +0 -202
  1302. package/src/resources/extensions/gsd/tests/metrics-io.test.ts +0 -176
  1303. package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +0 -54
  1304. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -120
  1305. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -226
  1306. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +0 -110
  1307. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -207
  1308. package/src/resources/extensions/gsd/tests/preferences-schema-validation.test.ts +0 -183
  1309. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +0 -168
  1310. package/src/resources/extensions/shared/bundled-extension-paths.ts +0 -11
@@ -17,8 +17,8 @@ import type {
17
17
  } from "@gsd/pi-coding-agent";
18
18
 
19
19
  import { deriveState } from "./state.js";
20
- import type { BudgetEnforcementMode, GSDState } from "./types.js";
21
- import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides, parsePlan, parseSummary } from "./files.js";
20
+ import type { GSDState } from "./types.js";
21
+ import { loadFile, getManifestStatus, resolveAllOverrides, parsePlan, parseSummary } from "./files.js";
22
22
  import { loadPrompt } from "./prompt-loader.js";
23
23
  import { runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit } from "./verification-gate.js";
24
24
  import { writeVerificationJSON } from "./verification-evidence.js";
@@ -27,26 +27,55 @@ import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
27
27
  import {
28
28
  gsdRoot, resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
29
29
  resolveMilestonePath, resolveDir, resolveTasksDir, resolveTaskFile,
30
- relMilestoneFile, relSliceFile, relSlicePath, relMilestonePath,
31
- milestonesDir,
32
- buildMilestoneFileName, buildSliceFileName, buildTaskFileName,
30
+ milestonesDir, buildTaskFileName,
33
31
  } from "./paths.js";
34
32
  import { invalidateAllCaches } from "./cache.js";
35
33
  import { saveActivityLog, clearActivityLogState } from "./activity-log.js";
36
34
  import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
37
35
  import { writeLock, clearLock, readCrashLock, formatCrashInfo, isLockProcessAlive } from "./crash-recovery.js";
36
+ import {
37
+ acquireSessionLock,
38
+ validateSessionLock,
39
+ releaseSessionLock,
40
+ updateSessionLock,
41
+ } from "./session-lock.js";
38
42
  import {
39
43
  clearUnitRuntimeRecord,
40
- formatExecuteTaskRecoveryStatus,
41
44
  inspectExecuteTaskDurability,
42
45
  readUnitRuntimeRecord,
43
46
  writeUnitRuntimeRecord,
44
47
  } from "./unit-runtime.js";
45
- import { resolveAutoSupervisorConfig, resolveModelWithFallbacksForUnit, loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, resolveDynamicRoutingConfig, getIsolationMode } from "./preferences.js";
48
+ import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode } from "./preferences.js";
46
49
  import { sendDesktopNotification } from "./notifications.js";
47
50
  import type { GSDPreferences } from "./preferences.js";
48
- import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js";
49
- import { resolveModelForComplexity } from "./model-router.js";
51
+ import {
52
+ type BudgetAlertLevel,
53
+ getBudgetAlertLevel,
54
+ getNewBudgetAlertLevel,
55
+ getBudgetEnforcementAction,
56
+ } from "./auto-budget.js";
57
+ import {
58
+ markToolStart as _markToolStart,
59
+ markToolEnd as _markToolEnd,
60
+ getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs,
61
+ getInFlightToolCount,
62
+ getOldestInFlightToolStart,
63
+ clearInFlightTools,
64
+ } from "./auto-tool-tracking.js";
65
+ import {
66
+ collectObservabilityWarnings as _collectObservabilityWarnings,
67
+ buildObservabilityRepairBlock,
68
+ } from "./auto-observability.js";
69
+ import { closeoutUnit } from "./auto-unit-closeout.js";
70
+ import { recoverTimedOutUnit } from "./auto-timeout-recovery.js";
71
+ import { selectAndApplyModel } from "./auto-model-selection.js";
72
+ import {
73
+ syncProjectRootToWorktree,
74
+ syncStateToProjectRoot,
75
+ readResourceVersion,
76
+ checkResourcesStale,
77
+ escapeStaleWorktree,
78
+ } from "./auto-worktree-sync.js";
50
79
  import { initRoutingHistory, resetRoutingHistory, recordOutcome } from "./routing-history.js";
51
80
  import {
52
81
  checkPostUnitHooks,
@@ -59,12 +88,6 @@ import {
59
88
  restoreHookState,
60
89
  clearPersistedHookState,
61
90
  } from "./post-unit-hooks.js";
62
- import {
63
- validatePlanBoundary,
64
- validateExecuteBoundary,
65
- validateCompleteBoundary,
66
- formatValidationIssues,
67
- } from "./observability-validator.js";
68
91
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
69
92
  import { runGSDDoctor, rebuildState, summarizeDoctorIssues } from "./doctor.js";
70
93
  import {
@@ -78,14 +101,16 @@ import {
78
101
  import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
79
102
  import { captureAvailableSkills, getAndClearSkills, resetSkillTelemetry } from "./skill-telemetry.js";
80
103
  import {
81
- initMetrics, resetMetrics, snapshotUnitMetrics, getLedger,
104
+ initMetrics, resetMetrics, getLedger,
82
105
  getProjectTotals, formatCost, formatTokenCount,
83
106
  } from "./metrics.js";
107
+ import { computeBudgets, resolveExecutorContextWindow } from "./context-budget.js";
108
+ import { GSDError, GSD_ARTIFACT_MISSING } from "./errors.js";
84
109
  import { join } from "node:path";
85
110
  import { sep as pathSep } from "node:path";
86
- import { homedir } from "node:os";
87
- import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync, cpSync } from "node:fs";
88
- import { nativeIsRepo, nativeInit, nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
111
+ import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync, statSync } from "node:fs";
112
+ import { atomicWriteSync } from "./atomic-write.js";
113
+ import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
89
114
  import {
90
115
  autoCommitCurrentBranch,
91
116
  captureIntegrationBranch,
@@ -111,7 +136,7 @@ import {
111
136
  } from "./auto-worktree.js";
112
137
  import { pruneQueueOrder } from "./queue-order.js";
113
138
  import { consumeSignal } from "./session-status-io.js";
114
- import { showNextAction } from "../shared/next-action-ui.js";
139
+ import { showNextAction } from "../shared/mod.js";
115
140
  import { debugLog, debugTime, debugCount, debugPeak, enableDebug, isDebugEnabled, writeDebugSummary, getDebugLogPath } from "./debug-logger.js";
116
141
  import {
117
142
  resolveExpectedArtifactPath,
@@ -128,18 +153,6 @@ import {
128
153
  reconcileMergeState,
129
154
  } from "./auto-recovery.js";
130
155
  import { resolveDispatch, resetRewriteCircuitBreaker } from "./auto-dispatch.js";
131
- import {
132
- buildResearchSlicePrompt,
133
- buildResearchMilestonePrompt,
134
- buildPlanSlicePrompt,
135
- buildPlanMilestonePrompt,
136
- buildExecuteTaskPrompt,
137
- buildCompleteSlicePrompt,
138
- buildCompleteMilestonePrompt,
139
- buildReassessRoadmapPrompt,
140
- buildRunUatPrompt,
141
- buildReplanSlicePrompt,
142
- } from "./auto-prompts.js";
143
156
  import {
144
157
  type AutoDashboardData,
145
158
  updateProgressWidget as _updateProgressWidget,
@@ -147,7 +160,6 @@ import {
147
160
  clearSliceProgressCache,
148
161
  describeNextUnit as _describeNextUnit,
149
162
  unitVerb,
150
- unitPhaseLabel,
151
163
  formatAutoElapsed as _formatAutoElapsed,
152
164
  formatWidgetTokens,
153
165
  hideFooter,
@@ -161,180 +173,49 @@ import {
161
173
  import { isDbAvailable } from "./gsd-db.js";
162
174
  import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from "./captures.js";
163
175
 
164
- // ─── Worktree Project Root State Sync ───────────────────────────────────────
165
- // When running in an auto-worktree, dispatch state (.gsd/ metadata) diverges
166
- // between the worktree (where work happens) and the project root (where
167
- // startAutoMode reads initial state on restart). Without syncing, restarting
168
- // auto-mode reads stale state from the project root and re-dispatches
169
- // already-completed units.
170
-
171
- /**
172
- * Sync milestone artifacts from project root INTO worktree before deriveState.
173
- * Covers the case where the LLM wrote artifacts to the main repo filesystem
174
- * (e.g. via absolute paths) but the worktree has stale data. Also deletes
175
- * gsd.db in the worktree so it rebuilds from fresh disk state (#853).
176
- * Non-fatal — sync failure should never block dispatch.
177
- */
178
- function syncProjectRootToWorktree(projectRoot: string, worktreePath: string, milestoneId: string | null): void {
179
- if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
180
- if (!milestoneId) return;
181
-
182
- const prGsd = join(projectRoot, ".gsd");
183
- const wtGsd = join(worktreePath, ".gsd");
184
-
185
- // Copy milestone directory from project root to worktree if the project root
186
- // has newer artifacts (e.g. slices that don't exist in the worktree yet)
187
- try {
188
- const srcMilestone = join(prGsd, "milestones", milestoneId);
189
- const dstMilestone = join(wtGsd, "milestones", milestoneId);
190
- if (existsSync(srcMilestone)) {
191
- mkdirSync(dstMilestone, { recursive: true });
192
- cpSync(srcMilestone, dstMilestone, { recursive: true, force: false });
193
- }
194
- } catch { /* non-fatal */ }
195
-
196
- // Delete worktree gsd.db so it rebuilds from the freshly synced files.
197
- // Stale DB rows are the root cause of the infinite skip loop (#853).
198
- try {
199
- const wtDb = join(wtGsd, "gsd.db");
200
- if (existsSync(wtDb)) {
201
- unlinkSync(wtDb);
202
- }
203
- } catch { /* non-fatal */ }
204
- }
205
-
206
- /**
207
- * Sync dispatch-critical .gsd/ state files from worktree to project root.
208
- * Only runs when inside an auto-worktree (worktreePath differs from projectRoot).
209
- * Copies: STATE.md + active milestone directory (roadmap, slice plans, task summaries).
210
- * Non-fatal — sync failure should never block dispatch.
211
- */
212
- function syncStateToProjectRoot(worktreePath: string, projectRoot: string, milestoneId: string | null): void {
213
- if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
214
- if (!milestoneId) return;
215
-
216
- const wtGsd = join(worktreePath, ".gsd");
217
- const prGsd = join(projectRoot, ".gsd");
218
-
219
- // 1. STATE.md — the quick-glance status used by initial deriveState()
220
- try {
221
- const src = join(wtGsd, "STATE.md");
222
- const dst = join(prGsd, "STATE.md");
223
- if (existsSync(src)) cpSync(src, dst, { force: true });
224
- } catch { /* non-fatal */ }
225
-
226
- // 2. Milestone directory — ROADMAP, slice PLANs, task summaries
227
- // Copy the entire milestone .gsd subtree so deriveState reads current checkboxes
228
- try {
229
- const srcMilestone = join(wtGsd, "milestones", milestoneId);
230
- const dstMilestone = join(prGsd, "milestones", milestoneId);
231
- if (existsSync(srcMilestone)) {
232
- mkdirSync(dstMilestone, { recursive: true });
233
- cpSync(srcMilestone, dstMilestone, { recursive: true, force: true });
234
- }
235
- } catch { /* non-fatal */ }
236
-
237
- // 3. Merge completed-units.json (set-union of both locations)
238
- // Prevents already-completed units from being re-dispatched after crash/restart.
239
- const srcKeysFile = join(wtGsd, "completed-units.json");
240
- const dstKeysFile = join(prGsd, "completed-units.json");
241
- if (existsSync(srcKeysFile)) {
242
- try {
243
- const srcKeys: string[] = JSON.parse(readFileSync(srcKeysFile, "utf8"));
244
- let dstKeys: string[] = [];
245
- if (existsSync(dstKeysFile)) {
246
- try { dstKeys = JSON.parse(readFileSync(dstKeysFile, "utf8")); } catch { /* ignore corrupt dst */ }
247
- }
248
- const merged = [...new Set([...dstKeys, ...srcKeys])];
249
- writeFileSync(dstKeysFile, JSON.stringify(merged, null, 2));
250
- } catch { /* non-fatal */ }
251
- }
176
+ // ── Extracted modules ──────────────────────────────────────────────────────
177
+ import { startUnitSupervision, type SupervisionContext } from "./auto-timers.js";
178
+ import { checkIdempotency, type IdempotencyContext } from "./auto-idempotency.js";
179
+ import { checkStuckAndRecover, type StuckContext } from "./auto-stuck-detection.js";
180
+ import { runPostUnitVerification, type VerificationContext } from "./auto-verification.js";
181
+ import { postUnitPreVerification, postUnitPostVerification, type PostUnitContext } from "./auto-post-unit.js";
182
+ import { bootstrapAutoSession, type BootstrapDeps } from "./auto-start.js";
252
183
 
253
- // 4. Runtime records unit dispatch state used by selfHealRuntimeRecords().
254
- // Without this, a crash during a unit leaves the runtime record only in the
255
- // worktree. If the next session resolves basePath before worktree re-entry,
256
- // selfHeal can't find or clear the stale record (#769).
257
- try {
258
- const srcRuntime = join(wtGsd, "runtime", "units");
259
- const dstRuntime = join(prGsd, "runtime", "units");
260
- if (existsSync(srcRuntime)) {
261
- mkdirSync(dstRuntime, { recursive: true });
262
- cpSync(srcRuntime, dstRuntime, { recursive: true, force: true });
263
- }
264
- } catch { /* non-fatal */ }
265
- }
184
+ // Worktree sync, resource staleness, stale worktree escape auto-worktree-sync.ts
266
185
 
267
- // ─── State ────────────────────────────────────────────────────────────────────
268
-
269
- let active = false;
270
- let paused = false;
271
- let stepMode = false;
272
- let verbose = false;
273
- let cmdCtx: ExtensionCommandContext | null = null;
274
- let basePath = "";
275
- let originalBasePath = "";
276
- let gitService: GitServiceImpl | null = null;
277
-
278
- /** Track total dispatches per unit to detect stuck loops (catches A→B→A→B patterns) */
279
- const unitDispatchCount = new Map<string, number>();
280
- const MAX_UNIT_DISPATCHES = 3;
281
- /** Retry index at which a stub summary placeholder is written when the summary is still absent. */
282
- const STUB_RECOVERY_THRESHOLD = 2;
283
- /** Hard cap on total dispatches per unit across ALL reconciliation cycles.
284
- * unitDispatchCount can be reset by loop-recovery/self-repair paths, but this
285
- * counter is never reset — it catches infinite reconciliation loops where
286
- * artifacts exist but deriveState keeps returning the same unit. */
287
- const unitLifetimeDispatches = new Map<string, number>();
288
- const MAX_LIFETIME_DISPATCHES = 6;
289
-
290
- /** Tracks recovery attempt count per unit for backoff and diagnostics. */
291
- const unitRecoveryCount = new Map<string, number>();
292
-
293
- /** Track consecutive skips per unit — catches infinite skip loops where deriveState
294
- * keeps returning the same already-completed unit. Reset on any real dispatch. */
295
- const unitConsecutiveSkips = new Map<string, number>();
296
- const MAX_CONSECUTIVE_SKIPS = 3;
297
-
298
- /** Persisted completed-unit keys — survives restarts. Loaded from .gsd/completed-units.json. */
299
- const completedKeySet = new Set<string>();
300
-
301
- /** Resource version captured at auto-mode start. If the managed-resources
302
- * manifest version changes mid-session (e.g. npm update -g gsd-pi),
303
- * templates on disk may expect variables the in-memory code doesn't provide.
304
- * Detect this and stop gracefully instead of crashing.
305
- * Uses gsdVersion (semver) instead of syncedAt (timestamp) so that
306
- * launching a second session doesn't falsely trigger staleness (#804). */
307
- let resourceVersionOnStart: string | null = null;
308
-
309
- function readResourceVersion(): string | null {
310
- const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
311
- const manifestPath = join(agentDir, "managed-resources.json");
312
- try {
313
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
314
- return typeof manifest?.gsdVersion === "string" ? manifest.gsdVersion : null;
315
- } catch {
316
- return null;
317
- }
318
- }
186
+ // ─── Session State ─────────────────────────────────────────────────────────
319
187
 
320
- function checkResourcesStale(): string | null {
321
- if (resourceVersionOnStart === null) return null;
322
- const current = readResourceVersion();
323
- if (current === null) return null;
324
- if (current !== resourceVersionOnStart) {
325
- return "GSD resources were updated since this session started. Restart gsd to load the new code.";
326
- }
327
- return null;
328
- }
188
+ import {
189
+ AutoSession,
190
+ MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES,
191
+ MAX_CONSECUTIVE_SKIPS, DISPATCH_GAP_TIMEOUT_MS, MAX_SKIP_DEPTH,
192
+ NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
193
+ } from "./auto/session.js";
194
+ import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js";
195
+ export {
196
+ MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES,
197
+ MAX_CONSECUTIVE_SKIPS, DISPATCH_GAP_TIMEOUT_MS, MAX_SKIP_DEPTH,
198
+ NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
199
+ } from "./auto/session.js";
200
+ export type { CompletedUnit, CurrentUnit, UnitRouting, StartModel } from "./auto/session.js";
201
+
202
+ // ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
203
+ // ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
204
+ // This file must NOT declare module-level `let` or `var` variables for state.
205
+ // The single `s` instance below is the only mutable module-level binding.
206
+ //
207
+ // When adding features or fixing bugs:
208
+ // - New mutable state → add a property to AutoSession, not a module-level variable
209
+ // - New constants → module-level `const` is fine (immutable)
210
+ // - New state that needs reset on stopAuto → add to AutoSession.reset()
211
+ //
212
+ // Tests in auto-session-encapsulation.test.ts enforce this invariant.
213
+ // ─────────────────────────────────────────────────────────────────────────────
214
+ const s = new AutoSession();
215
+
216
+ /** Throttle STATE.md rebuilds — at most once per 30 seconds */
217
+ const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
329
218
 
330
- /**
331
- * Resolve whether auto-mode should use worktree isolation.
332
- * Returns true for worktree mode (default), false for branch and none modes.
333
- * Branch mode works directly in the project root — useful for repos
334
- * with git submodules where worktrees don't work well (#531).
335
- * None mode skips all worktree and milestone-branch logic — commits
336
- * land on the current branch with no isolation (#M001-S02).
337
- */
338
219
  export function shouldUseWorktreeIsolation(): boolean {
339
220
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
340
221
  if (prefs?.isolation === "none") return false;
@@ -342,54 +223,19 @@ export function shouldUseWorktreeIsolation(): boolean {
342
223
  return true; // default: worktree
343
224
  }
344
225
 
345
- /**
346
- * Detect and escape a stale worktree cwd (#608).
347
- *
348
- * After milestone completion + merge, the worktree directory is removed but
349
- * the process cwd may still point inside `.gsd/worktrees/<MID>/`.
350
- * When a new session starts, `process.cwd()` is passed as `base` to startAuto
351
- * and all subsequent writes land in the wrong directory. This function detects
352
- * that scenario and chdir back to the project root.
353
- *
354
- * Returns the corrected base path.
355
- */
356
- function escapeStaleWorktree(base: string): string {
357
- const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
358
- const idx = base.indexOf(marker);
359
- if (idx === -1) return base;
360
-
361
- // base is inside .gsd/worktrees/<something> — extract the project root
362
- const projectRoot = base.slice(0, idx);
363
- try {
364
- process.chdir(projectRoot);
365
- } catch {
366
- // If chdir fails, return the original — caller will handle errors downstream
367
- return base;
368
- }
369
- return projectRoot;
370
- }
371
-
372
226
  /** Crash recovery prompt — set by startAuto, consumed by first dispatchNextUnit */
373
- let pendingCrashRecovery: string | null = null;
374
227
 
375
228
  /** Pending verification retry — set when gate fails with retries remaining, consumed by dispatchNextUnit */
376
- let pendingVerificationRetry: { unitId: string; failureContext: string; attempt: number } | null = null;
377
- /** Verification retry count per unitId — separate from unitDispatchCount which tracks artifact-missing retries */
378
- const verificationRetryCount = new Map<string, number>();
229
+
230
+ /** Verification retry count per unitId — separate from s.unitDispatchCount which tracks artifact-missing retries */
379
231
 
380
232
  /** Session file path captured at pause — used to synthesize recovery briefing on resume */
381
- let pausedSessionFile: string | null = null;
382
233
 
383
234
  /** Dashboard tracking */
384
- let autoStartTime: number = 0;
385
- let completedUnits: { type: string; id: string; startedAt: number; finishedAt: number }[] = [];
386
- let currentUnit: { type: string; id: string; startedAt: number } | null = null;
387
235
 
388
236
  /** Track dynamic routing decision for the current unit (for metrics) */
389
- let currentUnitRouting: { tier: string; modelDowngraded: boolean } | null = null;
390
237
 
391
238
  /** Queue of quick-task captures awaiting dispatch after triage resolution */
392
- let pendingQuickTasks: import("./captures.js").CaptureEntry[] = [];
393
239
 
394
240
  /**
395
241
  * Model captured at auto-mode start. Used to prevent model bleed between
@@ -398,78 +244,42 @@ let pendingQuickTasks: import("./captures.js").CaptureEntry[] = [];
398
244
  * the session's original model is re-applied instead of reading from
399
245
  * the shared global settings (which another instance may have overwritten).
400
246
  */
401
- let autoModeStartModel: { provider: string; id: string } | null = null;
402
247
 
403
248
  /** Track current milestone to detect transitions */
404
- let currentMilestoneId: string | null = null;
405
- let lastBudgetAlertLevel: BudgetAlertLevel = 0;
406
249
 
407
250
  /** Model the user had selected before auto-mode started */
408
- let originalModelId: string | null = null;
409
- let originalModelProvider: string | null = null;
410
251
 
411
252
  /** Progress-aware timeout supervision */
412
- let unitTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
413
- let wrapupWarningHandle: ReturnType<typeof setTimeout> | null = null;
414
- let idleWatchdogHandle: ReturnType<typeof setInterval> | null = null;
253
+
254
+ /** Context-pressure continue-here monitor fires once when context usage >= 70% */
415
255
 
416
256
  /** Dispatch gap watchdog — detects when the state machine stalls between units.
417
257
  * After handleAgentEnd completes, if auto-mode is still active but no new unit
418
258
  * has been dispatched (sendMessage not called), this timer fires to force a
419
259
  * re-evaluation. Covers the case where dispatchNextUnit silently fails or
420
260
  * an unhandled error kills the dispatch chain. */
421
- let dispatchGapHandle: ReturnType<typeof setTimeout> | null = null;
422
- const DISPATCH_GAP_TIMEOUT_MS = 5_000; // 5 seconds
423
261
 
424
262
  /** Prompt character measurement for token savings analysis (R051). */
425
- let lastPromptCharCount: number | undefined;
426
- let lastBaselineCharCount: number | undefined;
427
263
 
428
264
  /** SIGTERM handler registered while auto-mode is active — cleared on stop/pause. */
429
- let _sigtermHandler: (() => void) | null = null;
430
265
 
431
266
  /**
432
267
  * Tool calls currently being executed — prevents false idle detection during long-running tools.
433
268
  * Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
434
269
  * running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
435
270
  */
436
- const inFlightTools = new Map<string, number>();
437
-
438
- type BudgetAlertLevel = 0 | 75 | 80 | 90 | 100;
439
-
440
- export function getBudgetAlertLevel(budgetPct: number): BudgetAlertLevel {
441
- if (budgetPct >= 1.0) return 100;
442
- if (budgetPct >= 0.90) return 90;
443
- if (budgetPct >= 0.80) return 80;
444
- if (budgetPct >= 0.75) return 75;
445
- return 0;
446
- }
447
-
448
- export function getNewBudgetAlertLevel(previousLevel: BudgetAlertLevel, budgetPct: number): BudgetAlertLevel | null {
449
- const currentLevel = getBudgetAlertLevel(budgetPct);
450
- if (currentLevel === 0 || currentLevel <= previousLevel) return null;
451
- return currentLevel;
452
- }
453
-
454
- export function getBudgetEnforcementAction(
455
- enforcement: BudgetEnforcementMode,
456
- budgetPct: number,
457
- ): "none" | "warn" | "pause" | "halt" {
458
- if (budgetPct < 1.0) return "none";
459
- if (enforcement === "halt") return "halt";
460
- if (enforcement === "pause") return "pause";
461
- return "warn";
462
- }
271
+ // Re-export budget utilities for external consumers
272
+ export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction } from "./auto-budget.js";
463
273
 
464
274
  /** Wrapper: register SIGTERM handler and store reference. */
465
275
  function registerSigtermHandler(currentBasePath: string): void {
466
- _sigtermHandler = _registerSigtermHandler(currentBasePath, _sigtermHandler);
276
+ s.sigtermHandler = _registerSigtermHandler(currentBasePath, s.sigtermHandler);
467
277
  }
468
278
 
469
279
  /** Wrapper: deregister SIGTERM handler and clear reference. */
470
280
  function deregisterSigtermHandler(): void {
471
- _deregisterSigtermHandler(_sigtermHandler);
472
- _sigtermHandler = null;
281
+ _deregisterSigtermHandler(s.sigtermHandler);
282
+ s.sigtermHandler = null;
473
283
  }
474
284
 
475
285
  export { type AutoDashboardData } from "./auto-dashboard.js";
@@ -480,21 +290,18 @@ export function getAutoDashboardData(): AutoDashboardData {
480
290
  // Pending capture count — lazy check, non-fatal
481
291
  let pendingCaptureCount = 0;
482
292
  try {
483
- if (basePath) {
484
- pendingCaptureCount = countPendingCaptures(basePath);
293
+ if (s.basePath) {
294
+ pendingCaptureCount = countPendingCaptures(s.basePath);
485
295
  }
486
296
  } catch {
487
297
  // Non-fatal — captures module may not be loaded
488
298
  }
489
- return {
490
- active,
491
- paused,
492
- stepMode,
493
- startTime: autoStartTime,
494
- elapsed: (active || paused) ? Date.now() - autoStartTime : 0,
495
- currentUnit: currentUnit ? { ...currentUnit } : null,
496
- completedUnits: [...completedUnits],
497
- basePath,
299
+ return { active: s.active, paused: s.paused,
300
+ stepMode: s.stepMode,
301
+ startTime: s.autoStartTime,
302
+ elapsed: (s.active || s.paused) ? Date.now() - s.autoStartTime : 0,
303
+ currentUnit: s.currentUnit ? { ...s.currentUnit } : null,
304
+ completedUnits: [...s.completedUnits], basePath: s.basePath,
498
305
  totalCost: totals?.cost ?? 0,
499
306
  totalTokens: totals?.tokens.total ?? 0,
500
307
  pendingCaptureCount,
@@ -504,37 +311,33 @@ export function getAutoDashboardData(): AutoDashboardData {
504
311
  // ─── Public API ───────────────────────────────────────────────────────────────
505
312
 
506
313
  export function isAutoActive(): boolean {
507
- return active;
314
+ return s.active;
508
315
  }
509
316
 
510
317
  export function isAutoPaused(): boolean {
511
- return paused;
318
+ return s.paused;
512
319
  }
513
320
 
514
321
  /**
515
- * Mark a tool execution as in-flight. Called from index.ts on tool_execution_start.
516
- * Records start time so the idle watchdog can detect tools hung longer than the idle timeout.
322
+ * Return the model captured at auto-mode start for this session.
323
+ * Used by error-recovery to fall back to the session's own model
324
+ * instead of reading (potentially stale) preferences from disk (#1065).
517
325
  */
326
+ export function getAutoModeStartModel(): { provider: string; id: string } | null {
327
+ return s.autoModeStartModel;
328
+ }
329
+
330
+ // Tool tracking — delegates to auto-tool-tracking.ts
518
331
  export function markToolStart(toolCallId: string): void {
519
- if (!active) return;
520
- inFlightTools.set(toolCallId, Date.now());
332
+ _markToolStart(toolCallId, s.active);
521
333
  }
522
334
 
523
- /**
524
- * Mark a tool execution as completed. Called from index.ts on tool_execution_end.
525
- */
526
335
  export function markToolEnd(toolCallId: string): void {
527
- inFlightTools.delete(toolCallId);
336
+ _markToolEnd(toolCallId);
528
337
  }
529
338
 
530
- /**
531
- * Returns the age (ms) of the oldest currently in-flight tool, or 0 if none.
532
- * Exported for testing.
533
- */
534
339
  export function getOldestInFlightToolAgeMs(): number {
535
- if (inFlightTools.size === 0) return 0;
536
- const oldestStart = Math.min(...inFlightTools.values());
537
- return Date.now() - oldestStart;
340
+ return _getOldestInFlightToolAgeMs();
538
341
  }
539
342
 
540
343
  /**
@@ -543,7 +346,7 @@ export function getOldestInFlightToolAgeMs(): number {
543
346
  * a second terminal can discover and stop a running auto-mode session.
544
347
  */
545
348
  function lockBase(): string {
546
- return originalBasePath || basePath;
349
+ return s.originalBasePath || s.basePath;
547
350
  }
548
351
 
549
352
  /**
@@ -573,50 +376,63 @@ export function stopAutoRemote(projectRoot: string): { found: boolean; pid?: num
573
376
  }
574
377
 
575
378
  export function isStepMode(): boolean {
576
- return stepMode;
379
+ return s.stepMode;
577
380
  }
578
381
 
579
382
  function clearUnitTimeout(): void {
580
- if (unitTimeoutHandle) {
581
- clearTimeout(unitTimeoutHandle);
582
- unitTimeoutHandle = null;
383
+ if (s.unitTimeoutHandle) {
384
+ clearTimeout(s.unitTimeoutHandle);
385
+ s.unitTimeoutHandle = null;
583
386
  }
584
- if (wrapupWarningHandle) {
585
- clearTimeout(wrapupWarningHandle);
586
- wrapupWarningHandle = null;
387
+ if (s.wrapupWarningHandle) {
388
+ clearTimeout(s.wrapupWarningHandle);
389
+ s.wrapupWarningHandle = null;
587
390
  }
588
- if (idleWatchdogHandle) {
589
- clearInterval(idleWatchdogHandle);
590
- idleWatchdogHandle = null;
391
+ if (s.idleWatchdogHandle) {
392
+ clearInterval(s.idleWatchdogHandle);
393
+ s.idleWatchdogHandle = null;
591
394
  }
592
- inFlightTools.clear();
395
+ if (s.continueHereHandle) {
396
+ clearInterval(s.continueHereHandle);
397
+ s.continueHereHandle = null;
398
+ }
399
+ clearInFlightTools();
593
400
  clearDispatchGapWatchdog();
594
401
  }
595
402
 
596
403
  function clearDispatchGapWatchdog(): void {
597
- if (dispatchGapHandle) {
598
- clearTimeout(dispatchGapHandle);
599
- dispatchGapHandle = null;
404
+ if (s.dispatchGapHandle) {
405
+ clearTimeout(s.dispatchGapHandle);
406
+ s.dispatchGapHandle = null;
600
407
  }
601
408
  }
602
409
 
410
+ /** Build snapshot metric opts, enriching with continueHereFired from the runtime record. */
411
+ function buildSnapshotOpts(unitType: string, unitId: string): { continueHereFired?: boolean; promptCharCount?: number; baselineCharCount?: number } & Record<string, unknown> {
412
+ const runtime = s.currentUnit ? readUnitRuntimeRecord(s.basePath, unitType, unitId) : null;
413
+ return {
414
+ promptCharCount: s.lastPromptCharCount,
415
+ baselineCharCount: s.lastBaselineCharCount,
416
+ ...(s.currentUnitRouting ?? {}),
417
+ ...(runtime?.continueHereFired ? { continueHereFired: true } : {}),
418
+ };
419
+ }
420
+
603
421
  /**
604
422
  * Start a watchdog that fires if no new unit is dispatched within DISPATCH_GAP_TIMEOUT_MS
605
423
  * after handleAgentEnd completes. This catches the case where the dispatch chain silently
606
- * breaks (e.g., unhandled exception in dispatchNextUnit) and auto-mode is left active but idle.
424
+ * breaks (e.g., unhandled exception in dispatchNextUnit) and auto-mode is left s.active but idle.
607
425
  *
608
426
  * The watchdog is cleared on the next successful unit dispatch (clearUnitTimeout is called
609
427
  * at the start of handleAgentEnd, which calls clearDispatchGapWatchdog).
610
428
  */
611
429
  function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void {
612
430
  clearDispatchGapWatchdog();
613
- dispatchGapHandle = setTimeout(async () => {
614
- dispatchGapHandle = null;
615
- if (!active || !cmdCtx) return;
431
+ s.dispatchGapHandle = setTimeout(async () => {
432
+ s.dispatchGapHandle = null;
433
+ if (!s.active || !s.cmdCtx) return;
616
434
 
617
- // Auto-mode is active but no unit was dispatched — the state machine stalled.
618
- // Re-derive state and attempt a fresh dispatch.
619
- if (verbose) {
435
+ if (s.verbose) {
620
436
  ctx.ui.notify(
621
437
  "Dispatch gap detected — re-evaluating state.",
622
438
  "info",
@@ -631,39 +447,35 @@ function startDispatchGapWatchdog(ctx: ExtensionContext, pi: ExtensionAPI): void
631
447
  return;
632
448
  }
633
449
 
634
- // If dispatchNextUnit returned normally but still didn't dispatch a unit
635
- // (no sendMessage called → no timeout set), auto-mode is permanently
636
- // stalled. Stop cleanly instead of leaving it active but idle (#537).
637
- if (active && !unitTimeoutHandle && !wrapupWarningHandle) {
450
+ if (s.active && !s.unitTimeoutHandle && !s.wrapupWarningHandle) {
638
451
  await stopAuto(ctx, pi, "Stalled — no dispatchable unit after retry");
639
452
  }
640
453
  }, DISPATCH_GAP_TIMEOUT_MS);
641
454
  }
642
455
 
643
456
  export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason?: string): Promise<void> {
644
- if (!active && !paused) return;
457
+ if (!s.active && !s.paused) return;
645
458
  const reasonSuffix = reason ? ` — ${reason}` : "";
646
459
  clearUnitTimeout();
647
- if (lockBase()) clearLock(lockBase());
460
+ if (lockBase()) {
461
+ releaseSessionLock(lockBase());
462
+ clearLock(lockBase());
463
+ }
648
464
  clearSkillSnapshot();
649
465
  resetSkillTelemetry();
650
- _dispatching = false;
651
- _skipDepth = 0;
466
+ s.dispatching = false;
467
+ s.skipDepth = 0;
652
468
 
653
469
  // Remove SIGTERM handler registered at auto-mode start
654
470
  deregisterSigtermHandler();
655
471
 
656
- // ── Auto-worktree: exit worktree and reset basePath on stop ──
657
- // Preserve the milestone branch so the next /gsd auto can re-enter
658
- // where it left off. The branch is only deleted during milestone
659
- // completion (mergeMilestoneToMain) after the work has been squash-merged.
660
- if (currentMilestoneId && isInAutoWorktree(basePath)) {
472
+ // ── Auto-worktree: exit worktree and reset s.basePath on stop ──
473
+ if (s.currentMilestoneId && isInAutoWorktree(s.basePath)) {
661
474
  try {
662
- // Auto-commit any dirty state before leaving so work isn't lost
663
- try { autoCommitCurrentBranch(basePath, "stop", currentMilestoneId); } catch { /* non-fatal */ }
664
- teardownAutoWorktree(originalBasePath, currentMilestoneId, { preserveBranch: true });
665
- basePath = originalBasePath;
666
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
475
+ try { autoCommitCurrentBranch(s.basePath, "stop", s.currentMilestoneId); } catch (e) { debugLog("stop-auto-commit-failed", { error: e instanceof Error ? e.message : String(e) }); }
476
+ teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId, { preserveBranch: true });
477
+ s.basePath = s.originalBasePath;
478
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
667
479
  ctx?.ui.notify("Exited auto-worktree (branch preserved for resume).", "info");
668
480
  } catch (err) {
669
481
  ctx?.ui.notify(
@@ -678,16 +490,12 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
678
490
  try {
679
491
  const { closeDatabase } = await import("./gsd-db.js");
680
492
  closeDatabase();
681
- } catch { /* non-fatal */ }
493
+ } catch (e) { debugLog("db-close-failed", { error: e instanceof Error ? e.message : String(e) }); }
682
494
  }
683
495
 
684
- // Always restore cwd to project root on stop (#608).
685
- // Even if isInAutoWorktree returned false (e.g., module state was already
686
- // cleared by mergeMilestoneToMain), the process cwd may still be inside
687
- // the worktree directory. Force it back to originalBasePath.
688
- if (originalBasePath) {
689
- basePath = originalBasePath;
690
- try { process.chdir(basePath); } catch { /* best-effort */ }
496
+ if (s.originalBasePath) {
497
+ s.basePath = s.originalBasePath;
498
+ try { process.chdir(s.basePath); } catch { /* best-effort */ }
691
499
  }
692
500
 
693
501
  const ledger = getLedger();
@@ -701,12 +509,10 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
701
509
  ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
702
510
  }
703
511
 
704
- // Sync disk state so next resume starts from accurate state
705
- if (basePath) {
706
- try { await rebuildState(basePath); } catch { /* non-fatal */ }
512
+ if (s.basePath) {
513
+ try { await rebuildState(s.basePath); } catch (e) { debugLog("stop-rebuild-state-failed", { error: e instanceof Error ? e.message : String(e) }); }
707
514
  }
708
515
 
709
- // Write debug summary before resetting state
710
516
  if (isDebugEnabled()) {
711
517
  const logPath = writeDebugSummary();
712
518
  if (logPath) {
@@ -717,43 +523,44 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
717
523
  resetMetrics();
718
524
  resetRoutingHistory();
719
525
  resetHookState();
720
- if (basePath) clearPersistedHookState(basePath);
721
- active = false;
722
- paused = false;
723
- stepMode = false;
724
- unitDispatchCount.clear();
725
- unitRecoveryCount.clear();
726
- unitConsecutiveSkips.clear();
727
- inFlightTools.clear();
728
- lastBudgetAlertLevel = 0;
729
- unitLifetimeDispatches.clear();
730
- currentUnit = null;
731
- autoModeStartModel = null;
732
- currentMilestoneId = null;
733
- originalBasePath = "";
734
- completedUnits = [];
735
- pendingQuickTasks = [];
526
+ if (s.basePath) clearPersistedHookState(s.basePath);
527
+ s.active = false;
528
+ s.paused = false;
529
+ s.stepMode = false;
530
+ s.unitDispatchCount.clear();
531
+ s.unitRecoveryCount.clear();
532
+ s.unitConsecutiveSkips.clear();
533
+ clearInFlightTools();
534
+ s.lastBudgetAlertLevel = 0;
535
+ s.lastStateRebuildAt = 0;
536
+ s.unitLifetimeDispatches.clear();
537
+ s.currentUnit = null;
538
+ s.autoModeStartModel = null;
539
+ s.currentMilestoneId = null;
540
+ s.originalBasePath = "";
541
+ s.completedUnits = [];
542
+ s.pendingQuickTasks = [];
736
543
  clearSliceProgressCache();
737
544
  clearActivityLogState();
738
545
  resetProactiveHealing();
739
- pendingCrashRecovery = null;
740
- pendingVerificationRetry = null;
741
- verificationRetryCount.clear();
742
- pausedSessionFile = null;
743
- _handlingAgentEnd = false;
546
+ s.recentlyEvictedKeys.clear();
547
+ s.pendingCrashRecovery = null;
548
+ s.pendingVerificationRetry = null;
549
+ s.verificationRetryCount.clear();
550
+ s.pausedSessionFile = null;
551
+ s.handlingAgentEnd = false;
744
552
  ctx?.ui.setStatus("gsd-auto", undefined);
745
553
  ctx?.ui.setWidget("gsd-progress", undefined);
746
554
  ctx?.ui.setFooter(undefined);
747
555
 
748
- // Restore the user's original model
749
- if (pi && ctx && originalModelId && originalModelProvider) {
750
- const original = ctx.modelRegistry.find(originalModelProvider, originalModelId);
556
+ if (pi && ctx && s.originalModelId && s.originalModelProvider) {
557
+ const original = ctx.modelRegistry.find(s.originalModelProvider, s.originalModelId);
751
558
  if (original) await pi.setModel(original);
752
- originalModelId = null;
753
- originalModelProvider = null;
559
+ s.originalModelId = null;
560
+ s.originalModelProvider = null;
754
561
  }
755
562
 
756
- cmdCtx = null;
563
+ s.cmdCtx = null;
757
564
  }
758
565
 
759
566
  /**
@@ -762,31 +569,28 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI, reason
762
569
  * from disk state. Called when the user presses Escape during auto-mode.
763
570
  */
764
571
  export async function pauseAuto(ctx?: ExtensionContext, _pi?: ExtensionAPI): Promise<void> {
765
- if (!active) return;
572
+ if (!s.active) return;
766
573
  clearUnitTimeout();
767
574
 
768
- // Capture the current session file before clearing state — used for
769
- // recovery briefing on resume so the next agent knows what already happened.
770
- pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
575
+ s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
771
576
 
772
- if (lockBase()) clearLock(lockBase());
577
+ if (lockBase()) {
578
+ releaseSessionLock(lockBase());
579
+ clearLock(lockBase());
580
+ }
773
581
 
774
- // Remove SIGTERM handler registered at auto-mode start
775
582
  deregisterSigtermHandler();
776
583
 
777
- active = false;
778
- paused = true;
779
- pendingVerificationRetry = null;
780
- verificationRetryCount.clear();
781
- // Preserve: unitDispatchCount, currentUnit, basePath, verbose, cmdCtx,
782
- // completedUnits, autoStartTime, currentMilestoneId, originalModelId
783
- // — all needed for resume and dashboard display
584
+ s.active = false;
585
+ s.paused = true;
586
+ s.pendingVerificationRetry = null;
587
+ s.verificationRetryCount.clear();
784
588
  ctx?.ui.setStatus("gsd-auto", "paused");
785
589
  ctx?.ui.setWidget("gsd-progress", undefined);
786
590
  ctx?.ui.setFooter(undefined);
787
- const resumeCmd = stepMode ? "/gsd next" : "/gsd auto";
591
+ const resumeCmd = s.stepMode ? "/gsd next" : "/gsd auto";
788
592
  ctx?.ui.notify(
789
- `${stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`,
593
+ `${s.stepMode ? "Step" : "Auto"}-mode paused (Escape). Type to interact, or ${resumeCmd} to resume.`,
790
594
  "info",
791
595
  );
792
596
  }
@@ -802,44 +606,45 @@ export async function startAuto(
802
606
  const requestedStepMode = options?.step ?? false;
803
607
 
804
608
  // Escape stale worktree cwd from a previous milestone (#608).
805
- // After milestone merge + worktree removal, the process cwd may still point
806
- // inside .gsd/worktrees/<MID>/ — detect and chdir back to project root.
807
609
  base = escapeStaleWorktree(base);
808
610
 
809
611
  // If resuming from paused state, just re-activate and dispatch next unit.
810
- // The conversation is still intact — no need to reinitialize everything.
811
- if (paused) {
812
- paused = false;
813
- active = true;
814
- verbose = verboseMode;
815
- // Allow switching between step/auto on resume
816
- stepMode = requestedStepMode;
817
- cmdCtx = ctx;
818
- basePath = base;
819
- unitDispatchCount.clear();
820
- unitLifetimeDispatches.clear();
821
- unitConsecutiveSkips.clear();
822
- // Re-initialize metrics in case ledger was lost during pause
612
+ if (s.paused) {
613
+ // Re-acquire session lock before resuming
614
+ const resumeLock = acquireSessionLock(base);
615
+ if (!resumeLock.acquired) {
616
+ ctx.ui.notify(
617
+ `Cannot resume: ${resumeLock.reason}`,
618
+ "error",
619
+ );
620
+ return;
621
+ }
622
+
623
+ s.paused = false;
624
+ s.active = true;
625
+ s.verbose = verboseMode;
626
+ s.stepMode = requestedStepMode;
627
+ s.cmdCtx = ctx;
628
+ s.basePath = base;
629
+ s.unitDispatchCount.clear();
630
+ s.unitLifetimeDispatches.clear();
631
+ s.unitConsecutiveSkips.clear();
823
632
  if (!getLedger()) initMetrics(base);
824
- // Ensure milestone ID is set on git service for integration branch resolution
825
- if (currentMilestoneId) setActiveMilestoneId(base, currentMilestoneId);
633
+ if (s.currentMilestoneId) setActiveMilestoneId(base, s.currentMilestoneId);
826
634
 
827
- // ── Auto-worktree: re-enter worktree on resume if not already inside ──
828
- // Skip if already inside a worktree (manual /worktree) to prevent nesting.
829
- // Skip entirely in branch or none isolation mode (#531).
830
- if (currentMilestoneId && shouldUseWorktreeIsolation() && originalBasePath && !isInAutoWorktree(basePath) && !detectWorktreeName(basePath) && !detectWorktreeName(originalBasePath)) {
635
+ // ── Auto-worktree: re-enter worktree on resume ──
636
+ if (s.currentMilestoneId && shouldUseWorktreeIsolation() && s.originalBasePath && !isInAutoWorktree(s.basePath) && !detectWorktreeName(s.basePath) && !detectWorktreeName(s.originalBasePath)) {
831
637
  try {
832
- const existingWtPath = getAutoWorktreePath(originalBasePath, currentMilestoneId);
638
+ const existingWtPath = getAutoWorktreePath(s.originalBasePath, s.currentMilestoneId);
833
639
  if (existingWtPath) {
834
- const wtPath = enterAutoWorktree(originalBasePath, currentMilestoneId);
835
- basePath = wtPath;
836
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
640
+ const wtPath = enterAutoWorktree(s.originalBasePath, s.currentMilestoneId);
641
+ s.basePath = wtPath;
642
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
837
643
  ctx.ui.notify(`Re-entered auto-worktree at ${wtPath}`, "info");
838
644
  } else {
839
- // Worktree was deleted while paused — recreate it.
840
- const wtPath = createAutoWorktree(originalBasePath, currentMilestoneId);
841
- basePath = wtPath;
842
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
645
+ const wtPath = createAutoWorktree(s.originalBasePath, s.currentMilestoneId);
646
+ s.basePath = wtPath;
647
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
843
648
  ctx.ui.notify(`Recreated auto-worktree at ${wtPath}`, "info");
844
649
  }
845
650
  } catch (err) {
@@ -850,1276 +655,243 @@ export async function startAuto(
850
655
  }
851
656
  }
852
657
 
853
- // Re-register SIGTERM handler for the resumed session (use original base for lock)
854
658
  registerSigtermHandler(lockBase());
855
659
 
856
- ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
660
+ ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
857
661
  ctx.ui.setFooter(hideFooter);
858
- ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
859
- // Restore hook state from disk in case session was interrupted
860
- restoreHookState(basePath);
861
- // Rebuild disk state before resuming — user interaction during pause may have changed files
862
- try { await rebuildState(basePath); } catch { /* non-fatal */ }
662
+ ctx.ui.notify(s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
663
+ restoreHookState(s.basePath);
664
+ try { await rebuildState(s.basePath); } catch (e) { debugLog("resume-rebuild-state-failed", { error: e instanceof Error ? e.message : String(e) }); }
863
665
  try {
864
- const report = await runGSDDoctor(basePath, { fix: true });
666
+ const report = await runGSDDoctor(s.basePath, { fix: true });
865
667
  if (report.fixesApplied.length > 0) {
866
668
  ctx.ui.notify(`Resume: applied ${report.fixesApplied.length} fix(es) to state.`, "info");
867
669
  }
868
- } catch { /* non-fatal */ }
869
- // Self-heal: clear stale runtime records where artifacts already exist
870
- await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
670
+ } catch (e) { debugLog("resume-doctor-failed", { error: e instanceof Error ? e.message : String(e) }); }
671
+ await selfHealRuntimeRecords(s.basePath, ctx, s.completedKeySet);
871
672
  invalidateAllCaches();
872
673
 
873
- // Synthesize recovery briefing from the paused session so the next agent
874
- // knows what already happened (reuses crash recovery infrastructure).
875
- if (pausedSessionFile) {
876
- const activityDir = join(gsdRoot(basePath), "activity");
674
+ if (s.pausedSessionFile) {
675
+ const activityDir = join(gsdRoot(s.basePath), "activity");
877
676
  const recovery = synthesizeCrashRecovery(
878
- basePath,
879
- currentUnit?.type ?? "unknown",
880
- currentUnit?.id ?? "unknown",
881
- pausedSessionFile,
677
+ s.basePath,
678
+ s.currentUnit?.type ?? "unknown",
679
+ s.currentUnit?.id ?? "unknown", s.pausedSessionFile ?? undefined,
882
680
  activityDir,
883
681
  );
884
682
  if (recovery && recovery.trace.toolCallCount > 0) {
885
- pendingCrashRecovery = recovery.prompt;
683
+ s.pendingCrashRecovery = recovery.prompt;
886
684
  ctx.ui.notify(
887
685
  `Recovered ${recovery.trace.toolCallCount} tool calls from paused session. Resuming with context.`,
888
686
  "info",
889
687
  );
890
688
  }
891
- pausedSessionFile = null;
689
+ s.pausedSessionFile = null;
892
690
  }
893
691
 
894
- // Write lock on resume so cross-process status detection works (#723).
895
- writeLock(lockBase(), "resuming", currentMilestoneId ?? "unknown", completedUnits.length);
896
-
897
- await dispatchNextUnit(ctx, pi);
898
- return;
899
- }
900
-
901
- // Ensure git repo exists — GSD needs it for commits and state tracking
902
- if (!nativeIsRepo(base)) {
903
- const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
904
- nativeInit(base, mainBranch);
905
- }
906
-
907
- // Ensure .gitignore has baseline patterns
908
- const commitDocs = loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs;
909
- ensureGitignore(base, { commitDocs });
910
- untrackRuntimeFiles(base);
911
-
912
- // Bootstrap .gsd/ if it doesn't exist
913
- const gsdDir = join(base, ".gsd");
914
- if (!existsSync(gsdDir)) {
915
- mkdirSync(join(gsdDir, "milestones"), { recursive: true });
916
- // Only commit .gsd/ init when commit_docs is not explicitly false
917
- if (commitDocs !== false) {
692
+ // If resuming from a secrets pause, re-collect before dispatching (#1146)
693
+ if (s.pausedForSecrets && s.currentMilestoneId) {
918
694
  try {
919
- nativeAddPaths(base, [".gsd", ".gitignore"]);
920
- nativeCommit(base, "chore: init gsd");
921
- } catch { /* nothing to commit */ }
922
- }
923
- }
924
-
925
- // Initialize GitServiceImpl — basePath is set and git repo confirmed
926
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
927
-
928
- // Check for crash from previous session
929
- const crashLock = readCrashLock(base);
930
- if (crashLock) {
931
- if (isLockProcessAlive(crashLock)) {
932
- // The lock belongs to a process that is still running not a crash.
933
- // Warn the user and abort to avoid two concurrent auto-mode sessions.
934
- ctx.ui.notify(
935
- `Another auto-mode session (PID ${crashLock.pid}) appears to be running.\nStop it with \`kill ${crashLock.pid}\` before starting a new session.`,
936
- "error",
937
- );
938
- return;
939
- }
940
- // Stale lock from a dead process — validate before synthesizing recovery context.
941
- // If the recovered unit belongs to a fully-completed milestone (SUMMARY exists),
942
- // discard recovery context to prevent phantom skip loops (#790).
943
- const recoveredMid = crashLock.unitId.split("/")[0];
944
- const milestoneAlreadyComplete = recoveredMid
945
- ? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
946
- : false;
947
-
948
- if (milestoneAlreadyComplete) {
949
- ctx.ui.notify(
950
- `Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`,
951
- "info",
952
- );
953
- } else {
954
- const activityDir = join(gsdRoot(base), "activity");
955
- const recovery = synthesizeCrashRecovery(
956
- base, crashLock.unitType, crashLock.unitId,
957
- crashLock.sessionFile, activityDir,
958
- );
959
- if (recovery && recovery.trace.toolCallCount > 0) {
960
- pendingCrashRecovery = recovery.prompt;
961
- ctx.ui.notify(
962
- `${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
963
- "warning",
964
- );
965
- } else {
695
+ const manifestStatus = await getManifestStatus(s.basePath, s.currentMilestoneId);
696
+ if (manifestStatus && manifestStatus.pending.length > 0) {
697
+ const result = await collectSecretsFromManifest(s.basePath, s.currentMilestoneId, ctx);
698
+ if (result && result.applied.length > 0) {
699
+ ctx.ui.notify(
700
+ `Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
701
+ "info",
702
+ );
703
+ } else if (result && result.applied.length === 0 && result.skipped.length > 0) {
704
+ // All keys were skipped still pending, re-pause
705
+ s.paused = true;
706
+ s.active = false;
707
+ ctx.ui.notify(
708
+ `All env variables were skipped. Auto-mode remains paused.\nCollect them with /gsd secrets, then resume with /gsd auto.`,
709
+ "warning",
710
+ );
711
+ ctx.ui.setStatus("gsd-auto", "paused");
712
+ return;
713
+ }
714
+ }
715
+ } catch (err) {
966
716
  ctx.ui.notify(
967
- `${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
717
+ `Secrets check error: ${err instanceof Error ? err.message : String(err)}. Continuing without secrets.`,
968
718
  "warning",
969
719
  );
970
720
  }
721
+ s.pausedForSecrets = false;
971
722
  }
972
- clearLock(base);
973
- }
974
723
 
975
- // ── Debug mode: env-var activation ──────────────────────────────────────
976
- if (!isDebugEnabled() && process.env.GSD_DEBUG === "1") {
977
- enableDebug(base);
978
- }
979
- if (isDebugEnabled()) {
980
- const { isNativeParserAvailable } = await import("./native-parser-bridge.js");
981
- debugLog("debug-start", {
982
- platform: process.platform,
983
- arch: process.arch,
984
- node: process.version,
985
- model: ctx.model?.id ?? "unknown",
986
- provider: ctx.model?.provider ?? "unknown",
987
- nativeParser: isNativeParserAvailable(),
988
- cwd: base,
989
- });
990
- ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
991
- }
724
+ updateSessionLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
725
+ writeLock(lockBase(), "resuming", s.currentMilestoneId ?? "unknown", s.completedUnits.length);
992
726
 
993
- // Invalidate all caches before initial state derivation to ensure we read
994
- // fresh disk state. Without this, a stale cache from a prior session (e.g.
995
- // after a discussion that wrote new artifacts) may cause deriveState to
996
- // return pre-planning when the roadmap already exists (#800).
997
- invalidateAllCaches();
998
- let state = await deriveState(base);
999
-
1000
- // ── Stale worktree state recovery (#654) ─────────────────────────────────
1001
- // When auto-mode was previously stopped and restarted, the project root's
1002
- // .gsd/ directory may have stale metadata (completed units showing as
1003
- // incomplete). If an auto-worktree exists for the active milestone, it has
1004
- // the current state — re-derive from there to avoid re-dispatching
1005
- // finished work.
1006
- if (
1007
- state.activeMilestone &&
1008
- shouldUseWorktreeIsolation() &&
1009
- !detectWorktreeName(base)
1010
- ) {
1011
- const wtPath = getAutoWorktreePath(base, state.activeMilestone.id);
1012
- if (wtPath) {
1013
- state = await deriveState(wtPath);
1014
- }
727
+ await dispatchNextUnit(ctx, pi);
728
+ return;
1015
729
  }
1016
730
 
1017
- // ── Milestone branch recovery (#601) ─────────────────────────────────────
1018
- // When auto-mode was previously stopped, the milestone branch is preserved
1019
- // but the worktree is removed. The project root (integration branch) may
1020
- // not have the roadmap/artifacts — they live on the milestone branch.
1021
- // If state looks like pre-planning but a milestone branch exists with prior
1022
- // work, skip the early-return checks and let worktree setup + dispatch
1023
- // handle it correctly from the branch's state.
1024
- let hasSurvivorBranch = false;
1025
- if (
1026
- state.activeMilestone &&
1027
- (state.phase === "pre-planning" || state.phase === "needs-discussion") &&
1028
- shouldUseWorktreeIsolation() &&
1029
- !detectWorktreeName(base) &&
1030
- !base.includes(`${pathSep}.gsd${pathSep}worktrees${pathSep}`)
1031
- ) {
1032
- const milestoneBranch = `milestone/${state.activeMilestone.id}`;
1033
- const { nativeBranchExists } = await import("./native-git-bridge.js");
1034
- hasSurvivorBranch = nativeBranchExists(base, milestoneBranch);
1035
- if (hasSurvivorBranch) {
1036
- ctx.ui.notify(
1037
- `Found prior session branch ${milestoneBranch}. Resuming.`,
1038
- "info",
1039
- );
1040
- }
1041
- }
731
+ // ── Fresh start path delegated to auto-start.ts ──
732
+ const bootstrapDeps: BootstrapDeps = {
733
+ shouldUseWorktreeIsolation,
734
+ registerSigtermHandler,
735
+ lockBase,
736
+ };
1042
737
 
1043
- if (!hasSurvivorBranch) {
1044
- // No active work at all — start a new milestone via the discuss flow.
1045
- // After discussion completes, checkAutoStartAfterDiscuss() (fired from
1046
- // agent_end) will detect the new CONTEXT.md and restart auto mode.
1047
- // If the LLM didn't follow the discussion protocol (e.g. started editing
1048
- // files directly for a simple task), we re-derive state and either proceed
1049
- // with what was created or notify the user clearly (#609).
1050
- if (!state.activeMilestone || state.phase === "complete") {
1051
- const { showSmartEntry } = await import("./guided-flow.js");
1052
- await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
1053
-
1054
- // Re-derive state after discussion — the LLM may have created artifacts
1055
- // even if it didn't follow the full protocol.
1056
- invalidateAllCaches();
1057
- const postState = await deriveState(base);
1058
- if (postState.activeMilestone && postState.phase !== "complete" && postState.phase !== "pre-planning") {
1059
- state = postState;
1060
- } else if (postState.activeMilestone && postState.phase === "pre-planning") {
1061
- const contextFile = resolveMilestoneFile(base, postState.activeMilestone.id, "CONTEXT");
1062
- const hasContext = !!(contextFile && await loadFile(contextFile));
1063
- if (hasContext) {
1064
- state = postState;
1065
- } else {
1066
- ctx.ui.notify(
1067
- "Discussion completed but no milestone context was written. Run /gsd to try the discussion again, or /gsd auto after creating the milestone manually.",
1068
- "warning",
1069
- );
1070
- return;
1071
- }
1072
- } else {
1073
- return;
1074
- }
1075
- }
738
+ const ready = await bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, bootstrapDeps);
739
+ if (!ready) return;
1076
740
 
1077
- // Active milestone exists but has no roadmap — check if context exists.
1078
- // If context was pre-written (multi-milestone planning), auto-mode can
1079
- // research and plan it. If no context either, need user discussion.
1080
- if (state.phase === "pre-planning") {
1081
- const mid = state.activeMilestone!.id;
1082
- const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
1083
- const hasContext = !!(contextFile && await loadFile(contextFile));
1084
- if (!hasContext) {
1085
- const { showSmartEntry } = await import("./guided-flow.js");
1086
- await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
1087
-
1088
- // Same re-derive pattern as above
1089
- invalidateAllCaches();
1090
- const postState = await deriveState(base);
1091
- if (postState.activeMilestone && postState.phase !== "pre-planning") {
1092
- state = postState;
1093
- } else {
1094
- ctx.ui.notify(
1095
- "Discussion completed but milestone context is still missing. Run /gsd to try again.",
1096
- "warning",
1097
- );
1098
- return;
1099
- }
1100
- }
1101
- // Has context, no roadmap — auto-mode will research + plan it
1102
- }
1103
- }
741
+ // Dispatch the first unit
742
+ await dispatchNextUnit(ctx, pi);
743
+ }
1104
744
 
1105
- // At this point activeMilestone is guaranteed non-null: either
1106
- // hasSurvivorBranch is true (which requires activeMilestone) or
1107
- // the !activeMilestone early-return above would have fired.
1108
- if (!state.activeMilestone) {
1109
- // Unreachable — satisfies TypeScript's null check
1110
- const { showSmartEntry } = await import("./guided-flow.js");
1111
- await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
1112
- return;
1113
- }
745
+ // ─── Agent End Handler ────────────────────────────────────────────────────────
1114
746
 
1115
- active = true;
1116
- stepMode = requestedStepMode;
1117
- verbose = verboseMode;
1118
- cmdCtx = ctx;
1119
- basePath = base;
1120
- unitDispatchCount.clear();
1121
- unitRecoveryCount.clear();
1122
- unitConsecutiveSkips.clear();
1123
- lastBudgetAlertLevel = 0;
1124
- unitLifetimeDispatches.clear();
1125
- completedKeySet.clear();
1126
- loadPersistedKeys(base, completedKeySet);
1127
- resetHookState();
1128
- restoreHookState(base);
1129
- resetProactiveHealing();
1130
- autoStartTime = Date.now();
1131
- resourceVersionOnStart = readResourceVersion();
1132
- completedUnits = [];
1133
- pendingQuickTasks = [];
1134
- currentUnit = null;
1135
- currentMilestoneId = state.activeMilestone?.id ?? null;
1136
- originalModelId = ctx.model?.id ?? null;
1137
- originalModelProvider = ctx.model?.provider ?? null;
1138
-
1139
- // Register a SIGTERM handler so `kill <pid>` cleans up the lock and exits.
1140
- registerSigtermHandler(base);
1141
-
1142
- // Capture the integration branch — records the branch the user was on when
1143
- // auto-mode started. Slice branches will merge back to this branch instead
1144
- // of the repo's default (main/master). Idempotent when the branch is the
1145
- // same; updates the record when started from a different branch (#300).
1146
- if (currentMilestoneId) {
1147
- if (getIsolationMode() !== "none") {
1148
- captureIntegrationBranch(base, currentMilestoneId, { commitDocs });
1149
- }
1150
- setActiveMilestoneId(base, currentMilestoneId);
747
+ /** Guard against concurrent handleAgentEnd execution. */
748
+
749
+ export async function handleAgentEnd(
750
+ ctx: ExtensionContext,
751
+ pi: ExtensionAPI,
752
+ ): Promise<void> {
753
+ if (!s.active || !s.cmdCtx) return;
754
+ if (s.handlingAgentEnd) {
755
+ // Another agent_end arrived while we're still processing the previous one.
756
+ // This happens when a unit dispatched inside handleAgentEnd (e.g. via hooks,
757
+ // triage, or quick-task early-dispatch paths) completes before the outer
758
+ // handleAgentEnd returns. Queue a retry so the completed unit's agent_end
759
+ // is not silently dropped (#1072).
760
+ s.pendingAgentEndRetry = true;
761
+ return;
1151
762
  }
763
+ s.handlingAgentEnd = true;
1152
764
 
1153
- // ── Auto-worktree: create or enter worktree for the active milestone ──
1154
- // Store the original project root before any chdir so we can restore on stop.
1155
- // Skip if already inside a worktree (manual /worktree or another auto-worktree)
1156
- // to prevent nested worktree creation.
1157
- originalBasePath = base;
1158
-
1159
- const isUnderGsdWorktrees = (p: string): boolean => {
1160
- // Prevent creating nested auto-worktrees when running from within any
1161
- // `.gsd/worktrees/...` directory (including manual worktrees).
1162
- const marker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
1163
- if (p.includes(marker)) {
1164
- return true;
1165
- }
1166
- const worktreesSuffix = `${pathSep}.gsd${pathSep}worktrees`;
1167
- return p.endsWith(worktreesSuffix);
1168
- };
765
+ try {
1169
766
 
1170
- if (currentMilestoneId && shouldUseWorktreeIsolation() && !detectWorktreeName(base) && !isUnderGsdWorktrees(base)) {
1171
- try {
1172
- const existingWtPath = getAutoWorktreePath(base, currentMilestoneId);
1173
- if (existingWtPath) {
1174
- // Worktree already exists (e.g., previous session created it) — enter it.
1175
- const wtPath = enterAutoWorktree(base, currentMilestoneId);
1176
- basePath = wtPath;
1177
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1178
- ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
1179
- } else {
1180
- // Fresh start — create worktree and enter it.
1181
- const wtPath = createAutoWorktree(base, currentMilestoneId);
1182
- basePath = wtPath;
1183
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1184
- ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
1185
- }
1186
- // Re-register SIGTERM handler with the original basePath (lock lives there)
1187
- registerSigtermHandler(originalBasePath);
1188
-
1189
- // After worktree entry, load completed keys from BOTH locations (project root
1190
- // + worktree) so the in-memory set is the union. Prevents re-dispatch of units
1191
- // completed in either location after crash/restart (#769).
1192
- if (basePath !== originalBasePath) {
1193
- loadPersistedKeys(basePath, completedKeySet);
1194
- }
1195
- } catch (err) {
1196
- // Worktree creation is non-fatal — continue in the project root.
1197
- ctx.ui.notify(
1198
- `Auto-worktree setup failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
1199
- "warning",
1200
- );
1201
- }
1202
- }
767
+ // Unit completed clear its timeout
768
+ clearUnitTimeout();
1203
769
 
1204
- // ── DB lifecycle: auto-migrate or open existing database ──
1205
- const gsdDbPath = join(basePath, ".gsd", "gsd.db");
1206
- const gsdDirPath = join(basePath, ".gsd");
1207
- if (existsSync(gsdDirPath) && !existsSync(gsdDbPath)) {
1208
- const hasDecisions = existsSync(join(gsdDirPath, "DECISIONS.md"));
1209
- const hasRequirements = existsSync(join(gsdDirPath, "REQUIREMENTS.md"));
1210
- const hasMilestones = existsSync(join(gsdDirPath, "milestones"));
1211
- if (hasDecisions || hasRequirements || hasMilestones) {
1212
- try {
1213
- const { openDatabase: openDb } = await import("./gsd-db.js");
1214
- const { migrateFromMarkdown } = await import("./md-importer.js");
1215
- openDb(gsdDbPath);
1216
- migrateFromMarkdown(basePath);
1217
- } catch (err) {
1218
- process.stderr.write(`gsd-migrate: auto-migration failed: ${(err as Error).message}\n`);
1219
- }
1220
- }
1221
- }
1222
- if (existsSync(gsdDbPath) && !isDbAvailable()) {
1223
- try {
1224
- const { openDatabase: openDb } = await import("./gsd-db.js");
1225
- openDb(gsdDbPath);
1226
- } catch (err) {
1227
- process.stderr.write(`gsd-db: failed to open existing database: ${(err as Error).message}\n`);
1228
- }
1229
- }
770
+ // ── Pre-verification processing (commit, doctor, state rebuild, etc.) ──
771
+ const postUnitCtx: PostUnitContext = {
772
+ s,
773
+ ctx,
774
+ pi,
775
+ buildSnapshotOpts,
776
+ lockBase,
777
+ stopAuto,
778
+ pauseAuto,
779
+ updateProgressWidget,
780
+ };
1230
781
 
1231
- // Initialize metrics loads existing ledger from disk.
1232
- // Use basePath (not base) so worktree-mode reads the worktree ledger (#769).
1233
- initMetrics(basePath);
782
+ const preResult = await postUnitPreVerification(postUnitCtx);
783
+ if (preResult === "dispatched") return;
1234
784
 
1235
- // Initialize routing history for adaptive learning
1236
- initRoutingHistory(basePath);
785
+ // ── Verification gate: run typecheck/lint/test after execute-task ──
786
+ const verificationResult = await runPostUnitVerification(
787
+ { s, ctx, pi },
788
+ dispatchNextUnit,
789
+ startDispatchGapWatchdog,
790
+ pauseAuto,
791
+ );
792
+ if (verificationResult === "retry" || verificationResult === "pause") return;
1237
793
 
1238
- // Capture the session's current model at auto-mode start (#650).
1239
- // This prevents model bleed when multiple GSD instances share the
1240
- // same global settings.json each instance remembers its own model.
1241
- const currentModel = ctx.model;
1242
- if (currentModel) {
1243
- autoModeStartModel = { provider: currentModel.provider, id: currentModel.id };
794
+ // ── Post-verification processing (DB dual-write, hooks, triage, quick-tasks) ──
795
+ const postResult = await postUnitPostVerification(postUnitCtx);
796
+ if (postResult === "dispatched" || postResult === "stopped") return;
797
+ if (postResult === "step-wizard") {
798
+ await showStepWizard(ctx, pi);
799
+ return;
1244
800
  }
1245
801
 
1246
- // Snapshot installed skills so we can detect new ones after research
1247
- if (resolveSkillDiscoveryMode() !== "off") {
1248
- snapshotSkills();
1249
- }
802
+ // ── Dispatch with hang detection (#1073) ────────────────────────────────
803
+ // Start a safety watchdog BEFORE calling dispatchNextUnit. If dispatch
804
+ // hangs at any await (newSession, model selection, etc.), the gap watchdog
805
+ // inside handleAgentEnd never fires because we never reach the check.
806
+ // This pre-dispatch watchdog ensures recovery even when dispatchNextUnit
807
+ // itself is permanently blocked.
808
+ const dispatchHangGuard = setTimeout(() => {
809
+ if (!s.active) return;
810
+ // dispatchNextUnit has been running for too long — it's likely hung.
811
+ // Start the gap watchdog which will retry dispatch from scratch.
812
+ if (!s.unitTimeoutHandle && !s.wrapupWarningHandle) {
813
+ ctx.ui.notify(
814
+ `Dispatch hang detected (${DISPATCH_HANG_TIMEOUT_MS / 1000}s without completion). Starting recovery watchdog.`,
815
+ "warning",
816
+ );
817
+ startDispatchGapWatchdog(ctx, pi);
818
+ }
819
+ }, DISPATCH_HANG_TIMEOUT_MS);
1250
820
 
1251
- ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
1252
- ctx.ui.setFooter(hideFooter);
1253
- const modeLabel = stepMode ? "Step-mode" : "Auto-mode";
1254
- const pendingCount = state.registry.filter(m => m.status !== 'complete').length;
1255
- const scopeMsg = pendingCount > 1
1256
- ? `Will loop through ${pendingCount} milestones.`
1257
- : "Will loop until milestone complete.";
1258
- ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
1259
-
1260
- // Write initial lock file immediately so cross-process status detection
1261
- // works even before the first unit is dispatched (#723).
1262
- // The lock is updated with unit-specific info on each dispatch and cleared on stop.
1263
- writeLock(lockBase(), "starting", currentMilestoneId ?? "unknown", 0);
1264
-
1265
- // Secrets collection gate — collect pending secrets before first dispatch
1266
- const mid = state.activeMilestone!.id;
1267
821
  try {
1268
- const manifestStatus = await getManifestStatus(base, mid);
1269
- if (manifestStatus && manifestStatus.pending.length > 0) {
1270
- const result = await collectSecretsFromManifest(base, mid, ctx);
1271
- if (result && result.applied && result.skipped && result.existingSkipped) {
1272
- ctx.ui.notify(
1273
- `Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
1274
- "info",
1275
- );
1276
- } else {
1277
- ctx.ui.notify("Secrets collection skipped.", "info");
1278
- }
1279
- }
1280
- } catch (err) {
822
+ await dispatchNextUnit(ctx, pi);
823
+ } catch (dispatchErr) {
824
+ const message = dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr);
1281
825
  ctx.ui.notify(
1282
- `Secrets collection error: ${err instanceof Error ? err.message : String(err)}. Continuing with next task.`,
1283
- "warning",
826
+ `Dispatch error after unit completion: ${message}. Retrying in ${DISPATCH_GAP_TIMEOUT_MS / 1000}s.`,
827
+ "error",
1284
828
  );
829
+ startDispatchGapWatchdog(ctx, pi);
830
+ return;
831
+ } finally {
832
+ clearTimeout(dispatchHangGuard);
1285
833
  }
1286
834
 
1287
- // Self-heal: clear stale runtime records where artifacts already exist.
1288
- // Use basePath (not base) — in worktree mode, basePath points to the worktree
1289
- // where runtime records and artifacts actually live (#769).
1290
- await selfHealRuntimeRecords(basePath, ctx, completedKeySet);
1291
-
1292
- // Self-heal: remove stale .git/index.lock from prior crash.
1293
- // A stale lock file blocks all git operations (commit, merge, checkout).
1294
- // Only remove if older than 60 seconds (not from a concurrent process).
1295
- try {
1296
- const gitLockFile = join(base, ".git", "index.lock");
1297
- if (existsSync(gitLockFile)) {
1298
- const lockAge = Date.now() - statSync(gitLockFile).mtimeMs;
1299
- if (lockAge > 60_000) {
1300
- unlinkSync(gitLockFile);
1301
- ctx.ui.notify("Removed stale .git/index.lock from prior crash.", "info");
1302
- }
1303
- }
1304
- } catch { /* non-fatal */ }
835
+ if (s.active && !s.unitTimeoutHandle && !s.wrapupWarningHandle) {
836
+ startDispatchGapWatchdog(ctx, pi);
837
+ }
1305
838
 
1306
- // Pre-flight: validate milestone queue for multi-milestone runs.
1307
- // Warn about issues that will cause auto-mode to pause or block.
1308
- try {
1309
- const msDir = join(base, ".gsd", "milestones");
1310
- if (existsSync(msDir)) {
1311
- const milestoneIds = readdirSync(msDir, { withFileTypes: true })
1312
- .filter(d => d.isDirectory() && /^M\d{3}/.test(d.name))
1313
- .map(d => d.name.match(/^(M\d{3})/)?.[1] ?? d.name);
1314
- if (milestoneIds.length > 1) {
1315
- const issues: string[] = [];
1316
- for (const id of milestoneIds) {
1317
- const draft = resolveMilestoneFile(base, id, "CONTEXT-DRAFT");
1318
- if (draft) issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
1319
- }
1320
- if (issues.length > 0) {
1321
- ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued.\n${issues.map(i => ` ⚠ ${i}`).join("\n")}`, "warning");
1322
- } else {
1323
- ctx.ui.notify(`Pre-flight: ${milestoneIds.length} milestones queued. All have full context.`, "info");
1324
- }
1325
- }
839
+ } finally {
840
+ s.handlingAgentEnd = false;
841
+
842
+ // If an agent_end event was dropped by the reentrancy guard while we were
843
+ // processing, re-enter handleAgentEnd on the next microtask. This prevents
844
+ // the summarizing phase stall (#1072) where a unit dispatched inside
845
+ // handleAgentEnd (hooks, triage, quick-task) completes before we return,
846
+ // and its agent_end is silently dropped — leaving auto-mode active but
847
+ // permanently stalled with no unit running and no watchdog set.
848
+ if (s.pendingAgentEndRetry) {
849
+ s.pendingAgentEndRetry = false;
850
+ setImmediate(() => {
851
+ handleAgentEnd(ctx, pi).catch((err) => {
852
+ const msg = err instanceof Error ? err.message : String(err);
853
+ ctx.ui.notify(`Deferred agent_end retry failed: ${msg}`, "error");
854
+ pauseAuto(ctx, pi).catch(() => {});
855
+ });
856
+ });
1326
857
  }
1327
- } catch { /* non-fatal — pre-flight should never block auto-mode */ }
1328
-
1329
- // Dispatch the first unit
1330
- await dispatchNextUnit(ctx, pi);
858
+ }
1331
859
  }
1332
860
 
1333
- // ─── Agent End Handler ────────────────────────────────────────────────────────
1334
-
1335
- /** Guard against concurrent handleAgentEnd execution. Background job
1336
- * notifications and other system messages can trigger multiple agent_end
1337
- * events before the first handler finishes (the handler yields at every
1338
- * await). Without this guard, concurrent dispatchNextUnit calls race on
1339
- * newSession(), causing one to cancel the other and silently stopping
1340
- * auto-mode. */
1341
- let _handlingAgentEnd = false;
861
+ // ─── Step Mode Wizard ─────────────────────────────────────────────────────
1342
862
 
1343
- export async function handleAgentEnd(
863
+ /**
864
+ * Show the step-mode wizard after a unit completes.
865
+ */
866
+ async function showStepWizard(
1344
867
  ctx: ExtensionContext,
1345
868
  pi: ExtensionAPI,
1346
869
  ): Promise<void> {
1347
- if (!active || !cmdCtx) return;
1348
- if (_handlingAgentEnd) return;
1349
- _handlingAgentEnd = true;
870
+ if (!s.cmdCtx) return;
1350
871
 
1351
- try {
872
+ const state = await deriveState(s.basePath);
873
+ const mid = state.activeMilestone?.id;
1352
874
 
1353
- // Unit completed — clear its timeout
1354
- clearUnitTimeout();
875
+ const justFinished = s.currentUnit
876
+ ? `${unitVerb(s.currentUnit.type)} ${s.currentUnit.id}`
877
+ : "previous unit";
1355
878
 
1356
- // ── Parallel worker signal check ─────────────────────────────────────
1357
- // When running as a parallel worker (GSD_MILESTONE_LOCK set), check for
1358
- // coordinator signals before dispatching the next unit.
1359
- const milestoneLock = process.env.GSD_MILESTONE_LOCK;
1360
- if (milestoneLock) {
1361
- const signal = consumeSignal(basePath, milestoneLock);
1362
- if (signal) {
1363
- if (signal.signal === "stop") {
1364
- _handlingAgentEnd = false;
1365
- await stopAuto(ctx, pi);
1366
- return;
1367
- }
1368
- if (signal.signal === "pause") {
1369
- _handlingAgentEnd = false;
1370
- await pauseAuto(ctx, pi);
1371
- return;
1372
- }
1373
- // "resume" and "rebase" signals are handled elsewhere or no-op here
1374
- }
879
+ if (!mid || state.phase === "complete") {
880
+ const incomplete = state.registry.filter(m => m.status !== "complete" && m.status !== "parked");
881
+ if (incomplete.length > 0 && state.phase !== "complete" && state.phase !== "blocked" && state.phase !== "pre-planning") {
882
+ const ids = incomplete.map(m => m.id).join(", ");
883
+ const diag = `basePath=${s.basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
884
+ ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
885
+ await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids})`);
886
+ } else {
887
+ await stopAuto(ctx, pi, state.phase === "complete" ? "All work complete" : "No active milestone");
1375
888
  }
889
+ return;
890
+ }
1376
891
 
1377
- // Invalidate all caches — the unit just completed and may have
1378
- // written planning files (task summaries, roadmap checkboxes, etc.)
1379
- invalidateAllCaches();
1380
-
1381
- // Small delay to let files settle (git commits, file writes)
1382
- await new Promise(r => setTimeout(r, 500));
1383
-
1384
- // Commit any dirty files the LLM left behind on the current branch.
1385
- // For execute-task units, build a meaningful commit message from the
1386
- // task summary (one-liner, key_files, inferred type). For other unit
1387
- // types, fall back to the generic chore() message.
1388
- if (currentUnit) {
1389
- try {
1390
- let taskContext: TaskCommitContext | undefined;
1391
-
1392
- if (currentUnit.type === "execute-task") {
1393
- const parts = currentUnit.id.split("/");
1394
- const [mid, sid, tid] = parts;
1395
- if (mid && sid && tid) {
1396
- const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
1397
- if (summaryPath) {
1398
- try {
1399
- const summaryContent = await loadFile(summaryPath);
1400
- if (summaryContent) {
1401
- const summary = parseSummary(summaryContent);
1402
- taskContext = {
1403
- taskId: `${sid}/${tid}`,
1404
- taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
1405
- oneLiner: summary.oneLiner || undefined,
1406
- keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
1407
- };
1408
- }
1409
- } catch {
1410
- // Non-fatal — fall back to generic message
1411
- }
1412
- }
1413
- }
1414
- }
1415
-
1416
- const commitMsg = autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id, taskContext);
1417
- if (commitMsg) {
1418
- ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
1419
- }
1420
- } catch {
1421
- // Non-fatal
1422
- }
1423
-
1424
- // Post-hook: fix mechanical bookkeeping the LLM may have skipped.
1425
- // 1. Doctor handles: checkbox marking (task-level bookkeeping).
1426
- // 2. STATE.md is always rebuilt from disk state (purely derived, no LLM needed).
1427
- // fixLevel:"task" ensures doctor only fixes task-level issues (e.g. marking
1428
- // checkboxes). Slice/milestone completion transitions (summary stubs,
1429
- // roadmap [x] marking) are left for the complete-slice dispatch unit.
1430
- // Exception: after complete-slice itself, use fixLevel:"all" so roadmap
1431
- // checkboxes get fixed even if complete-slice crashed (#839).
1432
- try {
1433
- const scopeParts = currentUnit.id.split("/").slice(0, 2);
1434
- const doctorScope = scopeParts.join("/");
1435
- const effectiveFixLevel = currentUnit.type === "complete-slice" ? "all" as const : "task" as const;
1436
- const report = await runGSDDoctor(basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
1437
- if (report.fixesApplied.length > 0) {
1438
- ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
1439
- }
1440
-
1441
- // ── Proactive health tracking ──────────────────────────────────────
1442
- // Record health snapshot for trend analysis and escalation logic.
1443
- const summary = summarizeDoctorIssues(report.issues);
1444
- recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
1445
-
1446
- // Check if we should escalate to LLM-assisted heal
1447
- if (summary.errors > 0) {
1448
- const unresolvedErrors = report.issues
1449
- .filter(i => i.severity === "error" && !i.fixable)
1450
- .map(i => ({ code: i.code, message: i.message, unitId: i.unitId }));
1451
- const escalation = checkHealEscalation(summary.errors, unresolvedErrors);
1452
- if (escalation.shouldEscalate) {
1453
- ctx.ui.notify(
1454
- `Doctor heal escalation: ${escalation.reason}. Dispatching LLM-assisted heal.`,
1455
- "warning",
1456
- );
1457
- try {
1458
- const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
1459
- const { dispatchDoctorHeal } = await import("./commands.js");
1460
- const actionable = report.issues.filter(i => i.severity === "error");
1461
- const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
1462
- const structuredIssues = formatDoctorIssuesForPrompt(actionable);
1463
- dispatchDoctorHeal(pi, doctorScope, reportText, structuredIssues);
1464
- } catch {
1465
- // Non-fatal — escalation dispatch failure
1466
- }
1467
- }
1468
- }
1469
- } catch {
1470
- // Non-fatal — doctor failure should never block dispatch
1471
- }
1472
- try {
1473
- await rebuildState(basePath);
1474
- // State rebuild commit is bookkeeping — generic message is appropriate
1475
- autoCommitCurrentBranch(basePath, "state-rebuild", currentUnit.id);
1476
- } catch {
1477
- // Non-fatal
1478
- }
1479
-
1480
- // ── Sync worktree state back to project root ──────────────────────────
1481
- // Ensures that if auto-mode restarts, deriveState(projectRoot) reads
1482
- // current milestone progress instead of stale pre-worktree state (#654).
1483
- if (originalBasePath && originalBasePath !== basePath) {
1484
- try {
1485
- syncStateToProjectRoot(basePath, originalBasePath, currentMilestoneId);
1486
- } catch {
1487
- // Non-fatal — stale state is the existing behavior, sync is an improvement
1488
- }
1489
- }
1490
-
1491
- // ── Rewrite-docs completion: resolve overrides and reset circuit breaker ──
1492
- if (currentUnit.type === "rewrite-docs") {
1493
- try {
1494
- await resolveAllOverrides(basePath);
1495
- resetRewriteCircuitBreaker();
1496
- ctx.ui.notify("Override(s) resolved — rewrite-docs completed.", "info");
1497
- } catch {
1498
- // Non-fatal — verifyExpectedArtifact will catch unresolved overrides
1499
- }
1500
- }
1501
-
1502
- // ── Post-triage: execute actionable resolutions (inject, replan, queue quick-tasks) ──
1503
- // After a triage-captures unit completes, the LLM has classified captures and
1504
- // updated CAPTURES.md. Now we execute those classifications: inject tasks into
1505
- // the plan, write replan triggers, and queue quick-tasks for dispatch.
1506
- if (currentUnit.type === "triage-captures") {
1507
- try {
1508
- const { executeTriageResolutions } = await import("./triage-resolution.js");
1509
- const state = await deriveState(basePath);
1510
- const mid = state.activeMilestone?.id;
1511
- const sid = state.activeSlice?.id;
1512
-
1513
- if (mid && sid) {
1514
- const triageResult = executeTriageResolutions(basePath, mid, sid);
1515
-
1516
- if (triageResult.injected > 0) {
1517
- ctx.ui.notify(
1518
- `Triage: injected ${triageResult.injected} task${triageResult.injected === 1 ? "" : "s"} into ${sid} plan.`,
1519
- "info",
1520
- );
1521
- }
1522
- if (triageResult.replanned > 0) {
1523
- ctx.ui.notify(
1524
- `Triage: replan trigger written for ${sid} — next dispatch will enter replanning.`,
1525
- "info",
1526
- );
1527
- }
1528
- if (triageResult.quickTasks.length > 0) {
1529
- // Queue quick-tasks for dispatch. They'll be picked up by the
1530
- // quick-task dispatch block below the triage check.
1531
- for (const qt of triageResult.quickTasks) {
1532
- pendingQuickTasks.push(qt);
1533
- }
1534
- ctx.ui.notify(
1535
- `Triage: ${triageResult.quickTasks.length} quick-task${triageResult.quickTasks.length === 1 ? "" : "s"} queued for execution.`,
1536
- "info",
1537
- );
1538
- }
1539
- for (const action of triageResult.actions) {
1540
- process.stderr.write(`gsd-triage: ${action}\n`);
1541
- }
1542
- }
1543
- } catch (err) {
1544
- // Non-fatal — triage resolution failure shouldn't block dispatch
1545
- process.stderr.write(`gsd-triage: resolution execution failed: ${(err as Error).message}\n`);
1546
- }
1547
- }
1548
-
1549
- // ── Path A fix: verify artifact and persist completion before re-entering dispatch ──
1550
- // After doctor + rebuildState, check whether the just-completed unit actually
1551
- // produced its expected artifact. If so, persist the completion key now so the
1552
- // idempotency check at the top of dispatchNextUnit() skips it — even if
1553
- // deriveState() still returns this unit as active (e.g. branch mismatch).
1554
- //
1555
- // IMPORTANT: For non-hook units, defer persistence until after the hook check.
1556
- // If a post-unit hook requests a retry, we need to remove the completion key
1557
- // so dispatchNextUnit re-dispatches the trigger unit.
1558
- let triggerArtifactVerified = false;
1559
- if (!currentUnit.type.startsWith("hook/")) {
1560
- try {
1561
- triggerArtifactVerified = verifyExpectedArtifact(currentUnit.type, currentUnit.id, basePath);
1562
- if (triggerArtifactVerified) {
1563
- const completionKey = `${currentUnit.type}/${currentUnit.id}`;
1564
- if (!completedKeySet.has(completionKey)) {
1565
- persistCompletedKey(basePath, completionKey);
1566
- completedKeySet.add(completionKey);
1567
- }
1568
- invalidateAllCaches();
1569
- }
1570
- } catch {
1571
- // Non-fatal — worst case we fall through to normal dispatch which has its own checks
1572
- }
1573
- } else {
1574
- // Hook unit completed — finalize its runtime record and clear it
1575
- try {
1576
- writeUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id, currentUnit.startedAt, {
1577
- phase: "finalized",
1578
- progressCount: 1,
1579
- lastProgressKind: "hook-completed",
1580
- });
1581
- clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
1582
- } catch {
1583
- // Non-fatal
1584
- }
1585
- }
1586
- }
1587
-
1588
- // ── Verification gate: run typecheck/lint/test after execute-task ──
1589
- if (currentUnit && currentUnit.type === "execute-task") {
1590
- try {
1591
- const effectivePrefs = loadEffectiveGSDPreferences();
1592
- const prefs = effectivePrefs?.preferences;
1593
-
1594
- // Read task plan verify field from the current task's slice plan
1595
- // unitId format is "M001/S01/T03" — extract mid, sid, tid
1596
- const parts = currentUnit.id.split("/");
1597
- let taskPlanVerify: string | undefined;
1598
- if (parts.length >= 3) {
1599
- const [mid, sid, tid] = parts;
1600
- const planFile = resolveSliceFile(basePath, mid, sid, "PLAN");
1601
- if (planFile) {
1602
- const planContent = await loadFile(planFile);
1603
- if (planContent) {
1604
- const slicePlan = parsePlan(planContent);
1605
- const taskEntry = slicePlan?.tasks?.find(t => t.id === tid);
1606
- taskPlanVerify = taskEntry?.verify;
1607
- }
1608
- }
1609
- }
1610
-
1611
- const result = runVerificationGate({
1612
- basePath,
1613
- unitId: currentUnit.id,
1614
- cwd: basePath,
1615
- preferenceCommands: prefs?.verification_commands,
1616
- taskPlanVerify,
1617
- });
1618
-
1619
- // Capture runtime errors from bg-shell and browser console
1620
- const runtimeErrors = await captureRuntimeErrors();
1621
- if (runtimeErrors.length > 0) {
1622
- result.runtimeErrors = runtimeErrors;
1623
- // Blocking runtime errors override gate pass
1624
- if (runtimeErrors.some(e => e.blocking)) {
1625
- result.passed = false;
1626
- }
1627
- }
1628
-
1629
- // Conditional dependency audit (R008)
1630
- const auditWarnings = runDependencyAudit(basePath);
1631
- if (auditWarnings.length > 0) {
1632
- result.auditWarnings = auditWarnings;
1633
- process.stderr.write(`verification-gate: ${auditWarnings.length} audit warning(s)\n`);
1634
- for (const w of auditWarnings) {
1635
- process.stderr.write(` [${w.severity}] ${w.name}: ${w.title}\n`);
1636
- }
1637
- }
1638
-
1639
- // Auto-fix retry preferences (R005 / D005)
1640
- const autoFixEnabled = prefs?.verification_auto_fix !== false; // default true
1641
- const maxRetries = typeof prefs?.verification_max_retries === "number" ? prefs.verification_max_retries : 2;
1642
- const completionKey = `${currentUnit.type}/${currentUnit.id}`;
1643
-
1644
- if (result.checks.length > 0) {
1645
- const passCount = result.checks.filter(c => c.exitCode === 0).length;
1646
- const total = result.checks.length;
1647
- if (result.passed) {
1648
- ctx.ui.notify(`Verification gate: ${passCount}/${total} checks passed`);
1649
- } else {
1650
- const failures = result.checks.filter(c => c.exitCode !== 0);
1651
- const failNames = failures.map(f => f.command).join(", ");
1652
- ctx.ui.notify(`Verification gate: FAILED — ${failNames}`);
1653
- process.stderr.write(`verification-gate: ${total - passCount}/${total} checks failed\n`);
1654
- for (const f of failures) {
1655
- process.stderr.write(` ${f.command} exited ${f.exitCode}\n`);
1656
- if (f.stderr) process.stderr.write(` stderr: ${f.stderr.slice(0, 500)}\n`);
1657
- }
1658
- }
1659
- }
1660
-
1661
- // Log blocking runtime errors to stderr
1662
- if (result.runtimeErrors?.some(e => e.blocking)) {
1663
- const blockingErrors = result.runtimeErrors.filter(e => e.blocking);
1664
- process.stderr.write(`verification-gate: ${blockingErrors.length} blocking runtime error(s) detected\n`);
1665
- for (const err of blockingErrors) {
1666
- process.stderr.write(` [${err.source}] ${err.severity}: ${err.message.slice(0, 200)}\n`);
1667
- }
1668
- }
1669
-
1670
- // Write verification evidence JSON artifact
1671
- const attempt = verificationRetryCount.get(currentUnit.id) ?? 0;
1672
- if (parts.length >= 3) {
1673
- try {
1674
- const [mid, sid, tid] = parts;
1675
- const sDir = resolveSlicePath(basePath, mid, sid);
1676
- if (sDir) {
1677
- const tasksDir = join(sDir, "tasks");
1678
- if (result.passed) {
1679
- writeVerificationJSON(result, tasksDir, tid, currentUnit.id);
1680
- } else {
1681
- const nextAttempt = attempt + 1;
1682
- writeVerificationJSON(result, tasksDir, tid, currentUnit.id, nextAttempt, maxRetries);
1683
- }
1684
- }
1685
- } catch (evidenceErr) {
1686
- process.stderr.write(`verification-evidence: write error — ${(evidenceErr as Error).message}\n`);
1687
- }
1688
- }
1689
-
1690
- // ── Auto-fix retry logic ──
1691
- if (result.passed) {
1692
- // Gate passed — clear retry state and continue normal flow
1693
- verificationRetryCount.delete(currentUnit.id);
1694
- pendingVerificationRetry = null;
1695
- } else if (autoFixEnabled && attempt + 1 <= maxRetries) {
1696
- // Gate failed, retries remaining — set up retry and return early
1697
- const nextAttempt = attempt + 1;
1698
- verificationRetryCount.set(currentUnit.id, nextAttempt);
1699
- pendingVerificationRetry = {
1700
- unitId: currentUnit.id,
1701
- failureContext: formatFailureContext(result),
1702
- attempt: nextAttempt,
1703
- };
1704
- ctx.ui.notify(`Verification failed — auto-fix attempt ${nextAttempt}/${maxRetries}`, "warning");
1705
- // Remove completion key so dispatchNextUnit re-dispatches this unit
1706
- completedKeySet.delete(completionKey);
1707
- removePersistedKey(basePath, completionKey);
1708
- return; // ← Critical: exit before DB dual-write and post-unit hooks
1709
- } else {
1710
- // Gate failed, retries exhausted (or auto-fix disabled) — pause for human review
1711
- const exhaustedAttempt = attempt + 1;
1712
- verificationRetryCount.delete(currentUnit.id);
1713
- pendingVerificationRetry = null;
1714
- ctx.ui.notify(
1715
- `Verification gate FAILED after ${exhaustedAttempt > maxRetries ? exhaustedAttempt - 1 : exhaustedAttempt} retries — pausing for human review`,
1716
- "error",
1717
- );
1718
- await pauseAuto(ctx, pi);
1719
- return;
1720
- }
1721
- } catch (err) {
1722
- // Gate errors are non-fatal — log and continue
1723
- process.stderr.write(`verification-gate: error — ${(err as Error).message}\n`);
1724
- }
1725
- }
1726
-
1727
- // ── DB dual-write: re-import changed markdown files so next unit's prompts use fresh data ──
1728
- if (isDbAvailable()) {
1729
- try {
1730
- const { migrateFromMarkdown } = await import("./md-importer.js");
1731
- migrateFromMarkdown(basePath);
1732
- } catch (err) {
1733
- process.stderr.write(`gsd-db: re-import failed: ${(err as Error).message}\n`);
1734
- }
1735
- }
1736
-
1737
- // ── Post-unit hooks: check if a configured hook should run before normal dispatch ──
1738
- if (currentUnit && !stepMode) {
1739
- const hookUnit = checkPostUnitHooks(currentUnit.type, currentUnit.id, basePath);
1740
- if (hookUnit) {
1741
- // Dispatch the hook unit instead of normal flow
1742
- const hookStartedAt = Date.now();
1743
- if (currentUnit) {
1744
- const modelId = ctx.model?.id ?? "unknown";
1745
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
1746
- const hookActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1747
- if (hookActivityFile) {
1748
- try {
1749
- const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
1750
- const llmCallFn = buildMemoryLLMCall(ctx);
1751
- if (llmCallFn) {
1752
- extractMemoriesFromUnit(hookActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
1753
- }
1754
- } catch { /* non-fatal */ }
1755
- }
1756
- }
1757
- currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
1758
- writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
1759
- phase: "dispatched",
1760
- wrapupWarningSent: false,
1761
- timeoutAt: null,
1762
- lastProgressAt: hookStartedAt,
1763
- progressCount: 0,
1764
- lastProgressKind: "dispatch",
1765
- });
1766
-
1767
- const state = await deriveState(basePath);
1768
- updateProgressWidget(ctx, hookUnit.unitType, hookUnit.unitId, state);
1769
- const hookState = getActiveHook();
1770
- ctx.ui.notify(
1771
- `Running post-unit hook: ${hookUnit.hookName} (cycle ${hookState?.cycle ?? 1})`,
1772
- "info",
1773
- );
1774
-
1775
- // Switch model if the hook specifies one
1776
- if (hookUnit.model) {
1777
- const availableModels = ctx.modelRegistry.getAvailable();
1778
- const match = availableModels.find(m =>
1779
- m.id === hookUnit.model || `${m.provider}/${m.id}` === hookUnit.model,
1780
- );
1781
- if (match) {
1782
- try {
1783
- await pi.setModel(match);
1784
- } catch { /* non-fatal — use current model */ }
1785
- }
1786
- }
1787
-
1788
- const result = await cmdCtx!.newSession();
1789
- if (result.cancelled) {
1790
- resetHookState();
1791
- await stopAuto(ctx, pi, "Hook session cancelled");
1792
- return;
1793
- }
1794
- const sessionFile = ctx.sessionManager.getSessionFile();
1795
- writeLock(lockBase(), hookUnit.unitType, hookUnit.unitId, completedUnits.length, sessionFile);
1796
- // Persist hook state so cycle counts survive crashes
1797
- persistHookState(basePath);
1798
-
1799
- // Start supervision timers for hook units — hooks can get stuck just
1800
- // like normal units, and without a watchdog auto-mode would hang forever.
1801
- clearUnitTimeout();
1802
- const supervisor = resolveAutoSupervisorConfig();
1803
- const hookHardTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
1804
- unitTimeoutHandle = setTimeout(async () => {
1805
- unitTimeoutHandle = null;
1806
- if (!active) return;
1807
- if (currentUnit) {
1808
- writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, currentUnit.startedAt, {
1809
- phase: "timeout",
1810
- timeoutAt: Date.now(),
1811
- });
1812
- }
1813
- ctx.ui.notify(
1814
- `Hook ${hookUnit.hookName} exceeded ${supervisor.hard_timeout_minutes ?? 30}min timeout. Pausing auto-mode.`,
1815
- "warning",
1816
- );
1817
- resetHookState();
1818
- await pauseAuto(ctx, pi);
1819
- }, hookHardTimeoutMs);
1820
-
1821
- // Guard against race with timeout/pause before sending
1822
- if (!active) return;
1823
- pi.sendMessage(
1824
- { customType: "gsd-auto", content: hookUnit.prompt, display: verbose },
1825
- { triggerTurn: true },
1826
- );
1827
- return; // handleAgentEnd will fire again when hook session completes
1828
- }
1829
-
1830
- // Check if a hook requested a retry of the trigger unit
1831
- if (isRetryPending()) {
1832
- const trigger = consumeRetryTrigger();
1833
- if (trigger) {
1834
- // Remove the trigger unit's completion key so dispatchNextUnit
1835
- // will re-dispatch it instead of skipping it as already-complete.
1836
- const triggerKey = `${trigger.unitType}/${trigger.unitId}`;
1837
- completedKeySet.delete(triggerKey);
1838
- removePersistedKey(basePath, triggerKey);
1839
- ctx.ui.notify(
1840
- `Hook requested retry of ${trigger.unitType} ${trigger.unitId}.`,
1841
- "info",
1842
- );
1843
- // Fall through to normal dispatchNextUnit — state derivation will
1844
- // re-select the same unit since it hasn't been marked complete
1845
- }
1846
- }
1847
- }
1848
-
1849
- // ── Triage check: dispatch triage unit if pending captures exist ──────────
1850
- // Fires after hooks complete, before normal dispatch. Follows the same
1851
- // early-dispatch-and-return pattern as hooks and fix-merge.
1852
- // Skip for: step mode (shows wizard instead), triage units (prevent triage-on-triage),
1853
- // hook units (hooks run before triage conceptually).
1854
- if (
1855
- !stepMode &&
1856
- currentUnit &&
1857
- !currentUnit.type.startsWith("hook/") &&
1858
- currentUnit.type !== "triage-captures" &&
1859
- currentUnit.type !== "quick-task"
1860
- ) {
1861
- try {
1862
- if (hasPendingCaptures(basePath)) {
1863
- const pending = loadPendingCaptures(basePath);
1864
- if (pending.length > 0) {
1865
- const state = await deriveState(basePath);
1866
- const mid = state.activeMilestone?.id;
1867
- const sid = state.activeSlice?.id;
1868
-
1869
- if (mid && sid) {
1870
- // Build triage prompt with current context
1871
- let currentPlan = "";
1872
- let roadmapContext = "";
1873
- const planFile = resolveSliceFile(basePath, mid, sid, "PLAN");
1874
- if (planFile) currentPlan = (await loadFile(planFile)) ?? "";
1875
- const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
1876
- if (roadmapFile) roadmapContext = (await loadFile(roadmapFile)) ?? "";
1877
-
1878
- const capturesList = pending.map(c =>
1879
- `- **${c.id}**: "${c.text}" (captured: ${c.timestamp})`
1880
- ).join("\n");
1881
-
1882
- const prompt = loadPrompt("triage-captures", {
1883
- pendingCaptures: capturesList,
1884
- currentPlan: currentPlan || "(no active slice plan)",
1885
- roadmapContext: roadmapContext || "(no active roadmap)",
1886
- });
1887
-
1888
- ctx.ui.notify(
1889
- `Triaging ${pending.length} pending capture${pending.length === 1 ? "" : "s"}...`,
1890
- "info",
1891
- );
1892
-
1893
- // Close out previous unit metrics
1894
- if (currentUnit) {
1895
- const modelId = ctx.model?.id ?? "unknown";
1896
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1897
- const triageActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1898
- if (triageActivityFile) {
1899
- try {
1900
- const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
1901
- const llmCallFn = buildMemoryLLMCall(ctx);
1902
- if (llmCallFn) {
1903
- extractMemoriesFromUnit(triageActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
1904
- }
1905
- } catch { /* non-fatal */ }
1906
- }
1907
- }
1908
-
1909
- // Dispatch triage as a new unit (early-dispatch-and-return)
1910
- const triageUnitType = "triage-captures";
1911
- const triageUnitId = `${mid}/${sid}/triage`;
1912
- const triageStartedAt = Date.now();
1913
- currentUnit = { type: triageUnitType, id: triageUnitId, startedAt: triageStartedAt };
1914
- writeUnitRuntimeRecord(basePath, triageUnitType, triageUnitId, triageStartedAt, {
1915
- phase: "dispatched",
1916
- wrapupWarningSent: false,
1917
- timeoutAt: null,
1918
- lastProgressAt: triageStartedAt,
1919
- progressCount: 0,
1920
- lastProgressKind: "dispatch",
1921
- });
1922
- updateProgressWidget(ctx, triageUnitType, triageUnitId, state);
1923
-
1924
- const result = await cmdCtx!.newSession();
1925
- if (result.cancelled) {
1926
- await stopAuto(ctx, pi);
1927
- return;
1928
- }
1929
- const sessionFile = ctx.sessionManager.getSessionFile();
1930
- writeLock(lockBase(), triageUnitType, triageUnitId, completedUnits.length, sessionFile);
1931
-
1932
- // Start unit timeout for triage (use same supervisor config as hooks)
1933
- clearUnitTimeout();
1934
- const supervisor = resolveAutoSupervisorConfig();
1935
- const triageTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
1936
- unitTimeoutHandle = setTimeout(async () => {
1937
- unitTimeoutHandle = null;
1938
- if (!active) return;
1939
- ctx.ui.notify(
1940
- `Triage unit exceeded timeout. Pausing auto-mode.`,
1941
- "warning",
1942
- );
1943
- await pauseAuto(ctx, pi);
1944
- }, triageTimeoutMs);
1945
-
1946
- if (!active) return;
1947
- pi.sendMessage(
1948
- { customType: "gsd-auto", content: prompt, display: verbose },
1949
- { triggerTurn: true },
1950
- );
1951
- return; // handleAgentEnd will fire again when triage session completes
1952
- }
1953
- }
1954
- }
1955
- } catch {
1956
- // Triage check failure is non-fatal — proceed to normal dispatch
1957
- }
1958
- }
1959
-
1960
- // ── Quick-task dispatch: execute queued quick-tasks from triage resolution ──
1961
- // Quick-tasks are self-contained one-off tasks that don't modify the plan.
1962
- // They're queued during post-triage resolution and dispatched here one at a time.
1963
- if (
1964
- !stepMode &&
1965
- pendingQuickTasks.length > 0 &&
1966
- currentUnit &&
1967
- currentUnit.type !== "quick-task"
1968
- ) {
1969
- try {
1970
- const capture = pendingQuickTasks.shift()!;
1971
- const { buildQuickTaskPrompt } = await import("./triage-resolution.js");
1972
- const { markCaptureExecuted } = await import("./captures.js");
1973
- const prompt = buildQuickTaskPrompt(capture);
1974
-
1975
- ctx.ui.notify(
1976
- `Executing quick-task: ${capture.id} — "${capture.text}"`,
1977
- "info",
1978
- );
1979
-
1980
- // Close out previous unit metrics
1981
- if (currentUnit) {
1982
- const modelId = ctx.model?.id ?? "unknown";
1983
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1984
- const qtActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1985
- if (qtActivityFile) {
1986
- try {
1987
- const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
1988
- const llmCallFn = buildMemoryLLMCall(ctx);
1989
- if (llmCallFn) {
1990
- extractMemoriesFromUnit(qtActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
1991
- }
1992
- } catch { /* non-fatal */ }
1993
- }
1994
- }
1995
-
1996
- // Dispatch quick-task as a new unit
1997
- const qtUnitType = "quick-task";
1998
- const qtUnitId = `${currentMilestoneId}/${capture.id}`;
1999
- const qtStartedAt = Date.now();
2000
- currentUnit = { type: qtUnitType, id: qtUnitId, startedAt: qtStartedAt };
2001
- writeUnitRuntimeRecord(basePath, qtUnitType, qtUnitId, qtStartedAt, {
2002
- phase: "dispatched",
2003
- wrapupWarningSent: false,
2004
- timeoutAt: null,
2005
- lastProgressAt: qtStartedAt,
2006
- progressCount: 0,
2007
- lastProgressKind: "dispatch",
2008
- });
2009
- const state = await deriveState(basePath);
2010
- updateProgressWidget(ctx, qtUnitType, qtUnitId, state);
2011
-
2012
- const result = await cmdCtx!.newSession();
2013
- if (result.cancelled) {
2014
- await stopAuto(ctx, pi);
2015
- return;
2016
- }
2017
- const sessionFile = ctx.sessionManager.getSessionFile();
2018
- writeLock(lockBase(), qtUnitType, qtUnitId, completedUnits.length, sessionFile);
2019
-
2020
- // Mark capture as executed now that the unit is dispatched
2021
- markCaptureExecuted(basePath, capture.id);
2022
-
2023
- // Start unit timeout for quick-task
2024
- clearUnitTimeout();
2025
- const supervisor = resolveAutoSupervisorConfig();
2026
- const qtTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
2027
- unitTimeoutHandle = setTimeout(async () => {
2028
- unitTimeoutHandle = null;
2029
- if (!active) return;
2030
- ctx.ui.notify(
2031
- `Quick-task ${capture.id} exceeded timeout. Pausing auto-mode.`,
2032
- "warning",
2033
- );
2034
- await pauseAuto(ctx, pi);
2035
- }, qtTimeoutMs);
2036
-
2037
- if (!active) return;
2038
- pi.sendMessage(
2039
- { customType: "gsd-auto", content: prompt, display: verbose },
2040
- { triggerTurn: true },
2041
- );
2042
- return; // handleAgentEnd will fire again when quick-task session completes
2043
- } catch {
2044
- // Non-fatal — proceed to normal dispatch
2045
- }
2046
- }
2047
-
2048
- // In step mode, pause and show a wizard instead of immediately dispatching
2049
- if (stepMode) {
2050
- await showStepWizard(ctx, pi);
2051
- return;
2052
- }
2053
-
2054
- try {
2055
- await dispatchNextUnit(ctx, pi);
2056
- } catch (dispatchErr) {
2057
- // dispatchNextUnit threw — without this catch the error would propagate
2058
- // to the pi event emitter which may silently swallow async rejections,
2059
- // leaving auto-mode active but permanently stalled (see #381).
2060
- const message = dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr);
2061
- ctx.ui.notify(
2062
- `Dispatch error after unit completion: ${message}. Retrying in ${DISPATCH_GAP_TIMEOUT_MS / 1000}s.`,
2063
- "error",
2064
- );
2065
-
2066
- // Start the dispatch gap watchdog to retry after a delay.
2067
- // This gives transient issues (dirty working tree, branch state) time to settle.
2068
- startDispatchGapWatchdog(ctx, pi);
2069
- return;
2070
- }
2071
-
2072
- // If dispatchNextUnit returned normally but auto-mode is still active and
2073
- // no new unit timeout was set (meaning sendMessage was never called), start
2074
- // the dispatch gap watchdog as a safety net.
2075
- if (active && !unitTimeoutHandle && !wrapupWarningHandle) {
2076
- startDispatchGapWatchdog(ctx, pi);
2077
- }
2078
-
2079
- } finally {
2080
- _handlingAgentEnd = false;
2081
- }
2082
- }
2083
-
2084
- // ─── Step Mode Wizard ─────────────────────────────────────────────────────
2085
-
2086
- /**
2087
- * Show the step-mode wizard after a unit completes.
2088
- * Derives the next unit from disk state and presents it to the user.
2089
- * If the user confirms, dispatches the next unit. If not, pauses.
2090
- */
2091
- async function showStepWizard(
2092
- ctx: ExtensionContext,
2093
- pi: ExtensionAPI,
2094
- ): Promise<void> {
2095
- if (!cmdCtx) return;
2096
-
2097
- const state = await deriveState(basePath);
2098
- const mid = state.activeMilestone?.id;
2099
-
2100
- // Build summary of what just completed
2101
- const justFinished = currentUnit
2102
- ? `${unitVerb(currentUnit.type)} ${currentUnit.id}`
2103
- : "previous unit";
2104
-
2105
- // If no active milestone or everything is complete, stop
2106
- if (!mid || state.phase === "complete") {
2107
- const incomplete = state.registry.filter(m => m.status !== "complete");
2108
- if (incomplete.length > 0 && state.phase !== "complete" && state.phase !== "blocked") {
2109
- const ids = incomplete.map(m => m.id).join(", ");
2110
- const diag = `basePath=${basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
2111
- ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
2112
- await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids})`);
2113
- } else {
2114
- await stopAuto(ctx, pi, state.phase === "complete" ? "All work complete" : "No active milestone");
2115
- }
2116
- return;
2117
- }
2118
-
2119
- // Peek at what's next by examining state
2120
892
  const nextDesc = _describeNextUnit(state);
2121
893
 
2122
- const choice = await showNextAction(cmdCtx, {
894
+ const choice = await showNextAction(s.cmdCtx, {
2123
895
  title: `GSD — ${justFinished} complete`,
2124
896
  summary: [
2125
897
  `${mid}: ${state.activeMilestone?.title ?? mid}`,
@@ -2149,17 +921,15 @@ async function showStepWizard(
2149
921
  if (choice === "continue") {
2150
922
  await dispatchNextUnit(ctx, pi);
2151
923
  } else if (choice === "auto") {
2152
- stepMode = false;
924
+ s.stepMode = false;
2153
925
  ctx.ui.setStatus("gsd-auto", "auto");
2154
926
  ctx.ui.notify("Switched to auto-mode.", "info");
2155
927
  await dispatchNextUnit(ctx, pi);
2156
928
  } else if (choice === "status") {
2157
- // Show status then re-show the wizard
2158
929
  const { fireStatusViaCommand } = await import("./commands.js");
2159
930
  await fireStatusViaCommand(ctx as ExtensionCommandContext);
2160
931
  await showStepWizard(ctx, pi);
2161
932
  } else {
2162
- // "not_yet" — pause
2163
933
  await pauseAuto(ctx, pi);
2164
934
  }
2165
935
  }
@@ -2174,85 +944,81 @@ function updateProgressWidget(
2174
944
  unitId: string,
2175
945
  state: GSDState,
2176
946
  ): void {
2177
- const badge = currentUnitRouting?.tier
2178
- ? ({ light: "L", standard: "S", heavy: "H" }[currentUnitRouting.tier] ?? undefined)
947
+ const badge = s.currentUnitRouting?.tier
948
+ ? ({ light: "L", standard: "S", heavy: "H" }[s.currentUnitRouting.tier] ?? undefined)
2179
949
  : undefined;
2180
950
  _updateProgressWidget(ctx, unitType, unitId, state, widgetStateAccessors, badge);
2181
951
  }
2182
952
 
2183
953
  /** State accessors for the widget — closures over module globals. */
2184
954
  const widgetStateAccessors: WidgetStateAccessors = {
2185
- getAutoStartTime: () => autoStartTime,
2186
- isStepMode: () => stepMode,
2187
- getCmdCtx: () => cmdCtx,
2188
- getBasePath: () => basePath,
2189
- isVerbose: () => verbose,
955
+ getAutoStartTime: () => s.autoStartTime,
956
+ isStepMode: () => s.stepMode,
957
+ getCmdCtx: () => s.cmdCtx,
958
+ getBasePath: () => s.basePath,
959
+ isVerbose: () => s.verbose,
2190
960
  };
2191
961
 
2192
962
  // ─── Core Loop ────────────────────────────────────────────────────────────────
2193
963
 
2194
- /** Tracks recursive skip depth to prevent TUI freeze on cascading completed-unit skips */
2195
- let _skipDepth = 0;
2196
- const MAX_SKIP_DEPTH = 20;
2197
-
2198
- /** Reentrancy guard for dispatchNextUnit itself (not just handleAgentEnd).
2199
- * Prevents concurrent dispatch from watchdog timers, step wizard, and direct calls
2200
- * that bypass the _handlingAgentEnd guard. Recursive calls (from skip paths) are
2201
- * allowed via _skipDepth > 0. */
2202
- let _dispatching = false;
2203
-
2204
964
  async function dispatchNextUnit(
2205
965
  ctx: ExtensionContext,
2206
966
  pi: ExtensionAPI,
2207
967
  ): Promise<void> {
2208
- if (!active || !cmdCtx) {
2209
- debugLog(`dispatchNextUnit early return — active=${active}, cmdCtx=${!!cmdCtx}`);
2210
- if (active && !cmdCtx) {
968
+ if (!s.active || !s.cmdCtx) {
969
+ debugLog(`dispatchNextUnit early return — active=${s.active}, cmdCtx=${!!s.cmdCtx}`);
970
+ if (s.active && !s.cmdCtx) {
2211
971
  ctx.ui.notify("Auto-mode session expired. Run /gsd auto to restart.", "info");
2212
972
  }
2213
973
  return;
2214
974
  }
2215
975
 
2216
- // Reentrancy guard: allow recursive calls from skip paths (_skipDepth > 0)
2217
- // but block concurrent external calls (watchdog, step wizard, etc.)
2218
- if (_dispatching && _skipDepth === 0) {
976
+ // ── Session lock validation: detect if another process has taken over ──
977
+ if (lockBase() && !validateSessionLock(lockBase())) {
978
+ debugLog("dispatchNextUnit session-lock-lost another process may have taken over");
979
+ ctx.ui.notify(
980
+ "Session lock lost — another GSD process appears to have taken over. Stopping gracefully.",
981
+ "error",
982
+ );
983
+ // Don't call stopAuto here to avoid releasing the lock we don't own
984
+ s.active = false;
985
+ s.paused = false;
986
+ clearUnitTimeout();
987
+ deregisterSigtermHandler();
988
+ ctx.ui.setStatus("gsd-auto", undefined);
989
+ ctx.ui.setWidget("gsd-progress", undefined);
990
+ ctx.ui.setFooter(undefined);
991
+ return;
992
+ }
993
+
994
+ // Reentrancy guard
995
+ if (s.dispatching && s.skipDepth === 0) {
2219
996
  debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
2220
- return; // Another dispatch is in progress — bail silently
997
+ return;
2221
998
  }
2222
- _dispatching = true;
999
+ s.dispatching = true;
2223
1000
  try {
2224
- // Recursion depth guard: when many units are skipped in sequence (e.g., after
2225
- // crash recovery with 10+ completed units), recursive dispatchNextUnit calls
2226
- // can freeze the TUI or overflow the stack. Yield generously after MAX_SKIP_DEPTH.
2227
- if (_skipDepth > MAX_SKIP_DEPTH) {
2228
- _skipDepth = 0;
1001
+ // Recursion depth guard
1002
+ if (s.skipDepth > MAX_SKIP_DEPTH) {
1003
+ s.skipDepth = 0;
2229
1004
  ctx.ui.notify(`Skipped ${MAX_SKIP_DEPTH}+ completed units. Yielding to UI before continuing.`, "info");
2230
1005
  await new Promise(r => setTimeout(r, 200));
2231
1006
  }
2232
1007
 
2233
- // Resource version guard: detect mid-session resource updates.
2234
- // Templates are read from disk on each dispatch but extension code is loaded
2235
- // once at startup. If resources were re-synced (e.g. /gsd:update, npm update,
2236
- // or dev copy-resources), templates may expect variables the in-memory code
2237
- // doesn't provide. Stop gracefully instead of crashing.
2238
- const staleMsg = checkResourcesStale();
1008
+ // Resource version guard
1009
+ const staleMsg = checkResourcesStale(s.resourceVersionOnStart);
2239
1010
  if (staleMsg) {
2240
1011
  await stopAuto(ctx, pi, staleMsg);
2241
1012
  return;
2242
1013
  }
2243
1014
 
2244
- // Clear all caches so deriveState sees fresh disk state (#431).
2245
- // Parse cache is also cleared — doctor may have re-populated it with
2246
- // stale data between handleAgentEnd and this dispatch call (Path B fix).
2247
1015
  invalidateAllCaches();
2248
- lastPromptCharCount = undefined;
2249
- lastBaselineCharCount = undefined;
1016
+ s.lastPromptCharCount = undefined;
1017
+ s.lastBaselineCharCount = undefined;
2250
1018
 
2251
- // ── Pre-dispatch health gate ──────────────────────────────────────────
2252
- // Lightweight check for critical issues that would cause the next unit
2253
- // to fail or corrupt state. Auto-heals what it can, blocks on the rest.
1019
+ // ── Pre-dispatch health gate ──
2254
1020
  try {
2255
- const healthGate = await preDispatchHealthGate(basePath);
1021
+ const healthGate = await preDispatchHealthGate(s.basePath);
2256
1022
  if (healthGate.fixesApplied.length > 0) {
2257
1023
  ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
2258
1024
  }
@@ -2262,19 +1028,16 @@ async function dispatchNextUnit(
2262
1028
  return;
2263
1029
  }
2264
1030
  } catch {
2265
- // Non-fatal — health gate failure should never block dispatch
1031
+ // Non-fatal
2266
1032
  }
2267
1033
 
2268
- // ── Sync project root artifacts into worktree (#853) ─────────────────
2269
- // When the LLM writes artifacts to the main repo filesystem instead of
2270
- // the worktree, the worktree's gsd.db becomes stale. Sync before
2271
- // deriveState to ensure the worktree has the latest artifacts.
2272
- if (originalBasePath && basePath !== originalBasePath && currentMilestoneId) {
2273
- syncProjectRootToWorktree(originalBasePath, basePath, currentMilestoneId);
1034
+ // ── Sync project root artifacts into worktree ──
1035
+ if (s.originalBasePath && s.basePath !== s.originalBasePath && s.currentMilestoneId) {
1036
+ syncProjectRootToWorktree(s.originalBasePath, s.basePath, s.currentMilestoneId);
2274
1037
  }
2275
1038
 
2276
1039
  const stopDeriveTimer = debugTime("derive-state");
2277
- let state = await deriveState(basePath);
1040
+ let state = await deriveState(s.basePath);
2278
1041
  stopDeriveTimer({
2279
1042
  phase: state.phase,
2280
1043
  milestone: state.activeMilestone?.id,
@@ -2285,79 +1048,115 @@ async function dispatchNextUnit(
2285
1048
  let midTitle = state.activeMilestone?.title;
2286
1049
 
2287
1050
  // Detect milestone transition
2288
- if (mid && currentMilestoneId && mid !== currentMilestoneId) {
1051
+ if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
2289
1052
  ctx.ui.notify(
2290
- `Milestone ${currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`,
1053
+ `Milestone ${ s.currentMilestoneId } complete. Advancing to ${mid}: ${midTitle}.`,
2291
1054
  "info",
2292
1055
  );
2293
- sendDesktopNotification("GSD", `Milestone ${currentMilestoneId} complete!`, "success", "milestone");
2294
- // Hint: visualizer available after milestone transition
1056
+ sendDesktopNotification("GSD", `Milestone ${s.currentMilestoneId} complete!`, "success", "milestone");
2295
1057
  const vizPrefs = loadEffectiveGSDPreferences()?.preferences;
2296
1058
  if (vizPrefs?.auto_visualize) {
2297
1059
  ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
2298
1060
  }
1061
+ if (vizPrefs?.auto_report !== false) {
1062
+ try {
1063
+ const { loadVisualizerData } = await import("./visualizer-data.js");
1064
+ const { generateHtmlReport } = await import("./export-html.js");
1065
+ const { writeReportSnapshot, reportsDir } = await import("./reports.js");
1066
+ const { basename } = await import("node:path");
1067
+ const snapData = await loadVisualizerData(s.basePath);
1068
+ const completedMs = snapData.milestones.find(m => m.id === s.currentMilestoneId);
1069
+ const msTitle = completedMs?.title ?? s.currentMilestoneId;
1070
+ const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
1071
+ const projName = basename(s.basePath);
1072
+ const doneSlices = snapData.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
1073
+ const totalSlices = snapData.milestones.reduce((s, m) => s + m.slices.length, 0);
1074
+ const outPath = writeReportSnapshot({ basePath: s.basePath,
1075
+ html: generateHtmlReport(snapData, {
1076
+ projectName: projName,
1077
+ projectPath: s.basePath,
1078
+ gsdVersion,
1079
+ milestoneId: s.currentMilestoneId,
1080
+ indexRelPath: "index.html",
1081
+ }),
1082
+ milestoneId: s.currentMilestoneId,
1083
+ milestoneTitle: msTitle,
1084
+ kind: "milestone",
1085
+ projectName: projName,
1086
+ projectPath: s.basePath,
1087
+ gsdVersion,
1088
+ totalCost: snapData.totals?.cost ?? 0,
1089
+ totalTokens: snapData.totals?.tokens.total ?? 0,
1090
+ totalDuration: snapData.totals?.duration ?? 0,
1091
+ doneSlices,
1092
+ totalSlices,
1093
+ doneMilestones: snapData.milestones.filter(m => m.status === "complete").length,
1094
+ totalMilestones: snapData.milestones.length,
1095
+ phase: snapData.phase,
1096
+ });
1097
+ ctx.ui.notify(
1098
+ `Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`,
1099
+ "info",
1100
+ );
1101
+ } catch (err) {
1102
+ ctx.ui.notify(
1103
+ `Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
1104
+ "warning",
1105
+ );
1106
+ }
1107
+ }
2299
1108
  // Reset stuck detection for new milestone
2300
- unitDispatchCount.clear();
2301
- unitRecoveryCount.clear();
2302
- unitConsecutiveSkips.clear();
2303
- unitLifetimeDispatches.clear();
2304
- // Clear completed-units.json for the finished milestone
1109
+ s.unitDispatchCount.clear();
1110
+ s.unitRecoveryCount.clear();
1111
+ s.unitConsecutiveSkips.clear();
1112
+ s.unitLifetimeDispatches.clear();
2305
1113
  try {
2306
- const file = completedKeysPath(basePath);
2307
- if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
2308
- completedKeySet.clear();
2309
- } catch { /* non-fatal */ }
2310
-
2311
- // ── Worktree lifecycle on milestone transition (#616) ──────────────
2312
- // When transitioning from M_old to M_new inside a worktree, we must:
2313
- // 1. Merge the completed milestone's worktree back to main
2314
- // 2. Re-derive state from the project root
2315
- // 3. Create a new worktree for the incoming milestone
2316
- // Without this, M_new runs inside M_old's worktree on the wrong branch,
2317
- // and artifact paths resolve against the wrong .gsd/ directory.
2318
- if (isInAutoWorktree(basePath) && originalBasePath && shouldUseWorktreeIsolation()) {
1114
+ const file = completedKeysPath(s.basePath);
1115
+ if (existsSync(file)) {
1116
+ atomicWriteSync(file, JSON.stringify([]));
1117
+ }
1118
+ s.completedKeySet.clear();
1119
+ } catch (e) { debugLog("completed-keys-reset-failed", { error: e instanceof Error ? e.message : String(e) }); }
1120
+
1121
+ // ── Worktree lifecycle on milestone transition (#616) ──
1122
+ if (isInAutoWorktree(s.basePath) && s.originalBasePath && shouldUseWorktreeIsolation()) {
2319
1123
  try {
2320
- const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
1124
+ const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
2321
1125
  if (roadmapPath) {
2322
1126
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
2323
- const mergeResult = mergeMilestoneToMain(originalBasePath, currentMilestoneId, roadmapContent);
1127
+ const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
2324
1128
  ctx.ui.notify(
2325
- `Milestone ${currentMilestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1129
+ `Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
2326
1130
  "info",
2327
1131
  );
2328
1132
  } else {
2329
- // No roadmap found — teardown worktree without merge
2330
- teardownAutoWorktree(originalBasePath, currentMilestoneId);
2331
- ctx.ui.notify(`Exited worktree for ${currentMilestoneId} (no roadmap for merge).`, "info");
1133
+ teardownAutoWorktree(s.originalBasePath, s.currentMilestoneId);
1134
+ ctx.ui.notify(`Exited worktree for ${ s.currentMilestoneId } (no roadmap for merge).`, "info");
2332
1135
  }
2333
1136
  } catch (err) {
2334
1137
  ctx.ui.notify(
2335
1138
  `Milestone merge failed during transition: ${err instanceof Error ? err.message : String(err)}`,
2336
1139
  "warning",
2337
1140
  );
2338
- // Force cwd back to project root even if merge failed
2339
- if (originalBasePath) {
2340
- try { process.chdir(originalBasePath); } catch { /* best-effort */ }
1141
+ if (s.originalBasePath) {
1142
+ try { process.chdir(s.originalBasePath); } catch { /* best-effort */ }
2341
1143
  }
2342
1144
  }
2343
1145
 
2344
- // Update basePath to project root (mergeMilestoneToMain already chdir'd)
2345
- basePath = originalBasePath;
2346
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1146
+ s.basePath = s.originalBasePath;
1147
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
2347
1148
  invalidateAllCaches();
2348
1149
 
2349
- // Re-derive state from project root before creating new worktree
2350
- state = await deriveState(basePath);
1150
+ state = await deriveState(s.basePath);
2351
1151
  mid = state.activeMilestone?.id;
2352
1152
  midTitle = state.activeMilestone?.title;
2353
1153
 
2354
- // Create new worktree for the incoming milestone
2355
1154
  if (mid) {
2356
- captureIntegrationBranch(basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1155
+ captureIntegrationBranch(s.basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
2357
1156
  try {
2358
- const wtPath = createAutoWorktree(basePath, mid);
2359
- basePath = wtPath;
2360
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1157
+ const wtPath = createAutoWorktree(s.basePath, mid);
1158
+ s.basePath = wtPath;
1159
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
2361
1160
  ctx.ui.notify(`Created auto-worktree for ${mid} at ${wtPath}`, "info");
2362
1161
  } catch (err) {
2363
1162
  ctx.ui.notify(
@@ -2367,75 +1166,107 @@ async function dispatchNextUnit(
2367
1166
  }
2368
1167
  }
2369
1168
  } else {
2370
- // Not in worktree — capture integration branch for the new milestone (branch mode only).
2371
- // In none mode there's no milestone branch to merge back to, so skip.
2372
1169
  if (getIsolationMode() !== "none") {
2373
- captureIntegrationBranch(originalBasePath || basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1170
+ captureIntegrationBranch(s.originalBasePath || s.basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
2374
1171
  }
2375
1172
  }
2376
1173
 
2377
- // Prune completed milestone from queue order file
2378
1174
  const pendingIds = state.registry
2379
1175
  .filter(m => m.status !== "complete")
2380
1176
  .map(m => m.id);
2381
- pruneQueueOrder(basePath, pendingIds);
1177
+ pruneQueueOrder(s.basePath, pendingIds);
2382
1178
  }
2383
1179
  if (mid) {
2384
- currentMilestoneId = mid;
2385
- setActiveMilestoneId(basePath, mid);
1180
+ s.currentMilestoneId = mid;
1181
+ setActiveMilestoneId(s.basePath, mid);
2386
1182
  }
2387
1183
 
2388
1184
  if (!mid) {
2389
- // Save final session before stopping
2390
- if (currentUnit) {
2391
- const modelId = ctx.model?.id ?? "unknown";
2392
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2393
- saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1185
+ if (s.currentUnit) {
1186
+ await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
2394
1187
  }
2395
1188
 
2396
- const incomplete = state.registry.filter(m => m.status !== "complete");
1189
+ const incomplete = state.registry.filter(m => m.status !== "complete" && m.status !== "parked");
2397
1190
  if (incomplete.length === 0) {
2398
- // Genuinely all complete
1191
+ // Genuinely all complete (parked milestones excluded) — merge milestone branch to main before stopping (#962)
1192
+ if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {
1193
+ try {
1194
+ const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
1195
+ if (roadmapPath) {
1196
+ const roadmapContent = readFileSync(roadmapPath, "utf-8");
1197
+ const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
1198
+ s.basePath = s.originalBasePath;
1199
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1200
+ ctx.ui.notify(
1201
+ `Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1202
+ "info",
1203
+ );
1204
+ }
1205
+ } catch (err) {
1206
+ ctx.ui.notify(
1207
+ `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
1208
+ "warning",
1209
+ );
1210
+ if (s.originalBasePath) {
1211
+ s.basePath = s.originalBasePath;
1212
+ try { process.chdir(s.basePath); } catch { /* best-effort */ }
1213
+ }
1214
+ }
1215
+ } else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() !== "none") {
1216
+ try {
1217
+ const currentBranch = getCurrentBranch(s.basePath);
1218
+ const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
1219
+ if (currentBranch === milestoneBranch) {
1220
+ const roadmapPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "ROADMAP");
1221
+ if (roadmapPath) {
1222
+ const roadmapContent = readFileSync(roadmapPath, "utf-8");
1223
+ const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
1224
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1225
+ ctx.ui.notify(
1226
+ `Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
1227
+ "info",
1228
+ );
1229
+ }
1230
+ }
1231
+ } catch (err) {
1232
+ ctx.ui.notify(
1233
+ `Milestone merge failed (branch mode): ${err instanceof Error ? err.message : String(err)}`,
1234
+ "warning",
1235
+ );
1236
+ }
1237
+ }
2399
1238
  sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone");
2400
1239
  await stopAuto(ctx, pi, "All milestones complete");
2401
1240
  } else if (state.phase === "blocked") {
2402
- // Milestones exist but are dependency-blocked
2403
1241
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
2404
1242
  await stopAuto(ctx, pi, blockerMsg);
2405
1243
  ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
2406
1244
  sendDesktopNotification("GSD", blockerMsg, "error", "attention");
2407
1245
  } else {
2408
- // Milestones with remaining work exist but none became active — unexpected
2409
1246
  const ids = incomplete.map(m => m.id).join(", ");
2410
- const diag = `basePath=${basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
1247
+ const diag = `basePath=${s.basePath}, milestones=[${state.registry.map(m => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
2411
1248
  ctx.ui.notify(`Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`, "error");
2412
1249
  await stopAuto(ctx, pi, `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`);
2413
1250
  }
2414
1251
  return;
2415
1252
  }
2416
1253
 
2417
- // Guard: mid/midTitle must be defined strings from this point onward.
2418
- // The !mid check above returns early if mid is falsy; midTitle comes from
2419
- // the same object so it should always be present when mid is.
2420
1254
  if (!midTitle) {
2421
- midTitle = mid; // Defensive fallback: use milestone ID as title
1255
+ midTitle = mid;
2422
1256
  ctx.ui.notify(`Milestone ${mid} has no title in roadmap — using ID as fallback.`, "warning");
2423
1257
  }
2424
1258
 
2425
- // ── Mid-merge safety check: detect leftover merge state from a prior session ──
2426
- if (reconcileMergeState(basePath, ctx)) {
1259
+ // ── Mid-merge safety check ──
1260
+ if (reconcileMergeState(s.basePath, ctx)) {
2427
1261
  invalidateAllCaches();
2428
- state = await deriveState(basePath);
1262
+ state = await deriveState(s.basePath);
2429
1263
  mid = state.activeMilestone?.id;
2430
1264
  midTitle = state.activeMilestone?.title;
2431
1265
  }
2432
1266
 
2433
- // After merge guard removal (branchless architecture), mid/midTitle could be undefined
2434
1267
  if (!mid || !midTitle) {
2435
- if (currentUnit) {
2436
- const modelId = ctx.model?.id ?? "unknown";
2437
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2438
- saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1268
+ if (s.currentUnit) {
1269
+ await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
2439
1270
  }
2440
1271
  const noMilestoneReason = !mid
2441
1272
  ? "No active milestone after merge reconciliation"
@@ -2450,28 +1281,27 @@ async function dispatchNextUnit(
2450
1281
  let prompt: string;
2451
1282
 
2452
1283
  if (state.phase === "complete") {
2453
- if (currentUnit) {
2454
- const modelId = ctx.model?.id ?? "unknown";
2455
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2456
- saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1284
+ if (s.currentUnit) {
1285
+ await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
2457
1286
  }
2458
- // Clear completed-units.json for the finished milestone so it doesn't grow unbounded.
2459
1287
  try {
2460
- const file = completedKeysPath(basePath);
2461
- if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
2462
- completedKeySet.clear();
2463
- } catch { /* non-fatal */ }
2464
- // ── Milestone merge: squash-merge milestone branch to main before stopping ──
2465
- if (currentMilestoneId && isInAutoWorktree(basePath) && originalBasePath) {
1288
+ const file = completedKeysPath(s.basePath);
1289
+ if (existsSync(file)) {
1290
+ atomicWriteSync(file, JSON.stringify([]));
1291
+ }
1292
+ s.completedKeySet.clear();
1293
+ } catch (e) { debugLog("completed-keys-reset-failed", { error: e instanceof Error ? e.message : String(e) }); }
1294
+ // ── Milestone merge ──
1295
+ if (s.currentMilestoneId && isInAutoWorktree(s.basePath) && s.originalBasePath) {
2466
1296
  try {
2467
- const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
2468
- if (!roadmapPath) throw new Error(`Cannot resolve ROADMAP file for milestone ${currentMilestoneId}`);
1297
+ const roadmapPath = resolveMilestoneFile(s.originalBasePath, s.currentMilestoneId, "ROADMAP");
1298
+ if (!roadmapPath) throw new GSDError(GSD_ARTIFACT_MISSING, `Cannot resolve ROADMAP file for milestone ${ s.currentMilestoneId }`);
2469
1299
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
2470
- const mergeResult = mergeMilestoneToMain(originalBasePath, currentMilestoneId, roadmapContent);
2471
- basePath = originalBasePath;
2472
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1300
+ const mergeResult = mergeMilestoneToMain(s.originalBasePath, s.currentMilestoneId, roadmapContent);
1301
+ s.basePath = s.originalBasePath;
1302
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
2473
1303
  ctx.ui.notify(
2474
- `Milestone ${currentMilestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
1304
+ `Milestone ${ s.currentMilestoneId } merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
2475
1305
  "info",
2476
1306
  );
2477
1307
  } catch (err) {
@@ -2479,30 +1309,23 @@ async function dispatchNextUnit(
2479
1309
  `Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
2480
1310
  "warning",
2481
1311
  );
2482
- // Ensure cwd is restored even if merge failed partway through (#608).
2483
- // mergeMilestoneToMain may have chdir'd but then thrown, leaving us
2484
- // in an indeterminate location.
2485
- if (originalBasePath) {
2486
- basePath = originalBasePath;
2487
- try { process.chdir(basePath); } catch { /* best-effort */ }
1312
+ if (s.originalBasePath) {
1313
+ s.basePath = s.originalBasePath;
1314
+ try { process.chdir(s.basePath); } catch { /* best-effort */ }
2488
1315
  }
2489
1316
  }
2490
- } else if (currentMilestoneId && !isInAutoWorktree(basePath) && getIsolationMode() !== "none") {
2491
- // Branch isolation mode (#603): no worktree, but we may be on a milestone/* branch.
2492
- // Squash-merge back to the integration branch (or main) before stopping.
1317
+ } else if (s.currentMilestoneId && !isInAutoWorktree(s.basePath) && getIsolationMode() !== "none") {
2493
1318
  try {
2494
- const currentBranch = getCurrentBranch(basePath);
2495
- const milestoneBranch = autoWorktreeBranch(currentMilestoneId);
1319
+ const currentBranch = getCurrentBranch(s.basePath);
1320
+ const milestoneBranch = autoWorktreeBranch(s.currentMilestoneId);
2496
1321
  if (currentBranch === milestoneBranch) {
2497
- const roadmapPath = resolveMilestoneFile(basePath, currentMilestoneId, "ROADMAP");
1322
+ const roadmapPath = resolveMilestoneFile(s.basePath, s.currentMilestoneId, "ROADMAP");
2498
1323
  if (roadmapPath) {
2499
1324
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
2500
- // mergeMilestoneToMain handles: auto-commit, checkout integration branch,
2501
- // squash merge, commit, optional push, branch deletion.
2502
- const mergeResult = mergeMilestoneToMain(basePath, currentMilestoneId, roadmapContent);
2503
- gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
1325
+ const mergeResult = mergeMilestoneToMain(s.basePath, s.currentMilestoneId, roadmapContent);
1326
+ s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
2504
1327
  ctx.ui.notify(
2505
- `Milestone ${currentMilestoneId} merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
1328
+ `Milestone ${ s.currentMilestoneId } merged (branch mode).${mergeResult.pushed ? " Pushed to remote." : ""}`,
2506
1329
  "info",
2507
1330
  );
2508
1331
  }
@@ -2520,10 +1343,8 @@ async function dispatchNextUnit(
2520
1343
  }
2521
1344
 
2522
1345
  if (state.phase === "blocked") {
2523
- if (currentUnit) {
2524
- const modelId = ctx.model?.id ?? "unknown";
2525
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2526
- saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1346
+ if (s.currentUnit) {
1347
+ await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
2527
1348
  }
2528
1349
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
2529
1350
  await stopAuto(ctx, pi, blockerMsg);
@@ -2532,25 +1353,23 @@ async function dispatchNextUnit(
2532
1353
  return;
2533
1354
  }
2534
1355
 
2535
- // ── UAT Dispatch: run-uat fires after complete-slice merge, before reassessment ──
2536
- // Ensures the UAT file and slice summary are both on main when UAT runs.
1356
+ // Budget ceiling guard, context window guard, secrets gate, dispatch table
2537
1357
  const prefs = loadEffectiveGSDPreferences()?.preferences;
2538
1358
 
2539
- // Budget ceiling guard — enforce budget with configurable action
2540
1359
  const budgetCeiling = prefs?.budget_ceiling;
2541
1360
  if (budgetCeiling !== undefined && budgetCeiling > 0) {
2542
1361
  const currentLedger = getLedger();
2543
1362
  const totalCost = currentLedger ? getProjectTotals(currentLedger.units).cost : 0;
2544
1363
  const budgetPct = totalCost / budgetCeiling;
2545
1364
  const budgetAlertLevel = getBudgetAlertLevel(budgetPct);
2546
- const newBudgetAlertLevel = getNewBudgetAlertLevel(lastBudgetAlertLevel, budgetPct);
1365
+ const newBudgetAlertLevel = getNewBudgetAlertLevel(s.lastBudgetAlertLevel, budgetPct);
2547
1366
  const enforcement = prefs?.budget_enforcement ?? "pause";
2548
1367
 
2549
1368
  const budgetEnforcementAction = getBudgetEnforcementAction(enforcement, budgetPct);
2550
1369
 
2551
1370
  if (newBudgetAlertLevel === 100 && budgetEnforcementAction !== "none") {
2552
1371
  const msg = `Budget ceiling ${formatCost(budgetCeiling)} reached (spent ${formatCost(totalCost)}).`;
2553
- lastBudgetAlertLevel = newBudgetAlertLevel;
1372
+ s.lastBudgetAlertLevel = newBudgetAlertLevel;
2554
1373
  if (budgetEnforcementAction === "halt") {
2555
1374
  sendDesktopNotification("GSD", msg, "error", "budget");
2556
1375
  await stopAuto(ctx, pi, "Budget ceiling reached");
@@ -2565,28 +1384,27 @@ async function dispatchNextUnit(
2565
1384
  ctx.ui.notify(`${msg} Continuing (enforcement: warn).`, "warning");
2566
1385
  sendDesktopNotification("GSD", msg, "warning", "budget");
2567
1386
  } else if (newBudgetAlertLevel === 90) {
2568
- lastBudgetAlertLevel = newBudgetAlertLevel;
1387
+ s.lastBudgetAlertLevel = newBudgetAlertLevel;
2569
1388
  ctx.ui.notify(`Budget 90%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning");
2570
1389
  sendDesktopNotification("GSD", `Budget 90%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning", "budget");
2571
1390
  } else if (newBudgetAlertLevel === 80) {
2572
- lastBudgetAlertLevel = newBudgetAlertLevel;
1391
+ s.lastBudgetAlertLevel = newBudgetAlertLevel;
2573
1392
  ctx.ui.notify(`Approaching budget ceiling — 80%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning");
2574
1393
  sendDesktopNotification("GSD", `Approaching budget ceiling — 80%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "warning", "budget");
2575
1394
  } else if (newBudgetAlertLevel === 75) {
2576
- lastBudgetAlertLevel = newBudgetAlertLevel;
1395
+ s.lastBudgetAlertLevel = newBudgetAlertLevel;
2577
1396
  ctx.ui.notify(`Budget 75%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "info");
2578
1397
  sendDesktopNotification("GSD", `Budget 75%: ${formatCost(totalCost)} / ${formatCost(budgetCeiling)}`, "info", "budget");
2579
1398
  } else if (budgetAlertLevel === 0) {
2580
- lastBudgetAlertLevel = 0;
1399
+ s.lastBudgetAlertLevel = 0;
2581
1400
  }
2582
1401
  } else {
2583
- lastBudgetAlertLevel = 0;
1402
+ s.lastBudgetAlertLevel = 0;
2584
1403
  }
2585
1404
 
2586
- // Context window guard pause if approaching context limits
2587
- const contextThreshold = prefs?.context_pause_threshold ?? 0; // 0 = disabled by default
2588
- if (contextThreshold > 0 && cmdCtx) {
2589
- const contextUsage = cmdCtx.getContextUsage();
1405
+ const contextThreshold = prefs?.context_pause_threshold ?? 0;
1406
+ if (contextThreshold > 0 && s.cmdCtx) {
1407
+ const contextUsage = s.cmdCtx.getContextUsage();
2590
1408
  if (contextUsage && contextUsage.percent !== null && contextUsage.percent >= contextThreshold) {
2591
1409
  const msg = `Context window at ${contextUsage.percent}% (threshold: ${contextThreshold}%). Pausing to prevent truncated output.`;
2592
1410
  ctx.ui.notify(`${msg} Run /gsd auto to continue (will start fresh session).`, "warning");
@@ -2596,16 +1414,12 @@ async function dispatchNextUnit(
2596
1414
  }
2597
1415
  }
2598
1416
 
2599
- // ── Secrets re-check gate — runs before every dispatch, not just at startAuto ──
2600
- // plan-milestone writes the milestone SECRETS file (e.g., M001-SECRETS.md) during its unit. By the time we
2601
- // reach the next dispatchNextUnit call the manifest exists but hasn't been
2602
- // presented to the user yet. Without this re-check the model would proceed
2603
- // into plan-slice / execute-task with no real credentials and mock everything.
1417
+ // Secrets re-check gate
2604
1418
  const runSecretsGate = async () => {
2605
1419
  try {
2606
- const manifestStatus = await getManifestStatus(basePath, mid);
1420
+ const manifestStatus = await getManifestStatus(s.basePath, mid);
2607
1421
  if (manifestStatus && manifestStatus.pending.length > 0) {
2608
- const result = await collectSecretsFromManifest(basePath, mid, ctx);
1422
+ const result = await collectSecretsFromManifest(s.basePath, mid, ctx);
2609
1423
  if (result && result.applied && result.skipped && result.existingSkipped) {
2610
1424
  ctx.ui.notify(
2611
1425
  `Secrets collected: ${result.applied.length} applied, ${result.skipped.length} skipped, ${result.existingSkipped.length} already set.`,
@@ -2625,23 +1439,19 @@ async function dispatchNextUnit(
2625
1439
 
2626
1440
  await runSecretsGate();
2627
1441
 
2628
- // ── Dispatch table: resolve phase → unit type + prompt ──
2629
- const dispatchResult = await resolveDispatch({
2630
- basePath, mid, midTitle: midTitle!, state, prefs,
1442
+ // ── Dispatch table ──
1443
+ const dispatchResult = await resolveDispatch({ basePath: s.basePath, mid, midTitle: midTitle!, state, prefs,
2631
1444
  });
2632
1445
 
2633
1446
  if (dispatchResult.action === "stop") {
2634
- if (currentUnit) {
2635
- const modelId = ctx.model?.id ?? "unknown";
2636
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2637
- saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1447
+ if (s.currentUnit) {
1448
+ await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
2638
1449
  }
2639
1450
  await stopAuto(ctx, pi, dispatchResult.reason);
2640
1451
  return;
2641
1452
  }
2642
1453
 
2643
1454
  if (dispatchResult.action !== "dispatch") {
2644
- // skip action — yield and re-dispatch
2645
1455
  await new Promise(r => setImmediate(r));
2646
1456
  await dispatchNextUnit(ctx, pi);
2647
1457
  return;
@@ -2652,8 +1462,8 @@ async function dispatchNextUnit(
2652
1462
  prompt = dispatchResult.prompt;
2653
1463
  let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
2654
1464
 
2655
- // ── Pre-dispatch hooks: modify, skip, or replace the unit before dispatch ──
2656
- const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, basePath);
1465
+ // ── Pre-dispatch hooks ──
1466
+ const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
2657
1467
  if (preDispatchResult.firedHooks.length > 0) {
2658
1468
  ctx.ui.notify(
2659
1469
  `Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
@@ -2662,7 +1472,6 @@ async function dispatchNextUnit(
2662
1472
  }
2663
1473
  if (preDispatchResult.action === "skip") {
2664
1474
  ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
2665
- // Yield then re-dispatch to advance to next unit
2666
1475
  await new Promise(r => setImmediate(r));
2667
1476
  await dispatchNextUnit(ctx, pi);
2668
1477
  return;
@@ -2674,476 +1483,171 @@ async function dispatchNextUnit(
2674
1483
  prompt = preDispatchResult.prompt;
2675
1484
  }
2676
1485
 
2677
- const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
1486
+ const priorSliceBlocker = getPriorSliceCompletionBlocker(s.basePath, getMainBranch(s.basePath), unitType, unitId);
2678
1487
  if (priorSliceBlocker) {
2679
1488
  await stopAuto(ctx, pi, priorSliceBlocker);
2680
1489
  return;
2681
1490
  }
2682
1491
 
2683
- const observabilityIssues = await collectObservabilityWarnings(ctx, unitType, unitId);
2684
-
2685
- // Idempotency: skip units already completed in a prior session.
2686
- const idempotencyKey = `${unitType}/${unitId}`;
2687
- if (completedKeySet.has(idempotencyKey)) {
2688
- // Cross-validate: does the expected artifact actually exist?
2689
- const artifactExists = verifyExpectedArtifact(unitType, unitId, basePath);
2690
- if (artifactExists) {
2691
- // Guard against infinite skip loops: if deriveState keeps returning the
2692
- // same completed unit, consecutive skips will trip this breaker. Evict the
2693
- // key so the next dispatch forces full reconciliation instead of looping.
2694
- const skipCount = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
2695
- unitConsecutiveSkips.set(idempotencyKey, skipCount);
2696
- if (skipCount > MAX_CONSECUTIVE_SKIPS) {
2697
- // Cross-check: verify deriveState actually returns this unit (#790).
2698
- // If the unit's milestone is already complete, this is a phantom skip
2699
- // loop from stale crash recovery context — don't evict.
2700
- const skippedMid = unitId.split("/")[0];
2701
- const skippedMilestoneComplete = skippedMid
2702
- ? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
2703
- : false;
2704
- if (skippedMilestoneComplete) {
2705
- // Milestone is complete — evicting this key would fight self-heal.
2706
- // Clear skip counter and re-dispatch from fresh state.
2707
- unitConsecutiveSkips.delete(idempotencyKey);
2708
- invalidateAllCaches();
2709
- ctx.ui.notify(
2710
- `Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid}. Re-dispatching from fresh state.`,
2711
- "info",
2712
- );
2713
- _skipDepth++;
2714
- await new Promise(r => setTimeout(r, 50));
2715
- await dispatchNextUnit(ctx, pi);
2716
- _skipDepth = Math.max(0, _skipDepth - 1);
2717
- return;
2718
- }
2719
- unitConsecutiveSkips.delete(idempotencyKey);
2720
- completedKeySet.delete(idempotencyKey);
2721
- removePersistedKey(basePath, idempotencyKey);
2722
- invalidateAllCaches();
2723
- ctx.ui.notify(
2724
- `Skip loop detected: ${unitType} ${unitId} skipped ${skipCount} times without advancing. Evicting completion record and forcing reconciliation.`,
2725
- "warning",
2726
- );
2727
- if (!active) return;
2728
- _skipDepth++;
2729
- await new Promise(r => setTimeout(r, 150));
2730
- await dispatchNextUnit(ctx, pi);
2731
- _skipDepth = Math.max(0, _skipDepth - 1);
2732
- return;
2733
- }
2734
- // Count toward lifetime cap so hard-stop fires during skip loops (#792)
2735
- const lifeSkip = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
2736
- unitLifetimeDispatches.set(idempotencyKey, lifeSkip);
2737
- if (lifeSkip > MAX_LIFETIME_DISPATCHES) {
2738
- await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
2739
- ctx.ui.notify(
2740
- `Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip} iterations).`,
2741
- "error",
2742
- );
2743
- return;
2744
- }
2745
- ctx.ui.notify(
2746
- `Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
2747
- "info",
2748
- );
2749
- if (!active) return;
2750
- _skipDepth++;
2751
- await new Promise(r => setTimeout(r, 150));
2752
- await dispatchNextUnit(ctx, pi);
2753
- _skipDepth = Math.max(0, _skipDepth - 1);
2754
- return;
2755
- } else {
2756
- // Stale completion record — artifact missing. Remove and re-run.
2757
- completedKeySet.delete(idempotencyKey);
2758
- removePersistedKey(basePath, idempotencyKey);
2759
- ctx.ui.notify(
2760
- `Re-running ${unitType} ${unitId} — marked complete but expected artifact missing.`,
2761
- "warning",
2762
- );
2763
- }
2764
- }
1492
+ const observabilityIssues = await _collectObservabilityWarnings(ctx, s.basePath, unitType, unitId);
2765
1493
 
2766
- // Fallback: if the idempotency key is missing but the expected artifact already
2767
- // exists on disk, the task completed in a prior session without persisting the key.
2768
- // Persist it now and skip re-dispatch. This prevents infinite loops where a task
2769
- // completes successfully but the completion key was never written (e.g., completed
2770
- // on the first attempt before hitting the retry-threshold persistence logic).
2771
- if (verifyExpectedArtifact(unitType, unitId, basePath)) {
2772
- persistCompletedKey(basePath, idempotencyKey);
2773
- completedKeySet.add(idempotencyKey);
2774
- invalidateAllCaches();
2775
- // Same consecutive-skip guard as the idempotency path above.
2776
- const skipCount2 = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
2777
- unitConsecutiveSkips.set(idempotencyKey, skipCount2);
2778
- if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
2779
- // Cross-check: verify the unit's milestone is still active (#790).
2780
- const skippedMid2 = unitId.split("/")[0];
2781
- const skippedMilestoneComplete2 = skippedMid2
2782
- ? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
2783
- : false;
2784
- if (skippedMilestoneComplete2) {
2785
- unitConsecutiveSkips.delete(idempotencyKey);
2786
- invalidateAllCaches();
2787
- ctx.ui.notify(
2788
- `Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid2}. Re-dispatching from fresh state.`,
2789
- "info",
2790
- );
2791
- _skipDepth++;
2792
- await new Promise(r => setTimeout(r, 50));
2793
- await dispatchNextUnit(ctx, pi);
2794
- _skipDepth = Math.max(0, _skipDepth - 1);
2795
- return;
2796
- }
2797
- unitConsecutiveSkips.delete(idempotencyKey);
2798
- completedKeySet.delete(idempotencyKey);
2799
- removePersistedKey(basePath, idempotencyKey);
2800
- invalidateAllCaches();
2801
- ctx.ui.notify(
2802
- `Skip loop detected: ${unitType} ${unitId} skipped ${skipCount2} times without advancing. Evicting completion record and forcing reconciliation.`,
2803
- "warning",
2804
- );
2805
- if (!active) return;
2806
- _skipDepth++;
2807
- await new Promise(r => setTimeout(r, 150));
2808
- await dispatchNextUnit(ctx, pi);
2809
- _skipDepth = Math.max(0, _skipDepth - 1);
2810
- return;
2811
- }
2812
- // Count toward lifetime cap so hard-stop fires during skip loops (#792)
2813
- const lifeSkip2 = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
2814
- unitLifetimeDispatches.set(idempotencyKey, lifeSkip2);
2815
- if (lifeSkip2 > MAX_LIFETIME_DISPATCHES) {
2816
- await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
2817
- ctx.ui.notify(
2818
- `Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip2} iterations).`,
2819
- "error",
2820
- );
2821
- return;
2822
- }
2823
- ctx.ui.notify(
2824
- `Skipping ${unitType} ${unitId} — artifact exists but completion key was missing. Repaired and advancing.`,
2825
- "info",
2826
- );
2827
- if (!active) return;
2828
- _skipDepth++;
2829
- await new Promise(r => setTimeout(r, 150));
2830
- await dispatchNextUnit(ctx, pi);
2831
- _skipDepth = Math.max(0, _skipDepth - 1);
2832
- return;
2833
- }
2834
-
2835
- // Stuck detection — tracks total dispatches per unit (not just consecutive repeats).
2836
- // Pattern A→B→A→B would reset retryCount every time; this map catches it.
2837
- const dispatchKey = `${unitType}/${unitId}`;
2838
- const prevCount = unitDispatchCount.get(dispatchKey) ?? 0;
2839
- // Real dispatch reached — clear the consecutive-skip counter for this unit.
2840
- unitConsecutiveSkips.delete(dispatchKey);
2841
-
2842
- debugLog("dispatch-unit", {
2843
- type: unitType,
2844
- id: unitId,
2845
- cycle: prevCount + 1,
2846
- lifetime: (unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1,
1494
+ // ── Idempotency check (delegated to auto-idempotency.ts) ──
1495
+ const idempotencyResult = checkIdempotency({
1496
+ s,
1497
+ unitType,
1498
+ unitId,
1499
+ basePath: s.basePath,
1500
+ notify: (msg, level) => ctx.ui.notify(msg, level),
2847
1501
  });
2848
- debugCount("dispatches");
2849
-
2850
- // Hard lifetime cap — survives counter resets from loop-recovery/self-repair.
2851
- // Catches the case where reconciliation "succeeds" (artifacts exist) but
2852
- // deriveState keeps returning the same unit, creating an infinite cycle.
2853
- const lifetimeCount = (unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1;
2854
- unitLifetimeDispatches.set(dispatchKey, lifetimeCount);
2855
- if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
2856
- if (currentUnit) {
2857
- const modelId = ctx.model?.id ?? "unknown";
2858
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2859
- }
2860
- saveActivityLog(ctx, basePath, unitType, unitId);
2861
- const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
2862
- await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId}`);
2863
- ctx.ui.notify(
2864
- `Hard loop detected: ${unitType} ${unitId} dispatched ${lifetimeCount} times total (across reconciliation cycles).${expected ? `\n Expected artifact: ${expected}` : ""}\n This may indicate deriveState() keeps returning the same unit despite artifacts existing.\n Check .gsd/completed-units.json and the slice plan checkbox state.`,
2865
- "error",
2866
- );
2867
- return;
2868
- }
2869
- if (prevCount >= MAX_UNIT_DISPATCHES) {
2870
- if (currentUnit) {
2871
- const modelId = ctx.model?.id ?? "unknown";
2872
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
2873
- }
2874
- saveActivityLog(ctx, basePath, unitType, unitId);
2875
-
2876
- // Final reconciliation pass for execute-task: write any missing durable
2877
- // artifacts (summary placeholder + [x] checkbox) so the pipeline can
2878
- // advance instead of stopping. This is the last resort before halting.
2879
- if (unitType === "execute-task") {
2880
- const [mid, sid, tid] = unitId.split("/");
2881
- if (mid && sid && tid) {
2882
- const status = await inspectExecuteTaskDurability(basePath, unitId);
2883
- if (status) {
2884
- const reconciled = skipExecuteTask(basePath, mid, sid, tid, status, "loop-recovery", prevCount);
2885
- // reconciled: skipExecuteTask attempted to write missing artifacts.
2886
- // verifyExpectedArtifact: confirms physical artifacts (summary + [x]) now exist on disk.
2887
- // Both must pass before we clear the dispatch counter and advance.
2888
- if (reconciled && verifyExpectedArtifact(unitType, unitId, basePath)) {
2889
- ctx.ui.notify(
2890
- `Loop recovery: ${unitId} reconciled after ${prevCount + 1} dispatches — blocker artifacts written, pipeline advancing.\n Review ${status.summaryPath} and replace the placeholder with real work.`,
2891
- "warning",
2892
- );
2893
- // Persist completion so idempotency check prevents re-dispatch
2894
- // if deriveState keeps returning this unit (#462).
2895
- const reconciledKey = `${unitType}/${unitId}`;
2896
- persistCompletedKey(basePath, reconciledKey);
2897
- completedKeySet.add(reconciledKey);
2898
- unitDispatchCount.delete(dispatchKey);
2899
- invalidateAllCaches();
2900
- await new Promise(r => setImmediate(r));
2901
- await dispatchNextUnit(ctx, pi);
2902
- return;
2903
- }
2904
- }
2905
- }
2906
- }
2907
1502
 
2908
- // General reconciliation: if the last attempt DID produce the expected
2909
- // artifact on disk, clear the counter and advance instead of stopping.
2910
- // The execute-task path above handles its special case (writing placeholder
2911
- // summaries). This catch-all covers complete-slice, plan-slice,
2912
- // research-slice, and all other unit types where the Nth attempt at the
2913
- // dispatch limit succeeded but the counter check fires before anyone
2914
- // verifies disk state. Without this, a successful final attempt is
2915
- // indistinguishable from a failed one.
2916
- if (verifyExpectedArtifact(unitType, unitId, basePath)) {
2917
- ctx.ui.notify(
2918
- `Loop recovery: ${unitType} ${unitId} — artifact verified after ${prevCount + 1} dispatches. Advancing.`,
2919
- "info",
2920
- );
2921
- // Persist completion so the idempotency check prevents re-dispatch
2922
- // if deriveState keeps returning this unit (see #462).
2923
- persistCompletedKey(basePath, dispatchKey);
2924
- completedKeySet.add(dispatchKey);
2925
- unitDispatchCount.delete(dispatchKey);
2926
- invalidateAllCaches();
2927
- await new Promise(r => setImmediate(r));
1503
+ if (idempotencyResult.action === "skip") {
1504
+ if (idempotencyResult.reason === "completed" || idempotencyResult.reason === "fallback-persisted" || idempotencyResult.reason === "phantom-loop-cleared" || idempotencyResult.reason === "evicted") {
1505
+ if (!s.active) return;
1506
+ s.skipDepth++;
1507
+ await new Promise(r => setTimeout(r, idempotencyResult.reason === "phantom-loop-cleared" ? 50 : 150));
2928
1508
  await dispatchNextUnit(ctx, pi);
1509
+ s.skipDepth = Math.max(0, s.skipDepth - 1);
2929
1510
  return;
2930
1511
  }
2931
-
2932
- // Last resort for complete-milestone: generate stub summary to unblock pipeline.
2933
- // All slices are done (otherwise we wouldn't be in completing-milestone phase),
2934
- // but the LLM failed to write the summary N times. A stub lets the pipeline advance.
2935
- if (unitType === "complete-milestone") {
2936
- try {
2937
- const mPath = resolveMilestonePath(basePath, unitId);
2938
- if (mPath) {
2939
- const stubPath = join(mPath, `${unitId}-SUMMARY.md`);
2940
- if (!existsSync(stubPath)) {
2941
- writeFileSync(stubPath, `# ${unitId} Summary\n\nAuto-generated stub — milestone tasks completed but summary generation failed after ${prevCount + 1} attempts.\nReview and replace this stub with a proper summary.\n`);
2942
- ctx.ui.notify(`Generated stub summary for ${unitId} to unblock pipeline. Review later.`, "warning");
2943
- persistCompletedKey(basePath, dispatchKey);
2944
- completedKeySet.add(dispatchKey);
2945
- unitDispatchCount.delete(dispatchKey);
2946
- invalidateAllCaches();
2947
- await new Promise(r => setImmediate(r));
2948
- await dispatchNextUnit(ctx, pi);
2949
- return;
2950
- }
2951
- }
2952
- } catch { /* non-fatal — fall through to normal stop */ }
2953
- }
2954
-
2955
- const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
2956
- const remediation = buildLoopRemediationSteps(unitType, unitId, basePath);
2957
- await stopAuto(ctx, pi, `Loop: ${unitType} ${unitId}`);
2958
- sendDesktopNotification("GSD", `Loop detected: ${unitType} ${unitId}`, "error", "error");
1512
+ } else if (idempotencyResult.action === "stop") {
1513
+ await stopAuto(ctx, pi, idempotencyResult.reason);
2959
1514
  ctx.ui.notify(
2960
- `Loop detected: ${unitType} ${unitId} dispatched ${prevCount + 1} times total. Expected artifact not found.${expected ? `\n Expected: ${expected}` : ""}${remediation ? `\n\n Remediation steps:\n${remediation}` : "\n Check branch state and .gsd/ artifacts."}`,
1515
+ `Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle.`,
2961
1516
  "error",
2962
1517
  );
2963
1518
  return;
2964
1519
  }
2965
- unitDispatchCount.set(dispatchKey, prevCount + 1);
2966
- if (prevCount > 0) {
2967
- // Adaptive self-repair: each retry attempts a different remediation step.
2968
- if (unitType === "execute-task") {
2969
- const status = await inspectExecuteTaskDurability(basePath, unitId);
2970
- const [mid, sid, tid] = unitId.split("/");
2971
- if (status && mid && sid && tid) {
2972
- if (status.summaryExists && !status.taskChecked) {
2973
- // Retry 1+: summary exists but checkbox not marked — mark [x] and advance.
2974
- const repaired = skipExecuteTask(basePath, mid, sid, tid, status, "self-repair", 0);
2975
- // repaired: skipExecuteTask updated metadata (returned early-true even if regex missed).
2976
- // verifyExpectedArtifact: confirms the physical artifact (summary + [x]) now exists.
2977
- if (repaired && verifyExpectedArtifact(unitType, unitId, basePath)) {
2978
- ctx.ui.notify(
2979
- `Self-repaired ${unitId}: summary existed but checkbox was unmarked. Marked [x] and advancing.`,
2980
- "warning",
2981
- );
2982
- // Persist completion so idempotency check prevents re-dispatch (#462).
2983
- const repairedKey = `${unitType}/${unitId}`;
2984
- persistCompletedKey(basePath, repairedKey);
2985
- completedKeySet.add(repairedKey);
2986
- unitDispatchCount.delete(dispatchKey);
2987
- invalidateAllCaches();
2988
- await new Promise(r => setImmediate(r));
2989
- await dispatchNextUnit(ctx, pi);
2990
- return;
2991
- }
2992
- } else if (prevCount >= STUB_RECOVERY_THRESHOLD && !status.summaryExists) {
2993
- // Retry STUB_RECOVERY_THRESHOLD+: summary still missing after multiple attempts.
2994
- // Write a minimal stub summary so the next agent session has a recovery artifact
2995
- // to overwrite, rather than starting from scratch again.
2996
- const tasksDir = resolveTasksDir(basePath, mid, sid);
2997
- const sDir = resolveSlicePath(basePath, mid, sid);
2998
- const targetDir = tasksDir ?? (sDir ? join(sDir, "tasks") : null);
2999
- if (targetDir) {
3000
- if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
3001
- const summaryPath = join(targetDir, buildTaskFileName(tid, "SUMMARY"));
3002
- if (!existsSync(summaryPath)) {
3003
- const stubContent = [
3004
- `# PARTIAL RECOVERY — attempt ${prevCount + 1} of ${MAX_UNIT_DISPATCHES}`,
3005
- ``,
3006
- `Task \`${tid}\` in slice \`${sid}\` (milestone \`${mid}\`) has not yet produced a real summary.`,
3007
- `This placeholder was written by auto-mode after ${prevCount} dispatch attempts.`,
3008
- ``,
3009
- `The next agent session will retry this task. Replace this file with real work when done.`,
3010
- ].join("\n");
3011
- writeFileSync(summaryPath, stubContent, "utf-8");
3012
- ctx.ui.notify(
3013
- `Stub recovery (attempt ${prevCount + 1}/${MAX_UNIT_DISPATCHES}): ${unitId} stub summary placeholder written. Retrying with recovery context.`,
3014
- "warning",
3015
- );
3016
- }
3017
- }
3018
- }
3019
- }
1520
+ // "rerun" and "proceed" fall through to stuck detection
1521
+
1522
+ // ── Stuck detection (delegated to auto-stuck-detection.ts) ──
1523
+ const stuckResult = await checkStuckAndRecover({
1524
+ s,
1525
+ ctx,
1526
+ unitType,
1527
+ unitId,
1528
+ basePath: s.basePath,
1529
+ buildSnapshotOpts: () => buildSnapshotOpts(unitType, unitId),
1530
+ });
1531
+
1532
+ if (stuckResult.action === "stop") {
1533
+ await stopAuto(ctx, pi, stuckResult.reason);
1534
+ if (stuckResult.notifyMessage) {
1535
+ ctx.ui.notify(stuckResult.notifyMessage, "error");
3020
1536
  }
3021
- ctx.ui.notify(
3022
- `${unitType} ${unitId} didn't produce expected artifact. Retrying (${prevCount + 1}/${MAX_UNIT_DISPATCHES}).`,
3023
- "warning",
3024
- );
1537
+ return;
1538
+ }
1539
+ if (stuckResult.action === "recovered" && stuckResult.dispatchAgain) {
1540
+ await new Promise(r => setImmediate(r));
1541
+ await dispatchNextUnit(ctx, pi);
1542
+ return;
3025
1543
  }
1544
+
3026
1545
  // Snapshot metrics + activity log for the PREVIOUS unit before we reassign.
3027
- // The session still holds the previous unit's data (newSession hasn't fired yet).
3028
- if (currentUnit) {
3029
- const modelId = ctx.model?.id ?? "unknown";
3030
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
3031
- const activityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
3032
-
3033
- // Fire-and-forget memory extraction from completed unit
3034
- if (activityFile) {
3035
- try {
3036
- const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
3037
- const llmCallFn = buildMemoryLLMCall(ctx);
3038
- if (llmCallFn) {
3039
- extractMemoriesFromUnit(activityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
3040
- }
3041
- } catch { /* non-fatal */ }
3042
- }
1546
+ if (s.currentUnit) {
1547
+ await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
3043
1548
 
3044
- // Record routing outcome for adaptive learning
3045
- if (currentUnitRouting) {
3046
- const isRetry = currentUnit.type === unitType && currentUnit.id === unitId;
1549
+ if (s.currentUnitRouting) {
1550
+ const isRetry = s.currentUnit.type === unitType && s.currentUnit.id === unitId;
3047
1551
  recordOutcome(
3048
- currentUnit.type,
3049
- currentUnitRouting.tier as "light" | "standard" | "heavy",
3050
- !isRetry, // success = not being retried
1552
+ s.currentUnit.type,
1553
+ s.currentUnitRouting.tier as "light" | "standard" | "heavy",
1554
+ !isRetry,
3051
1555
  );
3052
1556
  }
3053
1557
 
3054
- // Only mark the previous unit as completed if:
3055
- // 1. We're not about to re-dispatch the same unit (retry scenario)
3056
- // 2. The expected artifact actually exists on disk
3057
- // For hook units, skip artifact verification — hooks don't produce standard
3058
- // artifacts and their runtime records were already finalized in handleAgentEnd.
3059
- const closeoutKey = `${currentUnit.type}/${currentUnit.id}`;
1558
+ const closeoutKey = `${s.currentUnit.type}/${s.currentUnit.id}`;
3060
1559
  const incomingKey = `${unitType}/${unitId}`;
3061
- const isHookUnit = currentUnit.type.startsWith("hook/");
3062
- const artifactVerified = isHookUnit || verifyExpectedArtifact(currentUnit.type, currentUnit.id, basePath);
1560
+ const isHookUnit = s.currentUnit.type.startsWith("hook/");
1561
+ const artifactVerified = isHookUnit || verifyExpectedArtifact(s.currentUnit.type, s.currentUnit.id, s.basePath);
3063
1562
  if (closeoutKey !== incomingKey && artifactVerified) {
3064
1563
  if (!isHookUnit) {
3065
- // Only persist completion keys for real units — hook keys are
3066
- // ephemeral and should not pollute the idempotency set.
3067
- persistCompletedKey(basePath, closeoutKey);
3068
- completedKeySet.add(closeoutKey);
1564
+ persistCompletedKey(s.basePath, closeoutKey);
1565
+ s.completedKeySet.add(closeoutKey);
3069
1566
  }
3070
1567
 
3071
- completedUnits.push({
3072
- type: currentUnit.type,
3073
- id: currentUnit.id,
3074
- startedAt: currentUnit.startedAt,
1568
+ s.completedUnits.push({
1569
+ type: s.currentUnit.type,
1570
+ id: s.currentUnit.id,
1571
+ startedAt: s.currentUnit.startedAt,
3075
1572
  finishedAt: Date.now(),
3076
1573
  });
3077
- // Cap to last 200 entries to prevent unbounded growth (#611)
3078
- if (completedUnits.length > 200) {
3079
- completedUnits = completedUnits.slice(-200);
1574
+ if (s.completedUnits.length > 200) {
1575
+ s.completedUnits = s.completedUnits.slice(-200);
3080
1576
  }
3081
- clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
3082
- unitDispatchCount.delete(`${currentUnit.type}/${currentUnit.id}`);
3083
- unitRecoveryCount.delete(`${currentUnit.type}/${currentUnit.id}`);
1577
+ clearUnitRuntimeRecord(s.basePath, s.currentUnit.type, s.currentUnit.id);
1578
+ s.unitDispatchCount.delete(`${s.currentUnit.type}/${s.currentUnit.id}`);
1579
+ s.unitRecoveryCount.delete(`${s.currentUnit.type}/${s.currentUnit.id}`);
3084
1580
  }
3085
1581
  }
3086
- currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
3087
- captureAvailableSkills(); // Capture skill telemetry at dispatch time (#599)
3088
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
1582
+ s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
1583
+ captureAvailableSkills();
1584
+ writeUnitRuntimeRecord(s.basePath, unitType, unitId, s.currentUnit.startedAt, {
3089
1585
  phase: "dispatched",
3090
1586
  wrapupWarningSent: false,
3091
1587
  timeoutAt: null,
3092
- lastProgressAt: currentUnit.startedAt,
1588
+ lastProgressAt: s.currentUnit.startedAt,
3093
1589
  progressCount: 0,
3094
1590
  lastProgressKind: "dispatch",
3095
1591
  });
3096
1592
 
3097
1593
  // Status bar + progress widget
3098
1594
  ctx.ui.setStatus("gsd-auto", "auto");
3099
- if (mid) updateSliceProgressCache(basePath, mid, state.activeSlice?.id);
1595
+ if (mid) updateSliceProgressCache(s.basePath, mid, state.activeSlice?.id);
3100
1596
  updateProgressWidget(ctx, unitType, unitId, state);
3101
1597
 
3102
- // Ensure preconditions — create directories, branches, etc.
3103
- // so the LLM doesn't have to get these right
3104
- ensurePreconditions(unitType, unitId, basePath, state);
1598
+ ensurePreconditions(unitType, unitId, s.basePath, state);
3105
1599
 
3106
- // Fresh session
3107
- const result = await cmdCtx!.newSession();
1600
+ // Fresh session — with timeout to prevent permanent hangs (#1073).
1601
+ // If newSession() hangs (e.g., session manager deadlock, network issue),
1602
+ // without this timeout the entire dispatch chain stalls permanently: no
1603
+ // timeouts are set, no gap watchdog fires, and auto-mode is left active
1604
+ // but idle until the user Ctrl+C's.
1605
+ let result: { cancelled: boolean };
1606
+ try {
1607
+ const sessionPromise = s.cmdCtx!.newSession();
1608
+ const timeoutPromise = new Promise<{ cancelled: true }>((resolve) =>
1609
+ setTimeout(() => resolve({ cancelled: true }), NEW_SESSION_TIMEOUT_MS),
1610
+ );
1611
+ result = await Promise.race([sessionPromise, timeoutPromise]);
1612
+ } catch (sessionErr) {
1613
+ const msg = sessionErr instanceof Error ? sessionErr.message : String(sessionErr);
1614
+ ctx.ui.notify(`Session creation failed: ${msg}. Retrying via watchdog.`, "error");
1615
+ throw new Error(`newSession() failed: ${msg}`);
1616
+ }
3108
1617
  if (result.cancelled) {
3109
- await stopAuto(ctx, pi, "Session cancelled");
1618
+ ctx.ui.notify(
1619
+ `Session creation timed out or was cancelled for ${unitType} ${unitId}. Will retry.`,
1620
+ "warning",
1621
+ );
1622
+ await stopAuto(ctx, pi, "Session creation failed");
3110
1623
  return;
3111
1624
  }
3112
1625
 
3113
- // Branchless architecture: all work commits sequentially on the milestone
3114
- // branch — no per-slice branches or slice-level merges. Milestone merge
3115
- // happens when phase === "complete" (see mergeMilestoneToMain above).
3116
-
3117
- // Write lock AFTER newSession so we capture the session file path.
3118
- // Pi appends entries incrementally via appendFileSync, so on crash the
3119
- // session file survives with every tool call up to the crash point.
3120
1626
  const sessionFile = ctx.sessionManager.getSessionFile();
3121
- writeLock(lockBase(), unitType, unitId, completedUnits.length, sessionFile);
1627
+ updateSessionLock(lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
1628
+ writeLock(lockBase(), unitType, unitId, s.completedUnits.length, sessionFile);
3122
1629
 
3123
- // On crash recovery, prepend the full recovery briefing
3124
- // On retry (stuck detection), prepend deep diagnostic from last attempt
3125
- // Cap injected content to prevent unbounded prompt growth → OOM
1630
+ // Prompt injection
3126
1631
  const MAX_RECOVERY_CHARS = 50_000;
3127
1632
  let finalPrompt = prompt;
3128
1633
 
3129
- // Verification retry — inject failure context so the agent can auto-fix
3130
- if (pendingVerificationRetry) {
3131
- const retryCtx = pendingVerificationRetry;
3132
- pendingVerificationRetry = null;
1634
+ if (s.pendingVerificationRetry) {
1635
+ const retryCtx = s.pendingVerificationRetry;
1636
+ s.pendingVerificationRetry = null;
3133
1637
  const capped = retryCtx.failureContext.length > MAX_RECOVERY_CHARS
3134
1638
  ? retryCtx.failureContext.slice(0, MAX_RECOVERY_CHARS) + "\n\n[...failure context truncated]"
3135
1639
  : retryCtx.failureContext;
3136
1640
  finalPrompt = `**VERIFICATION FAILED — AUTO-FIX ATTEMPT ${retryCtx.attempt}**\n\nThe verification gate ran after your previous attempt and found failures. Fix these issues before completing the task.\n\n${capped}\n\n---\n\n${finalPrompt}`;
3137
1641
  }
3138
1642
 
3139
- if (pendingCrashRecovery) {
3140
- const capped = pendingCrashRecovery.length > MAX_RECOVERY_CHARS
3141
- ? pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) + "\n\n[...recovery briefing truncated to prevent memory exhaustion]"
3142
- : pendingCrashRecovery;
1643
+ if (s.pendingCrashRecovery) {
1644
+ const capped = s.pendingCrashRecovery.length > MAX_RECOVERY_CHARS
1645
+ ? s.pendingCrashRecovery.slice(0, MAX_RECOVERY_CHARS) + "\n\n[...recovery briefing truncated to prevent memory exhaustion]"
1646
+ : s.pendingCrashRecovery;
3143
1647
  finalPrompt = `${capped}\n\n---\n\n${finalPrompt}`;
3144
- pendingCrashRecovery = null;
3145
- } else if ((unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
3146
- const diagnostic = getDeepDiagnostic(basePath);
1648
+ s.pendingCrashRecovery = null;
1649
+ } else if ((s.unitDispatchCount.get(`${unitType}/${unitId}`) ?? 0) > 1) {
1650
+ const diagnostic = getDeepDiagnostic(s.basePath);
3147
1651
  if (diagnostic) {
3148
1652
  const cappedDiag = diagnostic.length > MAX_RECOVERY_CHARS
3149
1653
  ? diagnostic.slice(0, MAX_RECOVERY_CHARS) + "\n\n[...diagnostic truncated to prevent memory exhaustion]"
@@ -3152,313 +1656,65 @@ async function dispatchNextUnit(
3152
1656
  }
3153
1657
  }
3154
1658
 
3155
- // Inject observability repair instructions so the agent fixes gaps before
3156
- // proceeding with the unit (see #174).
3157
1659
  const repairBlock = buildObservabilityRepairBlock(observabilityIssues);
3158
1660
  if (repairBlock) {
3159
1661
  finalPrompt = `${finalPrompt}${repairBlock}`;
3160
1662
  }
3161
1663
 
3162
- // ── Prompt char measurement (R051) ──
3163
- lastPromptCharCount = finalPrompt.length;
3164
- lastBaselineCharCount = undefined;
1664
+ // ── Prompt char measurement ──
1665
+ s.lastPromptCharCount = finalPrompt.length;
1666
+ s.lastBaselineCharCount = undefined;
3165
1667
  if (isDbAvailable()) {
3166
1668
  try {
3167
1669
  const { inlineGsdRootFile } = await import("./auto-prompts.js");
3168
1670
  const [decisionsContent, requirementsContent, projectContent] = await Promise.all([
3169
- inlineGsdRootFile(basePath, "decisions.md", "Decisions"),
3170
- inlineGsdRootFile(basePath, "requirements.md", "Requirements"),
3171
- inlineGsdRootFile(basePath, "project.md", "Project"),
1671
+ inlineGsdRootFile(s.basePath, "decisions.md", "Decisions"),
1672
+ inlineGsdRootFile(s.basePath, "requirements.md", "Requirements"),
1673
+ inlineGsdRootFile(s.basePath, "project.md", "Project"),
3172
1674
  ]);
3173
- lastBaselineCharCount =
1675
+ s.lastBaselineCharCount =
3174
1676
  (decisionsContent?.length ?? 0) +
3175
1677
  (requirementsContent?.length ?? 0) +
3176
1678
  (projectContent?.length ?? 0);
3177
1679
  } catch {
3178
- // Non-fatal — baseline measurement is best-effort
1680
+ // Non-fatal
3179
1681
  }
3180
1682
  }
3181
1683
 
3182
- // Switch model if preferences specify one for this unit type
3183
- // Try primary model, then fallbacks in order if setting fails
3184
- const modelConfig = resolveModelWithFallbacksForUnit(unitType);
3185
- if (modelConfig) {
3186
- const availableModels = ctx.modelRegistry.getAvailable();
3187
-
3188
- // ─── Dynamic Model Routing ─────────────────────────────────────────
3189
- // If enabled, classify unit complexity and potentially downgrade to a
3190
- // cheaper model. The user's configured model is the ceiling.
3191
- const routingConfig = resolveDynamicRoutingConfig();
3192
- let effectiveModelConfig = modelConfig;
3193
- let routingTierLabel = "";
3194
- currentUnitRouting = null;
3195
-
3196
- if (routingConfig.enabled) {
3197
- // Compute budget pressure if budget ceiling is set
3198
- let budgetPct: number | undefined;
3199
- if (routingConfig.budget_pressure !== false) {
3200
- const budgetCeiling = prefs?.budget_ceiling;
3201
- if (budgetCeiling !== undefined && budgetCeiling > 0) {
3202
- const currentLedger = getLedger();
3203
- const totalCost = currentLedger ? getProjectTotals(currentLedger.units).cost : 0;
3204
- budgetPct = totalCost / budgetCeiling;
3205
- }
3206
- }
3207
-
3208
- // Classify complexity (hook routing controlled by config.hooks)
3209
- const isHook = unitType.startsWith("hook/");
3210
- const shouldClassify = !isHook || routingConfig.hooks !== false;
3211
-
3212
- if (shouldClassify) {
3213
- const classification = classifyUnitComplexity(unitType, unitId, basePath, budgetPct);
3214
- const availableModelIds = availableModels.map(m => m.id);
3215
- const routing = resolveModelForComplexity(classification, modelConfig, routingConfig, availableModelIds);
3216
-
3217
- if (routing.wasDowngraded) {
3218
- effectiveModelConfig = {
3219
- primary: routing.modelId,
3220
- fallbacks: routing.fallbacks,
3221
- };
3222
- if (verbose) {
3223
- ctx.ui.notify(
3224
- `Dynamic routing [${tierLabel(classification.tier)}]: ${routing.modelId} (${classification.reason})`,
3225
- "info",
3226
- );
3227
- }
3228
- }
3229
- routingTierLabel = ` [${tierLabel(classification.tier)}]`;
3230
- currentUnitRouting = { tier: classification.tier, modelDowngraded: routing.wasDowngraded };
3231
- }
3232
- }
3233
-
3234
- const modelsToTry = [effectiveModelConfig.primary, ...effectiveModelConfig.fallbacks];
3235
- let modelSet = false;
3236
-
3237
- for (const modelId of modelsToTry) {
3238
- // Resolve model from available models.
3239
- // Handles multiple formats:
3240
- // "provider/model" → explicit provider targeting (e.g. "anthropic/claude-opus-4-6")
3241
- // "bare-id" → match by ID across providers
3242
- // "org/model-name" → OpenRouter-style IDs where the full string is the model ID
3243
- // "openrouter/org/model" → explicit provider + OpenRouter model ID
3244
- const slashIdx = modelId.indexOf("/");
3245
- let model;
3246
- if (slashIdx !== -1) {
3247
- const maybeProvider = modelId.substring(0, slashIdx);
3248
- const id = modelId.substring(slashIdx + 1);
3249
-
3250
- // Check if the prefix before the first slash is a known provider
3251
- const knownProviders = new Set(availableModels.map(m => m.provider.toLowerCase()));
3252
- if (knownProviders.has(maybeProvider.toLowerCase())) {
3253
- // Explicit "provider/model" format (handles "openrouter/org/model" too)
3254
- model = availableModels.find(
3255
- m => m.provider.toLowerCase() === maybeProvider.toLowerCase()
3256
- && m.id.toLowerCase() === id.toLowerCase(),
3257
- );
3258
- }
3259
-
3260
- // If the prefix wasn't a known provider, or no match was found within that provider,
3261
- // try matching the full string as a model ID (OpenRouter-style IDs like "org/model-name")
3262
- if (!model) {
3263
- const lower = modelId.toLowerCase();
3264
- model = availableModels.find(
3265
- m => m.id.toLowerCase() === lower
3266
- || `${m.provider}/${m.id}`.toLowerCase() === lower,
3267
- );
3268
- }
3269
- } else {
3270
- // For bare IDs, prefer the current session's provider, then first available match
3271
- const currentProvider = ctx.model?.provider;
3272
- const exactProviderMatch = availableModels.find(
3273
- m => m.id === modelId && m.provider === currentProvider,
3274
- );
3275
- const anyMatch = availableModels.find(m => m.id === modelId);
3276
- model = exactProviderMatch ?? anyMatch;
3277
-
3278
- // Warn if the ID is ambiguous across providers
3279
- if (anyMatch && !exactProviderMatch) {
3280
- const providers = availableModels
3281
- .filter(m => m.id === modelId)
3282
- .map(m => m.provider);
3283
- if (providers.length > 1) {
3284
- ctx.ui.notify(
3285
- `Model ID "${modelId}" exists in multiple providers (${providers.join(", ")}). ` +
3286
- `Resolved to ${anyMatch.provider}. Use "provider/model" format for explicit targeting.`,
3287
- "warning",
3288
- );
3289
- }
3290
- }
3291
- }
3292
- if (!model) {
3293
- if (verbose) ctx.ui.notify(`Model ${modelId} not found, trying fallback.`, "info");
3294
- continue;
3295
- }
3296
-
3297
- const ok = await pi.setModel(model, { persist: false });
3298
- if (ok) {
3299
- const fallbackNote = modelId === effectiveModelConfig.primary
3300
- ? ""
3301
- : ` (fallback from ${effectiveModelConfig.primary})`;
3302
- const phase = unitPhaseLabel(unitType);
3303
- ctx.ui.notify(`Model [${phase}]${routingTierLabel}: ${model.provider}/${model.id}${fallbackNote}`, "info");
3304
- modelSet = true;
3305
- break;
3306
- } else {
3307
- const nextModel = modelsToTry[modelsToTry.indexOf(modelId) + 1];
3308
- if (nextModel) {
3309
- if (verbose) ctx.ui.notify(`Failed to set model ${modelId}, trying ${nextModel}...`, "info");
3310
- } else {
3311
- ctx.ui.notify(`All preferred models unavailable for ${unitType}. Using default.`, "warning");
3312
- }
3313
- }
3314
- }
3315
-
3316
- // modelSet=false is already handled by the "all fallbacks exhausted" warning above
3317
- } else if (autoModeStartModel) {
3318
- // No model preference for this unit type — re-apply the model captured
3319
- // at auto-mode start to prevent bleed from the shared global settings.json
3320
- // when multiple GSD instances run concurrently (#650).
3321
- const availableModels = ctx.modelRegistry.getAvailable();
3322
- const startModel = availableModels.find(
3323
- m => m.provider === autoModeStartModel!.provider && m.id === autoModeStartModel!.id,
3324
- );
3325
- if (startModel) {
3326
- const ok = await pi.setModel(startModel, { persist: false });
3327
- if (!ok) {
3328
- // Fallback: try matching just by ID across providers
3329
- const byId = availableModels.find(m => m.id === autoModeStartModel!.id);
3330
- if (byId) await pi.setModel(byId, { persist: false });
3331
- }
3332
- }
1684
+ // Cache-optimize prompt section ordering
1685
+ try {
1686
+ const { reorderForCaching } = await import("./prompt-ordering.js");
1687
+ finalPrompt = reorderForCaching(finalPrompt);
1688
+ } catch (reorderErr) {
1689
+ const msg = reorderErr instanceof Error ? reorderErr.message : String(reorderErr);
1690
+ process.stderr.write(`[gsd] prompt reorder failed (non-fatal): ${msg}\n`);
3333
1691
  }
3334
1692
 
3335
- // Start progress-aware supervision: a soft warning, an idle watchdog, and
3336
- // a larger hard ceiling. Productive long-running tasks may continue past the
3337
- // soft timeout; only idle/stalled tasks pause early.
3338
- clearUnitTimeout();
3339
- const supervisor = resolveAutoSupervisorConfig();
3340
- const softTimeoutMs = (supervisor.soft_timeout_minutes ?? 0) * 60 * 1000;
3341
- const idleTimeoutMs = (supervisor.idle_timeout_minutes ?? 0) * 60 * 1000;
3342
- const hardTimeoutMs = (supervisor.hard_timeout_minutes ?? 0) * 60 * 1000;
3343
-
3344
- wrapupWarningHandle = setTimeout(() => {
3345
- wrapupWarningHandle = null;
3346
- if (!active || !currentUnit) return;
3347
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3348
- phase: "wrapup-warning-sent",
3349
- wrapupWarningSent: true,
3350
- });
3351
- pi.sendMessage(
3352
- {
3353
- customType: "gsd-auto-wrapup",
3354
- display: verbose,
3355
- content: [
3356
- "**TIME BUDGET WARNING — keep going only if progress is real.**",
3357
- "This unit crossed the soft time budget.",
3358
- "If you are making progress, continue. If not, switch to wrap-up mode now:",
3359
- "1. rerun the minimal required verification",
3360
- "2. write or update the required durable artifacts",
3361
- "3. mark task or slice state on disk correctly",
3362
- "4. leave precise resume notes if anything remains unfinished",
3363
- ].join("\n"),
3364
- },
3365
- { triggerTurn: true },
3366
- );
3367
- }, softTimeoutMs);
3368
-
3369
- idleWatchdogHandle = setInterval(async () => {
3370
- if (!active || !currentUnit) return;
3371
- const runtime = readUnitRuntimeRecord(basePath, unitType, unitId);
3372
- if (!runtime) return;
3373
- if (Date.now() - runtime.lastProgressAt < idleTimeoutMs) return;
3374
-
3375
- // Agent has tool calls currently executing (await_job, long bash, etc.) —
3376
- // not idle, just waiting for tool completion. But only suppress recovery
3377
- // if the tool started recently. A tool in-flight for longer than the idle
3378
- // timeout is likely stuck — e.g., `python -m http.server 8080 &` keeps the
3379
- // shell's stdout/stderr open, causing the Bash tool to hang indefinitely.
3380
- if (inFlightTools.size > 0) {
3381
- const oldestStart = Math.min(...inFlightTools.values());
3382
- const toolAgeMs = Date.now() - oldestStart;
3383
- if (toolAgeMs < idleTimeoutMs) {
3384
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3385
- lastProgressAt: Date.now(),
3386
- lastProgressKind: "tool-in-flight",
3387
- });
3388
- return;
3389
- }
3390
- // Oldest tool has been running >= idleTimeoutMs — treat as a stuck/hung
3391
- // tool (e.g., background process holding stdout open). Fall through to
3392
- // idle recovery without resetting the progress clock.
3393
- ctx.ui.notify(
3394
- `Stalled tool detected: a tool has been in-flight for ${Math.round(toolAgeMs / 60000)}min. Treating as hung — attempting idle recovery.`,
3395
- "warning",
3396
- );
3397
- }
3398
-
3399
- // Before triggering recovery, check if the agent is actually producing
3400
- // work on disk. `git status --porcelain` is cheap and catches any
3401
- // staged/unstaged/untracked changes the agent made since lastProgressAt.
3402
- if (detectWorkingTreeActivity(basePath)) {
3403
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3404
- lastProgressAt: Date.now(),
3405
- lastProgressKind: "filesystem-activity",
3406
- });
3407
- return;
3408
- }
3409
-
3410
- if (currentUnit) {
3411
- const modelId = ctx.model?.id ?? "unknown";
3412
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
3413
- }
3414
- saveActivityLog(ctx, basePath, unitType, unitId);
3415
-
3416
- const recovery = await recoverTimedOutUnit(ctx, pi, unitType, unitId, "idle");
3417
- if (recovery === "recovered") return;
3418
-
3419
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3420
- phase: "paused",
3421
- });
3422
- ctx.ui.notify(
3423
- `Unit ${unitType} ${unitId} made no meaningful progress for ${supervisor.idle_timeout_minutes}min. Pausing auto-mode.`,
3424
- "warning",
3425
- );
3426
- await pauseAuto(ctx, pi);
3427
- }, 15000);
3428
-
3429
- unitTimeoutHandle = setTimeout(async () => {
3430
- unitTimeoutHandle = null;
3431
- if (!active) return;
3432
- if (currentUnit) {
3433
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3434
- phase: "timeout",
3435
- timeoutAt: Date.now(),
3436
- });
3437
- const modelId = ctx.model?.id ?? "unknown";
3438
- snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
3439
- }
3440
- saveActivityLog(ctx, basePath, unitType, unitId);
3441
-
3442
- const recovery = await recoverTimedOutUnit(ctx, pi, unitType, unitId, "hard");
3443
- if (recovery === "recovered") return;
1693
+ // Select and apply model
1694
+ const modelResult = await selectAndApplyModel(ctx, pi, unitType, unitId, s.basePath, prefs, s.verbose, s.autoModeStartModel);
1695
+ s.currentUnitRouting = modelResult.routing;
3444
1696
 
3445
- ctx.ui.notify(
3446
- `Unit ${unitType} ${unitId} exceeded ${supervisor.hard_timeout_minutes}min hard timeout. Pausing auto-mode.`,
3447
- "warning",
3448
- );
3449
- await pauseAuto(ctx, pi);
3450
- }, hardTimeoutMs);
1697
+ // ── Start unit supervision (delegated to auto-timers.ts) ──
1698
+ clearUnitTimeout();
1699
+ startUnitSupervision({
1700
+ s,
1701
+ ctx,
1702
+ pi,
1703
+ unitType,
1704
+ unitId,
1705
+ prefs,
1706
+ buildSnapshotOpts: () => buildSnapshotOpts(unitType, unitId),
1707
+ buildRecoveryContext: () => buildRecoveryContext(),
1708
+ pauseAuto,
1709
+ });
3451
1710
 
3452
- // Inject prompt — verify auto-mode still active (guards against race with timeout/pause)
3453
- if (!active) return;
1711
+ // Inject prompt
1712
+ if (!s.active) return;
3454
1713
  pi.sendMessage(
3455
- { customType: "gsd-auto", content: finalPrompt, display: verbose },
1714
+ { customType: "gsd-auto", content: finalPrompt, display: s.verbose },
3456
1715
  { triggerTurn: true },
3457
1716
  );
3458
1717
 
3459
- // For non-artifact-driven UAT types, pause auto-mode after sending the prompt.
3460
- // The agent will write the UAT result file surfacing it for human review,
3461
- // then on resume the result file exists and run-uat is skipped automatically.
3462
1718
  if (pauseAfterUatDispatch) {
3463
1719
  ctx.ui.notify(
3464
1720
  "UAT requires human execution. Auto-mode will pause after this unit writes the result file.",
@@ -3467,7 +1723,7 @@ async function dispatchNextUnit(
3467
1723
  await pauseAuto(ctx, pi);
3468
1724
  }
3469
1725
  } finally {
3470
- _dispatching = false;
1726
+ s.dispatching = false;
3471
1727
  }
3472
1728
  }
3473
1729
 
@@ -3483,32 +1739,26 @@ function ensurePreconditions(
3483
1739
  const parts = unitId.split("/");
3484
1740
  const mid = parts[0]!;
3485
1741
 
3486
- // Always ensure milestone dir exists
3487
1742
  const mDir = resolveMilestonePath(base, mid);
3488
1743
  if (!mDir) {
3489
1744
  const newDir = join(milestonesDir(base), mid);
3490
1745
  mkdirSync(join(newDir, "slices"), { recursive: true });
3491
1746
  }
3492
1747
 
3493
- // For slice-level units, ensure slice dir exists
3494
1748
  if (parts.length >= 2) {
3495
1749
  const sid = parts[1]!;
3496
1750
 
3497
- // Re-resolve milestone path after potential creation
3498
1751
  const mDirResolved = resolveMilestonePath(base, mid);
3499
1752
  if (mDirResolved) {
3500
1753
  const slicesDir = join(mDirResolved, "slices");
3501
1754
  const sDir = resolveDir(slicesDir, sid);
3502
1755
  if (!sDir) {
3503
- // Create slice dir with bare ID
3504
- const newSliceDir = join(slicesDir, sid);
3505
- mkdirSync(join(newSliceDir, "tasks"), { recursive: true });
3506
- } else {
3507
- // Ensure tasks/ subdir exists
3508
- const tasksDir = join(slicesDir, sDir, "tasks");
3509
- if (!existsSync(tasksDir)) {
3510
- mkdirSync(tasksDir, { recursive: true });
3511
- }
1756
+ mkdirSync(join(slicesDir, sid, "tasks"), { recursive: true });
1757
+ }
1758
+ const resolvedSliceDir = resolveDir(slicesDir, sid) ?? sid;
1759
+ const tasksDir = join(slicesDir, resolvedSliceDir, "tasks");
1760
+ if (!existsSync(tasksDir)) {
1761
+ mkdirSync(tasksDir, { recursive: true });
3512
1762
  }
3513
1763
  }
3514
1764
  }
@@ -3517,296 +1767,12 @@ function ensurePreconditions(
3517
1767
 
3518
1768
  // ─── Diagnostics ──────────────────────────────────────────────────────────────
3519
1769
 
3520
- async function collectObservabilityWarnings(
3521
- ctx: ExtensionContext,
3522
- unitType: string,
3523
- unitId: string,
3524
- ): Promise<import("./observability-validator.ts").ValidationIssue[]> {
3525
- // Hook units have custom artifacts — skip standard observability checks
3526
- if (unitType.startsWith("hook/")) return [];
3527
-
3528
- const parts = unitId.split("/");
3529
- const mid = parts[0];
3530
- const sid = parts[1];
3531
- const tid = parts[2];
3532
-
3533
- if (!mid || !sid) return [];
3534
-
3535
- let issues = [] as Awaited<ReturnType<typeof validatePlanBoundary>>;
3536
-
3537
- if (unitType === "plan-slice") {
3538
- issues = await validatePlanBoundary(basePath, mid, sid);
3539
- } else if (unitType === "execute-task" && tid) {
3540
- issues = await validateExecuteBoundary(basePath, mid, sid, tid);
3541
- } else if (unitType === "complete-slice") {
3542
- issues = await validateCompleteBoundary(basePath, mid, sid);
3543
- }
3544
-
3545
- if (issues.length > 0) {
3546
- ctx.ui.notify(
3547
- `Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
3548
- "warning",
3549
- );
3550
- }
3551
-
3552
- return issues;
3553
- }
3554
-
3555
- function buildObservabilityRepairBlock(issues: import("./observability-validator.ts").ValidationIssue[]): string {
3556
- if (issues.length === 0) return "";
3557
- const items = issues.map(issue => {
3558
- const fileName = issue.file.split("/").pop() || issue.file;
3559
- let line = `- **${fileName}**: ${issue.message}`;
3560
- if (issue.suggestion) line += ` → ${issue.suggestion}`;
3561
- return line;
3562
- });
3563
- return [
3564
- "",
3565
- "---",
3566
- "",
3567
- "## Pre-flight: Observability gaps to fix FIRST",
3568
- "",
3569
- "The following issues were detected in plan/summary files for this unit.",
3570
- "**Read each flagged file, apply the fix described, then proceed with the unit.**",
3571
- "",
3572
- ...items,
3573
- "",
3574
- "---",
3575
- "",
3576
- ].join("\n");
3577
- }
3578
-
3579
- async function recoverTimedOutUnit(
3580
- ctx: ExtensionContext,
3581
- pi: ExtensionAPI,
3582
- unitType: string,
3583
- unitId: string,
3584
- reason: "idle" | "hard",
3585
- ): Promise<"recovered" | "paused"> {
3586
- if (!currentUnit) return "paused";
3587
-
3588
- const runtime = readUnitRuntimeRecord(basePath, unitType, unitId);
3589
- const recoveryAttempts = runtime?.recoveryAttempts ?? 0;
3590
- const maxRecoveryAttempts = reason === "idle" ? 2 : 1;
3591
-
3592
- const recoveryKey = `${unitType}/${unitId}`;
3593
- const attemptNumber = (unitRecoveryCount.get(recoveryKey) ?? 0) + 1;
3594
- unitRecoveryCount.set(recoveryKey, attemptNumber);
3595
-
3596
- if (attemptNumber > 1) {
3597
- // Exponential backoff: 2^(n-1) seconds, capped at 30s
3598
- const backoffMs = Math.min(1000 * Math.pow(2, attemptNumber - 2), 30000);
3599
- ctx.ui.notify(
3600
- `Recovery attempt ${attemptNumber} for ${unitType} ${unitId}. Waiting ${backoffMs / 1000}s before retry.`,
3601
- "info",
3602
- );
3603
- await new Promise(r => setTimeout(r, backoffMs));
3604
- }
3605
-
3606
- if (unitType === "execute-task") {
3607
- const status = await inspectExecuteTaskDurability(basePath, unitId);
3608
- if (!status) return "paused";
3609
-
3610
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3611
- recovery: status,
3612
- });
3613
-
3614
- const durableComplete = status.summaryExists && status.taskChecked && status.nextActionAdvanced;
3615
- if (durableComplete) {
3616
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3617
- phase: "finalized",
3618
- recovery: status,
3619
- });
3620
- ctx.ui.notify(
3621
- `${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} already completed on disk. Continuing auto-mode. (attempt ${attemptNumber})`,
3622
- "info",
3623
- );
3624
- unitRecoveryCount.delete(recoveryKey);
3625
- await dispatchNextUnit(ctx, pi);
3626
- return "recovered";
3627
- }
3628
-
3629
- if (recoveryAttempts < maxRecoveryAttempts) {
3630
- const isEscalation = recoveryAttempts > 0;
3631
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3632
- phase: "recovered",
3633
- recovery: status,
3634
- recoveryAttempts: recoveryAttempts + 1,
3635
- lastRecoveryReason: reason,
3636
- lastProgressAt: Date.now(),
3637
- progressCount: (runtime?.progressCount ?? 0) + 1,
3638
- lastProgressKind: reason === "idle" ? "idle-recovery-retry" : "hard-recovery-retry",
3639
- });
3640
-
3641
- const steeringLines = isEscalation
3642
- ? [
3643
- `**FINAL ${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — last chance before this task is skipped.**`,
3644
- `You are still executing ${unitType} ${unitId}.`,
3645
- `Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
3646
- `Current durability status: ${formatExecuteTaskRecoveryStatus(status)}.`,
3647
- "You MUST finish the durable output NOW, even if incomplete.",
3648
- "Write the task summary with whatever you have accomplished so far.",
3649
- "Mark the task [x] in the plan. Commit your work.",
3650
- "A partial summary is infinitely better than no summary.",
3651
- ]
3652
- : [
3653
- `**${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — do not stop.**`,
3654
- `You are still executing ${unitType} ${unitId}.`,
3655
- `Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
3656
- `Current durability status: ${formatExecuteTaskRecoveryStatus(status)}.`,
3657
- "Do not keep exploring.",
3658
- "Immediately finish the required durable output for this unit.",
3659
- "If full completion is impossible, write the partial artifact/state needed for recovery and make the blocker explicit.",
3660
- ];
3661
-
3662
- pi.sendMessage(
3663
- {
3664
- customType: "gsd-auto-timeout-recovery",
3665
- display: verbose,
3666
- content: steeringLines.join("\n"),
3667
- },
3668
- { triggerTurn: true, deliverAs: "steer" },
3669
- );
3670
- ctx.ui.notify(
3671
- `${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to finish durable output (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
3672
- "warning",
3673
- );
3674
- return "recovered";
3675
- }
3676
-
3677
- // Retries exhausted — write missing durable artifacts and advance.
3678
- const diagnostic = formatExecuteTaskRecoveryStatus(status);
3679
- const [mid, sid, tid] = unitId.split("/");
3680
- const skipped = mid && sid && tid
3681
- ? skipExecuteTask(basePath, mid, sid, tid, status, reason, maxRecoveryAttempts)
3682
- : false;
3683
-
3684
- if (skipped) {
3685
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3686
- phase: "skipped",
3687
- recovery: status,
3688
- recoveryAttempts: recoveryAttempts + 1,
3689
- lastRecoveryReason: reason,
3690
- });
3691
- ctx.ui.notify(
3692
- `${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts (${diagnostic}). Blocker artifacts written. Advancing pipeline. (attempt ${attemptNumber})`,
3693
- "warning",
3694
- );
3695
- unitRecoveryCount.delete(recoveryKey);
3696
- await dispatchNextUnit(ctx, pi);
3697
- return "recovered";
3698
- }
3699
-
3700
- // Fallback: couldn't write skip artifacts — pause as before.
3701
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3702
- phase: "paused",
3703
- recovery: status,
3704
- recoveryAttempts: recoveryAttempts + 1,
3705
- lastRecoveryReason: reason,
3706
- });
3707
- ctx.ui.notify(
3708
- `${reason === "idle" ? "Idle" : "Timeout"} recovery check for ${unitType} ${unitId}: ${diagnostic}`,
3709
- "warning",
3710
- );
3711
- return "paused";
3712
- }
3713
-
3714
- const expected = diagnoseExpectedArtifact(unitType, unitId, basePath) ?? "required durable artifact";
3715
-
3716
- // Check if the artifact already exists on disk — agent may have written it
3717
- // without signaling completion.
3718
- const artifactPath = resolveExpectedArtifactPath(unitType, unitId, basePath);
3719
- if (artifactPath && existsSync(artifactPath)) {
3720
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3721
- phase: "finalized",
3722
- recoveryAttempts: recoveryAttempts + 1,
3723
- lastRecoveryReason: reason,
3724
- });
3725
- ctx.ui.notify(
3726
- `${reason === "idle" ? "Idle" : "Timeout"} recovery: ${unitType} ${unitId} artifact already exists on disk. Advancing. (attempt ${attemptNumber})`,
3727
- "info",
3728
- );
3729
- unitRecoveryCount.delete(recoveryKey);
3730
- await dispatchNextUnit(ctx, pi);
3731
- return "recovered";
3732
- }
3733
-
3734
- if (recoveryAttempts < maxRecoveryAttempts) {
3735
- const isEscalation = recoveryAttempts > 0;
3736
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3737
- phase: "recovered",
3738
- recoveryAttempts: recoveryAttempts + 1,
3739
- lastRecoveryReason: reason,
3740
- lastProgressAt: Date.now(),
3741
- progressCount: (runtime?.progressCount ?? 0) + 1,
3742
- lastProgressKind: reason === "idle" ? "idle-recovery-retry" : "hard-recovery-retry",
3743
- });
3744
-
3745
- const steeringLines = isEscalation
3746
- ? [
3747
- `**FINAL ${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — last chance before skip.**`,
3748
- `You are still executing ${unitType} ${unitId}.`,
3749
- `Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts} — next failure skips this unit.`,
3750
- `Expected durable output: ${expected}.`,
3751
- "You MUST write the artifact file NOW, even if incomplete.",
3752
- "Write whatever you have — partial research, preliminary findings, best-effort analysis.",
3753
- "A partial artifact is infinitely better than no artifact.",
3754
- "If you are truly blocked, write the file with a BLOCKER section explaining why.",
3755
- ]
3756
- : [
3757
- `**${reason === "idle" ? "IDLE" : "HARD TIMEOUT"} RECOVERY — stay in auto-mode.**`,
3758
- `You are still executing ${unitType} ${unitId}.`,
3759
- `Recovery attempt ${recoveryAttempts + 1} of ${maxRecoveryAttempts}.`,
3760
- `Expected durable output: ${expected}.`,
3761
- "Stop broad exploration.",
3762
- "Write the required artifact now.",
3763
- "If blocked, write the partial artifact and explicitly record the blocker instead of going silent.",
3764
- ];
3765
-
3766
- pi.sendMessage(
3767
- {
3768
- customType: "gsd-auto-timeout-recovery",
3769
- display: verbose,
3770
- content: steeringLines.join("\n"),
3771
- },
3772
- { triggerTurn: true, deliverAs: "steer" },
3773
- );
3774
- ctx.ui.notify(
3775
- `${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to produce ${expected} (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`,
3776
- "warning",
3777
- );
3778
- return "recovered";
3779
- }
3780
-
3781
- // Retries exhausted — write a blocker placeholder and advance the pipeline
3782
- // instead of silently stalling.
3783
- const placeholder = writeBlockerPlaceholder(
3784
- unitType, unitId, basePath,
3785
- `${reason} recovery exhausted ${maxRecoveryAttempts} attempts without producing the artifact.`,
3786
- );
3787
-
3788
- if (placeholder) {
3789
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3790
- phase: "skipped",
3791
- recoveryAttempts: recoveryAttempts + 1,
3792
- lastRecoveryReason: reason,
3793
- });
3794
- ctx.ui.notify(
3795
- `${unitType} ${unitId} skipped after ${maxRecoveryAttempts} recovery attempts. Blocker placeholder written to ${placeholder}. Advancing pipeline. (attempt ${attemptNumber})`,
3796
- "warning",
3797
- );
3798
- unitRecoveryCount.delete(recoveryKey);
3799
- await dispatchNextUnit(ctx, pi);
3800
- return "recovered";
3801
- }
3802
-
3803
- // Fallback: couldn't resolve artifact path — pause as before.
3804
- writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
3805
- phase: "paused",
3806
- recoveryAttempts: recoveryAttempts + 1,
3807
- lastRecoveryReason: reason,
3808
- });
3809
- return "paused";
1770
+ /** Build recovery context from module state for recoverTimedOutUnit */
1771
+ function buildRecoveryContext(): import("./auto-timeout-recovery.js").RecoveryContext {
1772
+ return { basePath: s.basePath, verbose: s.verbose,
1773
+ currentUnitStartedAt: s.currentUnit?.startedAt ?? Date.now(), unitRecoveryCount: s.unitRecoveryCount,
1774
+ dispatchNextUnit,
1775
+ };
3810
1776
  }
3811
1777
 
3812
1778
  // Re-export recovery functions for external consumers
@@ -3822,9 +1788,8 @@ export {
3822
1788
  * Test-only: expose skip-loop state for unit tests.
3823
1789
  * Not part of the public API.
3824
1790
  */
3825
- export function _getUnitConsecutiveSkips(): Map<string, number> { return unitConsecutiveSkips; }
3826
- export function _resetUnitConsecutiveSkips(): void { unitConsecutiveSkips.clear(); }
3827
- export { MAX_CONSECUTIVE_SKIPS };
1791
+ export function _getUnitConsecutiveSkips(): Map<string, number> { return s.unitConsecutiveSkips; }
1792
+ export function _resetUnitConsecutiveSkips(): void { s.unitConsecutiveSkips.clear(); }
3828
1793
 
3829
1794
  /**
3830
1795
  * Dispatch a hook unit directly, bypassing normal pre-dispatch hooks.
@@ -3840,37 +1805,31 @@ export async function dispatchHookUnit(
3840
1805
  hookModel: string | undefined,
3841
1806
  targetBasePath: string,
3842
1807
  ): Promise<boolean> {
3843
- // Ensure auto-mode is active
3844
- if (!active) {
3845
- // Initialize auto-mode state minimally
3846
- active = true;
3847
- stepMode = true;
3848
- cmdCtx = ctx as ExtensionCommandContext;
3849
- basePath = targetBasePath;
3850
- autoStartTime = Date.now();
3851
- currentUnit = null;
3852
- completedUnits = [];
3853
- pendingQuickTasks = [];
1808
+ if (!s.active) {
1809
+ s.active = true;
1810
+ s.stepMode = true;
1811
+ s.cmdCtx = ctx as ExtensionCommandContext;
1812
+ s.basePath = targetBasePath;
1813
+ s.autoStartTime = Date.now();
1814
+ s.currentUnit = null;
1815
+ s.completedUnits = [];
1816
+ s.pendingQuickTasks = [];
3854
1817
  }
3855
1818
 
3856
1819
  const hookUnitType = `hook/${hookName}`;
3857
1820
  const hookStartedAt = Date.now();
3858
-
3859
- // Set up the trigger unit as the "current" unit so post-unit hooks can reference it
3860
- currentUnit = { type: triggerUnitType, id: triggerUnitId, startedAt: hookStartedAt };
3861
-
3862
- // Create a new session for the hook
3863
- const result = await cmdCtx!.newSession();
1821
+
1822
+ s.currentUnit = { type: triggerUnitType, id: triggerUnitId, startedAt: hookStartedAt };
1823
+
1824
+ const result = await s.cmdCtx!.newSession();
3864
1825
  if (result.cancelled) {
3865
1826
  await stopAuto(ctx, pi);
3866
1827
  return false;
3867
1828
  }
3868
1829
 
3869
- // Update current unit to the hook unit
3870
- currentUnit = { type: hookUnitType, id: triggerUnitId, startedAt: hookStartedAt };
3871
-
3872
- // Write runtime record
3873
- writeUnitRuntimeRecord(basePath, hookUnitType, triggerUnitId, hookStartedAt, {
1830
+ s.currentUnit = { type: hookUnitType, id: triggerUnitId, startedAt: hookStartedAt };
1831
+
1832
+ writeUnitRuntimeRecord(s.basePath, hookUnitType, triggerUnitId, hookStartedAt, {
3874
1833
  phase: "dispatched",
3875
1834
  wrapupWarningSent: false,
3876
1835
  timeoutAt: null,
@@ -3879,7 +1838,6 @@ export async function dispatchHookUnit(
3879
1838
  lastProgressKind: "dispatch",
3880
1839
  });
3881
1840
 
3882
- // Switch model if specified
3883
1841
  if (hookModel) {
3884
1842
  const availableModels = ctx.modelRegistry.getAvailable();
3885
1843
  const match = availableModels.find(m =>
@@ -3888,23 +1846,22 @@ export async function dispatchHookUnit(
3888
1846
  if (match) {
3889
1847
  try {
3890
1848
  await pi.setModel(match);
3891
- } catch { /* non-fatal — use current model */ }
1849
+ } catch { /* non-fatal */ }
3892
1850
  }
3893
1851
  }
3894
1852
 
3895
- // Write lock
3896
1853
  const sessionFile = ctx.sessionManager.getSessionFile();
3897
- writeLock(lockBase(), hookUnitType, triggerUnitId, completedUnits.length, sessionFile);
1854
+ updateSessionLock(lockBase(), hookUnitType, triggerUnitId, s.completedUnits.length, sessionFile);
1855
+ writeLock(lockBase(), hookUnitType, triggerUnitId, s.completedUnits.length, sessionFile);
3898
1856
 
3899
- // Set up timeout
3900
1857
  clearUnitTimeout();
3901
1858
  const supervisor = resolveAutoSupervisorConfig();
3902
1859
  const hookHardTimeoutMs = (supervisor.hard_timeout_minutes ?? 30) * 60 * 1000;
3903
- unitTimeoutHandle = setTimeout(async () => {
3904
- unitTimeoutHandle = null;
3905
- if (!active) return;
3906
- if (currentUnit) {
3907
- writeUnitRuntimeRecord(basePath, hookUnitType, triggerUnitId, hookStartedAt, {
1860
+ s.unitTimeoutHandle = setTimeout(async () => {
1861
+ s.unitTimeoutHandle = null;
1862
+ if (!s.active) return;
1863
+ if (s.currentUnit) {
1864
+ writeUnitRuntimeRecord(s.basePath, hookUnitType, triggerUnitId, hookStartedAt, {
3908
1865
  phase: "timeout",
3909
1866
  timeoutAt: Date.now(),
3910
1867
  });
@@ -3917,220 +1874,19 @@ export async function dispatchHookUnit(
3917
1874
  await pauseAuto(ctx, pi);
3918
1875
  }, hookHardTimeoutMs);
3919
1876
 
3920
- // Update status
3921
- ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
1877
+ ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
3922
1878
  ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
3923
1879
 
3924
- // Send the hook prompt
3925
1880
  console.log(`[dispatchHookUnit] Sending prompt of length ${hookPrompt.length}`);
3926
1881
  console.log(`[dispatchHookUnit] Prompt preview: ${hookPrompt.substring(0, 200)}...`);
3927
1882
  pi.sendMessage(
3928
1883
  { customType: "gsd-auto", content: hookPrompt, display: true },
3929
1884
  { triggerTurn: true },
3930
1885
  );
3931
-
1886
+
3932
1887
  return true;
3933
1888
  }
3934
1889
 
3935
1890
 
3936
- // ─── Direct Phase Dispatch ────────────────────────────────────────────────────
3937
-
3938
- export async function dispatchDirectPhase(
3939
- ctx: ExtensionCommandContext,
3940
- pi: ExtensionAPI,
3941
- phase: string,
3942
- base: string,
3943
- ): Promise<void> {
3944
- const state = await deriveState(base);
3945
- const mid = state.activeMilestone?.id;
3946
- const midTitle = state.activeMilestone?.title ?? "";
3947
-
3948
- if (!mid) {
3949
- ctx.ui.notify("Cannot dispatch: no active milestone.", "warning");
3950
- return;
3951
- }
3952
-
3953
- const normalized = phase.toLowerCase();
3954
- let unitType: string;
3955
- let unitId: string;
3956
- let prompt: string;
3957
-
3958
- switch (normalized) {
3959
- case "research":
3960
- case "research-milestone":
3961
- case "research-slice": {
3962
- const isSlice = normalized === "research-slice" || (normalized === "research" && state.phase !== "pre-planning");
3963
- if (isSlice) {
3964
- const sid = state.activeSlice?.id;
3965
- const sTitle = state.activeSlice?.title ?? "";
3966
- if (!sid) {
3967
- ctx.ui.notify("Cannot dispatch research-slice: no active slice.", "warning");
3968
- return;
3969
- }
3970
-
3971
- // When require_slice_discussion is enabled, pause auto-mode before
3972
- // each new slice so the user can discuss requirements first (#789).
3973
- const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
3974
- const requireDiscussion = loadEffectiveGSDPreferences()?.preferences?.phases?.require_slice_discussion;
3975
- if (requireDiscussion && !sliceContextFile) {
3976
- ctx.ui.notify(
3977
- `Slice ${sid} requires discussion before planning. Run /gsd discuss to discuss this slice, then /gsd auto to resume.`,
3978
- "info",
3979
- );
3980
- await pauseAuto(ctx, pi);
3981
- return;
3982
- }
3983
-
3984
- unitType = "research-slice";
3985
- unitId = `${mid}/${sid}`;
3986
- prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
3987
- } else {
3988
- unitType = "research-milestone";
3989
- unitId = mid;
3990
- prompt = await buildResearchMilestonePrompt(mid, midTitle, base);
3991
- }
3992
- break;
3993
- }
3994
-
3995
- case "plan":
3996
- case "plan-milestone":
3997
- case "plan-slice": {
3998
- const isSlice = normalized === "plan-slice" || (normalized === "plan" && state.phase !== "pre-planning");
3999
- if (isSlice) {
4000
- const sid = state.activeSlice?.id;
4001
- const sTitle = state.activeSlice?.title ?? "";
4002
- if (!sid) {
4003
- ctx.ui.notify("Cannot dispatch plan-slice: no active slice.", "warning");
4004
- return;
4005
- }
4006
- unitType = "plan-slice";
4007
- unitId = `${mid}/${sid}`;
4008
- prompt = await buildPlanSlicePrompt(mid, midTitle, sid, sTitle, base);
4009
- } else {
4010
- unitType = "plan-milestone";
4011
- unitId = mid;
4012
- prompt = await buildPlanMilestonePrompt(mid, midTitle, base);
4013
- }
4014
- break;
4015
- }
4016
-
4017
- case "execute":
4018
- case "execute-task": {
4019
- const sid = state.activeSlice?.id;
4020
- const sTitle = state.activeSlice?.title ?? "";
4021
- const tid = state.activeTask?.id;
4022
- const tTitle = state.activeTask?.title ?? "";
4023
- if (!sid) {
4024
- ctx.ui.notify("Cannot dispatch execute-task: no active slice.", "warning");
4025
- return;
4026
- }
4027
- if (!tid) {
4028
- ctx.ui.notify("Cannot dispatch execute-task: no active task.", "warning");
4029
- return;
4030
- }
4031
- unitType = "execute-task";
4032
- unitId = `${mid}/${sid}/${tid}`;
4033
- prompt = await buildExecuteTaskPrompt(mid, sid, sTitle, tid, tTitle, base);
4034
- break;
4035
- }
4036
-
4037
- case "complete":
4038
- case "complete-slice":
4039
- case "complete-milestone": {
4040
- const isSlice = normalized === "complete-slice" || (normalized === "complete" && state.phase === "summarizing");
4041
- if (isSlice) {
4042
- const sid = state.activeSlice?.id;
4043
- const sTitle = state.activeSlice?.title ?? "";
4044
- if (!sid) {
4045
- ctx.ui.notify("Cannot dispatch complete-slice: no active slice.", "warning");
4046
- return;
4047
- }
4048
- unitType = "complete-slice";
4049
- unitId = `${mid}/${sid}`;
4050
- prompt = await buildCompleteSlicePrompt(mid, midTitle, sid, sTitle, base);
4051
- } else {
4052
- unitType = "complete-milestone";
4053
- unitId = mid;
4054
- prompt = await buildCompleteMilestonePrompt(mid, midTitle, base);
4055
- }
4056
- break;
4057
- }
4058
-
4059
- case "reassess":
4060
- case "reassess-roadmap": {
4061
- const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
4062
- const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
4063
- if (!roadmapContent) {
4064
- ctx.ui.notify("Cannot dispatch reassess-roadmap: no roadmap found.", "warning");
4065
- return;
4066
- }
4067
- const roadmap = parseRoadmap(roadmapContent);
4068
- const completedSlices = roadmap.slices.filter(s => s.done);
4069
- if (completedSlices.length === 0) {
4070
- ctx.ui.notify("Cannot dispatch reassess-roadmap: no completed slices.", "warning");
4071
- return;
4072
- }
4073
- const completedSliceId = completedSlices[completedSlices.length - 1].id;
4074
- unitType = "reassess-roadmap";
4075
- unitId = `${mid}/${completedSliceId}`;
4076
- prompt = await buildReassessRoadmapPrompt(mid, midTitle, completedSliceId, base);
4077
- break;
4078
- }
4079
-
4080
- case "uat":
4081
- case "run-uat": {
4082
- const sid = state.activeSlice?.id;
4083
- if (!sid) {
4084
- ctx.ui.notify("Cannot dispatch run-uat: no active slice.", "warning");
4085
- return;
4086
- }
4087
- const uatFile = resolveSliceFile(base, mid, sid, "UAT");
4088
- if (!uatFile) {
4089
- ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
4090
- return;
4091
- }
4092
- const uatContent = await loadFile(uatFile);
4093
- if (!uatContent) {
4094
- ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
4095
- return;
4096
- }
4097
- const uatPath = relSliceFile(base, mid, sid, "UAT");
4098
- unitType = "run-uat";
4099
- unitId = `${mid}/${sid}`;
4100
- prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
4101
- break;
4102
- }
4103
-
4104
- case "replan":
4105
- case "replan-slice": {
4106
- const sid = state.activeSlice?.id;
4107
- const sTitle = state.activeSlice?.title ?? "";
4108
- if (!sid) {
4109
- ctx.ui.notify("Cannot dispatch replan-slice: no active slice.", "warning");
4110
- return;
4111
- }
4112
- unitType = "replan-slice";
4113
- unitId = `${mid}/${sid}`;
4114
- prompt = await buildReplanSlicePrompt(mid, midTitle, sid, sTitle, base);
4115
- break;
4116
- }
4117
-
4118
- default:
4119
- ctx.ui.notify(
4120
- `Unknown phase "${phase}". Valid phases: research, plan, execute, complete, reassess, uat, replan.`,
4121
- "warning",
4122
- );
4123
- return;
4124
- }
4125
-
4126
- ctx.ui.notify(`Dispatching ${unitType} for ${unitId}...`, "info");
4127
- const result = await ctx.newSession();
4128
- if (result.cancelled) {
4129
- ctx.ui.notify("Session creation cancelled.", "warning");
4130
- return;
4131
- }
4132
- pi.sendMessage(
4133
- { customType: "gsd-dispatch", content: prompt, display: false },
4134
- { triggerTurn: true },
4135
- );
4136
- }
1891
+ // Direct phase dispatch → auto-direct-dispatch.ts
1892
+ export { dispatchDirectPhase } from "./auto-direct-dispatch.js";