gsd-pi 2.44.0-dev.848dd4c → 2.44.0-dev.8dd0d8e

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 (249) 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/bootstrap/system-context.js +46 -12
  12. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  13. package/dist/resources/extensions/gsd/commands/handlers/core.js +2 -0
  14. package/dist/resources/extensions/gsd/commands/handlers/ops.js +10 -0
  15. package/dist/resources/extensions/gsd/commands-mcp-status.js +187 -0
  16. package/dist/resources/extensions/gsd/db-writer.js +34 -16
  17. package/dist/resources/extensions/gsd/doctor.js +8 -0
  18. package/dist/resources/extensions/gsd/git-service.js +8 -3
  19. package/dist/resources/extensions/gsd/gsd-db.js +12 -1
  20. package/dist/resources/extensions/gsd/markdown-renderer.js +1 -1
  21. package/dist/resources/extensions/gsd/preferences.js +9 -1
  22. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  23. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  24. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  25. package/dist/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  26. package/dist/resources/extensions/gsd/prompts/rethink.md +78 -0
  27. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  28. package/dist/resources/extensions/gsd/provider-error-pause.js +7 -0
  29. package/dist/resources/extensions/gsd/repo-identity.js +45 -7
  30. package/dist/resources/extensions/gsd/rethink.js +115 -0
  31. package/dist/resources/extensions/gsd/state.js +41 -3
  32. package/dist/resources/extensions/gsd/tools/plan-slice.js +1 -0
  33. package/dist/resources/extensions/gsd/tools/plan-task.js +1 -0
  34. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -0
  35. package/dist/resources/extensions/gsd/tools/validate-milestone.js +88 -0
  36. package/dist/resources/extensions/gsd/worktree-manager.js +32 -2
  37. package/dist/resources/extensions/gsd/worktree-resolver.js +6 -0
  38. package/dist/resources/extensions/mcp-client/index.js +14 -0
  39. package/dist/web/standalone/.next/BUILD_ID +1 -1
  40. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  41. package/dist/web/standalone/.next/build-manifest.json +3 -3
  42. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  43. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  44. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  46. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found/page.js +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  63. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/index.html +1 -1
  104. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  105. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  106. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  107. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  108. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  109. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  110. package/dist/web/standalone/.next/server/app/page.js +1 -1
  111. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  113. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  116. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  117. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  118. package/dist/web/standalone/.next/static/chunks/4024.11ca5c01938e5948.js +9 -0
  119. package/dist/web/standalone/.next/static/chunks/{3721.bf31263de6d5fa46.js → 485.243af25f0cdf50d6.js} +2 -2
  120. package/dist/web/standalone/.next/static/chunks/app/{page-b9367c5ae13b99c6.js → page-6654a8cca61a3d1c.js} +1 -1
  121. package/dist/web/standalone/.next/static/chunks/webpack-0a4cd455ec4197d2.js +1 -0
  122. package/dist/web/standalone/.next/static/css/dd4ae3f58ac9b600.css +1 -0
  123. package/package.json +1 -1
  124. package/packages/native/dist/stream-process/index.js +2 -2
  125. package/packages/native/src/__tests__/stream-process.test.mjs +34 -0
  126. package/packages/native/src/stream-process/index.ts +2 -2
  127. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +3 -1
  128. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/auth-storage.js +15 -1
  130. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts +15 -0
  132. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts.map +1 -0
  133. package/packages/pi-coding-agent/dist/core/local-model-check.js +41 -0
  134. package/packages/pi-coding-agent/dist/core/local-model-check.js.map +1 -0
  135. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +11 -0
  136. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/model-registry.js +20 -1
  138. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  140. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/settings-manager.js +6 -0
  142. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  144. package/packages/pi-coding-agent/dist/main.js +17 -0
  145. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts +2 -0
  147. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts.map +1 -0
  148. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js +32 -0
  149. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js.map +1 -0
  150. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  151. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  152. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +8 -1
  153. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  154. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  155. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  156. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
  157. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  158. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts +15 -0
  159. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts.map +1 -0
  160. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js +40 -0
  161. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js.map +1 -0
  162. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  163. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +4 -1
  164. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  165. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +5 -2
  166. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  167. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +13 -2
  168. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  169. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  170. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +17 -8
  171. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  172. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  173. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -3
  174. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  175. package/packages/pi-coding-agent/src/core/auth-storage.ts +15 -1
  176. package/packages/pi-coding-agent/src/core/local-model-check.ts +45 -0
  177. package/packages/pi-coding-agent/src/core/model-registry.ts +21 -1
  178. package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
  179. package/packages/pi-coding-agent/src/main.ts +19 -0
  180. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +38 -0
  181. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +10 -0
  182. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
  183. package/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +48 -0
  184. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +3 -1
  185. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +18 -3
  186. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +16 -7
  187. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +8 -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/bootstrap/system-context.ts +48 -11
  199. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  200. package/src/resources/extensions/gsd/commands/handlers/core.ts +2 -0
  201. package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
  202. package/src/resources/extensions/gsd/commands-mcp-status.ts +247 -0
  203. package/src/resources/extensions/gsd/db-writer.ts +39 -17
  204. package/src/resources/extensions/gsd/doctor.ts +7 -1
  205. package/src/resources/extensions/gsd/git-service.ts +6 -2
  206. package/src/resources/extensions/gsd/gsd-db.ts +16 -1
  207. package/src/resources/extensions/gsd/markdown-renderer.ts +1 -1
  208. package/src/resources/extensions/gsd/preferences.ts +11 -1
  209. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  210. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  211. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  212. package/src/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  213. package/src/resources/extensions/gsd/prompts/rethink.md +78 -0
  214. package/src/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  215. package/src/resources/extensions/gsd/provider-error-pause.ts +9 -0
  216. package/src/resources/extensions/gsd/repo-identity.ts +46 -7
  217. package/src/resources/extensions/gsd/rethink.ts +154 -0
  218. package/src/resources/extensions/gsd/state.ts +41 -1
  219. package/src/resources/extensions/gsd/tests/auto-pr-bugs.test.ts +88 -0
  220. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +114 -0
  221. package/src/resources/extensions/gsd/tests/db-writer.test.ts +79 -0
  222. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +121 -0
  223. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +60 -0
  224. package/src/resources/extensions/gsd/tests/est-annotation-timeout.test.ts +120 -0
  225. package/src/resources/extensions/gsd/tests/infra-error.test.ts +20 -2
  226. package/src/resources/extensions/gsd/tests/inherited-repo-home-dir.test.ts +121 -0
  227. package/src/resources/extensions/gsd/tests/knowledge.test.ts +89 -0
  228. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +103 -0
  229. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +66 -0
  230. package/src/resources/extensions/gsd/tests/preferences.test.ts +27 -0
  231. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -7
  232. package/src/resources/extensions/gsd/tests/recovery-attempts-reset.test.ts +176 -0
  233. package/src/resources/extensions/gsd/tests/stop-auto-merge-back.test.ts +67 -0
  234. package/src/resources/extensions/gsd/tests/survivor-branch-complete.test.ts +108 -0
  235. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +49 -0
  236. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +2 -1
  237. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +65 -0
  238. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
  239. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -0
  240. package/src/resources/extensions/gsd/tools/replan-slice.ts +3 -0
  241. package/src/resources/extensions/gsd/tools/validate-milestone.ts +127 -0
  242. package/src/resources/extensions/gsd/worktree-manager.ts +43 -2
  243. package/src/resources/extensions/gsd/worktree-resolver.ts +7 -0
  244. package/src/resources/extensions/mcp-client/index.ts +20 -0
  245. package/dist/web/standalone/.next/static/chunks/4024.0de81b543b28b9fe.js +0 -9
  246. package/dist/web/standalone/.next/static/chunks/webpack-9014b5adb127a98a.js +0 -1
  247. package/dist/web/standalone/.next/static/css/8a727f372cf53002.css +0 -1
  248. /package/dist/web/standalone/.next/static/{-zps1Q9mQmioAKLcQiCr8 → enTIm32JJtw--VTtLPSC3}/_buildManifest.js +0 -0
  249. /package/dist/web/standalone/.next/static/{-zps1Q9mQmioAKLcQiCr8 → enTIm32JJtw--VTtLPSC3}/_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
+ });
@@ -6,6 +6,7 @@
6
6
  * - resolveGsdRootFile resolves KNOWLEDGE paths correctly
7
7
  * - inlineGsdRootFile works with the KNOWLEDGE key
8
8
  * - before_agent_start hook includes/omits knowledge block appropriately
9
+ * - loadKnowledgeBlock merges global and project knowledge correctly
9
10
  */
10
11
 
11
12
  import test from 'node:test';
@@ -16,6 +17,7 @@ import { tmpdir } from 'node:os';
16
17
  import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
17
18
  import { inlineGsdRootFile } from '../auto-prompts.ts';
18
19
  import { appendKnowledge } from '../files.ts';
20
+ import { loadKnowledgeBlock } from '../bootstrap/system-context.ts';
19
21
 
20
22
  // ─── KNOWLEDGE is registered in GSD_ROOT_FILES ─────────────────────────────
21
23
 
@@ -159,3 +161,90 @@ test('knowledge: appendKnowledge handles lesson type', async () => {
159
161
 
160
162
  rmSync(tmp, { recursive: true, force: true });
161
163
  });
164
+
165
+ // ─── loadKnowledgeBlock — global + project merge ────────────────────────────
166
+
167
+ test('loadKnowledgeBlock: returns empty block when neither file exists', () => {
168
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
169
+ const gsdHome = join(tmp, 'home');
170
+ const cwd = join(tmp, 'project');
171
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
172
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
173
+
174
+ const result = loadKnowledgeBlock(gsdHome, cwd);
175
+ assert.strictEqual(result.block, '');
176
+ assert.strictEqual(result.globalSizeKb, 0);
177
+
178
+ rmSync(tmp, { recursive: true, force: true });
179
+ });
180
+
181
+ test('loadKnowledgeBlock: uses project knowledge alone when no global file', () => {
182
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
183
+ const gsdHome = join(tmp, 'home');
184
+ const cwd = join(tmp, 'project');
185
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
186
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
187
+ writeFileSync(join(cwd, '.gsd', 'KNOWLEDGE.md'), 'K001: Use real DB');
188
+
189
+ const result = loadKnowledgeBlock(gsdHome, cwd);
190
+ assert.ok(result.block.includes('[KNOWLEDGE — Rules, patterns, and lessons learned]'));
191
+ assert.ok(result.block.includes('## Project Knowledge'));
192
+ assert.ok(result.block.includes('K001: Use real DB'));
193
+ assert.ok(!result.block.includes('## Global Knowledge'));
194
+ assert.strictEqual(result.globalSizeKb, 0);
195
+
196
+ rmSync(tmp, { recursive: true, force: true });
197
+ });
198
+
199
+ test('loadKnowledgeBlock: uses global knowledge alone when no project file', () => {
200
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
201
+ const gsdHome = join(tmp, 'home');
202
+ const cwd = join(tmp, 'project');
203
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
204
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
205
+ writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'G001: Respond in English');
206
+
207
+ const result = loadKnowledgeBlock(gsdHome, cwd);
208
+ assert.ok(result.block.includes('[KNOWLEDGE — Rules, patterns, and lessons learned]'));
209
+ assert.ok(result.block.includes('## Global Knowledge'));
210
+ assert.ok(result.block.includes('G001: Respond in English'));
211
+ assert.ok(!result.block.includes('## Project Knowledge'));
212
+ assert.ok(result.globalSizeKb > 0);
213
+
214
+ rmSync(tmp, { recursive: true, force: true });
215
+ });
216
+
217
+ test('loadKnowledgeBlock: merges global before project when both exist', () => {
218
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
219
+ const gsdHome = join(tmp, 'home');
220
+ const cwd = join(tmp, 'project');
221
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
222
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
223
+ writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'G001: Global rule');
224
+ writeFileSync(join(cwd, '.gsd', 'KNOWLEDGE.md'), 'K001: Project rule');
225
+
226
+ const result = loadKnowledgeBlock(gsdHome, cwd);
227
+ assert.ok(result.block.includes('## Global Knowledge'));
228
+ assert.ok(result.block.includes('## Project Knowledge'));
229
+ assert.ok(result.block.includes('G001: Global rule'));
230
+ assert.ok(result.block.includes('K001: Project rule'));
231
+ // Global section appears before project section
232
+ assert.ok(result.block.indexOf('## Global Knowledge') < result.block.indexOf('## Project Knowledge'));
233
+
234
+ rmSync(tmp, { recursive: true, force: true });
235
+ });
236
+
237
+ test('loadKnowledgeBlock: reports globalSizeKb above 4KB threshold', () => {
238
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
239
+ const gsdHome = join(tmp, 'home');
240
+ const cwd = join(tmp, 'project');
241
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
242
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
243
+ // Write > 4KB of content
244
+ writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'x'.repeat(5000));
245
+
246
+ const result = loadKnowledgeBlock(gsdHome, cwd);
247
+ assert.ok(result.globalSizeKb > 4, `expected > 4KB, got ${result.globalSizeKb}`);
248
+
249
+ rmSync(tmp, { recursive: true, force: true });
250
+ });
@@ -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
+ });