@opengsd/gsd-pi 1.0.2-dev.867e002 → 1.0.2-dev.88c4dc5

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 (337) hide show
  1. package/dist/onboarding.js +22 -3
  2. package/dist/resource-loader.js +3 -1
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/extensions/context7/index.js +12 -2
  5. package/dist/resources/extensions/get-secrets-from-user.js +16 -16
  6. package/dist/resources/extensions/google-cli/index.js +30 -0
  7. package/dist/resources/extensions/google-cli/models.js +55 -0
  8. package/dist/resources/extensions/google-cli/package.json +11 -0
  9. package/dist/resources/extensions/google-cli/readiness.js +12 -0
  10. package/dist/resources/extensions/google-cli/stream-adapter.js +191 -0
  11. package/dist/resources/extensions/gsd/auto/loop.js +62 -1
  12. package/dist/resources/extensions/gsd/auto/orchestrator.js +4 -2
  13. package/dist/resources/extensions/gsd/auto/phases.js +37 -0
  14. package/dist/resources/extensions/gsd/auto/run-unit.js +8 -0
  15. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  16. package/dist/resources/extensions/gsd/auto-dispatch.js +17 -7
  17. package/dist/resources/extensions/gsd/auto-post-unit.js +21 -11
  18. package/dist/resources/extensions/gsd/auto-prompts.js +5 -236
  19. package/dist/resources/extensions/gsd/auto-recovery.js +10 -5
  20. package/dist/resources/extensions/gsd/auto-start.js +232 -49
  21. package/dist/resources/extensions/gsd/auto.js +6 -1
  22. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +4 -3
  23. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +7 -2
  24. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +39 -5
  25. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -7
  26. package/dist/resources/extensions/gsd/bootstrap/system-context.js +3 -27
  27. package/dist/resources/extensions/gsd/closeout-recovery.js +7 -1
  28. package/dist/resources/extensions/gsd/commands/handlers/auto.js +9 -1
  29. package/dist/resources/extensions/gsd/commands-usage.js +105 -1
  30. package/dist/resources/extensions/gsd/config-overlay.js +20 -14
  31. package/dist/resources/extensions/gsd/context-overlay.js +22 -16
  32. package/dist/resources/extensions/gsd/dashboard-overlay.js +10 -23
  33. package/dist/resources/extensions/gsd/doctor-engine-checks.js +87 -0
  34. package/dist/resources/extensions/gsd/doctor-providers.js +54 -24
  35. package/dist/resources/extensions/gsd/doctor.js +6 -1
  36. package/dist/resources/extensions/gsd/git-conflict-state.js +26 -1
  37. package/dist/resources/extensions/gsd/guided-flow.js +5 -6
  38. package/dist/resources/extensions/gsd/key-manager.js +45 -13
  39. package/dist/resources/extensions/gsd/milestone-reopen-events.js +28 -0
  40. package/dist/resources/extensions/gsd/notification-overlay.js +8 -9
  41. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +15 -13
  42. package/dist/resources/extensions/gsd/preferences-skills.js +11 -4
  43. package/dist/resources/extensions/gsd/preferences.js +14 -2
  44. package/dist/resources/extensions/gsd/prompt-loader.js +2 -0
  45. package/dist/resources/extensions/gsd/prompts/discuss.md +4 -2
  46. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  47. package/dist/resources/extensions/gsd/prompts/system.md +1 -3
  48. package/dist/resources/extensions/gsd/queue-reorder-ui.js +28 -18
  49. package/dist/resources/extensions/gsd/repository-registry.js +3 -1
  50. package/dist/resources/extensions/gsd/safety/evidence-collector.js +11 -4
  51. package/dist/resources/extensions/gsd/skill-activation.js +233 -0
  52. package/dist/resources/extensions/gsd/skill-catalog.data.js +820 -0
  53. package/dist/resources/extensions/gsd/skill-catalog.install.js +179 -0
  54. package/dist/resources/extensions/gsd/skill-catalog.js +5 -1028
  55. package/dist/resources/extensions/gsd/skill-discovery.js +121 -79
  56. package/dist/resources/extensions/gsd/skill-scope.js +52 -0
  57. package/dist/resources/extensions/gsd/skill-telemetry.js +6 -39
  58. package/dist/resources/extensions/gsd/skills.js +60 -0
  59. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +351 -0
  60. package/dist/resources/extensions/gsd/state-reconciliation/index.js +41 -0
  61. package/dist/resources/extensions/gsd/state-reconciliation/registry.js +4 -0
  62. package/dist/resources/extensions/gsd/tools/complete-task.js +9 -0
  63. package/dist/resources/extensions/gsd/tools/exec-tool.js +42 -8
  64. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +63 -2
  65. package/dist/resources/extensions/gsd/tui/render-kit.js +51 -0
  66. package/dist/resources/extensions/gsd/unit-context-manifest.js +35 -26
  67. package/dist/resources/extensions/gsd/user-input-boundary.js +1 -1
  68. package/dist/resources/extensions/gsd/vision-ask.js +22 -0
  69. package/dist/resources/extensions/gsd/visualizer-overlay.js +8 -36
  70. package/dist/resources/extensions/gsd/worktree-lifecycle.js +24 -3
  71. package/dist/resources/extensions/search-the-web/native-search.js +57 -8
  72. package/dist/resources/extensions/shared/confirm-ui.js +9 -6
  73. package/dist/resources/extensions/shared/dialog-frame.js +42 -0
  74. package/dist/resources/extensions/shared/interview-ui.js +42 -30
  75. package/dist/resources/extensions/shared/next-action-ui.js +6 -6
  76. package/dist/resources/skills/create-skill/references/gsd-skill-ecosystem.md +1 -1
  77. package/dist/web/standalone/.next/BUILD_ID +1 -1
  78. package/dist/web/standalone/.next/app-path-routes-manifest.json +4 -4
  79. package/dist/web/standalone/.next/build-manifest.json +2 -2
  80. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  81. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  82. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  90. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.html +1 -1
  98. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  102. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  103. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  104. package/dist/web/standalone/.next/server/app-paths-manifest.json +4 -4
  105. package/dist/web/standalone/.next/server/chunks/1834.js +2 -2
  106. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  108. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  109. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  110. package/package.json +1 -1
  111. package/packages/cloud-mcp-gateway/package.json +2 -2
  112. package/packages/contracts/dist/rpc.test.js +5 -0
  113. package/packages/contracts/dist/rpc.test.js.map +1 -1
  114. package/packages/contracts/dist/workflow.d.ts +7 -0
  115. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  116. package/packages/contracts/dist/workflow.js +8 -0
  117. package/packages/contracts/dist/workflow.js.map +1 -1
  118. package/packages/contracts/dist/workflow.test.js +1 -0
  119. package/packages/contracts/dist/workflow.test.js.map +1 -1
  120. package/packages/contracts/package.json +1 -1
  121. package/packages/daemon/package.json +4 -4
  122. package/packages/gsd-agent-core/dist/session/agent-session-extensions.d.ts +1 -0
  123. package/packages/gsd-agent-core/dist/session/agent-session-extensions.d.ts.map +1 -1
  124. package/packages/gsd-agent-core/dist/session/agent-session-extensions.js +22 -8
  125. package/packages/gsd-agent-core/dist/session/agent-session-extensions.js.map +1 -1
  126. package/packages/gsd-agent-core/package.json +5 -5
  127. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.d.ts +12 -0
  128. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.d.ts.map +1 -0
  129. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.js +45 -0
  130. package/packages/gsd-agent-modes/dist/modes/interactive/components/dialog-container.js.map +1 -0
  131. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.d.ts +3 -2
  132. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  133. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.js +11 -11
  134. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-editor.js.map +1 -1
  135. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.d.ts +3 -3
  136. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  137. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.js +13 -11
  138. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-input.js.map +1 -1
  139. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.d.ts +3 -3
  140. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
  141. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.js +12 -10
  142. package/packages/gsd-agent-modes/dist/modes/interactive/components/extension-selector.js.map +1 -1
  143. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.d.ts +1 -0
  144. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.d.ts.map +1 -1
  145. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.js +1 -0
  146. package/packages/gsd-agent-modes/dist/modes/interactive/components/index.js.map +1 -1
  147. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts +1 -1
  148. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  149. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js +2 -2
  150. package/packages/gsd-agent-modes/dist/modes/interactive/components/login-dialog.js.map +1 -1
  151. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts +6 -1
  152. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  153. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js +9 -6
  154. package/packages/gsd-agent-modes/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  155. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  156. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +0 -1
  157. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  158. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts +3 -0
  159. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.d.ts.map +1 -1
  160. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js +144 -2
  161. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-auth.js.map +1 -1
  162. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.d.ts.map +1 -1
  163. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js +2 -14
  164. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-session.js.map +1 -1
  165. package/packages/gsd-agent-modes/package.json +7 -7
  166. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  167. package/packages/mcp-server/dist/workflow-tools.js +28 -5
  168. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  169. package/packages/mcp-server/package.json +3 -3
  170. package/packages/native/package.json +1 -1
  171. package/packages/pi-agent-core/dist/harness/skills.d.ts.map +1 -1
  172. package/packages/pi-agent-core/dist/harness/skills.js +6 -0
  173. package/packages/pi-agent-core/dist/harness/skills.js.map +1 -1
  174. package/packages/pi-agent-core/dist/harness/system-prompt.d.ts +7 -0
  175. package/packages/pi-agent-core/dist/harness/system-prompt.d.ts.map +1 -1
  176. package/packages/pi-agent-core/dist/harness/system-prompt.js +7 -0
  177. package/packages/pi-agent-core/dist/harness/system-prompt.js.map +1 -1
  178. package/packages/pi-agent-core/package.json +1 -1
  179. package/packages/pi-ai/dist/models.generated.d.ts +8 -59
  180. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  181. package/packages/pi-ai/dist/models.generated.js +21 -72
  182. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  183. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  184. package/packages/pi-ai/dist/providers/anthropic.js +50 -0
  185. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  186. package/packages/pi-ai/dist/providers/openai-responses-shared.d.ts.map +1 -1
  187. package/packages/pi-ai/dist/providers/openai-responses-shared.js +28 -4
  188. package/packages/pi-ai/dist/providers/openai-responses-shared.js.map +1 -1
  189. package/packages/pi-ai/dist/types.d.ts +2 -0
  190. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  191. package/packages/pi-ai/dist/types.js.map +1 -1
  192. package/packages/pi-ai/package.json +1 -1
  193. package/packages/pi-coding-agent/README.md +1 -1
  194. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts +2 -2
  195. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.d.ts.map +1 -1
  196. package/packages/pi-coding-agent/dist/core/extensions/extension-upstream-types.js.map +1 -1
  197. package/packages/pi-coding-agent/dist/core/extensions/loader.js +1 -1
  198. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  199. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  200. package/packages/pi-coding-agent/dist/core/extensions/runner.js +8 -2
  201. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  202. package/packages/pi-coding-agent/dist/core/skills.d.ts +3 -0
  203. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  204. package/packages/pi-coding-agent/dist/core/skills.js +3 -0
  205. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  206. package/packages/pi-coding-agent/package.json +7 -7
  207. package/packages/pi-tui/package.json +1 -1
  208. package/packages/rpc-client/package.json +2 -2
  209. package/pkg/package.json +1 -1
  210. package/src/resources/extensions/context7/index.ts +15 -2
  211. package/src/resources/extensions/get-secrets-from-user.ts +17 -16
  212. package/src/resources/extensions/google-cli/index.ts +34 -0
  213. package/src/resources/extensions/google-cli/models.ts +57 -0
  214. package/src/resources/extensions/google-cli/package.json +11 -0
  215. package/src/resources/extensions/google-cli/readiness.ts +15 -0
  216. package/src/resources/extensions/google-cli/stream-adapter.ts +245 -0
  217. package/src/resources/extensions/gsd/auto/loop.ts +74 -1
  218. package/src/resources/extensions/gsd/auto/orchestrator.ts +4 -2
  219. package/src/resources/extensions/gsd/auto/phases.ts +46 -0
  220. package/src/resources/extensions/gsd/auto/run-unit.ts +10 -0
  221. package/src/resources/extensions/gsd/auto/session.ts +3 -0
  222. package/src/resources/extensions/gsd/auto-dispatch.ts +31 -11
  223. package/src/resources/extensions/gsd/auto-post-unit.ts +43 -14
  224. package/src/resources/extensions/gsd/auto-prompts.ts +4 -284
  225. package/src/resources/extensions/gsd/auto-recovery.ts +10 -7
  226. package/src/resources/extensions/gsd/auto-start.ts +307 -56
  227. package/src/resources/extensions/gsd/auto.ts +6 -1
  228. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +4 -3
  229. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +9 -4
  230. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +42 -5
  231. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +18 -6
  232. package/src/resources/extensions/gsd/bootstrap/system-context.ts +3 -28
  233. package/src/resources/extensions/gsd/closeout-recovery.ts +6 -1
  234. package/src/resources/extensions/gsd/commands/handlers/auto.ts +9 -1
  235. package/src/resources/extensions/gsd/commands-usage.ts +110 -5
  236. package/src/resources/extensions/gsd/config-overlay.ts +19 -16
  237. package/src/resources/extensions/gsd/context-overlay.ts +24 -19
  238. package/src/resources/extensions/gsd/dashboard-overlay.ts +14 -27
  239. package/src/resources/extensions/gsd/doctor-engine-checks.ts +99 -0
  240. package/src/resources/extensions/gsd/doctor-providers.ts +55 -27
  241. package/src/resources/extensions/gsd/doctor-types.ts +2 -0
  242. package/src/resources/extensions/gsd/doctor.ts +6 -1
  243. package/src/resources/extensions/gsd/git-conflict-state.ts +25 -1
  244. package/src/resources/extensions/gsd/guided-flow.ts +5 -6
  245. package/src/resources/extensions/gsd/key-manager.ts +57 -14
  246. package/src/resources/extensions/gsd/milestone-reopen-events.ts +28 -0
  247. package/src/resources/extensions/gsd/notification-overlay.ts +12 -11
  248. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +16 -12
  249. package/src/resources/extensions/gsd/preferences-skills.ts +11 -4
  250. package/src/resources/extensions/gsd/preferences.ts +17 -2
  251. package/src/resources/extensions/gsd/prompt-loader.ts +2 -0
  252. package/src/resources/extensions/gsd/prompts/discuss.md +4 -2
  253. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
  254. package/src/resources/extensions/gsd/prompts/system.md +1 -3
  255. package/src/resources/extensions/gsd/queue-reorder-ui.ts +29 -20
  256. package/src/resources/extensions/gsd/repository-registry.ts +3 -1
  257. package/src/resources/extensions/gsd/safety/evidence-collector.ts +11 -4
  258. package/src/resources/extensions/gsd/skill-activation.ts +292 -0
  259. package/src/resources/extensions/gsd/skill-catalog.data.ts +858 -0
  260. package/src/resources/extensions/gsd/skill-catalog.install.ts +205 -0
  261. package/src/resources/extensions/gsd/skill-catalog.ts +16 -1087
  262. package/src/resources/extensions/gsd/skill-discovery.ts +134 -78
  263. package/src/resources/extensions/gsd/skill-scope.ts +63 -0
  264. package/src/resources/extensions/gsd/skill-telemetry.ts +6 -40
  265. package/src/resources/extensions/gsd/skills.ts +75 -0
  266. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +499 -0
  267. package/src/resources/extensions/gsd/state-reconciliation/index.ts +40 -0
  268. package/src/resources/extensions/gsd/state-reconciliation/registry.ts +8 -0
  269. package/src/resources/extensions/gsd/state-reconciliation/types.ts +30 -0
  270. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +328 -2
  271. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +21 -0
  272. package/src/resources/extensions/gsd/tests/auto-post-unit-artifact-diagnostic.test.ts +28 -2
  273. package/src/resources/extensions/gsd/tests/auto-post-unit-evidence-crossref-4909.test.ts +30 -0
  274. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +41 -0
  275. package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +12 -0
  276. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +436 -0
  277. package/src/resources/extensions/gsd/tests/closeout-recovery.test.ts +15 -0
  278. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +31 -0
  279. package/src/resources/extensions/gsd/tests/commands-context.test.ts +5 -3
  280. package/src/resources/extensions/gsd/tests/commands-dispatcher-workspace-git.test.ts +15 -2
  281. package/src/resources/extensions/gsd/tests/commands-usage.test.ts +97 -0
  282. package/src/resources/extensions/gsd/tests/context-chart.test.ts +9 -0
  283. package/src/resources/extensions/gsd/tests/dashboard-overlay.test.ts +25 -0
  284. package/src/resources/extensions/gsd/tests/discord-invite-links.test.ts +1 -0
  285. package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +4 -2
  286. package/src/resources/extensions/gsd/tests/discuss-tool-scoping.test.ts +1 -1
  287. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +105 -0
  288. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +101 -1
  289. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +30 -0
  290. package/src/resources/extensions/gsd/tests/guided-discuss-milestone-prompt-rendering.test.ts +6 -0
  291. package/src/resources/extensions/gsd/tests/key-manager.test.ts +23 -4
  292. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +6 -1
  293. package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +70 -10
  294. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +7 -1
  295. package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +93 -0
  296. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +46 -0
  297. package/src/resources/extensions/gsd/tests/register-extension-guard.test.ts +116 -11
  298. package/src/resources/extensions/gsd/tests/repository-registry.test.ts +30 -1
  299. package/src/resources/extensions/gsd/tests/show-config-command.test.ts +4 -0
  300. package/src/resources/extensions/gsd/tests/skill-discovery.test.ts +111 -0
  301. package/src/resources/extensions/gsd/tests/skill-scope-auto.test.ts +67 -0
  302. package/src/resources/extensions/gsd/tests/skills.test.ts +55 -0
  303. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +13 -2
  304. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +303 -0
  305. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +19 -0
  306. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +24 -1
  307. package/src/resources/extensions/gsd/tests/tui-border-assertions.ts +28 -0
  308. package/src/resources/extensions/gsd/tests/tui-render-kit.test.ts +14 -0
  309. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +18 -0
  310. package/src/resources/extensions/gsd/tests/user-input-boundary.test.ts +26 -0
  311. package/src/resources/extensions/gsd/tests/vision-ask.test.ts +23 -0
  312. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +6 -1
  313. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +74 -1
  314. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +82 -0
  315. package/src/resources/extensions/gsd/tests/workspace-git-preflight.test.ts +16 -1
  316. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +28 -0
  317. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +45 -1
  318. package/src/resources/extensions/gsd/tools/complete-task.ts +9 -0
  319. package/src/resources/extensions/gsd/tools/exec-tool.ts +42 -10
  320. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +82 -5
  321. package/src/resources/extensions/gsd/tui/render-kit.ts +82 -0
  322. package/src/resources/extensions/gsd/unit-context-manifest.ts +37 -26
  323. package/src/resources/extensions/gsd/user-input-boundary.ts +1 -1
  324. package/src/resources/extensions/gsd/vision-ask.ts +28 -0
  325. package/src/resources/extensions/gsd/visualizer-overlay.ts +12 -40
  326. package/src/resources/extensions/gsd/worktree-lifecycle.ts +37 -2
  327. package/src/resources/extensions/search-the-web/native-search.ts +60 -8
  328. package/src/resources/extensions/shared/confirm-ui.ts +8 -12
  329. package/src/resources/extensions/shared/dialog-frame.ts +71 -0
  330. package/src/resources/extensions/shared/interview-ui.ts +43 -42
  331. package/src/resources/extensions/shared/next-action-ui.ts +6 -6
  332. package/src/resources/extensions/shared/tests/confirm-ui.test.ts +57 -0
  333. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +163 -0
  334. package/src/resources/extensions/shared/tests/next-action-ui-hasui.test.ts +55 -0
  335. package/src/resources/skills/create-skill/references/gsd-skill-ecosystem.md +1 -1
  336. /package/dist/web/standalone/.next/static/{praHP_OATcjBkvAVejjGK → PkhJfy4kKo4yUHj1wY7_q}/_buildManifest.js +0 -0
  337. /package/dist/web/standalone/.next/static/{praHP_OATcjBkvAVejjGK → PkhJfy4kKo4yUHj1wY7_q}/_ssgManifest.js +0 -0
@@ -48,20 +48,27 @@ describe("auditOrphanedMilestoneBranches", () => {
48
48
  const result = auditOrphanedMilestoneBranches(dir, "worktree");
49
49
  assert.deepStrictEqual(result.recovered, []);
50
50
  assert.deepStrictEqual(result.warnings, []);
51
+ assert.deepStrictEqual(result.actions, []);
52
+ assert.equal(result.blockingStrandedWork, null);
51
53
  });
52
54
 
53
- test("skips in none isolation mode", () => {
54
- // Create a milestone branch that would otherwise be detected
55
+ test("runs in none isolation mode and cleans safe completed residue", () => {
55
56
  run("git branch milestone/M001", dir);
56
57
  insertMilestone({ id: "M001", title: "Test", status: "complete" });
57
58
 
58
59
  const result = auditOrphanedMilestoneBranches(dir, "none");
59
- assert.deepStrictEqual(result.recovered, []);
60
+ assert.ok(
61
+ result.recovered.some((r) => r.includes("Deleted merged branch milestone/M001")),
62
+ `should clean merged completed residue even in none mode; got: ${JSON.stringify(result.recovered)}`,
63
+ );
60
64
  assert.deepStrictEqual(result.warnings, []);
65
+ assert.ok(
66
+ result.actions.some((action) => action.kind === "complete-merged-branch"),
67
+ "should record structured cleanup action",
68
+ );
61
69
 
62
- // Branch should still exist
63
70
  const branches = run("git branch --list milestone/M001", dir);
64
- assert.ok(branches.includes("milestone/M001"), "branch should be preserved in none mode");
71
+ assert.equal(branches, "", "safe completed branch should be cleaned in none mode");
65
72
  });
66
73
 
67
74
  test("deletes merged branch for completed milestone", () => {
@@ -149,9 +156,12 @@ describe("auditOrphanedMilestoneBranches", () => {
149
156
  // Must surface a warning so the user knows the worktree holds uncollapsed work
150
157
  assert.ok(result.warnings.length > 0, "should warn about in-progress orphan");
151
158
  assert.ok(
152
- result.warnings.some(w => w.includes("milestone/M001") && w.includes("in-progress")),
153
- `warning should mention milestone/M001 and in-progress state; got: ${JSON.stringify(result.warnings)}`,
159
+ result.warnings.some(w => w.includes("Stranded work") && w.includes("milestone/M001") && w.includes("in-progress")),
160
+ `warning should mention stranded milestone/M001 and in-progress state; got: ${JSON.stringify(result.warnings)}`,
154
161
  );
162
+ assert.equal(result.blockingStrandedWork?.milestoneId, "M001");
163
+ assert.equal(result.blockingStrandedWork?.recoveryMode, "branch");
164
+ assert.equal(result.blockingStrandedWork?.commitsAhead, 1);
155
165
 
156
166
  // Branch must still exist
157
167
  const branches = run("git branch --list milestone/M001", dir);
@@ -184,6 +194,53 @@ describe("auditOrphanedMilestoneBranches", () => {
184
194
  result.warnings.some(w => w.includes(".gsd/worktrees/M001") || w.includes("worktree")),
185
195
  `warning should reference the worktree location; got: ${JSON.stringify(result.warnings)}`,
186
196
  );
197
+ assert.equal(result.blockingStrandedWork?.recoveryMode, "worktree");
198
+ });
199
+
200
+ test("detects dirty in-progress worktree even when branch has no commits ahead", () => {
201
+ run("git branch milestone/M001", dir);
202
+
203
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
204
+ mkdirSync(wtDir, { recursive: true });
205
+ writeFileSync(join(wtDir, ".git"), `gitdir: ${join(dir, ".git", "worktrees", "M001")}\n`);
206
+ writeFileSync(join(wtDir, "dirty.txt"), "uncommitted work\n");
207
+
208
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
209
+
210
+ const result = auditOrphanedMilestoneBranches(dir, "worktree", {
211
+ hasChanges: (basePath) => basePath === wtDir,
212
+ });
213
+
214
+ assert.deepStrictEqual(result.recovered, []);
215
+ assert.ok(
216
+ result.warnings.some((w) => w.includes("uncommitted changes")),
217
+ `dirty worktree should be treated as stranded work; got: ${JSON.stringify(result.warnings)}`,
218
+ );
219
+ assert.equal(result.blockingStrandedWork?.milestoneId, "M001");
220
+ assert.equal(result.blockingStrandedWork?.dirtyWorktree, true);
221
+ assert.equal(result.blockingStrandedWork?.recoveryMode, "worktree");
222
+ });
223
+
224
+ test("detects dirty in-progress worktree even when milestone branch is absent", () => {
225
+ const wtDir = join(dir, ".gsd", "worktrees", "M001");
226
+ mkdirSync(wtDir, { recursive: true });
227
+ writeFileSync(join(wtDir, ".git"), `gitdir: ${join(dir, ".git", "worktrees", "M001")}\n`);
228
+ writeFileSync(join(wtDir, "dirty.txt"), "branchless work\n");
229
+
230
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
231
+
232
+ const result = auditOrphanedMilestoneBranches(dir, "none", {
233
+ hasChanges: (basePath) => basePath === wtDir,
234
+ });
235
+
236
+ assert.deepStrictEqual(result.recovered, []);
237
+ assert.ok(
238
+ result.warnings.some((w) => w.includes("Stranded work") && w.includes("M001")),
239
+ `branchless dirty worktree should block; got: ${JSON.stringify(result.warnings)}`,
240
+ );
241
+ assert.equal(result.blockingStrandedWork?.branch, undefined);
242
+ assert.equal(result.blockingStrandedWork?.dirtyWorktree, true);
243
+ assert.equal(result.blockingStrandedWork?.recoveryMode, "worktree");
187
244
  });
188
245
 
189
246
  test("cleans up orphaned worktree directory for merged milestone", () => {
@@ -359,7 +416,7 @@ describe("auditOrphanedMilestoneBranches", () => {
359
416
  assert.ok(existsSync(wtDir), "active milestone worktree dir must be preserved");
360
417
  });
361
418
 
362
- test("#5879 — skips branch-less orphan in 'none' isolation mode", () => {
419
+ test("#5879 — cleans branch-less complete orphan in 'none' isolation mode", () => {
363
420
  insertMilestone({ id: "M001", title: "Test", status: "complete" });
364
421
 
365
422
  const wtDir = join(dir, ".gsd", "worktrees", "M001");
@@ -368,7 +425,10 @@ describe("auditOrphanedMilestoneBranches", () => {
368
425
 
369
426
  const result = auditOrphanedMilestoneBranches(dir, "none");
370
427
 
371
- assert.deepStrictEqual(result.recovered, []);
372
- assert.ok(existsSync(wtDir), "'none' mode must not touch worktree dirs");
428
+ assert.ok(
429
+ result.recovered.some((r) => r.includes("M001") && r.includes("branch already deleted")),
430
+ `none mode should still clean safe completed residue; got: ${JSON.stringify(result.recovered)}`,
431
+ );
432
+ assert.ok(!existsSync(wtDir), "completed orphan worktree dir should be cleaned in none mode");
373
433
  });
374
434
  });
@@ -5,6 +5,7 @@ import { describe, it } from "node:test";
5
5
  import assert from "node:assert/strict";
6
6
 
7
7
  import { visibleWidth } from "@gsd/pi-tui";
8
+ import { assertFullOuterBorder } from "./tui-border-assertions.ts";
8
9
 
9
10
  function assertLinesFit(lines: string[], width: number): void {
10
11
  for (const line of lines) {
@@ -51,6 +52,9 @@ describe("parallel-monitor-overlay", () => {
51
52
  const joined = lines.join("\n");
52
53
  assert.ok(joined.includes("Parallel Monitor"), "should include title");
53
54
  assert.ok(joined.includes("No parallel workers found"), "should show empty state");
55
+ assertFullOuterBorder(lines, 80);
56
+ assert.match(lines[0] ?? "", /^╭─ GSD Parallel Monitor /);
57
+ assert.match(lines.at(-1) ?? "", /^╰─+╯$/);
54
58
 
55
59
  // Dispose should not throw
56
60
  overlay.dispose();
@@ -105,7 +109,9 @@ describe("parallel-monitor-overlay", () => {
105
109
  );
106
110
 
107
111
  for (const width of [40, 80, 120]) {
108
- assertLinesFit(overlay.render(width), width);
112
+ const lines = overlay.render(width);
113
+ assertLinesFit(lines, width);
114
+ assertFullOuterBorder(lines, width);
109
115
  overlay.invalidate();
110
116
  }
111
117
 
@@ -0,0 +1,93 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { mock } from "node:test";
7
+
8
+ import { postUnitPostVerification, type PostUnitContext } from "../auto-post-unit.ts";
9
+ import { AutoSession } from "../auto/session.ts";
10
+ import { checkPostUnitHooks, resetHookState, resolveHookArtifactPath } from "../post-unit-hooks.ts";
11
+ import { _clearGsdRootCache } from "../paths.ts";
12
+ import { invalidateAllCaches } from "../cache.ts";
13
+
14
+ function writePreferences(basePath: string): void {
15
+ const content = `---
16
+ post_unit_hooks:
17
+ - name: review-arbiter
18
+ after:
19
+ - execute-task
20
+ prompt: Review {taskId}
21
+ agent: arbiter
22
+ artifact: REVIEW-DEBATE.md
23
+ retry_on: NEEDS-REWORK.md
24
+ max_cycles: 3
25
+ enabled: true
26
+ ---
27
+ `;
28
+ writeFileSync(join(basePath, ".gsd", "PREFERENCES.md"), content, "utf-8");
29
+ }
30
+
31
+ test("post-unit retry_on marks trigger unit as retry in orchestrator before redispatch", async () => {
32
+ const originalCwd = process.cwd();
33
+ const base = mkdtempSync(join(tmpdir(), "gsd-post-unit-retry-"));
34
+ const taskDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
35
+ mkdirSync(taskDir, { recursive: true });
36
+
37
+ try {
38
+ process.chdir(base);
39
+ _clearGsdRootCache();
40
+ invalidateAllCaches();
41
+ resetHookState();
42
+ writePreferences(base);
43
+
44
+ const hookDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
45
+ assert.ok(hookDispatch, "hook should dispatch for execute-task");
46
+
47
+ const retryPath = resolveHookArtifactPath(base, "M001/S01/T01", "NEEDS-REWORK.md");
48
+ writeFileSync(retryPath, "rework requested", "utf-8");
49
+
50
+ const retryActiveUnit = mock.fn(async (_unit: { unitType: string; unitId: string }) => {});
51
+ const s = new AutoSession();
52
+ s.basePath = base;
53
+ s.active = true;
54
+ s.currentUnit = { type: "hook/review-arbiter", id: "M001/S01/T01", startedAt: Date.now() };
55
+ s.orchestration = {
56
+ start: async () => ({ kind: "started" }),
57
+ advance: async () => ({ kind: "stopped", reason: "unused" }),
58
+ completeActiveUnit: async () => {},
59
+ retryActiveUnit,
60
+ resume: async () => ({ kind: "resumed" }),
61
+ stop: async (reason: string) => ({ kind: "stopped", reason }),
62
+ getStatus: () => ({ phase: "running", transitionCount: 0 }),
63
+ };
64
+
65
+ const pctx: PostUnitContext = {
66
+ s,
67
+ ctx: {
68
+ ui: { notify: () => {}, setStatus: () => {}, setWidget: () => {}, setFooter: () => {} },
69
+ model: { id: "test-model" },
70
+ } as any,
71
+ pi: { sendMessage: async () => {}, setModel: async () => true } as any,
72
+ buildSnapshotOpts: () => ({}),
73
+ lockBase: () => base,
74
+ stopAuto: async () => {},
75
+ pauseAuto: async () => {},
76
+ updateProgressWidget: () => {},
77
+ };
78
+
79
+ const result = await postUnitPostVerification(pctx);
80
+ assert.equal(result, "continue");
81
+ assert.equal(retryActiveUnit.mock.callCount(), 1);
82
+ assert.deepEqual(retryActiveUnit.mock.calls[0]?.arguments[0], {
83
+ unitType: "execute-task",
84
+ unitId: "M001/S01/T01",
85
+ });
86
+ } finally {
87
+ process.chdir(originalCwd);
88
+ resetHookState();
89
+ invalidateAllCaches();
90
+ _clearGsdRootCache();
91
+ rmSync(base, { recursive: true, force: true });
92
+ }
93
+ });
@@ -2,6 +2,7 @@ import { describe, test } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
 
4
4
  import { showQueueReorder } from "../queue-reorder-ui.ts";
5
+ import { assertFullOuterBorder } from "./tui-border-assertions.ts";
5
6
 
6
7
  const fakeTheme = {
7
8
  fg: (_color: string, text: string) => text,
@@ -43,6 +44,51 @@ describe("queue-reorder-ui", () => {
43
44
  const joined = lastRender.join("\n");
44
45
  assert.ok(joined.includes("M016"), "selected item should stay visible after scrolling");
45
46
  assert.ok(lastRender.length <= 16, `overlay should fit terminal max-height, got ${lastRender.length}`);
47
+ assertFullOuterBorder(lastRender, 100);
48
+ assert.match(lastRender[0] ?? "", /^╭─ Queue Reorder /);
49
+ assert.match(lastRender.at(-1) ?? "", /^╰─+╯$/);
50
+ } finally {
51
+ if (originalRowsDescriptor) {
52
+ Object.defineProperty(process.stdout, "rows", originalRowsDescriptor);
53
+ } else {
54
+ delete (process.stdout as { rows?: number }).rows;
55
+ }
56
+ }
57
+ });
58
+
59
+ test("draws queue scroll thumb beside queue rows when completed rows are shown", async () => {
60
+ const originalRowsDescriptor = Object.getOwnPropertyDescriptor(process.stdout, "rows");
61
+ Object.defineProperty(process.stdout, "rows", { value: 12, configurable: true });
62
+
63
+ try {
64
+ const completed = [
65
+ { id: "M000", title: "Already done" },
66
+ ];
67
+ const pending = Array.from({ length: 8 }, (_, idx) => ({
68
+ id: `M${String(idx + 1).padStart(3, "0")}`,
69
+ title: `Milestone ${idx + 1}`,
70
+ }));
71
+ let rendered: string[] = [];
72
+
73
+ const ctx = {
74
+ hasUI: true,
75
+ ui: {
76
+ custom: async (factory: any) => {
77
+ const component = factory({ requestRender() {} }, fakeTheme, null, () => {});
78
+ rendered = component.render(80);
79
+ return null;
80
+ },
81
+ },
82
+ } as any;
83
+
84
+ await showQueueReorder(ctx, completed, pending);
85
+
86
+ const completedHeader = rendered.find(line => line.includes("Completed:"));
87
+ const firstQueueRow = rendered.find(line => line.includes("M001"));
88
+ assert.ok(completedHeader, "completed header should be rendered before queue rows");
89
+ assert.ok(firstQueueRow, "first queue row should be rendered");
90
+ assert.match(completedHeader, /│$/);
91
+ assert.match(firstQueueRow, /┃$/);
46
92
  } finally {
47
93
  if (originalRowsDescriptor) {
48
94
  Object.defineProperty(process.stdout, "rows", originalRowsDescriptor);
@@ -134,6 +134,72 @@ test("handleRecoverableExtensionProcessError swallows EPIPE without writing a cr
134
134
  }
135
135
  });
136
136
 
137
+ test("handleRecoverableExtensionProcessError tolerates stderr.write throwing EPIPE (re-entry guard)", () => {
138
+ // process.stderr.write itself can EPIPE; safeStderr() must swallow it so the
139
+ // handler doesn't re-enter the EPIPE branch and loop forever.
140
+ const originalWrite = process.stderr.write.bind(process.stderr);
141
+ process.stderr.write = (() => {
142
+ throw Object.assign(new Error("stderr broken pipe"), {
143
+ code: "EPIPE",
144
+ syscall: "write",
145
+ });
146
+ }) as typeof process.stderr.write;
147
+
148
+ try {
149
+ const handled = handleRecoverableExtensionProcessError(
150
+ Object.assign(new Error("broken pipe"), {
151
+ code: "EPIPE",
152
+ syscall: "write",
153
+ }),
154
+ );
155
+ assert.equal(handled, true);
156
+ } finally {
157
+ process.stderr.write = originalWrite;
158
+ }
159
+ });
160
+
161
+ // #181: Windows surfaces a closed pipe mid-write as `Error: write EOF` (or
162
+ // `read EOF`) with no `code` set. Both are the same logical condition as POSIX
163
+ // EPIPE and must be swallowed, else they escape to the uncaught-exception path
164
+ // and crash auto-mode workers. These non-storm cases run before the storm test
165
+ // below so the shared storm counter stays under threshold. ECONNRESET is
166
+ // intentionally NOT in this set — see the dedicated "leaves ECONNRESET network
167
+ // errors unhandled" test above.
168
+ test("handleRecoverableExtensionProcessError swallows Windows 'write EOF' (no code)", () => {
169
+ let stderr = "";
170
+ const originalWrite = process.stderr.write.bind(process.stderr);
171
+ process.stderr.write = ((chunk: string | Uint8Array) => {
172
+ stderr += String(chunk);
173
+ return true;
174
+ }) as typeof process.stderr.write;
175
+
176
+ try {
177
+ // Mirror the real Windows error: message "write EOF", no `code`, no `syscall`.
178
+ const handled = handleRecoverableExtensionProcessError(new Error("write EOF"));
179
+ assert.equal(handled, true);
180
+ assert.match(stderr, /swallowed write EOF/);
181
+ } finally {
182
+ process.stderr.write = originalWrite;
183
+ }
184
+ });
185
+
186
+ test("handleRecoverableExtensionProcessError swallows 'read EOF' (no code)", () => {
187
+ let stderr = "";
188
+ const originalWrite = process.stderr.write.bind(process.stderr);
189
+ process.stderr.write = ((chunk: string | Uint8Array) => {
190
+ stderr += String(chunk);
191
+ return true;
192
+ }) as typeof process.stderr.write;
193
+
194
+ try {
195
+ const handled = handleRecoverableExtensionProcessError(new Error("read EOF"));
196
+ assert.equal(handled, true);
197
+ assert.match(stderr, /swallowed read EOF/);
198
+ } finally {
199
+ process.stderr.write = originalWrite;
200
+ }
201
+ });
202
+
137
203
  test("handleRecoverableExtensionProcessError swallows dead transport control write errors", () => {
138
204
  let stderr = "";
139
205
  const originalWrite = process.stderr.write.bind(process.stderr);
@@ -153,7 +219,16 @@ test("handleRecoverableExtensionProcessError swallows dead transport control wri
153
219
  }
154
220
  });
155
221
 
156
- test("handleRecoverableExtensionProcessError swallows write EOF without code", () => {
222
+ test("handleRecoverableExtensionProcessError leaves a plain EOF-substring error unhandled", () => {
223
+ // Guard against over-matching: only the exact "write EOF"/"read EOF" messages
224
+ // are the Windows pipe-closed signature; an unrelated error must not be eaten.
225
+ const handled = handleRecoverableExtensionProcessError(new Error("could not write EOF marker to log"));
226
+ assert.equal(handled, false);
227
+ });
228
+
229
+ test("handleRecoverableExtensionProcessError exits on EPIPE storm (>100 within 10s)", () => {
230
+ // After the storm threshold, the pipe is gone for good — handler must exit
231
+ // cleanly instead of swallowing forever in a tight CPU loop.
157
232
  let stderr = "";
158
233
  const originalWrite = process.stderr.write.bind(process.stderr);
159
234
  process.stderr.write = ((chunk: string | Uint8Array) => {
@@ -161,18 +236,38 @@ test("handleRecoverableExtensionProcessError swallows write EOF without code", (
161
236
  return true;
162
237
  }) as typeof process.stderr.write;
163
238
 
239
+ const originalExit = process.exit;
240
+ const exitCalls: Array<number | undefined> = [];
241
+ process.exit = ((code?: number) => {
242
+ exitCalls.push(code);
243
+ // Don't actually exit; just record the call.
244
+ return undefined as never;
245
+ }) as typeof process.exit;
246
+
164
247
  try {
165
- const handled = handleRecoverableExtensionProcessError(
166
- new Error("write EOF"),
248
+ const err = Object.assign(new Error("broken pipe"), {
249
+ code: "EPIPE",
250
+ syscall: "write",
251
+ });
252
+ // Fire well above the 100-event threshold inside the 10s window.
253
+ for (let i = 0; i < 150; i++) {
254
+ handleRecoverableExtensionProcessError(err);
255
+ }
256
+ assert.ok(
257
+ exitCalls.length > 0,
258
+ `expected process.exit to be called during EPIPE storm, got ${exitCalls.length} calls`,
167
259
  );
168
- assert.equal(handled, true);
169
- assert.match(stderr, /swallowed write EOF/);
260
+ assert.equal(exitCalls[0], 0);
261
+ assert.match(stderr, /EPIPE storm/);
170
262
  } finally {
171
263
  process.stderr.write = originalWrite;
264
+ process.exit = originalExit;
172
265
  }
173
266
  });
174
267
 
175
- test("handleRecoverableExtensionProcessError swallows read EOF without code", () => {
268
+ test("handleRecoverableExtensionProcessError exits on a Windows 'write EOF' storm too (#181)", () => {
269
+ // The storm counter is shared across all pipe-closed encodings, so a runaway
270
+ // Windows `write EOF` loop must trip the same clean-exit guard as EPIPE.
176
271
  let stderr = "";
177
272
  const originalWrite = process.stderr.write.bind(process.stderr);
178
273
  process.stderr.write = ((chunk: string | Uint8Array) => {
@@ -180,13 +275,23 @@ test("handleRecoverableExtensionProcessError swallows read EOF without code", ()
180
275
  return true;
181
276
  }) as typeof process.stderr.write;
182
277
 
278
+ const originalExit = process.exit;
279
+ const exitCalls: Array<number | undefined> = [];
280
+ process.exit = ((code?: number) => {
281
+ exitCalls.push(code);
282
+ return undefined as never;
283
+ }) as typeof process.exit;
284
+
183
285
  try {
184
- const handled = handleRecoverableExtensionProcessError(
185
- new Error("read EOF"),
186
- );
187
- assert.equal(handled, true);
188
- assert.match(stderr, /swallowed read EOF/);
286
+ const err = new Error("write EOF");
287
+ for (let i = 0; i < 150; i++) {
288
+ handleRecoverableExtensionProcessError(err);
289
+ }
290
+ assert.ok(exitCalls.length > 0, "expected process.exit during write EOF storm");
291
+ assert.equal(exitCalls[0], 0);
292
+ assert.match(stderr, /write EOF storm/);
189
293
  } finally {
190
294
  process.stderr.write = originalWrite;
295
+ process.exit = originalExit;
191
296
  }
192
297
  });
@@ -2,9 +2,10 @@
2
2
 
3
3
  import test from "node:test";
4
4
  import assert from "node:assert/strict";
5
- import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
5
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
+ import { execFileSync } from "node:child_process";
8
9
  import { createRepositoryRegistryFromPreferences, defaultRepositoryTargets } from "../repository-registry.ts";
9
10
 
10
11
  test("repository registry includes implicit project root and declared child repos", (t) => {
@@ -99,3 +100,31 @@ test("defaultRepositoryTargets returns [project] for a parent-mode registry", (t
99
100
 
100
101
  assert.deepEqual(defaultRepositoryTargets(registry), ["project"]);
101
102
  });
103
+
104
+ test("repository registry keeps project root anchored to .gsd project in monorepo subdirectory", (t) => {
105
+ const monorepo = mkdtempSync(join(tmpdir(), "gsd-repo-registry-mono-"));
106
+ t.after(() => rmSync(monorepo, { recursive: true, force: true }));
107
+ execFileSync("git", ["init"], { cwd: monorepo, stdio: "ignore" });
108
+
109
+ const subproject = join(monorepo, "fieldkit-tools");
110
+ mkdirSync(join(subproject, ".gsd"), { recursive: true });
111
+ writeFileSync(join(subproject, ".gsd", "PREFERENCES.md"), "---\nversion: 1\n---\n");
112
+
113
+ const registry = createRepositoryRegistryFromPreferences(subproject, undefined);
114
+
115
+ assert.equal(registry.projectRoot, subproject);
116
+ assert.equal(registry.byId.get("project")?.root, subproject);
117
+ });
118
+
119
+ test("repository registry uses external-state worktree checkout as project root", (t) => {
120
+ const base = mkdtempSync(join(tmpdir(), "gsd-repo-registry-external-"));
121
+ t.after(() => rmSync(base, { recursive: true, force: true }));
122
+ const worktree = join(base, ".gsd", "projects", "abc123", "worktrees", "M001");
123
+ mkdirSync(worktree, { recursive: true });
124
+ execFileSync("git", ["init"], { cwd: worktree, stdio: "ignore" });
125
+
126
+ const registry = createRepositoryRegistryFromPreferences(worktree, undefined);
127
+
128
+ assert.equal(registry.projectRoot, worktree);
129
+ assert.equal(registry.byId.get("project")?.root, worktree);
130
+ });
@@ -7,6 +7,7 @@ import assert from "node:assert/strict";
7
7
 
8
8
  import { GSDConfigOverlay, formatConfigText } from "../config-overlay.ts";
9
9
  import { handleCoreCommand } from "../commands/handlers/core.ts";
10
+ import { assertFullOuterBorder } from "./tui-border-assertions.ts";
10
11
 
11
12
  const theme = {
12
13
  bold: (s: string) => s,
@@ -24,6 +25,9 @@ test("GSDConfigOverlay renders and responds to input", () => {
24
25
 
25
26
  const lines = overlay.render(60);
26
27
  assert.ok(lines.some((line) => line.includes("GSD Configuration")));
28
+ assertFullOuterBorder(lines, 60);
29
+ assert.match(lines[0] ?? "", /^╭─ GSD Configuration /);
30
+ assert.match(lines.at(-1) ?? "", /^╰─+╯$/);
27
31
 
28
32
  overlay.handleInput("j");
29
33
  assert.equal(renderRequests, 1);
@@ -0,0 +1,111 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import {
7
+ appendDiscoveredSkillsFallback,
8
+ clearSkillSnapshot,
9
+ detectNewSkills,
10
+ refreshCatalogForNewSkills,
11
+ snapshotSkills,
12
+ } from "../skill-discovery.js";
13
+
14
+ function makeTempHome(): string {
15
+ return mkdtempSync(join(tmpdir(), "gsd-skill-discovery-"));
16
+ }
17
+
18
+ async function withTempSkillHome<T>(fn: (home: string) => T | Promise<T>): Promise<T> {
19
+ const previousHome = process.env.HOME;
20
+ const previousGsdHome = process.env.GSD_HOME;
21
+ const home = makeTempHome();
22
+ process.env.HOME = home;
23
+ process.env.GSD_HOME = join(home, ".gsd");
24
+ try {
25
+ return await fn(home);
26
+ } finally {
27
+ clearSkillSnapshot();
28
+ if (previousHome === undefined) {
29
+ delete process.env.HOME;
30
+ } else {
31
+ process.env.HOME = previousHome;
32
+ }
33
+ if (previousGsdHome === undefined) {
34
+ delete process.env.GSD_HOME;
35
+ } else {
36
+ process.env.GSD_HOME = previousGsdHome;
37
+ }
38
+ rmSync(home, { recursive: true, force: true });
39
+ }
40
+ }
41
+
42
+ function writeDiskSkill(root: string, name: string, description = `Use for ${name}.`): void {
43
+ const dir = join(root, name);
44
+ mkdirSync(dir, { recursive: true });
45
+ writeFileSync(join(dir, "SKILL.md"), `---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n`);
46
+ }
47
+
48
+ test("detectNewSkills detects skills added on disk after the baseline snapshot", async () => {
49
+ await withTempSkillHome((home) => {
50
+ const skillsRoot = join(home, ".agents", "skills");
51
+ writeDiskSkill(skillsRoot, "existing-skill");
52
+ snapshotSkills();
53
+
54
+ writeDiskSkill(skillsRoot, "new-disk-skill", "New disk skill.");
55
+ const detected = detectNewSkills();
56
+
57
+ assert.deepEqual(detected.map(skill => skill.name), ["new-disk-skill"]);
58
+ assert.equal(detected[0].description, "New disk skill.");
59
+ assert.equal(detected[0].location, join(skillsRoot, "new-disk-skill", "SKILL.md"));
60
+ });
61
+ });
62
+
63
+ test("refreshCatalogForNewSkills retries discovery after reload failure", async () => {
64
+ await withTempSkillHome(async (home) => {
65
+ const skillsRoot = join(home, ".agents", "skills");
66
+ snapshotSkills();
67
+ writeDiskSkill(skillsRoot, "reload-retry-skill");
68
+
69
+ const messages: Array<{ message: string; level: "info" | "warning" }> = [];
70
+ const failed = await refreshCatalogForNewSkills({
71
+ reload: async () => { throw new Error("reload failed"); },
72
+ notify: (message, level) => messages.push({ message, level }),
73
+ });
74
+
75
+ assert.deepEqual(failed.map(skill => skill.name), ["reload-retry-skill"]);
76
+ assert.deepEqual(detectNewSkills().map(skill => skill.name), ["reload-retry-skill"]);
77
+ assert.equal(messages[0]?.level, "warning");
78
+
79
+ const loaded = await refreshCatalogForNewSkills({
80
+ reload: async () => {},
81
+ notify: (message, level) => messages.push({ message, level }),
82
+ });
83
+
84
+ assert.deepEqual(loaded.map(skill => skill.name), ["reload-retry-skill"]);
85
+ assert.deepEqual(detectNewSkills(), []);
86
+ assert.ok(messages.some(({ level, message }) => level === "info" && message.includes("reload-retry-skill")));
87
+ });
88
+ });
89
+
90
+ test("appendDiscoveredSkillsFallback exposes newly detected skills missing from the prompt", () => {
91
+ const prompt = appendDiscoveredSkillsFallback("base system prompt", [{
92
+ name: "fallback-skill",
93
+ description: "Use when reload fails & skill is needed.",
94
+ location: "/tmp/fallback-skill/SKILL.md",
95
+ }]);
96
+
97
+ assert.match(prompt, /<newly_discovered_skills>/);
98
+ assert.match(prompt, /fallback-skill/);
99
+ assert.match(prompt, /Use when reload fails &amp; skill is needed\./);
100
+ assert.match(prompt, /\/tmp\/fallback-skill\/SKILL.md/);
101
+ });
102
+
103
+ test("appendDiscoveredSkillsFallback does not duplicate skills already in the prompt", () => {
104
+ const prompt = "base system prompt\n/tmp/already-loaded/SKILL.md";
105
+
106
+ assert.equal(appendDiscoveredSkillsFallback(prompt, [{
107
+ name: "already-loaded",
108
+ description: "Already present.",
109
+ location: "/tmp/already-loaded/SKILL.md",
110
+ }]), prompt);
111
+ });