gsd-pi 2.18.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +3 -3
  3. package/dist/onboarding.d.ts +3 -1
  4. package/dist/onboarding.js +77 -3
  5. package/dist/remote-questions-config.d.ts +1 -1
  6. package/dist/resources/extensions/google-search/index.ts +164 -47
  7. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +148 -39
  9. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  10. package/dist/resources/extensions/gsd/auto.ts +690 -39
  11. package/dist/resources/extensions/gsd/captures.ts +384 -0
  12. package/dist/resources/extensions/gsd/commands.ts +654 -36
  13. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  14. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  15. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  16. package/dist/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  17. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  18. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  19. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  20. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  21. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  22. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  23. package/dist/resources/extensions/gsd/export.ts +81 -2
  24. package/dist/resources/extensions/gsd/files.ts +39 -9
  25. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  26. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  27. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  28. package/dist/resources/extensions/gsd/history.ts +0 -1
  29. package/dist/resources/extensions/gsd/index.ts +277 -1
  30. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  31. package/dist/resources/extensions/gsd/metrics.ts +84 -0
  32. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  33. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  34. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  35. package/dist/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  36. package/dist/resources/extensions/gsd/preferences.ts +198 -150
  37. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  39. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  40. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  41. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  42. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  43. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  44. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  45. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  46. package/dist/resources/extensions/gsd/quick.ts +156 -0
  47. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  48. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  49. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  50. package/dist/resources/extensions/gsd/state.ts +30 -0
  51. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  52. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  53. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  54. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  55. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  57. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  58. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  59. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  60. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  61. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  62. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  63. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  64. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  65. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  66. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  67. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  68. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  69. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  70. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  71. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  72. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  73. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  74. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  75. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  76. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  77. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  78. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  79. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  80. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  81. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  82. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  83. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  84. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  85. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  86. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  87. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  88. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  89. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  90. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  91. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  92. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  93. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  94. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  95. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  96. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  97. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  98. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  99. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  100. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  101. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  102. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  103. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  104. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  105. package/dist/resources/extensions/gsd/types.ts +29 -0
  106. package/dist/resources/extensions/gsd/undo.ts +0 -1
  107. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  108. package/dist/resources/extensions/gsd/visualizer-data.ts +505 -0
  109. package/dist/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  110. package/dist/resources/extensions/gsd/visualizer-views.ts +755 -0
  111. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  112. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  113. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  114. package/dist/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  115. package/dist/resources/extensions/remote-questions/format.ts +166 -14
  116. package/dist/resources/extensions/remote-questions/manager.ts +14 -4
  117. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  118. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  119. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  120. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  121. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  122. package/dist/resources/extensions/voice/index.ts +4 -3
  123. package/package.json +1 -1
  124. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  126. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  129. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  131. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  133. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  137. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  139. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  140. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  142. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  144. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  146. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  148. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  150. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  152. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  153. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  154. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  156. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  158. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  160. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  161. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  162. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  163. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  164. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  165. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  166. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  167. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  168. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  169. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  170. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  171. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  172. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  173. package/src/resources/extensions/google-search/index.ts +164 -47
  174. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  175. package/src/resources/extensions/gsd/auto-prompts.ts +148 -39
  176. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  177. package/src/resources/extensions/gsd/auto.ts +690 -39
  178. package/src/resources/extensions/gsd/captures.ts +384 -0
  179. package/src/resources/extensions/gsd/commands.ts +654 -36
  180. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  181. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  182. package/src/resources/extensions/gsd/context-store.ts +195 -0
  183. package/src/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  184. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  185. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  186. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  187. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  188. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  189. package/src/resources/extensions/gsd/doctor.ts +283 -2
  190. package/src/resources/extensions/gsd/export.ts +81 -2
  191. package/src/resources/extensions/gsd/files.ts +39 -9
  192. package/src/resources/extensions/gsd/git-service.ts +6 -0
  193. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  194. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  195. package/src/resources/extensions/gsd/history.ts +0 -1
  196. package/src/resources/extensions/gsd/index.ts +277 -1
  197. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  198. package/src/resources/extensions/gsd/metrics.ts +84 -0
  199. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  200. package/src/resources/extensions/gsd/model-router.ts +256 -0
  201. package/src/resources/extensions/gsd/notifications.ts +0 -1
  202. package/src/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  203. package/src/resources/extensions/gsd/preferences.ts +198 -150
  204. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  205. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  206. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  207. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  208. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  209. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  210. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  211. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  212. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  213. package/src/resources/extensions/gsd/quick.ts +156 -0
  214. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  215. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  216. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  217. package/src/resources/extensions/gsd/state.ts +30 -0
  218. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  219. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  220. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  221. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  222. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  223. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  224. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  225. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  226. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  227. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  228. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  229. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  230. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  231. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  232. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  233. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  234. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  235. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  236. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  237. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  238. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  239. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  240. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  241. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  242. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  243. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  244. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  245. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  246. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  247. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  248. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  249. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  250. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  251. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  252. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  253. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  254. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  255. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  256. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  257. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  258. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  259. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  260. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  261. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  262. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  263. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  264. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  265. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  266. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  267. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  268. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  269. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  270. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  271. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  272. package/src/resources/extensions/gsd/types.ts +29 -0
  273. package/src/resources/extensions/gsd/undo.ts +0 -1
  274. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  275. package/src/resources/extensions/gsd/visualizer-data.ts +505 -0
  276. package/src/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  277. package/src/resources/extensions/gsd/visualizer-views.ts +755 -0
  278. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  279. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  280. package/src/resources/extensions/remote-questions/config.ts +4 -2
  281. package/src/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  282. package/src/resources/extensions/remote-questions/format.ts +166 -14
  283. package/src/resources/extensions/remote-questions/manager.ts +14 -4
  284. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  285. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  286. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  287. package/src/resources/extensions/remote-questions/types.ts +2 -1
  288. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  289. package/src/resources/extensions/voice/index.ts +4 -3
@@ -0,0 +1,244 @@
1
+ /**
2
+ * doctor-proactive.test.ts — Tests for proactive healing layer.
3
+ *
4
+ * Tests:
5
+ * - Pre-dispatch health gate (stale lock, merge state)
6
+ * - Health score tracking (snapshots, trends)
7
+ * - Auto-heal escalation (consecutive errors, threshold)
8
+ */
9
+
10
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+ import { execSync } from "node:child_process";
14
+
15
+ import {
16
+ preDispatchHealthGate,
17
+ recordHealthSnapshot,
18
+ getHealthTrend,
19
+ getConsecutiveErrorUnits,
20
+ getHealthHistory,
21
+ checkHealEscalation,
22
+ resetProactiveHealing,
23
+ formatHealthSummary,
24
+ } from "../doctor-proactive.ts";
25
+ import { createTestContext } from "./test-helpers.ts";
26
+
27
+ const { assertEq, assertTrue, report } = createTestContext();
28
+
29
+ function run(cmd: string, cwd: string): string {
30
+ return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
31
+ }
32
+
33
+ function createGitRepo(): string {
34
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-proactive-")));
35
+ run("git init", dir);
36
+ run("git config user.email test@test.com", dir);
37
+ run("git config user.name Test", dir);
38
+ writeFileSync(join(dir, "README.md"), "# test\n");
39
+ run("git add .", dir);
40
+ run("git commit -m init", dir);
41
+ run("git branch -M main", dir);
42
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
43
+ return dir;
44
+ }
45
+
46
+ async function main(): Promise<void> {
47
+ const cleanups: string[] = [];
48
+
49
+ try {
50
+ // ─── Health Score Tracking ─────────────────────────────────────────
51
+ console.log("\n=== health tracking: initial state ===");
52
+ {
53
+ resetProactiveHealing();
54
+ assertEq(getHealthTrend(), "unknown", "trend is unknown with no data");
55
+ assertEq(getConsecutiveErrorUnits(), 0, "no consecutive errors initially");
56
+ assertEq(getHealthHistory().length, 0, "no history initially");
57
+ }
58
+
59
+ console.log("\n=== health tracking: recording snapshots ===");
60
+ {
61
+ resetProactiveHealing();
62
+ recordHealthSnapshot(0, 2, 1);
63
+ recordHealthSnapshot(0, 1, 0);
64
+ recordHealthSnapshot(0, 0, 0);
65
+
66
+ assertEq(getHealthHistory().length, 3, "3 snapshots recorded");
67
+ assertEq(getConsecutiveErrorUnits(), 0, "no consecutive errors after clean units");
68
+ }
69
+
70
+ console.log("\n=== health tracking: consecutive error counting ===");
71
+ {
72
+ resetProactiveHealing();
73
+ recordHealthSnapshot(2, 1, 0); // errors
74
+ recordHealthSnapshot(1, 0, 0); // errors
75
+ recordHealthSnapshot(1, 0, 0); // errors
76
+ assertEq(getConsecutiveErrorUnits(), 3, "3 consecutive error units");
77
+
78
+ recordHealthSnapshot(0, 0, 0); // clean
79
+ assertEq(getConsecutiveErrorUnits(), 0, "streak reset on clean unit");
80
+ }
81
+
82
+ console.log("\n=== health tracking: trend detection ===");
83
+ {
84
+ resetProactiveHealing();
85
+ // Record 5 older snapshots with low issues
86
+ for (let i = 0; i < 5; i++) {
87
+ recordHealthSnapshot(0, 1, 0);
88
+ }
89
+ // Record 5 recent snapshots with high issues
90
+ for (let i = 0; i < 5; i++) {
91
+ recordHealthSnapshot(3, 5, 0);
92
+ }
93
+ assertEq(getHealthTrend(), "degrading", "detects degrading trend");
94
+ }
95
+
96
+ console.log("\n=== health tracking: improving trend ===");
97
+ {
98
+ resetProactiveHealing();
99
+ // Record 5 older snapshots with high issues
100
+ for (let i = 0; i < 5; i++) {
101
+ recordHealthSnapshot(3, 5, 0);
102
+ }
103
+ // Record 5 recent snapshots with low issues
104
+ for (let i = 0; i < 5; i++) {
105
+ recordHealthSnapshot(0, 0, 0);
106
+ }
107
+ assertEq(getHealthTrend(), "improving", "detects improving trend");
108
+ }
109
+
110
+ console.log("\n=== health tracking: stable trend ===");
111
+ {
112
+ resetProactiveHealing();
113
+ for (let i = 0; i < 10; i++) {
114
+ recordHealthSnapshot(1, 1, 0);
115
+ }
116
+ assertEq(getHealthTrend(), "stable", "detects stable trend");
117
+ }
118
+
119
+ // ─── Auto-Heal Escalation ─────────────────────────────────────────
120
+ console.log("\n=== escalation: below threshold ===");
121
+ {
122
+ resetProactiveHealing();
123
+ recordHealthSnapshot(1, 0, 0);
124
+ recordHealthSnapshot(1, 0, 0);
125
+ recordHealthSnapshot(1, 0, 0);
126
+ const result = checkHealEscalation(1, [{ code: "test", message: "test error", unitId: "M001/S01" }]);
127
+ assertEq(result.shouldEscalate, false, "no escalation below threshold");
128
+ assertTrue(result.reason.includes("3/5"), "reason shows progress toward threshold");
129
+ }
130
+
131
+ console.log("\n=== escalation: at threshold ===");
132
+ {
133
+ resetProactiveHealing();
134
+ // Need 5+ consecutive error units AND degrading/stable trend
135
+ for (let i = 0; i < 5; i++) {
136
+ recordHealthSnapshot(0, 0, 0); // older clean snapshots
137
+ }
138
+ for (let i = 0; i < 5; i++) {
139
+ recordHealthSnapshot(2, 1, 0); // recent error snapshots
140
+ }
141
+ const result = checkHealEscalation(2, [{ code: "test", message: "test error", unitId: "M001/S01" }]);
142
+ assertEq(result.shouldEscalate, true, "escalates at threshold with degrading trend");
143
+ assertTrue(result.reason.includes("5 consecutive"), "reason mentions consecutive count");
144
+ }
145
+
146
+ console.log("\n=== escalation: no double escalation ===");
147
+ {
148
+ // Don't reset — should already be escalated from previous test
149
+ recordHealthSnapshot(2, 0, 0);
150
+ const result = checkHealEscalation(2, [{ code: "test", message: "test error", unitId: "M001/S01" }]);
151
+ assertEq(result.shouldEscalate, false, "no double escalation in same session");
152
+ assertTrue(result.reason.includes("already escalated"), "reason explains why no escalation");
153
+ }
154
+
155
+ console.log("\n=== escalation: deferred when improving ===");
156
+ {
157
+ resetProactiveHealing();
158
+ // 5 older snapshots with high errors
159
+ for (let i = 0; i < 5; i++) {
160
+ recordHealthSnapshot(5, 5, 0);
161
+ }
162
+ // 5 recent snapshots with fewer errors (still > 0)
163
+ for (let i = 0; i < 5; i++) {
164
+ recordHealthSnapshot(1, 0, 0);
165
+ }
166
+ const result = checkHealEscalation(1, [{ code: "test", message: "test error", unitId: "M001/S01" }]);
167
+ assertEq(result.shouldEscalate, false, "no escalation when trend is improving");
168
+ assertTrue(result.reason.includes("improving"), "reason mentions improving trend");
169
+ }
170
+
171
+ // ─── Health Summary Formatting ────────────────────────────────────
172
+ console.log("\n=== formatHealthSummary ===");
173
+ {
174
+ resetProactiveHealing();
175
+ assertEq(formatHealthSummary(), "No health data yet.", "empty summary when no data");
176
+
177
+ recordHealthSnapshot(2, 3, 1);
178
+ const summary = formatHealthSummary();
179
+ assertTrue(summary.includes("2E/3W"), "summary includes error/warning counts");
180
+ assertTrue(summary.includes("fixes:1"), "summary includes fix count");
181
+ assertTrue(summary.includes("streak:1/5"), "summary includes error streak");
182
+ }
183
+
184
+ // ─── Pre-Dispatch Health Gate ─────────────────────────────────────
185
+ console.log("\n=== health gate: clean state ===");
186
+ {
187
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-proactive-")));
188
+ cleanups.push(dir);
189
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
190
+
191
+ const result = preDispatchHealthGate(dir);
192
+ assertTrue(result.proceed, "gate passes on clean state");
193
+ assertEq(result.issues.length, 0, "no issues on clean state");
194
+ }
195
+
196
+ console.log("\n=== health gate: stale crash lock auto-cleared ===");
197
+ {
198
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-proactive-")));
199
+ cleanups.push(dir);
200
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
201
+
202
+ // Write a stale lock
203
+ writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify({
204
+ pid: 9999999, startedAt: "2026-03-10T00:00:00Z",
205
+ unitType: "execute-task", unitId: "M001/S01/T01",
206
+ unitStartedAt: "2026-03-10T00:01:00Z", completedUnits: 3,
207
+ }));
208
+
209
+ const result = preDispatchHealthGate(dir);
210
+ assertTrue(result.proceed, "gate passes after auto-clearing stale lock");
211
+ assertTrue(result.fixesApplied.some(f => f.includes("cleared stale auto.lock")), "reports lock cleared");
212
+ assertTrue(!existsSync(join(dir, ".gsd", "auto.lock")), "lock file removed");
213
+ }
214
+
215
+ console.log("\n=== health gate: corrupt merge state auto-healed ===");
216
+ if (process.platform !== "win32") {
217
+ {
218
+ const dir = createGitRepo();
219
+ cleanups.push(dir);
220
+
221
+ // Inject MERGE_HEAD
222
+ const headHash = run("git rev-parse HEAD", dir);
223
+ writeFileSync(join(dir, ".git", "MERGE_HEAD"), headHash + "\n");
224
+
225
+ const result = preDispatchHealthGate(dir);
226
+ assertTrue(result.proceed, "gate passes after auto-healing merge state");
227
+ assertTrue(result.fixesApplied.some(f => f.includes("cleaned merge state")), "reports merge state cleaned");
228
+ assertTrue(!existsSync(join(dir, ".git", "MERGE_HEAD")), "MERGE_HEAD removed");
229
+ }
230
+ } else {
231
+ console.log(" (skipped on Windows)");
232
+ }
233
+
234
+ } finally {
235
+ resetProactiveHealing();
236
+ for (const dir of cleanups) {
237
+ try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
238
+ }
239
+ }
240
+
241
+ report();
242
+ }
243
+
244
+ main();
@@ -0,0 +1,303 @@
1
+ /**
2
+ * doctor-runtime.test.ts — Tests for doctor runtime health checks.
3
+ *
4
+ * Tests detection and auto-fix of:
5
+ * stale_crash_lock, orphaned_completed_units, stale_hook_state,
6
+ * activity_log_bloat, state_file_missing, state_file_stale,
7
+ * gitignore_missing_patterns
8
+ */
9
+
10
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync, realpathSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+ import { execSync } from "node:child_process";
14
+
15
+ import { runGSDDoctor } from "../doctor.ts";
16
+ import { createTestContext } from "./test-helpers.ts";
17
+
18
+ const { assertEq, assertTrue, report } = createTestContext();
19
+
20
+ function run(cmd: string, cwd: string): string {
21
+ return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
22
+ }
23
+
24
+ /** Create a minimal .gsd project with a milestone for STATE.md tests. */
25
+ function createMinimalProject(): string {
26
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-runtime-test-")));
27
+ const msDir = join(dir, ".gsd", "milestones", "M001");
28
+ mkdirSync(msDir, { recursive: true });
29
+ writeFileSync(join(msDir, "M001-ROADMAP.md"), `# M001: Test
30
+
31
+ ## Slices
32
+ - [ ] **S01: Demo** \`risk:low\` \`depends:[]\`
33
+ > After this: done
34
+ `);
35
+ const sDir = join(msDir, "slices", "S01", "tasks");
36
+ mkdirSync(sDir, { recursive: true });
37
+ writeFileSync(join(msDir, "slices", "S01", "S01-PLAN.md"), `# S01: Demo
38
+
39
+ **Goal:** Demo
40
+
41
+ ## Tasks
42
+ - [ ] **T01: Do thing** \`est:10m\`
43
+ `);
44
+ return dir;
45
+ }
46
+
47
+ /** Create a minimal git repo with .gsd for gitignore tests. */
48
+ function createGitProject(): string {
49
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-runtime-git-")));
50
+ run("git init", dir);
51
+ run("git config user.email test@test.com", dir);
52
+ run("git config user.name Test", dir);
53
+ writeFileSync(join(dir, "README.md"), "# test\n");
54
+ run("git add .", dir);
55
+ run("git commit -m init", dir);
56
+ run("git branch -M main", dir);
57
+ return dir;
58
+ }
59
+
60
+ async function main(): Promise<void> {
61
+ const cleanups: string[] = [];
62
+
63
+ try {
64
+ // ─── Test 1: Stale crash lock detection & fix ─────────────────────
65
+ console.log("\n=== stale_crash_lock ===");
66
+ {
67
+ const dir = createMinimalProject();
68
+ cleanups.push(dir);
69
+
70
+ // Write a lock file with a PID that is definitely dead (use PID 1 million+)
71
+ const lockData = {
72
+ pid: 9999999,
73
+ startedAt: "2026-03-10T00:00:00Z",
74
+ unitType: "execute-task",
75
+ unitId: "M001/S01/T01",
76
+ unitStartedAt: "2026-03-10T00:01:00Z",
77
+ completedUnits: 3,
78
+ };
79
+ writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
80
+
81
+ const detect = await runGSDDoctor(dir);
82
+ const lockIssues = detect.issues.filter(i => i.code === "stale_crash_lock");
83
+ assertTrue(lockIssues.length > 0, "detects stale crash lock");
84
+ assertTrue(lockIssues[0]?.message.includes("9999999"), "message includes PID");
85
+ assertTrue(lockIssues[0]?.fixable === true, "stale lock is fixable");
86
+
87
+ const fixed = await runGSDDoctor(dir, { fix: true });
88
+ assertTrue(fixed.fixesApplied.some(f => f.includes("cleared stale auto.lock")), "fix clears stale lock");
89
+ assertTrue(!existsSync(join(dir, ".gsd", "auto.lock")), "auto.lock removed after fix");
90
+ }
91
+
92
+ // ─── Test 2: No false positive for missing lock ───────────────────
93
+ console.log("\n=== stale_crash_lock — no false positive ===");
94
+ {
95
+ const dir = createMinimalProject();
96
+ cleanups.push(dir);
97
+
98
+ const detect = await runGSDDoctor(dir);
99
+ const lockIssues = detect.issues.filter(i => i.code === "stale_crash_lock");
100
+ assertEq(lockIssues.length, 0, "no stale lock issue when no lock file exists");
101
+ }
102
+
103
+ // ─── Test 3: Stale hook state detection & fix ─────────────────────
104
+ console.log("\n=== stale_hook_state ===");
105
+ {
106
+ const dir = createMinimalProject();
107
+ cleanups.push(dir);
108
+
109
+ // Write hook state with active cycle counts and no auto.lock (no running session)
110
+ const hookState = {
111
+ cycleCounts: {
112
+ "code-review/execute-task/M001/S01/T01": 2,
113
+ "lint-check/execute-task/M001/S01/T02": 1,
114
+ },
115
+ savedAt: "2026-03-10T00:00:00Z",
116
+ };
117
+ writeFileSync(join(dir, ".gsd", "hook-state.json"), JSON.stringify(hookState, null, 2));
118
+
119
+ const detect = await runGSDDoctor(dir);
120
+ const hookIssues = detect.issues.filter(i => i.code === "stale_hook_state");
121
+ assertTrue(hookIssues.length > 0, "detects stale hook state");
122
+ assertTrue(hookIssues[0]?.message.includes("2 residual cycle count"), "message includes count");
123
+
124
+ const fixed = await runGSDDoctor(dir, { fix: true });
125
+ assertTrue(fixed.fixesApplied.some(f => f.includes("cleared stale hook-state.json")), "fix clears hook state");
126
+
127
+ // Verify the file was cleaned
128
+ const content = JSON.parse(readFileSync(join(dir, ".gsd", "hook-state.json"), "utf-8"));
129
+ assertEq(Object.keys(content.cycleCounts).length, 0, "hook state cycle counts cleared");
130
+ }
131
+
132
+ // ─── Test 4: Activity log bloat detection ─────────────────────────
133
+ console.log("\n=== activity_log_bloat ===");
134
+ {
135
+ const dir = createMinimalProject();
136
+ cleanups.push(dir);
137
+
138
+ // Create an activity dir with > 500 files
139
+ const activityDir = join(dir, ".gsd", "activity");
140
+ mkdirSync(activityDir, { recursive: true });
141
+ for (let i = 0; i < 510; i++) {
142
+ writeFileSync(join(activityDir, `${String(i).padStart(3, "0")}-execute-task-M001-S01-T01.jsonl`), `{"test":${i}}\n`);
143
+ }
144
+
145
+ const detect = await runGSDDoctor(dir);
146
+ const bloatIssues = detect.issues.filter(i => i.code === "activity_log_bloat");
147
+ assertTrue(bloatIssues.length > 0, "detects activity log bloat");
148
+ assertTrue(bloatIssues[0]?.message.includes("510 files"), "message includes file count");
149
+ }
150
+
151
+ // ─── Test 5: STATE.md missing detection & fix ─────────────────────
152
+ console.log("\n=== state_file_missing ===");
153
+ {
154
+ const dir = createMinimalProject();
155
+ cleanups.push(dir);
156
+
157
+ // No STATE.md exists by default in our minimal setup
158
+ const stateFilePath = join(dir, ".gsd", "STATE.md");
159
+ assertTrue(!existsSync(stateFilePath), "STATE.md does not exist initially");
160
+
161
+ const detect = await runGSDDoctor(dir);
162
+ const stateIssues = detect.issues.filter(i => i.code === "state_file_missing");
163
+ assertTrue(stateIssues.length > 0, "detects missing STATE.md");
164
+ assertTrue(stateIssues[0]?.fixable === true, "missing STATE.md is fixable");
165
+ assertEq(stateIssues[0]?.severity, "warning", "missing STATE.md is a warning (derived file)");
166
+
167
+ const fixed = await runGSDDoctor(dir, { fix: true });
168
+ assertTrue(fixed.fixesApplied.some(f => f.includes("created STATE.md")), "fix creates STATE.md");
169
+ assertTrue(existsSync(stateFilePath), "STATE.md exists after fix");
170
+
171
+ // Verify content has expected structure
172
+ const content = readFileSync(stateFilePath, "utf-8");
173
+ assertTrue(content.includes("# GSD State"), "STATE.md has header");
174
+ assertTrue(content.includes("M001"), "STATE.md references milestone");
175
+ }
176
+
177
+ // ─── Test 6: STATE.md stale detection & fix ───────────────────────
178
+ console.log("\n=== state_file_stale ===");
179
+ {
180
+ const dir = createMinimalProject();
181
+ cleanups.push(dir);
182
+
183
+ // Write a STATE.md with wrong phase/milestone info
184
+ const stateFilePath = join(dir, ".gsd", "STATE.md");
185
+ writeFileSync(stateFilePath, `# GSD State
186
+
187
+ **Active Milestone:** None
188
+ **Active Slice:** None
189
+ **Phase:** idle
190
+
191
+ ## Milestone Registry
192
+
193
+ ## Recent Decisions
194
+ - None recorded
195
+
196
+ ## Blockers
197
+ - None
198
+
199
+ ## Next Action
200
+ None
201
+ `);
202
+
203
+ const detect = await runGSDDoctor(dir);
204
+ const staleIssues = detect.issues.filter(i => i.code === "state_file_stale");
205
+ assertTrue(staleIssues.length > 0, "detects stale STATE.md");
206
+ assertTrue(staleIssues[0]?.message.includes("idle"), "message references old phase");
207
+
208
+ const fixed = await runGSDDoctor(dir, { fix: true });
209
+ assertTrue(fixed.fixesApplied.some(f => f.includes("rebuilt STATE.md")), "fix rebuilds STATE.md");
210
+
211
+ // Verify updated content matches derived state
212
+ const content = readFileSync(stateFilePath, "utf-8");
213
+ assertTrue(content.includes("M001"), "rebuilt STATE.md references milestone");
214
+ }
215
+
216
+ // ─── Test 7: Gitignore missing patterns detection & fix ───────────
217
+ if (process.platform !== "win32") {
218
+ console.log("\n=== gitignore_missing_patterns ===");
219
+ {
220
+ const dir = createGitProject();
221
+ cleanups.push(dir);
222
+
223
+ // Create .gsd dir so checks can run
224
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
225
+
226
+ // Write a .gitignore missing GSD runtime patterns
227
+ writeFileSync(join(dir, ".gitignore"), `node_modules/
228
+ .env
229
+ `);
230
+
231
+ const detect = await runGSDDoctor(dir);
232
+ const gitignoreIssues = detect.issues.filter(i => i.code === "gitignore_missing_patterns");
233
+ assertTrue(gitignoreIssues.length > 0, "detects missing gitignore patterns");
234
+ assertTrue(gitignoreIssues[0]?.message.includes(".gsd/activity/"), "message lists missing patterns");
235
+
236
+ const fixed = await runGSDDoctor(dir, { fix: true });
237
+ assertTrue(fixed.fixesApplied.some(f => f.includes("added missing GSD runtime patterns")), "fix adds patterns");
238
+
239
+ // Verify patterns were added
240
+ const content = readFileSync(join(dir, ".gitignore"), "utf-8");
241
+ assertTrue(content.includes(".gsd/activity/"), "gitignore now has activity pattern");
242
+ assertTrue(content.includes(".gsd/auto.lock"), "gitignore now has auto.lock pattern");
243
+ }
244
+ } else {
245
+ console.log("\n=== gitignore_missing_patterns (skipped on Windows) ===");
246
+ }
247
+
248
+ // ─── Test 8: No false positive when gitignore has blanket .gsd/ ───
249
+ if (process.platform !== "win32") {
250
+ console.log("\n=== gitignore — blanket .gsd/ ===");
251
+ {
252
+ const dir = createGitProject();
253
+ cleanups.push(dir);
254
+
255
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
256
+ writeFileSync(join(dir, ".gitignore"), `.gsd/
257
+ node_modules/
258
+ `);
259
+
260
+ const detect = await runGSDDoctor(dir);
261
+ const gitignoreIssues = detect.issues.filter(i => i.code === "gitignore_missing_patterns");
262
+ assertEq(gitignoreIssues.length, 0, "no missing patterns when blanket .gsd/ present");
263
+ }
264
+ } else {
265
+ console.log("\n=== gitignore — blanket .gsd/ (skipped on Windows) ===");
266
+ }
267
+
268
+ // ─── Test 9: Orphaned completed-units detection & fix ─────────────
269
+ console.log("\n=== orphaned_completed_units ===");
270
+ {
271
+ const dir = createMinimalProject();
272
+ cleanups.push(dir);
273
+
274
+ // Write completed-units.json with keys that reference non-existent artifacts
275
+ const completedKeys = [
276
+ "execute-task/M001/S01/T99", // T99 doesn't exist
277
+ "complete-slice/M001/S99", // S99 doesn't exist
278
+ ];
279
+ writeFileSync(join(dir, ".gsd", "completed-units.json"), JSON.stringify(completedKeys));
280
+
281
+ const detect = await runGSDDoctor(dir);
282
+ const orphanIssues = detect.issues.filter(i => i.code === "orphaned_completed_units");
283
+ assertTrue(orphanIssues.length > 0, "detects orphaned completed-unit keys");
284
+ assertTrue(orphanIssues[0]?.message.includes("2 completed-unit key"), "message includes count");
285
+
286
+ const fixed = await runGSDDoctor(dir, { fix: true });
287
+ assertTrue(fixed.fixesApplied.some(f => f.includes("removed") && f.includes("orphaned")), "fix removes orphaned keys");
288
+
289
+ // Verify keys were cleaned
290
+ const content = JSON.parse(readFileSync(join(dir, ".gsd", "completed-units.json"), "utf-8"));
291
+ assertEq(content.length, 0, "all orphaned keys removed");
292
+ }
293
+
294
+ } finally {
295
+ for (const dir of cleanups) {
296
+ try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
297
+ }
298
+ }
299
+
300
+ report();
301
+ }
302
+
303
+ main();