gsd-pi 2.78.0-dev.aeeb2ca00 → 2.78.1-dev.84a383f51

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 (383) hide show
  1. package/README.md +7 -7
  2. package/dist/claude-cli-check.js +64 -37
  3. package/dist/cli-policy.d.ts +13 -0
  4. package/dist/cli-policy.js +17 -0
  5. package/dist/cli.js +95 -55
  6. package/dist/headless-query.d.ts +22 -0
  7. package/dist/headless-query.js +24 -4
  8. package/dist/headless.d.ts +10 -0
  9. package/dist/headless.js +16 -1
  10. package/dist/loader.js +7 -10
  11. package/dist/onboarding.d.ts +10 -0
  12. package/dist/onboarding.js +2 -2
  13. package/dist/provider-migrations.d.ts +2 -2
  14. package/dist/provider-migrations.js +5 -2
  15. package/dist/resource-loader.d.ts +5 -2
  16. package/dist/resource-loader.js +28 -5
  17. package/dist/resources/.managed-resources-content-hash +1 -0
  18. package/dist/resources/extensions/claude-code-cli/readiness.js +77 -45
  19. package/dist/resources/extensions/gsd/auto/loop.js +23 -0
  20. package/dist/resources/extensions/gsd/auto/phases.js +2 -2
  21. package/dist/resources/extensions/gsd/auto/run-unit.js +3 -1
  22. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  23. package/dist/resources/extensions/gsd/auto-recovery.js +43 -4
  24. package/dist/resources/extensions/gsd/auto-runtime-state.js +31 -0
  25. package/dist/resources/extensions/gsd/auto-start.js +1 -1
  26. package/dist/resources/extensions/gsd/auto-tool-tracking.js +2 -2
  27. package/dist/resources/extensions/gsd/auto-worktree.js +30 -0
  28. package/dist/resources/extensions/gsd/auto.js +14 -5
  29. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +14 -2
  30. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +7 -5
  31. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +2 -2
  32. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +5 -4
  33. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +94 -31
  34. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +11 -6
  35. package/dist/resources/extensions/gsd/bootstrap/system-context.js +34 -8
  36. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +38 -2
  37. package/dist/resources/extensions/gsd/commands/catalog.js +69 -5
  38. package/dist/resources/extensions/gsd/commands/handlers/core.js +22 -1
  39. package/dist/resources/extensions/gsd/commands-mcp-status.js +3 -1
  40. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +10 -1
  41. package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -1
  42. package/dist/resources/extensions/gsd/docs/preferences-reference.md +4 -0
  43. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +39 -1
  44. package/dist/resources/extensions/gsd/error-classifier.js +1 -1
  45. package/dist/resources/extensions/gsd/forensics.js +2 -2
  46. package/dist/resources/extensions/gsd/git-service.js +12 -5
  47. package/dist/resources/extensions/gsd/gsd-db.js +11 -2
  48. package/dist/resources/extensions/gsd/guided-flow.js +23 -23
  49. package/dist/resources/extensions/gsd/memory-store.js +66 -31
  50. package/dist/resources/extensions/gsd/milestone-id-reservation.js +36 -0
  51. package/dist/resources/extensions/gsd/model-router.js +114 -9
  52. package/dist/resources/extensions/gsd/native-git-bridge.js +7 -1
  53. package/dist/resources/extensions/gsd/preferences-models.js +91 -15
  54. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  55. package/dist/resources/extensions/gsd/preferences-validation.js +32 -0
  56. package/dist/resources/extensions/gsd/preferences.js +5 -3
  57. package/dist/resources/extensions/gsd/prompt-loader.js +23 -12
  58. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +9 -3
  59. package/dist/resources/extensions/gsd/state.js +42 -0
  60. package/dist/resources/extensions/gsd/templates/PREFERENCES.md +1 -0
  61. package/dist/resources/extensions/gsd/tools/memory-tools.js +18 -1
  62. package/dist/resources/extensions/gsd/visualizer-overlay.js +1 -1
  63. package/dist/resources/extensions/gsd/watch/header-renderer.js +3 -1
  64. package/dist/resources/extensions/gsd/worktree-command.js +26 -46
  65. package/dist/resources/extensions/gsd/worktree-session-state.js +33 -0
  66. package/dist/resources/extensions/mcp-client/index.js +6 -3
  67. package/dist/resources/extensions/slash-commands/create-extension.js +36 -22
  68. package/dist/resources/skills/create-gsd-extension/SKILL.md +9 -5
  69. package/dist/resources/skills/create-gsd-extension/references/custom-commands.md +1 -1
  70. package/dist/resources/skills/create-gsd-extension/references/custom-rendering.md +5 -5
  71. package/dist/resources/skills/create-gsd-extension/references/custom-tools.md +4 -4
  72. package/dist/resources/skills/create-gsd-extension/references/custom-ui.md +6 -6
  73. package/dist/resources/skills/create-gsd-extension/references/events-reference.md +3 -3
  74. package/dist/resources/skills/create-gsd-extension/references/packaging-distribution.md +1 -1
  75. package/dist/resources/skills/create-gsd-extension/references/remote-execution-overrides.md +3 -3
  76. package/dist/resources/skills/create-gsd-extension/workflows/create-extension.md +32 -12
  77. package/dist/rtk-shared.d.ts +3 -0
  78. package/dist/rtk-shared.js +17 -0
  79. package/dist/rtk.d.ts +2 -5
  80. package/dist/rtk.js +3 -20
  81. package/dist/runtime-checks.d.ts +27 -0
  82. package/dist/runtime-checks.js +38 -0
  83. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  84. package/dist/web/standalone/.next/BUILD_ID +1 -1
  85. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  86. package/dist/web/standalone/.next/build-manifest.json +3 -3
  87. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  88. package/dist/web/standalone/.next/react-loadable-manifest.json +44 -4
  89. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  91. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  100. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  102. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  103. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  104. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  105. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  106. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  107. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/session/events/route.js +4 -2
  110. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/index.html +1 -1
  112. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  113. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  114. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  115. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  116. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  117. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  118. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  120. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  122. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  124. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  125. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  126. package/dist/web/standalone/.next/server/webpack-runtime.js +1 -1
  127. package/dist/web/standalone/.next/static/chunks/2556.0527fea66e123b7f.js +1 -0
  128. package/dist/web/standalone/.next/static/chunks/2824.08296bc2f9654698.js +1 -0
  129. package/dist/web/standalone/.next/static/chunks/3026.3af53b279375f082.js +1 -0
  130. package/dist/web/standalone/.next/static/chunks/315.6f68ae79b67d25cf.js +1 -0
  131. package/dist/web/standalone/.next/static/chunks/3497.4bfc60a3b3dea717.js +1 -0
  132. package/dist/web/standalone/.next/static/chunks/5516.4a07c872b5c3a663.js +1 -0
  133. package/dist/web/standalone/.next/static/chunks/8336.31b019697882acfb.js +10 -0
  134. package/dist/web/standalone/.next/static/chunks/8845.c9702695e8c5a9c5.js +2 -0
  135. package/dist/web/standalone/.next/static/chunks/9058.01ef3a463bda88f1.js +20 -0
  136. package/dist/web/standalone/.next/static/chunks/9441.1081da1125d1764f.js +1 -0
  137. package/dist/web/standalone/.next/static/chunks/app/{page-5b113fd32bc2a1c3.js → page-9bf2e0c50fb2ca05.js} +1 -1
  138. package/dist/web/standalone/.next/static/chunks/webpack-f9f0dc45e4f3ac10.js +1 -0
  139. package/dist/web/standalone/package.json +2 -1
  140. package/dist/worktree-status-banner.d.ts +1 -0
  141. package/dist/worktree-status-banner.js +132 -0
  142. package/package.json +1 -1
  143. package/packages/daemon/package.json +2 -2
  144. package/packages/mcp-server/dist/alias-telemetry.d.ts +8 -0
  145. package/packages/mcp-server/dist/alias-telemetry.d.ts.map +1 -0
  146. package/packages/mcp-server/dist/alias-telemetry.js +30 -0
  147. package/packages/mcp-server/dist/alias-telemetry.js.map +1 -0
  148. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  149. package/packages/mcp-server/dist/workflow-tools.js +74 -46
  150. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  151. package/packages/mcp-server/package.json +2 -2
  152. package/packages/mcp-server/src/alias-telemetry.test.ts +78 -0
  153. package/packages/mcp-server/src/alias-telemetry.ts +30 -0
  154. package/packages/mcp-server/src/workflow-tools.test.ts +26 -0
  155. package/packages/mcp-server/src/workflow-tools.ts +93 -58
  156. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  157. package/packages/native/package.json +1 -1
  158. package/packages/pi-agent-core/package.json +1 -1
  159. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  160. package/packages/pi-ai/dist/providers/anthropic-shared.cache-breakpoint.test.d.ts +2 -0
  161. package/packages/pi-ai/dist/providers/anthropic-shared.cache-breakpoint.test.d.ts.map +1 -0
  162. package/packages/pi-ai/dist/providers/anthropic-shared.cache-breakpoint.test.js +231 -0
  163. package/packages/pi-ai/dist/providers/anthropic-shared.cache-breakpoint.test.js.map +1 -0
  164. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  165. package/packages/pi-ai/dist/providers/anthropic-shared.js +48 -19
  166. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  167. package/packages/pi-ai/dist/types.d.ts +13 -0
  168. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  169. package/packages/pi-ai/dist/types.js.map +1 -1
  170. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
  171. package/packages/pi-ai/dist/utils/repair-tool-json.js +24 -3
  172. package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
  173. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +26 -0
  174. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
  175. package/packages/pi-ai/package.json +1 -1
  176. package/packages/pi-ai/src/providers/anthropic-shared.cache-breakpoint.test.ts +289 -0
  177. package/packages/pi-ai/src/providers/anthropic-shared.ts +52 -20
  178. package/packages/pi-ai/src/types.ts +13 -0
  179. package/packages/pi-ai/src/utils/repair-tool-json.ts +24 -3
  180. package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +32 -0
  181. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  182. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  183. package/packages/pi-coding-agent/dist/core/agent-session.js +6 -0
  184. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  185. package/packages/pi-coding-agent/dist/core/messages.d.ts.map +1 -1
  186. package/packages/pi-coding-agent/dist/core/messages.js +4 -0
  187. package/packages/pi-coding-agent/dist/core/messages.js.map +1 -1
  188. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +19 -2
  189. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
  190. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +10 -0
  191. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  192. package/packages/pi-coding-agent/dist/core/model-registry.js +18 -0
  193. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  194. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +13 -0
  195. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  196. package/packages/pi-coding-agent/dist/core/system-prompt.js +20 -16
  197. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  198. package/packages/pi-coding-agent/dist/core/token-telemetry.d.ts +37 -0
  199. package/packages/pi-coding-agent/dist/core/token-telemetry.d.ts.map +1 -0
  200. package/packages/pi-coding-agent/dist/core/token-telemetry.js +49 -0
  201. package/packages/pi-coding-agent/dist/core/token-telemetry.js.map +1 -0
  202. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.d.ts +2 -0
  203. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.d.ts.map +1 -0
  204. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js +133 -0
  205. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.js.map +1 -0
  206. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +1 -1
  207. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  208. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js +14 -1
  209. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js.map +1 -1
  210. package/packages/pi-coding-agent/dist/tests/system-prompt-cache-stability.test.d.ts +2 -0
  211. package/packages/pi-coding-agent/dist/tests/system-prompt-cache-stability.test.d.ts.map +1 -0
  212. package/packages/pi-coding-agent/dist/tests/system-prompt-cache-stability.test.js +78 -0
  213. package/packages/pi-coding-agent/dist/tests/system-prompt-cache-stability.test.js.map +1 -0
  214. package/packages/pi-coding-agent/dist/tests/token-telemetry.test.d.ts +2 -0
  215. package/packages/pi-coding-agent/dist/tests/token-telemetry.test.d.ts.map +1 -0
  216. package/packages/pi-coding-agent/dist/tests/token-telemetry.test.js +181 -0
  217. package/packages/pi-coding-agent/dist/tests/token-telemetry.test.js.map +1 -0
  218. package/packages/pi-coding-agent/package.json +1 -1
  219. package/packages/pi-coding-agent/src/core/agent-session.ts +7 -0
  220. package/packages/pi-coding-agent/src/core/messages.ts +4 -0
  221. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +32 -2
  222. package/packages/pi-coding-agent/src/core/model-registry.ts +21 -0
  223. package/packages/pi-coding-agent/src/core/system-prompt.ts +33 -15
  224. package/packages/pi-coding-agent/src/core/token-telemetry.ts +77 -0
  225. package/packages/pi-coding-agent/src/modes/interactive/components/tool-card-cleanup-and-success-runtime.test.ts +212 -0
  226. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts +17 -1
  227. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +1 -1
  228. package/packages/pi-coding-agent/src/tests/system-prompt-cache-stability.test.ts +102 -0
  229. package/packages/pi-coding-agent/src/tests/token-telemetry.test.ts +200 -0
  230. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  231. package/packages/pi-tui/dist/__tests__/autocomplete.test.js +17 -3
  232. package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -1
  233. package/packages/pi-tui/dist/components/__tests__/leak-fixes-runtime.test.d.ts +2 -0
  234. package/packages/pi-tui/dist/components/__tests__/leak-fixes-runtime.test.d.ts.map +1 -0
  235. package/packages/pi-tui/dist/components/__tests__/leak-fixes-runtime.test.js +161 -0
  236. package/packages/pi-tui/dist/components/__tests__/leak-fixes-runtime.test.js.map +1 -0
  237. package/packages/pi-tui/package.json +1 -1
  238. package/packages/pi-tui/src/__tests__/autocomplete.test.ts +20 -3
  239. package/packages/pi-tui/src/components/__tests__/leak-fixes-runtime.test.ts +219 -0
  240. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  241. package/packages/rpc-client/package.json +1 -1
  242. package/pkg/package.json +1 -1
  243. package/src/resources/extensions/claude-code-cli/readiness.ts +78 -46
  244. package/src/resources/extensions/gsd/auto/loop.ts +24 -2
  245. package/src/resources/extensions/gsd/auto/phases.ts +3 -3
  246. package/src/resources/extensions/gsd/auto/run-unit.ts +3 -1
  247. package/src/resources/extensions/gsd/auto/session.ts +3 -0
  248. package/src/resources/extensions/gsd/auto/types.ts +1 -0
  249. package/src/resources/extensions/gsd/auto-recovery.ts +46 -8
  250. package/src/resources/extensions/gsd/auto-runtime-state.ts +51 -0
  251. package/src/resources/extensions/gsd/auto-start.ts +1 -1
  252. package/src/resources/extensions/gsd/auto-tool-tracking.ts +2 -4
  253. package/src/resources/extensions/gsd/auto-worktree.ts +38 -0
  254. package/src/resources/extensions/gsd/auto.ts +14 -4
  255. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +15 -13
  256. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +8 -7
  257. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +2 -2
  258. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +10 -9
  259. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +102 -31
  260. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +12 -6
  261. package/src/resources/extensions/gsd/bootstrap/system-context.ts +39 -8
  262. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +39 -11
  263. package/src/resources/extensions/gsd/commands/catalog.ts +75 -5
  264. package/src/resources/extensions/gsd/commands/handlers/core.ts +22 -1
  265. package/src/resources/extensions/gsd/commands-mcp-status.ts +3 -1
  266. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +15 -1
  267. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -1
  268. package/src/resources/extensions/gsd/docs/preferences-reference.md +4 -0
  269. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +39 -1
  270. package/src/resources/extensions/gsd/doctor-types.ts +3 -1
  271. package/src/resources/extensions/gsd/error-classifier.ts +1 -1
  272. package/src/resources/extensions/gsd/forensics.ts +2 -2
  273. package/src/resources/extensions/gsd/git-service.ts +13 -5
  274. package/src/resources/extensions/gsd/gsd-db.ts +12 -2
  275. package/src/resources/extensions/gsd/guided-flow.ts +25 -25
  276. package/src/resources/extensions/gsd/memory-store.ts +81 -28
  277. package/src/resources/extensions/gsd/milestone-id-reservation.ts +47 -0
  278. package/src/resources/extensions/gsd/model-router.ts +172 -9
  279. package/src/resources/extensions/gsd/native-git-bridge.ts +7 -1
  280. package/src/resources/extensions/gsd/preferences-models.ts +101 -15
  281. package/src/resources/extensions/gsd/preferences-types.ts +6 -0
  282. package/src/resources/extensions/gsd/preferences-validation.ts +35 -0
  283. package/src/resources/extensions/gsd/preferences.ts +16 -2
  284. package/src/resources/extensions/gsd/prompt-loader.ts +26 -12
  285. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +9 -3
  286. package/src/resources/extensions/gsd/state.ts +42 -0
  287. package/src/resources/extensions/gsd/templates/PREFERENCES.md +1 -0
  288. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +178 -1
  289. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +58 -0
  290. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +9 -5
  291. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +21 -4
  292. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +1 -1
  293. package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +138 -211
  294. package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +142 -59
  295. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +7 -4
  296. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +89 -32
  297. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +41 -23
  298. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +3 -43
  299. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +5 -3
  300. package/src/resources/extensions/gsd/tests/deferred-milestone-dir-4996.test.ts +116 -0
  301. package/src/resources/extensions/gsd/tests/discuss-empty-db-fallback.test.ts +22 -87
  302. package/src/resources/extensions/gsd/tests/discuss-queued-milestones.test.ts +7 -118
  303. package/src/resources/extensions/gsd/tests/discuss-tool-scope-leak.test.ts +18 -60
  304. package/src/resources/extensions/gsd/tests/doctor-orphan-milestone-4996.test.ts +100 -0
  305. package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +14 -76
  306. package/src/resources/extensions/gsd/tests/ensure-preconditions-guard-4996.test.ts +93 -0
  307. package/src/resources/extensions/gsd/tests/false-degraded-mode-warning.test.ts +22 -83
  308. package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +1 -63
  309. package/src/resources/extensions/gsd/tests/find-missing-summaries-closed-runtime.test.ts +47 -0
  310. package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +26 -1
  311. package/src/resources/extensions/gsd/tests/gitignore-bg-shell-runtime.test.ts +63 -0
  312. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +30 -0
  313. package/src/resources/extensions/gsd/tests/gsd-no-project-error-runtime.test.ts +81 -0
  314. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +14 -4
  315. package/src/resources/extensions/gsd/tests/health-widget.test.ts +22 -12
  316. package/src/resources/extensions/gsd/tests/help-menu-coverage.test.ts +57 -0
  317. package/src/resources/extensions/gsd/tests/import-done-milestones-runtime.test.ts +145 -0
  318. package/src/resources/extensions/gsd/tests/init-prefs-routing.test.ts +64 -1
  319. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -0
  320. package/src/resources/extensions/gsd/tests/integration/token-savings.test.ts +0 -23
  321. package/src/resources/extensions/gsd/tests/memory-store.test.ts +128 -0
  322. package/src/resources/extensions/gsd/tests/memory-tools.test.ts +33 -1
  323. package/src/resources/extensions/gsd/tests/merge-self-branch-guard.test.ts +124 -0
  324. package/src/resources/extensions/gsd/tests/milestone-id-gap-reuse-4996.test.ts +152 -0
  325. package/src/resources/extensions/gsd/tests/model-router.test.ts +169 -8
  326. package/src/resources/extensions/gsd/tests/native-git-infra-errors.test.ts +50 -0
  327. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +8 -0
  328. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +32 -43
  329. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +4 -10
  330. package/src/resources/extensions/gsd/tests/preferences.test.ts +127 -0
  331. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +16 -0
  332. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +7 -0
  333. package/src/resources/extensions/gsd/tests/quick-turn-end-cleanup.test.ts +6 -6
  334. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +93 -0
  335. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +168 -19
  336. package/src/resources/extensions/gsd/tests/slice-parallel-orchestrator.test.ts +7 -1
  337. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +23 -1
  338. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +101 -0
  339. package/src/resources/extensions/gsd/tests/token-profile.test.ts +51 -4
  340. package/src/resources/extensions/gsd/tests/turn-epoch.test.ts +7 -16
  341. package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -7
  342. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +15 -1
  343. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +6 -6
  344. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -0
  345. package/src/resources/extensions/gsd/tools/memory-tools.ts +17 -1
  346. package/src/resources/extensions/gsd/unit-context-manifest.ts +8 -8
  347. package/src/resources/extensions/gsd/visualizer-overlay.ts +1 -1
  348. package/src/resources/extensions/gsd/watch/header-renderer.ts +3 -1
  349. package/src/resources/extensions/gsd/workflow-logger.ts +1 -0
  350. package/src/resources/extensions/gsd/worktree-command.ts +31 -44
  351. package/src/resources/extensions/gsd/worktree-session-state.ts +35 -0
  352. package/src/resources/extensions/mcp-client/index.ts +6 -3
  353. package/src/resources/extensions/mcp-client/tests/global-config.test.ts +91 -0
  354. package/src/resources/extensions/slash-commands/create-extension.ts +38 -24
  355. package/src/resources/skills/create-gsd-extension/SKILL.md +9 -5
  356. package/src/resources/skills/create-gsd-extension/references/custom-commands.md +1 -1
  357. package/src/resources/skills/create-gsd-extension/references/custom-rendering.md +5 -5
  358. package/src/resources/skills/create-gsd-extension/references/custom-tools.md +4 -4
  359. package/src/resources/skills/create-gsd-extension/references/custom-ui.md +6 -6
  360. package/src/resources/skills/create-gsd-extension/references/events-reference.md +3 -3
  361. package/src/resources/skills/create-gsd-extension/references/packaging-distribution.md +1 -1
  362. package/src/resources/skills/create-gsd-extension/references/remote-execution-overrides.md +3 -3
  363. package/src/resources/skills/create-gsd-extension/templates/extension-skeleton.ts +2 -2
  364. package/src/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts +3 -3
  365. package/src/resources/skills/create-gsd-extension/templates/templates.test.ts +58 -0
  366. package/src/resources/skills/create-gsd-extension/workflows/create-extension.md +32 -12
  367. package/dist/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +0 -601
  368. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +0 -651
  369. package/dist/resources/extensions/browser-tools/tests/capture-sharp-optional.test.cjs +0 -91
  370. package/dist/resources/extensions/gsd/tests/auto-supervisor.test.mjs +0 -53
  371. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +0 -112
  372. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +0 -23
  373. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +0 -5
  374. package/dist/resources/skills/github-workflows/references/gh/tests/__init__.py +0 -0
  375. package/dist/resources/skills/github-workflows/references/gh/tests/test_github_project_setup.py +0 -608
  376. package/dist/web/standalone/.next/static/chunks/2826.e9f5195e91f9cad2.js +0 -11
  377. package/dist/web/standalone/.next/static/chunks/3621.fc7480022c972438.js +0 -20
  378. package/dist/web/standalone/.next/static/chunks/webpack-2e68521d7c82f7c2.js +0 -1
  379. package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +0 -22
  380. package/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts +0 -47
  381. package/src/resources/extensions/gsd/tests/empty-content-abort-loop.test.ts +0 -75
  382. /package/dist/web/standalone/.next/static/{cAJH99yNS1UPbeSEiNRrV → UF5VF4F1tB0miEtJS7LyX}/_buildManifest.js +0 -0
  383. /package/dist/web/standalone/.next/static/{cAJH99yNS1UPbeSEiNRrV → UF5VF4F1tB0miEtJS7LyX}/_ssgManifest.js +0 -0
@@ -1,42 +1,99 @@
1
1
  /**
2
- * Regression test for #4129: tasks.completed_at stays NULL when status is
3
- * reconciled to 'complete' via the file-existence path in state.ts.
2
+ * Behavioural regression test for #4129.
4
3
  *
5
- * Root cause: reconcileSliceTasks called
6
- * updateTaskStatus(milestoneId, sliceId, t.id, "complete")
7
- * without a completedAt timestamp, so the column stays NULL.
4
+ * When deriveStateFromDb's reconcileSliceTasks finds a SUMMARY.md on disk
5
+ * for a task whose DB row is still pending, it flips the row to "complete".
6
+ * Before #4129, the call to updateTaskStatus omitted the completedAt
7
+ * timestamp, leaving completed_at NULL forever.
8
8
  *
9
- * Fix: pass new Date().toISOString() as the 5th argument.
9
+ * The fix passes new Date().toISOString() as the 5th argument; this test
10
+ * exercises that path end-to-end and asserts the column is populated.
11
+ *
12
+ * Refs #4829 (rewrite from positional source-grep).
10
13
  */
11
14
 
12
- import { describe, test } from "node:test";
13
- import assert from "node:assert/strict";
14
- import { readFileSync } from "node:fs";
15
- import { join, dirname } from "node:path";
16
- import { fileURLToPath } from "node:url";
17
-
18
- const __dirname = dirname(fileURLToPath(import.meta.url));
19
- const stateSource = readFileSync(join(__dirname, "..", "state.ts"), "utf-8");
20
-
21
- describe("completed-at reconcile (#4129)", () => {
22
- test("reconcileSliceTasks passes a completedAt timestamp when setting status to complete", () => {
23
- // Before the fix, state.ts had:
24
- // updateTaskStatus(milestoneId, sliceId, t.id, "complete")
25
- // which leaves completed_at NULL in the DB.
26
- // After the fix, a timestamp must be passed as the 5th argument.
27
- assert.doesNotMatch(
28
- stateSource,
29
- /updateTaskStatus\(\s*milestoneId\s*,\s*sliceId\s*,\s*t\.id\s*,\s*["']complete["']\s*\)/,
30
- "updateTaskStatus must not be called without a completedAt timestamp when reconciling tasks to 'complete' (#4129)",
31
- );
15
+ import { describe, test, beforeEach, afterEach } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { tmpdir } from 'node:os';
20
+
21
+ import { deriveStateFromDb, invalidateStateCache } from '../state.ts';
22
+ import {
23
+ openDatabase,
24
+ closeDatabase,
25
+ insertMilestone,
26
+ insertSlice,
27
+ insertTask,
28
+ getTask,
29
+ } from '../gsd-db.ts';
30
+
31
+ let basePath: string;
32
+
33
+ function setupProject(): void {
34
+ basePath = mkdtempSync(join(tmpdir(), 'gsd-completed-at-'));
35
+ // Project structure with active milestone, one slice, one task whose
36
+ // SUMMARY.md is already on disk — but the DB row is still "pending".
37
+ mkdirSync(join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks'), { recursive: true });
38
+
39
+ // CONTEXT + ROADMAP so deriveState identifies M001 as active and S01 as the active slice.
40
+ writeFileSync(
41
+ join(basePath, '.gsd', 'milestones', 'M001', 'M001-CONTEXT.md'),
42
+ '# M001\nActive milestone.\n',
43
+ );
44
+ writeFileSync(
45
+ join(basePath, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md'),
46
+ `# M001\n\n## Slices\n\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n - After this: works\n`,
47
+ );
48
+
49
+ // Plan file for the slice so reconcile can populate task list if DB is empty.
50
+ writeFileSync(
51
+ join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-PLAN.md'),
52
+ `# S01: Slice\n\n## Tasks\n\n- [ ] **T01: Test task** \`est:30m\`\n - Do: x\n - Verify: y\n`,
53
+ );
54
+
55
+ // The summary file: this is the on-disk evidence that flips the task
56
+ // status to "complete" inside reconcileSliceTasks.
57
+ writeFileSync(
58
+ join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks', 'T01-SUMMARY.md'),
59
+ '---\nid: T01\nparent: S01\nmilestone: M001\nblocker_discovered: false\n---\n# T01\n',
60
+ );
61
+ }
62
+
63
+ describe('completed_at reconcile (#4129)', () => {
64
+ beforeEach(() => {
65
+ setupProject();
66
+ openDatabase(join(basePath, '.gsd', 'gsd.db'));
67
+ insertMilestone({ id: 'M001', title: 'M001', status: 'active' });
68
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice', status: 'active' });
69
+ // Task is "pending" in DB, but SUMMARY.md exists on disk → reconcile flips it.
70
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
71
+ invalidateStateCache();
72
+ });
73
+
74
+ afterEach(() => {
75
+ closeDatabase();
76
+ try { rmSync(basePath, { recursive: true, force: true }); } catch { /* */ }
32
77
  });
33
78
 
34
- test("reconcileSliceTasks passes new Date().toISOString() as the completedAt argument", () => {
35
- // Positive assertion: the fixed call must include a timestamp.
36
- assert.match(
37
- stateSource,
38
- /updateTaskStatus\(\s*milestoneId\s*,\s*sliceId\s*,\s*t\.id\s*,\s*["']complete["']\s*,\s*new Date\(\)\.toISOString\(\)\s*\)/,
39
- "reconcileSliceTasks must pass new Date().toISOString() as completedAt when setting task status to 'complete' (#4129)",
79
+ test('reconcileSliceTasks sets completed_at when flipping a pending task to complete via SUMMARY.md', async () => {
80
+ const before = getTask('M001', 'S01', 'T01');
81
+ assert.strictEqual(before?.status, 'pending', 'task starts pending');
82
+ assert.strictEqual(before?.completed_at, null, 'task starts with completed_at NULL');
83
+
84
+ // Trigger the reconcile path (state.ts reconcileSliceTasks).
85
+ await deriveStateFromDb(basePath);
86
+
87
+ const after = getTask('M001', 'S01', 'T01');
88
+ assert.strictEqual(after?.status, 'complete', 'task should be flipped to complete');
89
+ assert.ok(
90
+ typeof after?.completed_at === 'string' && after.completed_at.length > 0,
91
+ `completed_at must be populated by reconcileSliceTasks (#4129); got ${JSON.stringify(after?.completed_at)}`,
92
+ );
93
+ // Sanity: timestamp parses as a valid ISO date.
94
+ assert.ok(
95
+ !Number.isNaN(Date.parse(after!.completed_at!)),
96
+ `completed_at should be a valid ISO timestamp, got ${after!.completed_at}`,
40
97
  );
41
98
  });
42
99
  });
@@ -12,7 +12,7 @@ import { mkdtempSync, rmSync, existsSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { tmpdir } from "node:os";
14
14
 
15
- import { autoLoop, resolveAgentEnd, _resetPendingResolve } from "../auto-loop.js";
15
+ import { autoLoop, resolveAgentEnd, _hasPendingResolveForTest, _resetPendingResolve } from "../auto-loop.js";
16
16
  import type { LoopDeps } from "../auto/loop-deps.js";
17
17
  import type { SessionLockStatus } from "../session-lock.js";
18
18
  import { writeGraph, readGraph, type WorkflowGraph, type GraphStep } from "../graph.ts";
@@ -29,6 +29,17 @@ function makeTmpDir(): string {
29
29
  return dir;
30
30
  }
31
31
 
32
+ async function resolveNextAgentEnd(timeoutMs = 3_000): Promise<void> {
33
+ const deadline = Date.now() + timeoutMs;
34
+ while (!_hasPendingResolveForTest()) {
35
+ if (Date.now() > deadline) {
36
+ throw new Error("Timed out waiting for pending agent_end resolver");
37
+ }
38
+ await new Promise((r) => setTimeout(r, 5));
39
+ }
40
+ resolveAgentEnd({ messages: [{ role: "assistant" }] });
41
+ }
42
+
32
43
  afterEach(() => {
33
44
  _resetPendingResolve();
34
45
  for (const d of tmpDirs) {
@@ -276,19 +287,16 @@ describe("Custom engine loop integration", () => {
276
287
  // We need to resolve resolveAgentEnd for each step.
277
288
 
278
289
  // Step 1: step-a
279
- await new Promise((r) => setTimeout(r, 80));
280
290
  unitCount++;
281
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
291
+ await resolveNextAgentEnd();
282
292
 
283
293
  // Step 2: step-b
284
- await new Promise((r) => setTimeout(r, 80));
285
294
  unitCount++;
286
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
295
+ await resolveNextAgentEnd();
287
296
 
288
297
  // Step 3: step-c
289
- await new Promise((r) => setTimeout(r, 80));
290
298
  unitCount++;
291
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
299
+ await resolveNextAgentEnd();
292
300
 
293
301
  // After step-c completes, engine.reconcile marks it complete, then
294
302
  // next deriveState sees isComplete=true → stopAuto → loop exits
@@ -398,8 +406,7 @@ describe("Custom engine loop integration", () => {
398
406
 
399
407
  const loopPromise = autoLoop(ctx, pi, s, deps);
400
408
 
401
- await new Promise((r) => setTimeout(r, 80));
402
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
409
+ await resolveNextAgentEnd();
403
410
 
404
411
  await loopPromise;
405
412
 
@@ -460,12 +467,10 @@ describe("Custom engine loop integration", () => {
460
467
  const loopPromise = autoLoop(ctx, pi, s, deps);
461
468
 
462
469
  // Resolve step-a
463
- await new Promise((r) => setTimeout(r, 80));
464
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
470
+ await resolveNextAgentEnd();
465
471
 
466
472
  // Resolve step-b
467
- await new Promise((r) => setTimeout(r, 80));
468
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
473
+ await resolveNextAgentEnd();
469
474
 
470
475
  await loopPromise;
471
476
 
@@ -514,7 +519,9 @@ describe("Custom engine loop integration", () => {
514
519
  });
515
520
 
516
521
  const resolver = setInterval(() => {
517
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
522
+ if (_hasPendingResolveForTest()) {
523
+ resolveAgentEnd({ messages: [{ role: "assistant" }] });
524
+ }
518
525
  }, 25);
519
526
  let timeout: NodeJS.Timeout | undefined;
520
527
  try {
@@ -569,7 +576,9 @@ describe("Custom engine loop integration", () => {
569
576
  });
570
577
  const deps1 = makeMockDeps();
571
578
  const resolver1 = setInterval(() => {
572
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
579
+ if (_hasPendingResolveForTest()) {
580
+ resolveAgentEnd({ messages: [{ role: "assistant" }] });
581
+ }
573
582
  if (pi1.calls.length >= 2) {
574
583
  s1.active = false;
575
584
  }
@@ -614,7 +623,9 @@ describe("Custom engine loop integration", () => {
614
623
  },
615
624
  });
616
625
  const resolver2 = setInterval(() => {
617
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
626
+ if (_hasPendingResolveForTest()) {
627
+ resolveAgentEnd({ messages: [{ role: "assistant" }] });
628
+ }
618
629
  }, 25);
619
630
  let timeout2: NodeJS.Timeout | undefined;
620
631
  try {
@@ -640,7 +651,12 @@ describe("Custom engine loop integration", () => {
640
651
  assert.match(stopEntry ?? "", /requested retry 4 times without passing/);
641
652
  });
642
653
 
643
- it("GRAPH.yaml step stays pending when session deactivates before reconcile", async () => {
654
+ it("two-step workflow drives both steps to complete and stops when isComplete fires", async () => {
655
+ // Note (#4831): renamed from "GRAPH.yaml step stays pending when session
656
+ // deactivates before reconcile" — the assertion body never proved the
657
+ // pending-on-deactivate claim and even comments that "the reconcile
658
+ // will still run for step-b". The behaviour this test actually pins is:
659
+ // both steps reconcile complete and stopAuto fires once isComplete.
644
660
  _resetPendingResolve();
645
661
 
646
662
  // Two-step workflow: a → b. We will complete step-a, then force a break
@@ -672,28 +688,30 @@ describe("Custom engine loop integration", () => {
672
688
  const loopPromise = autoLoop(ctx, pi, s, deps);
673
689
 
674
690
  // Resolve step-a successfully
675
- await new Promise((r) => setTimeout(r, 80));
676
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
691
+ await resolveNextAgentEnd();
677
692
 
678
693
  // Step-b enters runUnit — deactivate the session before resolving.
679
694
  // runUnit checks s.active after newSession and returns cancelled if false.
680
695
  // But since newSession resolves synchronously in our mock (before the
681
696
  // active check), the unit still runs. Instead, let's just cancel it.
682
- await new Promise((r) => setTimeout(r, 80));
683
697
  // Resolve as cancelled to simulate a failed session
684
- resolveAgentEnd({ messages: [{ role: "assistant" }] });
698
+ await resolveNextAgentEnd();
685
699
 
686
700
  // The reconcile will still run for step-b in this flow since
687
701
  // runUnitPhase returns "next" (not "break") for completed units.
688
702
  // After both steps complete, the engine detects isComplete and stops.
689
703
  await loopPromise;
690
704
 
691
- // Verify step-a is complete
705
+ // Both steps reconcile complete; the renamed expectation pins that the
706
+ // engine drives the workflow through isComplete rather than leaving any
707
+ // step pending.
692
708
  const finalGraph = readGraph(runDir);
693
709
  const stepA = finalGraph.steps.find(s => s.id === "step-a");
710
+ const stepB = finalGraph.steps.find(s => s.id === "step-b");
694
711
  assert.equal(stepA?.status, "complete", "Step-a should be complete");
712
+ assert.equal(stepB?.status, "complete", "Step-b should be complete");
695
713
 
696
- // Verify the loop stopped appropriately
714
+ // The loop must stop once isComplete fires.
697
715
  assert.ok(
698
716
  deps.callLog.some((e: string) => e.startsWith("stopAuto:")),
699
717
  "stopAuto should have been called",
@@ -16,7 +16,6 @@
16
16
  * retrying can never succeed and causes cost spikes.
17
17
  */
18
18
 
19
- import { readFileSync } from "node:fs";
20
19
  import { join, sep } from "node:path";
21
20
  import { createTestContext } from "./test-helpers.ts";
22
21
 
@@ -89,47 +88,8 @@ assertEq(
89
88
  "Non-worktree path is unchanged",
90
89
  );
91
90
 
92
- // ── Part 2: ensureDbOpen returns structured failure context ──────────────
93
-
94
- console.log("\n=== #2517 Part 2: ensureDbOpen structured diagnostics ===");
95
-
96
- const dynamicToolsSrc = readFileSync(
97
- join(import.meta.dirname, "..", "bootstrap", "dynamic-tools.ts"),
98
- "utf-8",
99
- );
100
-
101
- // ensureDbOpen should surface diagnostic context, not just boolean false
102
- // Check that the catch block logs error details via workflow-logger
103
- assertTrue(
104
- dynamicToolsSrc.includes("ensureDbOpen failed") && dynamicToolsSrc.includes("logWarning"),
105
- "ensureDbOpen catch block surfaces diagnostic information via logWarning instead of bare false (#2517)",
106
- );
107
-
108
- // ── Part 3: post-unit does NOT artifact-retry on db_unavailable ──────────
109
-
110
- console.log("\n=== #2517 Part 3: post-unit db_unavailable is infra-fatal ===");
111
-
112
- const postUnitSrc = readFileSync(
113
- join(import.meta.dirname, "..", "auto-post-unit.ts"),
114
- "utf-8",
115
- );
116
-
117
- // The artifact retry block should check DB availability and skip retry
118
- // when the DB is unavailable (infra failure, not a missing artifact).
119
- assertTrue(
120
- postUnitSrc.includes("db_unavailable") || postUnitSrc.includes("isDbAvailable"),
121
- "post-unit artifact retry path checks DB availability to avoid retry loop (#2517)",
122
- );
123
-
124
- // Verify the retry block is guarded: when !isDbAvailable(), the code must
125
- // NOT return "retry". The pattern should be: if (!verified && !isDbAvailable()) { skip }
126
- // followed by else if (!verified) { ... return "retry" }
127
- const dbUnavailableGuard = postUnitSrc.match(
128
- /!triggerArtifactVerified\s*&&\s*!isDbAvailable\(\)/,
129
- );
130
- assertTrue(
131
- !!dbUnavailableGuard,
132
- "The retry block explicitly guards against !isDbAvailable() before returning 'retry' (#2517)",
133
- );
91
+ // Source-grep checks for ensureDbOpen diagnostics + post-unit retry guard
92
+ // were removed (#4826) — the behavioural retry-loop tests live in
93
+ // auto-post-unit.test.ts and exercise isDbAvailable() directly.
134
94
 
135
95
  report();
@@ -167,9 +167,11 @@ test('auto-prunes old debug logs', () => {
167
167
  enableDebug(tmp);
168
168
 
169
169
  const files = readdirSync(debugDir).filter(f => f.startsWith('debug-') && f.endsWith('.log'));
170
- // Should have at most MAX_DEBUG_LOGS (5) = 5 old + 1 new, but pruned to 5 total
171
- // Actually: prunes to < 5 old, then creates 1 new = at most 5
172
- assert.ok(files.length <= 6, `should have pruned old logs, got ${files.length}`);
170
+ // MAX_DEBUG_LOGS is 5 enableDebug prunes to < 5 old and then creates 1 new,
171
+ // so the directory must hold at most 5 files in total. The previous
172
+ // assertion (<= 6) would have passed even with one stale log left behind
173
+ // (Refs #4831).
174
+ assert.ok(files.length <= 5, `should have pruned old logs to <= 5, got ${files.length}`);
173
175
 
174
176
  disableDebug();
175
177
  });
@@ -0,0 +1,116 @@
1
+ // GSD Extension — Regression test for #4996: deferred milestone dir creation
2
+ // Verifies that showHeadlessMilestoneCreation does not pre-create the milestone
3
+ // directory before the discuss flow runs. The dir should only appear after a
4
+ // writer (saveArtifactToDb / atomicWriteAsync) emits the first artifact.
5
+
6
+ import { describe, it, beforeEach, afterEach } from "node:test";
7
+ import assert from "node:assert/strict";
8
+ import { mkdtempSync, mkdirSync, existsSync, rmSync, readFileSync } from "node:fs";
9
+ import { dirname, join } from "node:path";
10
+ import { tmpdir } from "node:os";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ import { isReusableGhostMilestone } from "../state.ts";
14
+ import { nextMilestoneIdReserved } from "../milestone-id-reservation.ts";
15
+ import { clearReservedMilestoneIds, findMilestoneIds } from "../milestone-ids.ts";
16
+ import { invalidateAllCaches } from "../cache.ts";
17
+ import { closeDatabase, openDatabase } from "../gsd-db.ts";
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const GUIDED_FLOW_PATH = join(__dirname, "..", "guided-flow.ts");
21
+
22
+ function getShowHeadlessBody(): string {
23
+ const source = readFileSync(GUIDED_FLOW_PATH, "utf-8");
24
+ const fnStart = source.indexOf("export async function showHeadlessMilestoneCreation");
25
+ assert.ok(fnStart > -1, "showHeadlessMilestoneCreation must exist in guided-flow.ts");
26
+ const nextExport = source.indexOf("\nexport ", fnStart + 1);
27
+ return source.slice(fnStart, nextExport === -1 ? source.length : nextExport);
28
+ }
29
+
30
+ function makeBase(prefix = "gsd-deferred-dir-"): string {
31
+ const base = mkdtempSync(join(tmpdir(), prefix));
32
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
33
+ return base;
34
+ }
35
+
36
+ describe("showHeadlessMilestoneCreation source guard (#4996)", () => {
37
+ it("does not call mkdirSync in the headless milestone creation path", () => {
38
+ const body = getShowHeadlessBody();
39
+ assert.doesNotMatch(
40
+ body,
41
+ /\bmkdirSync\s*\(/,
42
+ "showHeadlessMilestoneCreation must not pre-create milestone directories",
43
+ );
44
+ });
45
+
46
+ it("does not call mkdir or mkdirp before dispatchWorkflow", () => {
47
+ const body = getShowHeadlessBody();
48
+ const dispatchIdx = body.indexOf("dispatchWorkflow");
49
+ assert.ok(dispatchIdx > -1, "dispatchWorkflow must be present");
50
+
51
+ const beforeDispatch = body.slice(0, dispatchIdx);
52
+ assert.doesNotMatch(
53
+ beforeDispatch,
54
+ /\b(?:mkdir|mkdirp)\s*\(/,
55
+ "showHeadlessMilestoneCreation must defer directory creation until artifact write",
56
+ );
57
+ });
58
+ });
59
+
60
+ describe("deferred milestone dir creation (#4996)", () => {
61
+ let base: string;
62
+
63
+ beforeEach(() => {
64
+ clearReservedMilestoneIds();
65
+ });
66
+
67
+ afterEach(() => {
68
+ try { closeDatabase(); } catch { /* ignore */ }
69
+ try { invalidateAllCaches(); } catch { /* ignore */ }
70
+ try { clearReservedMilestoneIds(); } catch { /* ignore */ }
71
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* ignore */ }
72
+ });
73
+
74
+ it("(a) fresh project: milestones dir has no M001 entry before any discuss flow", () => {
75
+ base = makeBase();
76
+ const nextId = nextMilestoneIdReserved(findMilestoneIds(base), false, base);
77
+ assert.equal(nextId, "M001", "reservation should choose M001 for a fresh project");
78
+
79
+ const ids = findMilestoneIds(base);
80
+ assert.equal(ids.length, 0, "no milestone dirs should exist before any discuss flow");
81
+
82
+ // And specifically M001 should not exist
83
+ const m001Dir = join(base, ".gsd", "milestones", "M001");
84
+ assert.ok(!existsSync(m001Dir), "M001 dir must not exist before the discuss flow runs");
85
+ });
86
+
87
+ it("(b) abandoned discuss flow leaves no orphan: isReusableGhostMilestone returns false for non-existent dir", () => {
88
+ base = makeBase();
89
+ const nextId = nextMilestoneIdReserved(findMilestoneIds(base), false, base);
90
+ assert.equal(nextId, "M001", "reservation should not require a pre-created directory");
91
+
92
+ const m001Dir = join(base, ".gsd", "milestones", "M001");
93
+ assert.ok(!existsSync(m001Dir), "no M001 dir should exist");
94
+ assert.equal(isReusableGhostMilestone(base, "M001"), false, "non-existent milestone should not be reusable");
95
+ // findMilestoneIds only returns dirs that exist
96
+ const ids = findMilestoneIds(base);
97
+ assert.ok(!ids.includes("M001"), "M001 should not appear in findMilestoneIds when no dir exists");
98
+ });
99
+
100
+ it("(c) a stub dir left from a previous bug IS reusable but a newly-generated ID with no dir is not in the ghost list", () => {
101
+ base = makeBase();
102
+ openDatabase(join(base, ".gsd", "gsd.db"));
103
+ // Create a stub to represent a pre-existing phantom
104
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices"), { recursive: true });
105
+
106
+ // isReusableGhostMilestone identifies the orphaned stub
107
+ assert.ok(isReusableGhostMilestone(base, "M001"), "pre-existing stub should be identified as reusable ghost");
108
+ const nextId = nextMilestoneIdReserved(findMilestoneIds(base), false, base);
109
+ assert.equal(nextId, "M001", "reservation should reuse the pre-existing ghost");
110
+
111
+ // The new ID (M002, which would be max+1 in this scenario but ghost reuse returns M001)
112
+ // should not have a dir
113
+ const m002Dir = join(base, ".gsd", "milestones", "M002");
114
+ assert.ok(!existsSync(m002Dir), "a freshly-requested ID should have no dir until first artifact write");
115
+ });
116
+ });
@@ -1,28 +1,22 @@
1
1
  /**
2
- * discuss-empty-db-fallback.test.ts Tests for #2892.
2
+ * Behavioural regression tests for #2892.
3
3
  *
4
- * When the DB is open but empty (e.g., after crash/truncation),
5
- * getMilestoneSlices() returns [] and showDiscuss() incorrectly declares
6
- * "All slices are complete." The fix adds a roadmap fallback: when the DB
7
- * returns zero slices but a ROADMAP file exists, parse slices from the
8
- * roadmap instead of treating zero slices as "all complete."
4
+ * When the DB is open but empty (e.g. after crash/truncation),
5
+ * getMilestoneSlices() returns []. The fix in showDiscuss() falls back to
6
+ * parsing slices from the on-disk ROADMAP file instead of declaring "all
7
+ * slices are complete." These tests pin the parser contract that the
8
+ * fallback depends on: incomplete checkboxes (`[ ]`) yield `done=false`
9
+ * slices and completed checkboxes (`[x]`) yield `done=true`.
10
+ *
11
+ * The earlier source-grep / regex-on-showDiscuss-body tests (audit verdicts
12
+ * SOURCE_GREP / POSITIONAL — see #4826/#4829) were dropped; they pinned a
13
+ * specific surface form rather than behaviour.
9
14
  */
10
15
 
11
16
  import { describe, test } from "node:test";
12
17
  import assert from "node:assert/strict";
13
- import { readFileSync } from "node:fs";
14
- import { fileURLToPath } from "node:url";
15
- import { dirname, join } from "node:path";
16
18
  import { parseRoadmapSlices } from "../roadmap-slices.ts";
17
19
 
18
- // ─── Helpers ─────────────────────────────────────────────────────────────────
19
-
20
- function readGuidedFlowSource(): string {
21
- const thisFile = fileURLToPath(import.meta.url);
22
- const thisDir = dirname(thisFile);
23
- return readFileSync(join(thisDir, "..", "guided-flow.ts"), "utf-8");
24
- }
25
-
26
20
  const SAMPLE_ROADMAP = `# M012 Roadmap
27
21
 
28
22
  ## Slices
@@ -34,80 +28,23 @@ const SAMPLE_ROADMAP = `# M012 Roadmap
34
28
  > After this: dashboard renders
35
29
  `;
36
30
 
37
- // ─── Tests ───────────────────────────────────────────────────────────────────
38
-
39
- describe("discuss-empty-db-fallback (#2892)", () => {
40
-
41
- test("1. parseRoadmapSlices extracts slices from a valid ROADMAP", () => {
31
+ describe("discuss-empty-db-fallback parser contract (#2892)", () => {
32
+ test("parseRoadmapSlices extracts slices from a valid ROADMAP", () => {
42
33
  const slices = parseRoadmapSlices(SAMPLE_ROADMAP);
43
34
  assert.strictEqual(slices.length, 3, "should parse 3 slices from sample roadmap");
44
- assert.strictEqual(slices[0]!.id, "S01");
45
- assert.strictEqual(slices[1]!.id, "S02");
46
- assert.strictEqual(slices[2]!.id, "S03");
47
- // All slices are incomplete ([ ] not [x])
48
- assert.ok(slices.every(s => !s.done), "all slices should be incomplete");
49
- });
50
-
51
- test("2. guided-flow imports parseRoadmapSlices for roadmap fallback", () => {
52
- const source = readGuidedFlowSource();
53
- assert.ok(
54
- source.includes("parseRoadmapSlices"),
55
- "guided-flow must import parseRoadmapSlices to support roadmap fallback when DB is empty",
56
- );
35
+ const ids = slices.map(s => s.id).sort();
36
+ assert.deepStrictEqual(ids, ["S01", "S02", "S03"]);
57
37
  });
58
38
 
59
- test("3. guided-flow has roadmap fallback when normSlices is empty but roadmapContent exists", () => {
60
- const source = readGuidedFlowSource();
61
- // The fix must add a fallback that checks normSlices.length === 0 && roadmapContent
62
- // and repopulates normSlices from the roadmap before the pendingSlices guard.
63
- //
64
- // Pattern: after DB query produces normSlices, if empty + roadmap exists,
65
- // fall back to parseRoadmapSlices(roadmapContent).
66
- const fallbackPattern = /normSlices\.length\s*===\s*0\s*&&\s*roadmapContent/;
39
+ test("incomplete checkboxes yield done=false (so fallback shows them as pending)", () => {
40
+ const slices = parseRoadmapSlices(SAMPLE_ROADMAP);
67
41
  assert.ok(
68
- fallbackPattern.test(source),
69
- "guided-flow must check normSlices.length === 0 && roadmapContent to trigger roadmap fallback",
42
+ slices.every(s => s.done === false),
43
+ "all 3 incomplete roadmap slices must be done=false otherwise the empty-DB fallback would falsely report them complete (#2892)",
70
44
  );
71
45
  });
72
46
 
73
- test("4. guided-flow no longer has unguarded pendingSlices === 0 exit after DB-only query", () => {
74
- const source = readGuidedFlowSource();
75
- // Extract the showDiscuss function body
76
- const fnMatch = source.match(
77
- /async function showDiscuss\s*\([^)]*\)[^{]*\{([\s\S]*?)\nfunction\s/,
78
- );
79
- assert.ok(!!fnMatch, "showDiscuss function body must be found");
80
-
81
- if (fnMatch) {
82
- const body = fnMatch[1]!;
83
- // After the DB query block (isDbAvailable/getMilestoneSlices), there should
84
- // be a roadmap fallback BEFORE the pendingSlices.length === 0 check.
85
- // Find the getMilestoneSlices call and the pendingSlices === 0 check
86
- const dbQueryIdx = body.indexOf("getMilestoneSlices");
87
- const fallbackIdx = body.indexOf("parseRoadmapSlices");
88
- const pendingGuardIdx = body.indexOf('pendingSlices.length === 0');
89
-
90
- assert.ok(dbQueryIdx > 0, "getMilestoneSlices call must exist");
91
- assert.ok(fallbackIdx > 0, "parseRoadmapSlices fallback must exist");
92
- assert.ok(pendingGuardIdx > 0, "pendingSlices.length === 0 guard must exist");
93
- assert.ok(
94
- fallbackIdx > dbQueryIdx && fallbackIdx < pendingGuardIdx,
95
- "parseRoadmapSlices fallback must appear BETWEEN DB query and pendingSlices === 0 guard",
96
- );
97
- }
98
- });
99
-
100
- test("5. roadmap-parsed slices map to NormSlice format with done=false by default", () => {
101
- // When falling back to roadmap, incomplete slices ([ ]) should map to done:false,
102
- // ensuring they appear as pending and are NOT falsely reported as complete.
103
- const slices = parseRoadmapSlices(SAMPLE_ROADMAP);
104
- const normSlices = slices.map(s => ({ id: s.id, done: s.done, title: s.title }));
105
- const pendingSlices = normSlices.filter(s => !s.done);
106
- assert.strictEqual(pendingSlices.length, 3,
107
- "all 3 incomplete roadmap slices should be pending — not falsely treated as complete");
108
- });
109
-
110
- test("6. roadmap with completed slices correctly reports them as done", () => {
47
+ test("completed checkboxes yield done=true; mixed roadmap surfaces only the open slices as pending", () => {
111
48
  const completedRoadmap = `# M012 Roadmap
112
49
 
113
50
  ## Slices
@@ -119,9 +56,7 @@ describe("discuss-empty-db-fallback (#2892)", () => {
119
56
  > After this: dashboard renders
120
57
  `;
121
58
  const slices = parseRoadmapSlices(completedRoadmap);
122
- const normSlices = slices.map(s => ({ id: s.id, done: s.done, title: s.title }));
123
- const pendingSlices = normSlices.filter(s => !s.done);
124
- assert.strictEqual(pendingSlices.length, 1, "only S02 should be pending");
125
- assert.strictEqual(pendingSlices[0]!.id, "S02");
59
+ const pendingIds = slices.filter(s => !s.done).map(s => s.id);
60
+ assert.deepStrictEqual(pendingIds, ["S02"], "only S02 should be reported as pending");
126
61
  });
127
62
  });