gsd-pi 2.44.0-dev.d25d507 → 2.45.0-dev.6b9da3e

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 (245) hide show
  1. package/dist/resources/extensions/gsd/activity-log.js +7 -0
  2. package/dist/resources/extensions/gsd/auto/infra-errors.js +3 -0
  3. package/dist/resources/extensions/gsd/auto/phases.js +37 -36
  4. package/dist/resources/extensions/gsd/auto-prompts.js +24 -1
  5. package/dist/resources/extensions/gsd/auto-start.js +21 -2
  6. package/dist/resources/extensions/gsd/auto-timers.js +57 -3
  7. package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -0
  8. package/dist/resources/extensions/gsd/auto-worktree.js +9 -6
  9. package/dist/resources/extensions/gsd/auto.js +30 -3
  10. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +156 -0
  11. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  12. package/dist/resources/extensions/gsd/commands/handlers/core.js +2 -0
  13. package/dist/resources/extensions/gsd/commands/handlers/ops.js +10 -0
  14. package/dist/resources/extensions/gsd/commands-mcp-status.js +187 -0
  15. package/dist/resources/extensions/gsd/db-writer.js +34 -16
  16. package/dist/resources/extensions/gsd/doctor.js +8 -0
  17. package/dist/resources/extensions/gsd/git-service.js +8 -3
  18. package/dist/resources/extensions/gsd/gsd-db.js +12 -1
  19. package/dist/resources/extensions/gsd/markdown-renderer.js +1 -1
  20. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  21. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  22. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  23. package/dist/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  24. package/dist/resources/extensions/gsd/prompts/rethink.md +78 -0
  25. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  26. package/dist/resources/extensions/gsd/provider-error-pause.js +7 -0
  27. package/dist/resources/extensions/gsd/repo-identity.js +45 -7
  28. package/dist/resources/extensions/gsd/rethink.js +115 -0
  29. package/dist/resources/extensions/gsd/state.js +41 -3
  30. package/dist/resources/extensions/gsd/tools/plan-slice.js +1 -0
  31. package/dist/resources/extensions/gsd/tools/plan-task.js +1 -0
  32. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -0
  33. package/dist/resources/extensions/gsd/tools/validate-milestone.js +88 -0
  34. package/dist/resources/extensions/gsd/worktree-manager.js +32 -2
  35. package/dist/resources/extensions/gsd/worktree-resolver.js +6 -0
  36. package/dist/resources/extensions/mcp-client/index.js +14 -0
  37. package/dist/web/standalone/.next/BUILD_ID +1 -1
  38. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  39. package/dist/web/standalone/.next/build-manifest.json +3 -3
  40. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  41. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  42. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  44. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found/page.js +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  61. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/index.html +1 -1
  102. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  103. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  104. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  105. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  106. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  107. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  108. package/dist/web/standalone/.next/server/app/page.js +1 -1
  109. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  111. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  114. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  115. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  116. package/dist/web/standalone/.next/static/chunks/4024.11ca5c01938e5948.js +9 -0
  117. package/dist/web/standalone/.next/static/chunks/{3721.bf31263de6d5fa46.js → 485.243af25f0cdf50d6.js} +2 -2
  118. package/dist/web/standalone/.next/static/chunks/app/{page-b9367c5ae13b99c6.js → page-6654a8cca61a3d1c.js} +1 -1
  119. package/dist/web/standalone/.next/static/chunks/webpack-0a4cd455ec4197d2.js +1 -0
  120. package/dist/web/standalone/.next/static/css/dd4ae3f58ac9b600.css +1 -0
  121. package/package.json +1 -1
  122. package/packages/native/dist/stream-process/index.js +2 -2
  123. package/packages/native/src/__tests__/stream-process.test.mjs +34 -0
  124. package/packages/native/src/stream-process/index.ts +2 -2
  125. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +3 -1
  126. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/auth-storage.js +15 -1
  128. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts +15 -0
  130. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/core/local-model-check.js +41 -0
  132. package/packages/pi-coding-agent/dist/core/local-model-check.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +11 -0
  134. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/model-registry.js +20 -1
  136. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  138. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/settings-manager.js +6 -0
  140. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  141. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/main.js +17 -0
  143. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  144. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts +2 -0
  145. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts.map +1 -0
  146. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js +32 -0
  147. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js.map +1 -0
  148. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  150. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +8 -1
  151. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  152. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  153. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  154. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
  155. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  156. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts +15 -0
  157. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts.map +1 -0
  158. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js +40 -0
  159. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js.map +1 -0
  160. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +4 -1
  162. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  163. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +5 -2
  164. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  165. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +13 -2
  166. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  167. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  168. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +17 -8
  169. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  170. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  171. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -3
  172. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  173. package/packages/pi-coding-agent/package.json +1 -1
  174. package/packages/pi-coding-agent/src/core/auth-storage.ts +15 -1
  175. package/packages/pi-coding-agent/src/core/local-model-check.ts +45 -0
  176. package/packages/pi-coding-agent/src/core/model-registry.ts +21 -1
  177. package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
  178. package/packages/pi-coding-agent/src/main.ts +19 -0
  179. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +38 -0
  180. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +10 -0
  181. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
  182. package/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +48 -0
  183. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +3 -1
  184. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +18 -3
  185. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +16 -7
  186. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +8 -1
  187. package/pkg/package.json +1 -1
  188. package/src/resources/extensions/gsd/activity-log.ts +1 -0
  189. package/src/resources/extensions/gsd/auto/infra-errors.ts +3 -0
  190. package/src/resources/extensions/gsd/auto/phases.ts +46 -48
  191. package/src/resources/extensions/gsd/auto-prompts.ts +24 -1
  192. package/src/resources/extensions/gsd/auto-start.ts +25 -2
  193. package/src/resources/extensions/gsd/auto-timers.ts +64 -3
  194. package/src/resources/extensions/gsd/auto-worktree-sync.ts +5 -0
  195. package/src/resources/extensions/gsd/auto-worktree.ts +9 -6
  196. package/src/resources/extensions/gsd/auto.ts +37 -3
  197. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +148 -0
  198. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  199. package/src/resources/extensions/gsd/commands/handlers/core.ts +2 -0
  200. package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
  201. package/src/resources/extensions/gsd/commands-mcp-status.ts +247 -0
  202. package/src/resources/extensions/gsd/db-writer.ts +39 -17
  203. package/src/resources/extensions/gsd/doctor.ts +7 -1
  204. package/src/resources/extensions/gsd/git-service.ts +6 -2
  205. package/src/resources/extensions/gsd/gsd-db.ts +16 -1
  206. package/src/resources/extensions/gsd/markdown-renderer.ts +1 -1
  207. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  208. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  209. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  210. package/src/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  211. package/src/resources/extensions/gsd/prompts/rethink.md +78 -0
  212. package/src/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  213. package/src/resources/extensions/gsd/provider-error-pause.ts +9 -0
  214. package/src/resources/extensions/gsd/repo-identity.ts +46 -7
  215. package/src/resources/extensions/gsd/rethink.ts +154 -0
  216. package/src/resources/extensions/gsd/state.ts +41 -1
  217. package/src/resources/extensions/gsd/tests/auto-pr-bugs.test.ts +88 -0
  218. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +114 -0
  219. package/src/resources/extensions/gsd/tests/db-writer.test.ts +79 -0
  220. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +121 -0
  221. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +60 -0
  222. package/src/resources/extensions/gsd/tests/est-annotation-timeout.test.ts +120 -0
  223. package/src/resources/extensions/gsd/tests/infra-error.test.ts +20 -2
  224. package/src/resources/extensions/gsd/tests/inherited-repo-home-dir.test.ts +121 -0
  225. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +103 -0
  226. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +66 -0
  227. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -7
  228. package/src/resources/extensions/gsd/tests/recovery-attempts-reset.test.ts +176 -0
  229. package/src/resources/extensions/gsd/tests/stop-auto-merge-back.test.ts +67 -0
  230. package/src/resources/extensions/gsd/tests/survivor-branch-complete.test.ts +108 -0
  231. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +49 -0
  232. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +2 -1
  233. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +65 -0
  234. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
  235. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -0
  236. package/src/resources/extensions/gsd/tools/replan-slice.ts +3 -0
  237. package/src/resources/extensions/gsd/tools/validate-milestone.ts +127 -0
  238. package/src/resources/extensions/gsd/worktree-manager.ts +43 -2
  239. package/src/resources/extensions/gsd/worktree-resolver.ts +7 -0
  240. package/src/resources/extensions/mcp-client/index.ts +20 -0
  241. package/dist/web/standalone/.next/static/chunks/4024.0de81b543b28b9fe.js +0 -9
  242. package/dist/web/standalone/.next/static/chunks/webpack-9014b5adb127a98a.js +0 -1
  243. package/dist/web/standalone/.next/static/css/8a727f372cf53002.css +0 -1
  244. /package/dist/web/standalone/.next/static/{tokoGmfkYfWf1_Yl_Gz7i → rzO54ZboyINyEt7cVM_uS}/_buildManifest.js +0 -0
  245. /package/dist/web/standalone/.next/static/{tokoGmfkYfWf1_Yl_Gz7i → rzO54ZboyINyEt7cVM_uS}/_ssgManifest.js +0 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * derive-state-db-disk-reconcile.test.ts — #2416
3
+ *
4
+ * After migration to DB-backed state, milestones that exist on disk
5
+ * (in .gsd/milestones/) but were never imported into the DB become
6
+ * invisible to deriveStateFromDb(). This test verifies that
7
+ * deriveStateFromDb reconciles disk milestones with DB milestones.
8
+ */
9
+
10
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+
14
+ import { deriveStateFromDb, invalidateStateCache } from "../state.ts";
15
+ import {
16
+ openDatabase,
17
+ closeDatabase,
18
+ insertMilestone,
19
+ insertSlice,
20
+ insertTask,
21
+ } from "../gsd-db.ts";
22
+ import { createTestContext } from "./test-helpers.ts";
23
+
24
+ const { assertEq, assertTrue, report } = createTestContext();
25
+
26
+ function createFixtureBase(): string {
27
+ const base = mkdtempSync(join(tmpdir(), "gsd-disk-reconcile-"));
28
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
29
+ return base;
30
+ }
31
+
32
+ function writeFile(base: string, relativePath: string, content: string): void {
33
+ const full = join(base, ".gsd", relativePath);
34
+ mkdirSync(join(full, ".."), { recursive: true });
35
+ writeFileSync(full, content);
36
+ }
37
+
38
+ function cleanup(base: string): void {
39
+ rmSync(base, { recursive: true, force: true });
40
+ }
41
+
42
+ const CONTEXT_CONTENT = `# M002: Disk-Only Milestone
43
+
44
+ This milestone exists on disk but not in the DB.
45
+
46
+ ## Must-Haves
47
+ - Something important
48
+ `;
49
+
50
+ const ROADMAP_CONTENT = `# M002: Disk-Only Milestone
51
+
52
+ **Vision:** Test disk reconciliation.
53
+
54
+ ## Slices
55
+
56
+ - [ ] **S01: First Slice** \`risk:low\` \`depends:[]\`
57
+ > Do something.
58
+ `;
59
+
60
+ async function main(): Promise<void> {
61
+ console.log("\n=== #2416: deriveStateFromDb reconciles disk milestones ===");
62
+
63
+ // Set up: M001 in DB, M002 on disk only
64
+ const base = createFixtureBase();
65
+ const dbPath = join(base, ".gsd", "gsd.db");
66
+
67
+ try {
68
+ openDatabase(dbPath);
69
+
70
+ // M001 is in the DB with a complete status
71
+ insertMilestone({ id: "M001", title: "M001: DB Milestone", status: "complete", depends_on: [] });
72
+ insertSlice({ id: "S01", milestoneId: "M001", title: "S01: Done Slice", status: "complete", depends: [] });
73
+
74
+ // Write M001 summary on disk (marks it complete on filesystem too)
75
+ writeFile(base, "milestones/M001/SUMMARY.md", "# M001: DB Milestone\n\nDone.");
76
+
77
+ // M002 exists ONLY on disk, not in DB
78
+ writeFile(base, "milestones/M002/CONTEXT.md", CONTEXT_CONTENT);
79
+ writeFile(base, "milestones/M002/ROADMAP.md", ROADMAP_CONTENT);
80
+
81
+ invalidateStateCache();
82
+ const state = await deriveStateFromDb(base);
83
+
84
+ // M002 should be visible in the registry
85
+ const m002Entry = state.registry.find((m) => m.id === "M002");
86
+ assertTrue(
87
+ m002Entry !== undefined,
88
+ "M002 (disk-only milestone) should appear in state.registry (#2416)",
89
+ );
90
+
91
+ // M001 should still be in the registry
92
+ const m001Entry = state.registry.find((m) => m.id === "M001");
93
+ assertTrue(
94
+ m001Entry !== undefined,
95
+ "M001 (DB milestone) should still appear in state.registry",
96
+ );
97
+
98
+ // The active milestone should be M002 (since M001 is complete)
99
+ assertTrue(
100
+ state.activeMilestone !== null,
101
+ "There should be an active milestone",
102
+ );
103
+ if (state.activeMilestone) {
104
+ assertEq(
105
+ state.activeMilestone.id,
106
+ "M002",
107
+ "Active milestone should be M002 (disk-only, not complete) (#2416)",
108
+ );
109
+ }
110
+ } finally {
111
+ closeDatabase();
112
+ cleanup(base);
113
+ }
114
+
115
+ report();
116
+ }
117
+
118
+ main().catch((err) => {
119
+ console.error(err);
120
+ process.exit(1);
121
+ });
@@ -11,6 +11,7 @@ import {
11
11
  insertArtifact,
12
12
  isDbAvailable,
13
13
  insertMilestone,
14
+ getAllMilestones,
14
15
  insertSlice,
15
16
  insertTask,
16
17
  } from '../gsd-db.ts';
@@ -962,4 +963,63 @@ describe('derive-state-db', async () => {
962
963
  cleanup(base);
963
964
  }
964
965
  });
966
+
967
+ // ─── Regression: disk-only milestones synced into DB (#2416) ─────────
968
+ test('derive-state-db: disk-only milestone auto-synced into DB (#2416)', async () => {
969
+ const base = createFixtureBase();
970
+ try {
971
+ // M001 is complete and exists in DB. M002 was queued on disk only — no DB row.
972
+ writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Summary\n\nDone.');
973
+ writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002: Queued\n\nQueued milestone.');
974
+
975
+ openDatabase(':memory:');
976
+ // Only insert M001 — simulates the state after migration guard ran then /gsd queue added M002
977
+ insertMilestone({ id: 'M001', title: 'First', status: 'complete' });
978
+
979
+ invalidateStateCache();
980
+ const state = await deriveStateFromDb(base);
981
+
982
+ // Before the fix, M002 was invisible: getAllMilestones() returned only M001
983
+ // (complete) → phase='complete' → auto-mode stopped.
984
+ // After the fix, deriveStateFromDb reconciles disk dirs and inserts M002.
985
+ assert.deepStrictEqual(state.phase, 'pre-planning', 'disk-sync-2416: phase is pre-planning, not complete');
986
+ assert.deepStrictEqual(state.registry.length, 2, 'disk-sync-2416: both milestones visible in registry');
987
+ assert.deepStrictEqual(state.registry[0]?.id, 'M001', 'disk-sync-2416: registry[0] is M001');
988
+ assert.deepStrictEqual(state.registry[0]?.status, 'complete', 'disk-sync-2416: M001 is complete');
989
+ assert.deepStrictEqual(state.registry[1]?.id, 'M002', 'disk-sync-2416: registry[1] is M002');
990
+ assert.deepStrictEqual(state.registry[1]?.status, 'active', 'disk-sync-2416: M002 is active');
991
+ assert.deepStrictEqual(state.activeMilestone?.id, 'M002', 'disk-sync-2416: activeMilestone is M002');
992
+
993
+ closeDatabase();
994
+ } finally {
995
+ closeDatabase();
996
+ cleanup(base);
997
+ }
998
+ });
999
+
1000
+ // ─── Queued milestone row not clobbered by later plan (#2416 root cause) ──
1001
+ test('derive-state-db: queued milestone row survives gsd_plan_milestone INSERT OR IGNORE', async () => {
1002
+ try {
1003
+ openDatabase(':memory:');
1004
+
1005
+ // Simulates gsd_milestone_generate_id inserting a minimal queued row
1006
+ insertMilestone({ id: 'M001', status: 'queued' });
1007
+
1008
+ const before = getAllMilestones();
1009
+ assert.equal(before.length, 1, 'queued-row: one row after generate_id');
1010
+ assert.equal(before[0]!.status, 'queued', 'queued-row: status is queued');
1011
+
1012
+ // Simulates gsd_plan_milestone calling insertMilestone (INSERT OR IGNORE)
1013
+ insertMilestone({ id: 'M001', title: 'Planned Title', status: 'active' });
1014
+
1015
+ const after = getAllMilestones();
1016
+ assert.equal(after.length, 1, 'queued-row: still one row after plan');
1017
+ // INSERT OR IGNORE keeps the original row — status stays 'queued'
1018
+ assert.equal(after[0]!.status, 'queued', 'queued-row: INSERT OR IGNORE preserves original status');
1019
+
1020
+ closeDatabase();
1021
+ } finally {
1022
+ closeDatabase();
1023
+ }
1024
+ });
965
1025
  });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * est-annotation-timeout.test.ts — Regression tests for #2243.
3
+ *
4
+ * Tasks with `est: 30m` or `est: 2h` annotations should get extended
5
+ * supervision timeouts. The parseEstimateMinutes helper should parse
6
+ * estimate strings, and startUnitSupervision should use them.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ const timersSrcPath = join(import.meta.dirname, "..", "auto-timers.ts");
15
+ const timersSrc = readFileSync(timersSrcPath, "utf-8");
16
+
17
+ // ─── Source analysis: parseEstimateMinutes exists and is exported ────────────
18
+
19
+ test("#2243: auto-timers.ts should export parseEstimateMinutes", () => {
20
+ assert.ok(
21
+ timersSrc.includes("export function parseEstimateMinutes"),
22
+ "parseEstimateMinutes should be exported from auto-timers.ts",
23
+ );
24
+ });
25
+
26
+ // ─── Inline unit test of parseEstimateMinutes logic ─────────────────────────
27
+ // Since importing the module pulls in heavy deps, test the parsing logic inline.
28
+
29
+ function parseEstimateMinutes(estimate: string): number | null {
30
+ if (!estimate || typeof estimate !== "string") return null;
31
+ const trimmed = estimate.trim();
32
+ if (!trimmed) return null;
33
+
34
+ let totalMinutes = 0;
35
+ let matched = false;
36
+
37
+ const hoursMatch = trimmed.match(/(\d+)\s*h/i);
38
+ if (hoursMatch) {
39
+ totalMinutes += Number(hoursMatch[1]) * 60;
40
+ matched = true;
41
+ }
42
+
43
+ const minutesMatch = trimmed.match(/(\d+)\s*m/i);
44
+ if (minutesMatch) {
45
+ totalMinutes += Number(minutesMatch[1]);
46
+ matched = true;
47
+ }
48
+
49
+ return matched ? totalMinutes : null;
50
+ }
51
+
52
+ test("#2243: parseEstimateMinutes parses '30m' correctly", () => {
53
+ assert.equal(parseEstimateMinutes("30m"), 30);
54
+ });
55
+
56
+ test("#2243: parseEstimateMinutes parses '2h' correctly", () => {
57
+ assert.equal(parseEstimateMinutes("2h"), 120);
58
+ });
59
+
60
+ test("#2243: parseEstimateMinutes parses '1h30m' correctly", () => {
61
+ assert.equal(parseEstimateMinutes("1h30m"), 90);
62
+ });
63
+
64
+ test("#2243: parseEstimateMinutes parses '15m' correctly", () => {
65
+ assert.equal(parseEstimateMinutes("15m"), 15);
66
+ });
67
+
68
+ test("#2243: parseEstimateMinutes returns null for empty string", () => {
69
+ assert.equal(parseEstimateMinutes(""), null);
70
+ });
71
+
72
+ test("#2243: parseEstimateMinutes returns null for invalid string", () => {
73
+ assert.equal(parseEstimateMinutes("not a time"), null);
74
+ });
75
+
76
+ // ─── Source analysis: startUnitSupervision uses task estimates ───────────────
77
+
78
+ test("#2243: startUnitSupervision should reference task estimates for timeout scaling", () => {
79
+ const usesEstimate =
80
+ timersSrc.includes("parseEstimateMinutes") &&
81
+ timersSrc.includes("estimateMinutes") &&
82
+ timersSrc.includes("taskEstimate");
83
+
84
+ assert.ok(
85
+ usesEstimate,
86
+ "startUnitSupervision should use task estimate annotations for timeout scaling",
87
+ );
88
+ });
89
+
90
+ test("#2243: SupervisionContext should accept an optional taskEstimate field", () => {
91
+ const ctxIdx = timersSrc.indexOf("SupervisionContext");
92
+ assert.ok(ctxIdx !== -1, "SupervisionContext interface exists");
93
+
94
+ const ctxEnd = timersSrc.indexOf("}", ctxIdx);
95
+ const ctxBlock = timersSrc.slice(ctxIdx, ctxEnd);
96
+
97
+ assert.ok(
98
+ ctxBlock.includes("taskEstimate"),
99
+ "SupervisionContext should include a taskEstimate field",
100
+ );
101
+ });
102
+
103
+ test("#2243: timeouts should be scaled by estimate (timeoutScale in source)", () => {
104
+ assert.ok(
105
+ timersSrc.includes("timeoutScale"),
106
+ "auto-timers.ts should use a timeoutScale factor derived from est: annotations",
107
+ );
108
+ });
109
+
110
+ test("#2243: idle timeout should NOT be scaled (idle is idle regardless of estimate)", () => {
111
+ // Find the idleTimeoutMs line
112
+ const idleIdx = timersSrc.indexOf("const idleTimeoutMs");
113
+ assert.ok(idleIdx !== -1, "idleTimeoutMs variable exists");
114
+
115
+ const idleLine = timersSrc.slice(idleIdx, timersSrc.indexOf("\n", idleIdx));
116
+ assert.ok(
117
+ !idleLine.includes("timeoutScale"),
118
+ "idleTimeoutMs should NOT be scaled — idle is idle",
119
+ );
120
+ });
@@ -7,10 +7,13 @@ import { isInfrastructureError, INFRA_ERROR_CODES } from "../auto/infra-errors.j
7
7
  // ── INFRA_ERROR_CODES constant ───────────────────────────────────────────────
8
8
 
9
9
  test("INFRA_ERROR_CODES contains the expected codes", () => {
10
- for (const code of ["ENOSPC", "ENOMEM", "EROFS", "EDQUOT", "EMFILE", "ENFILE"]) {
10
+ for (const code of [
11
+ "ENOSPC", "ENOMEM", "EROFS", "EDQUOT", "EMFILE", "ENFILE",
12
+ "ECONNREFUSED", "ENOTFOUND", "ENETUNREACH",
13
+ ]) {
11
14
  assert.ok(INFRA_ERROR_CODES.has(code), `missing ${code}`);
12
15
  }
13
- assert.equal(INFRA_ERROR_CODES.size, 6, "unexpected extra codes");
16
+ assert.equal(INFRA_ERROR_CODES.size, 9, "unexpected extra codes");
14
17
  });
15
18
 
16
19
  // ── isInfrastructureError: code property detection ───────────────────────────
@@ -45,6 +48,21 @@ test("detects ENFILE via code property", () => {
45
48
  assert.equal(isInfrastructureError(err), "ENFILE");
46
49
  });
47
50
 
51
+ test("detects ECONNREFUSED via code property", () => {
52
+ const err = Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:3000"), { code: "ECONNREFUSED" });
53
+ assert.equal(isInfrastructureError(err), "ECONNREFUSED");
54
+ });
55
+
56
+ test("detects ENOTFOUND via code property", () => {
57
+ const err = Object.assign(new Error("getaddrinfo ENOTFOUND api.example.com"), { code: "ENOTFOUND" });
58
+ assert.equal(isInfrastructureError(err), "ENOTFOUND");
59
+ });
60
+
61
+ test("detects ENETUNREACH via code property", () => {
62
+ const err = Object.assign(new Error("connect ENETUNREACH 2607:f8b0:4004::"), { code: "ENETUNREACH" });
63
+ assert.equal(isInfrastructureError(err), "ENETUNREACH");
64
+ });
65
+
48
66
  // ── isInfrastructureError: message fallback ──────────────────────────────────
49
67
 
50
68
  test("falls back to message scanning when no code property", () => {
@@ -0,0 +1,121 @@
1
+ /**
2
+ * inherited-repo-home-dir.test.ts — Regression test for #2393.
3
+ *
4
+ * When the user's home directory IS a git repo (common with dotfile
5
+ * managers like yadm), isInheritedRepo() must not treat ~/.gsd (the
6
+ * global GSD state directory) as a project .gsd belonging to the home
7
+ * repo. Without the fix, isInheritedRepo() returns false for project
8
+ * subdirectories because it sees ~/.gsd and concludes the parent repo
9
+ * has already been initialised with GSD — causing the wrong project
10
+ * state to be loaded.
11
+ */
12
+
13
+ import { describe, test, beforeEach, afterEach } from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import {
16
+ mkdtempSync,
17
+ mkdirSync,
18
+ rmSync,
19
+ writeFileSync,
20
+ realpathSync,
21
+ symlinkSync,
22
+ } from "node:fs";
23
+ import { join } from "node:path";
24
+ import { tmpdir } from "node:os";
25
+ import { execFileSync } from "node:child_process";
26
+
27
+ import { isInheritedRepo } from "../repo-identity.ts";
28
+
29
+ function run(cmd: string, args: string[], cwd: string): string {
30
+ return execFileSync(cmd, args, {
31
+ cwd,
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ encoding: "utf-8",
34
+ }).trim();
35
+ }
36
+
37
+ describe("isInheritedRepo when git root is HOME (#2393)", () => {
38
+ let fakeHome: string;
39
+ let stateDir: string;
40
+ let origGsdHome: string | undefined;
41
+ let origGsdStateDir: string | undefined;
42
+
43
+ beforeEach(() => {
44
+ // Create a fake HOME that is itself a git repo (dotfile manager scenario).
45
+ fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-home-repo-")));
46
+ run("git", ["init", "-b", "main"], fakeHome);
47
+ run("git", ["config", "user.name", "Test"], fakeHome);
48
+ run("git", ["config", "user.email", "test@example.com"], fakeHome);
49
+ writeFileSync(join(fakeHome, ".bashrc"), "# dotfiles\n", "utf-8");
50
+ run("git", ["add", ".bashrc"], fakeHome);
51
+ run("git", ["commit", "-m", "init dotfiles"], fakeHome);
52
+
53
+ // Create a plain ~/.gsd directory at fakeHome — this simulates the
54
+ // global GSD home directory, NOT a project .gsd.
55
+ mkdirSync(join(fakeHome, ".gsd", "projects"), { recursive: true });
56
+
57
+ // Save and override env. Point GSD_HOME at fakeHome/.gsd so the
58
+ // function recognizes it as the global state directory.
59
+ origGsdHome = process.env.GSD_HOME;
60
+ origGsdStateDir = process.env.GSD_STATE_DIR;
61
+ process.env.GSD_HOME = join(fakeHome, ".gsd");
62
+ stateDir = mkdtempSync(join(tmpdir(), "gsd-state-"));
63
+ process.env.GSD_STATE_DIR = stateDir;
64
+ });
65
+
66
+ afterEach(() => {
67
+ if (origGsdHome !== undefined) process.env.GSD_HOME = origGsdHome;
68
+ else delete process.env.GSD_HOME;
69
+ if (origGsdStateDir !== undefined) process.env.GSD_STATE_DIR = origGsdStateDir;
70
+ else delete process.env.GSD_STATE_DIR;
71
+
72
+ rmSync(fakeHome, { recursive: true, force: true });
73
+ rmSync(stateDir, { recursive: true, force: true });
74
+ });
75
+
76
+ test("subdirectory of home-as-git-root is detected as inherited even when ~/.gsd exists", () => {
77
+ // Create a project directory inside fake HOME
78
+ const projectDir = join(fakeHome, "projects", "my-app");
79
+ mkdirSync(projectDir, { recursive: true });
80
+
81
+ // The bug: isInheritedRepo sees ~/.gsd and returns false, thinking
82
+ // the home repo is a legitimate GSD project. It should return true
83
+ // because ~/.gsd is the global state dir, not a project .gsd.
84
+ assert.strictEqual(
85
+ isInheritedRepo(projectDir),
86
+ true,
87
+ "project inside home-as-git-root must be detected as inherited repo, " +
88
+ "even when ~/.gsd (global state dir) exists",
89
+ );
90
+ });
91
+
92
+ test("subdirectory with a real project .gsd symlink at git root is NOT inherited", () => {
93
+ // Simulate a legitimately initialised GSD project at the home repo root:
94
+ // .gsd is a symlink to an external state directory.
95
+ const externalState = join(stateDir, "projects", "home-project");
96
+ mkdirSync(externalState, { recursive: true });
97
+ const gsdDir = join(fakeHome, ".gsd");
98
+
99
+ // Remove the plain directory and replace with a symlink (real project .gsd)
100
+ rmSync(gsdDir, { recursive: true, force: true });
101
+ symlinkSync(externalState, gsdDir);
102
+
103
+ const projectDir = join(fakeHome, "projects", "my-app");
104
+ mkdirSync(projectDir, { recursive: true });
105
+
106
+ // When .gsd at root IS a project symlink, subdirectories are legitimate children
107
+ assert.strictEqual(
108
+ isInheritedRepo(projectDir),
109
+ false,
110
+ "subdirectory of a legitimately-initialised GSD project should NOT be inherited",
111
+ );
112
+ });
113
+
114
+ test("home-as-git-root itself is never inherited", () => {
115
+ assert.strictEqual(
116
+ isInheritedRepo(fakeHome),
117
+ false,
118
+ "the git root itself is never inherited",
119
+ );
120
+ });
121
+ });
@@ -0,0 +1,103 @@
1
+ import test, { describe } from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import {
5
+ formatMcpStatusReport,
6
+ formatMcpServerDetail,
7
+ type McpServerStatus,
8
+ } from "../commands-mcp-status.ts";
9
+
10
+ // ─── formatMcpStatusReport ──────────────────────────────────────────────────
11
+
12
+ describe("formatMcpStatusReport", () => {
13
+ test("returns no-servers message when list is empty", () => {
14
+ const result = formatMcpStatusReport([]);
15
+ assert.match(result, /no mcp servers configured/i);
16
+ });
17
+
18
+ test("lists all servers with connection status", () => {
19
+ const servers: McpServerStatus[] = [
20
+ { name: "railway", transport: "stdio", connected: true, toolCount: 5, error: undefined },
21
+ { name: "linear", transport: "http", connected: false, toolCount: 0, error: undefined },
22
+ ];
23
+ const result = formatMcpStatusReport(servers);
24
+ assert.match(result, /railway/);
25
+ assert.match(result, /linear/);
26
+ assert.match(result, /connected/i);
27
+ assert.match(result, /disconnected/i);
28
+ assert.match(result, /5 tools/);
29
+ });
30
+
31
+ test("shows error state for servers with errors", () => {
32
+ const servers: McpServerStatus[] = [
33
+ { name: "broken", transport: "stdio", connected: false, toolCount: 0, error: "Connection refused" },
34
+ ];
35
+ const result = formatMcpStatusReport(servers);
36
+ assert.match(result, /error/i);
37
+ assert.match(result, /Connection refused/);
38
+ });
39
+
40
+ test("includes server count in header", () => {
41
+ const servers: McpServerStatus[] = [
42
+ { name: "a", transport: "stdio", connected: true, toolCount: 3, error: undefined },
43
+ { name: "b", transport: "http", connected: true, toolCount: 2, error: undefined },
44
+ ];
45
+ const result = formatMcpStatusReport(servers);
46
+ assert.match(result, /2/);
47
+ });
48
+ });
49
+
50
+ // ─── formatMcpServerDetail ──────────────────────────────────────────────────
51
+
52
+ describe("formatMcpServerDetail", () => {
53
+ test("shows server name and transport", () => {
54
+ const result = formatMcpServerDetail({
55
+ name: "railway",
56
+ transport: "stdio",
57
+ connected: true,
58
+ toolCount: 3,
59
+ tools: ["railway_list_projects", "railway_deploy", "railway_logs"],
60
+ error: undefined,
61
+ });
62
+ assert.match(result, /railway/);
63
+ assert.match(result, /stdio/);
64
+ });
65
+
66
+ test("lists individual tools when available", () => {
67
+ const result = formatMcpServerDetail({
68
+ name: "railway",
69
+ transport: "stdio",
70
+ connected: true,
71
+ toolCount: 2,
72
+ tools: ["railway_list_projects", "railway_deploy"],
73
+ error: undefined,
74
+ });
75
+ assert.match(result, /railway_list_projects/);
76
+ assert.match(result, /railway_deploy/);
77
+ });
78
+
79
+ test("shows error message for failed servers", () => {
80
+ const result = formatMcpServerDetail({
81
+ name: "broken",
82
+ transport: "stdio",
83
+ connected: false,
84
+ toolCount: 0,
85
+ tools: [],
86
+ error: "spawn ENOENT",
87
+ });
88
+ assert.match(result, /error/i);
89
+ assert.match(result, /spawn ENOENT/);
90
+ });
91
+
92
+ test("shows disconnected status with no tools", () => {
93
+ const result = formatMcpServerDetail({
94
+ name: "offline",
95
+ transport: "http",
96
+ connected: false,
97
+ toolCount: 0,
98
+ tools: [],
99
+ error: undefined,
100
+ });
101
+ assert.match(result, /disconnected/i);
102
+ });
103
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * merge-conflict-stops-loop.test.ts — #2330
3
+ *
4
+ * When a squash merge has real code conflicts (not just .gsd/ files),
5
+ * the merge retries forever because MergeConflictError is caught
6
+ * silently in mergeAndExit. This test verifies that:
7
+ * 1. worktree-resolver re-throws MergeConflictError for code conflicts
8
+ * 2. auto/phases.ts wraps mergeAndExit calls to stop the loop on conflict
9
+ */
10
+
11
+ import { readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { createTestContext } from "./test-helpers.ts";
14
+
15
+ const { assertTrue, report } = createTestContext();
16
+
17
+ const resolverPath = join(import.meta.dirname, "..", "worktree-resolver.ts");
18
+ const resolverSrc = readFileSync(resolverPath, "utf-8");
19
+
20
+ const phasesPath = join(import.meta.dirname, "..", "auto", "phases.ts");
21
+ const phasesSrc = readFileSync(phasesPath, "utf-8");
22
+
23
+ console.log("\n=== #2330: Merge conflict stops auto loop ===");
24
+
25
+ // ── Test 1: worktree-resolver re-throws MergeConflictError ──────────────
26
+
27
+ const methodStart = resolverSrc.indexOf("Worktree-mode merge:");
28
+ assertTrue(methodStart > 0, "worktree-resolver has _mergeWorktreeMode method");
29
+
30
+ const methodBody = resolverSrc.slice(methodStart, methodStart + 5000);
31
+ const rethrowsConflict =
32
+ methodBody.includes("MergeConflictError") &&
33
+ methodBody.includes("throw err");
34
+
35
+ assertTrue(
36
+ rethrowsConflict,
37
+ "worktree-resolver._mergeWorktreeMode re-throws MergeConflictError (#2330)",
38
+ );
39
+
40
+ // ── Test 2: auto/phases.ts imports and uses MergeConflictError ──────────
41
+
42
+ assertTrue(
43
+ phasesSrc.includes("MergeConflictError") && phasesSrc.includes("mergeAndExit"),
44
+ "auto/phases.ts handles MergeConflictError from mergeAndExit (#2330)",
45
+ );
46
+
47
+ // ── Test 3: The handler stops the loop (doesn't just warn) ──────────────
48
+
49
+ // Find the instanceof MergeConflictError check (not the import line)
50
+ const instanceofIdx = phasesSrc.indexOf("instanceof MergeConflictError");
51
+ assertTrue(instanceofIdx > 0, "auto/phases.ts has instanceof MergeConflictError check");
52
+
53
+ if (instanceofIdx > 0) {
54
+ const afterHandler = phasesSrc.slice(instanceofIdx, instanceofIdx + 500);
55
+ const stopsLoop =
56
+ afterHandler.includes("stopAuto") ||
57
+ afterHandler.includes('action: "break"') ||
58
+ afterHandler.includes("reason: \"merge-conflict\"");
59
+
60
+ assertTrue(
61
+ stopsLoop,
62
+ "auto/phases.ts stops the loop when merge conflict is detected (#2330)",
63
+ );
64
+ }
65
+
66
+ report();
@@ -147,12 +147,12 @@ test("plan-slice prompt no longer frames direct PLAN writes as the source of tru
147
147
  assert.match(prompt, /Do \*\*not\*\* rely on direct `PLAN\.md` writes as the source of truth/i);
148
148
  });
149
149
 
150
- test("plan-slice prompt explicitly names gsd_plan_slice and gsd_plan_task as DB-backed planning tools", () => {
150
+ test("plan-slice prompt explicitly names gsd_plan_slice as DB-backed planning tool", () => {
151
151
  const prompt = readPrompt("plan-slice");
152
152
  assert.match(prompt, /gsd_plan_slice/);
153
153
  assert.match(prompt, /gsd_plan_task/);
154
- // The prompt should describe these as the canonical write path
155
- assert.match(prompt, /DB-backed tools are the canonical write path/i);
154
+ // The prompt should describe the DB-backed tool as the canonical write path
155
+ assert.match(prompt, /DB-backed tool is the canonical write path/i);
156
156
  });
157
157
 
158
158
  test("plan-slice prompt does not instruct direct file writes as a primary step", () => {
@@ -161,14 +161,18 @@ test("plan-slice prompt does not instruct direct file writes as a primary step",
161
161
  assert.doesNotMatch(prompt, /^\d+\.\s+Write `?\{\{outputPath\}\}`?\s*$/m);
162
162
  });
163
163
 
164
- test("plan-slice prompt instructs calling gsd_plan_task for each task", () => {
164
+ test("plan-slice prompt clarifies gsd_plan_slice handles task persistence", () => {
165
165
  const prompt = readPrompt("plan-slice");
166
- assert.match(prompt, /call `gsd_plan_task` for each task/i);
166
+ // gsd_plan_slice persists tasks in its transaction — no separate gsd_plan_task calls needed
167
+ assert.match(prompt, /gsd_plan_task/);
168
+ assert.match(prompt, /gsd_plan_slice` handles task persistence/i);
167
169
  });
168
170
 
169
- test("replan-slice prompt requires DB-backed planning state when available", () => {
171
+ test("replan-slice prompt uses gsd_replan_slice as canonical DB-backed tool", () => {
170
172
  const prompt = readPrompt("replan-slice");
171
- assert.match(prompt, /DB-backed planning tool exists for this phase, use it as the source of truth/i);
173
+ assert.match(prompt, /gsd_replan_slice/);
174
+ // Degraded fallback (direct file writes) was removed — DB tools are always available
175
+ assert.doesNotMatch(prompt, /Degraded fallback/i);
172
176
  });
173
177
 
174
178
  test("reassess-roadmap prompt references gsd_reassess_roadmap tool", () => {