gsd-pi 2.76.0-dev.4100bd590 → 2.76.0-dev.479ad0e78

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 (362) hide show
  1. package/dist/claude-cli-check.js +32 -3
  2. package/dist/mcp-server.d.ts +7 -0
  3. package/dist/mcp-server.js +35 -1
  4. package/dist/onboarding.js +45 -0
  5. package/dist/resource-loader.d.ts +1 -1
  6. package/dist/resource-loader.js +2 -8
  7. package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
  8. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  9. package/dist/resources/extensions/gsd/auto/loop.js +9 -0
  10. package/dist/resources/extensions/gsd/auto/phases.js +58 -5
  11. package/dist/resources/extensions/gsd/auto/run-unit.js +38 -2
  12. package/dist/resources/extensions/gsd/auto/session.js +22 -1
  13. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -3
  14. package/dist/resources/extensions/gsd/auto-model-selection.js +14 -3
  15. package/dist/resources/extensions/gsd/auto-post-unit.js +25 -2
  16. package/dist/resources/extensions/gsd/auto-prompts.js +14 -0
  17. package/dist/resources/extensions/gsd/auto-recovery.js +32 -1
  18. package/dist/resources/extensions/gsd/auto-start.js +58 -57
  19. package/dist/resources/extensions/gsd/auto-worktree.js +51 -53
  20. package/dist/resources/extensions/gsd/auto.js +70 -28
  21. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
  22. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  23. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  24. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  25. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +52 -6
  26. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +34 -2
  27. package/dist/resources/extensions/gsd/clean-root-preflight.js +93 -0
  28. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  29. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  30. package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
  31. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  32. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  33. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  34. package/dist/resources/extensions/gsd/gitignore.js +1 -0
  35. package/dist/resources/extensions/gsd/gsd-db.js +149 -31
  36. package/dist/resources/extensions/gsd/guided-flow.js +190 -1
  37. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  38. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  39. package/dist/resources/extensions/gsd/key-manager.js +28 -0
  40. package/dist/resources/extensions/gsd/model-router.js +36 -3
  41. package/dist/resources/extensions/gsd/pre-execution-checks.js +44 -9
  42. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  43. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  44. package/dist/resources/extensions/gsd/preferences.js +17 -17
  45. package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
  46. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  47. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  48. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  49. package/dist/resources/extensions/gsd/safety/evidence-collector.js +96 -0
  50. package/dist/resources/extensions/gsd/safety/file-change-validator.js +13 -5
  51. package/dist/resources/extensions/gsd/safety/safety-harness.js +5 -1
  52. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  53. package/dist/resources/extensions/gsd/tools/complete-milestone.js +16 -10
  54. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  55. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  56. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  57. package/dist/resources/extensions/gsd/uok/plan-v2.js +20 -3
  58. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  59. package/dist/resources/extensions/gsd/worktree-resolver.js +50 -10
  60. package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
  61. package/dist/resources/skills/write-docs/SKILL.md +2 -1
  62. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  63. package/dist/web/standalone/.next/BUILD_ID +1 -1
  64. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  65. package/dist/web/standalone/.next/build-manifest.json +2 -2
  66. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  67. package/dist/web/standalone/.next/required-server-files.json +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/index.html +1 -1
  85. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  92. package/dist/web/standalone/.next/server/chunks/6897.js +2 -2
  93. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  95. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  96. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  97. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  98. package/dist/web/standalone/server.js +1 -1
  99. package/dist/welcome-screen.js +6 -1
  100. package/dist/wizard.js +2 -0
  101. package/package.json +1 -1
  102. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  103. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  104. package/packages/mcp-server/dist/remote-questions.js +732 -0
  105. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  106. package/packages/mcp-server/dist/server.d.ts +7 -0
  107. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  108. package/packages/mcp-server/dist/server.js +70 -8
  109. package/packages/mcp-server/dist/server.js.map +1 -1
  110. package/packages/mcp-server/dist/session-manager.d.ts +14 -0
  111. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  112. package/packages/mcp-server/dist/session-manager.js +49 -1
  113. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  114. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  115. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  116. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  117. package/packages/mcp-server/package.json +2 -1
  118. package/packages/mcp-server/src/mcp-server.test.ts +67 -0
  119. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  120. package/packages/mcp-server/src/remote-questions.ts +916 -0
  121. package/packages/mcp-server/src/server.ts +89 -14
  122. package/packages/mcp-server/src/session-manager.ts +43 -1
  123. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  124. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  125. package/packages/mcp-server/tsconfig.test.json +19 -0
  126. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  127. package/packages/pi-ai/dist/models/custom.d.ts +38 -0
  128. package/packages/pi-ai/dist/models/custom.d.ts.map +1 -1
  129. package/packages/pi-ai/dist/models/custom.js +41 -0
  130. package/packages/pi-ai/dist/models/custom.js.map +1 -1
  131. package/packages/pi-ai/dist/providers/anthropic-auth.test.js +1 -1
  132. package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -1
  133. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  134. package/packages/pi-ai/dist/providers/anthropic-shared.js +27 -4
  135. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  136. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  137. package/packages/pi-ai/dist/providers/anthropic.js +8 -3
  138. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  139. package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts +2 -0
  140. package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts.map +1 -0
  141. package/packages/pi-ai/dist/providers/minimax-tool-name.test.js +80 -0
  142. package/packages/pi-ai/dist/providers/minimax-tool-name.test.js.map +1 -0
  143. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  144. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  145. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  146. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  147. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  148. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  149. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  150. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  151. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  152. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  153. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  154. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  155. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  156. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  157. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  158. package/packages/pi-ai/src/models/custom.ts +42 -0
  159. package/packages/pi-ai/src/providers/anthropic-auth.test.ts +1 -1
  160. package/packages/pi-ai/src/providers/anthropic-shared.ts +26 -5
  161. package/packages/pi-ai/src/providers/anthropic.ts +9 -3
  162. package/packages/pi-ai/src/providers/minimax-tool-name.test.ts +98 -0
  163. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  164. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  165. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  166. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  167. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  168. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +3 -2
  169. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  170. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -0
  171. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  172. package/packages/pi-coding-agent/dist/core/agent-session.js +7 -0
  173. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  175. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  176. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  178. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  179. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  180. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  181. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  183. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  185. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  187. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  188. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  189. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  190. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  191. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  192. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  193. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  194. package/packages/pi-coding-agent/dist/core/model-registry.js +90 -10
  195. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  196. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  197. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  198. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  199. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  200. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  201. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  202. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  203. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  204. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  205. package/packages/pi-coding-agent/dist/core/session-manager.js +10 -6
  206. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  207. package/packages/pi-coding-agent/dist/core/session-manager.test.js +45 -1
  208. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  209. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  210. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  211. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  212. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  213. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  214. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  215. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  216. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  217. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  218. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  219. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  220. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  221. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  222. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  223. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +3 -2
  224. package/packages/pi-coding-agent/src/core/agent-session.ts +11 -0
  225. package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -0
  226. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  227. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  228. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  229. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  230. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  231. package/packages/pi-coding-agent/src/core/model-registry.ts +102 -10
  232. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  233. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  234. package/packages/pi-coding-agent/src/core/session-manager.test.ts +65 -1
  235. package/packages/pi-coding-agent/src/core/session-manager.ts +10 -6
  236. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  237. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
  238. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  239. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  240. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  241. package/scripts/link-workspace-packages.cjs +1 -0
  242. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  243. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  244. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  245. package/src/resources/extensions/gsd/auto/loop-deps.ts +14 -0
  246. package/src/resources/extensions/gsd/auto/loop.ts +9 -0
  247. package/src/resources/extensions/gsd/auto/phases.ts +82 -4
  248. package/src/resources/extensions/gsd/auto/run-unit.ts +40 -2
  249. package/src/resources/extensions/gsd/auto/session.ts +35 -2
  250. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -3
  251. package/src/resources/extensions/gsd/auto-model-selection.ts +17 -2
  252. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  253. package/src/resources/extensions/gsd/auto-prompts.ts +28 -1
  254. package/src/resources/extensions/gsd/auto-recovery.ts +26 -1
  255. package/src/resources/extensions/gsd/auto-start.ts +60 -68
  256. package/src/resources/extensions/gsd/auto-worktree.ts +62 -63
  257. package/src/resources/extensions/gsd/auto.ts +73 -28
  258. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  259. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  260. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  261. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  262. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +54 -6
  263. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +35 -2
  264. package/src/resources/extensions/gsd/clean-root-preflight.ts +111 -0
  265. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  266. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  267. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  268. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  269. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  270. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  271. package/src/resources/extensions/gsd/gitignore.ts +1 -1
  272. package/src/resources/extensions/gsd/gsd-db.ts +157 -33
  273. package/src/resources/extensions/gsd/guided-flow.ts +222 -1
  274. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  275. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  276. package/src/resources/extensions/gsd/journal.ts +2 -1
  277. package/src/resources/extensions/gsd/key-manager.ts +28 -0
  278. package/src/resources/extensions/gsd/model-router.ts +42 -1
  279. package/src/resources/extensions/gsd/pre-execution-checks.ts +46 -10
  280. package/src/resources/extensions/gsd/preferences-types.ts +46 -0
  281. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  282. package/src/resources/extensions/gsd/preferences.ts +17 -17
  283. package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
  284. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  285. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  286. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  287. package/src/resources/extensions/gsd/safety/evidence-collector.ts +119 -0
  288. package/src/resources/extensions/gsd/safety/file-change-validator.ts +17 -4
  289. package/src/resources/extensions/gsd/safety/safety-harness.ts +9 -0
  290. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +188 -2
  291. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +12 -0
  292. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +12 -0
  293. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
  294. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +141 -0
  295. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  296. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  297. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +23 -0
  298. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +186 -0
  299. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  300. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +61 -1
  301. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  302. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  303. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  304. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  305. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  306. package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +1 -1
  307. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
  308. package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
  309. package/src/resources/extensions/gsd/tests/exec-history.test.ts +237 -0
  310. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  311. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +58 -0
  312. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +447 -1
  313. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  314. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +1 -0
  315. package/src/resources/extensions/gsd/tests/integration/gitignore-tracked-gsd.test.ts +1 -0
  316. package/src/resources/extensions/gsd/tests/integration/idle-recovery.test.ts +30 -0
  317. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  318. package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
  319. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +2 -0
  320. package/src/resources/extensions/gsd/tests/key-manager.test.ts +9 -0
  321. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  322. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +12 -0
  323. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  324. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
  325. package/src/resources/extensions/gsd/tests/plan-gate-failed-doctor-heal-hint.test.ts +37 -0
  326. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  327. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +272 -0
  328. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +356 -0
  329. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  330. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  331. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  332. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  333. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  334. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  335. package/src/resources/extensions/gsd/tests/resume-dispatch-worktree.test.ts +230 -0
  336. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +205 -0
  337. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  338. package/src/resources/extensions/gsd/tests/schema-v21-sequence.test.ts +413 -0
  339. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  340. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
  341. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  342. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  343. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +23 -0
  344. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  345. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +35 -0
  346. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +6 -1
  347. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -5
  348. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  349. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  350. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  351. package/src/resources/extensions/gsd/tools/complete-milestone.ts +15 -9
  352. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  353. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  354. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  355. package/src/resources/extensions/gsd/uok/plan-v2.ts +26 -3
  356. package/src/resources/extensions/gsd/workflow-logger.ts +3 -1
  357. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  358. package/src/resources/extensions/gsd/worktree-resolver.ts +54 -9
  359. package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
  360. package/src/resources/skills/write-docs/SKILL.md +2 -1
  361. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → JgU2F-5N9mTyB7kUSSk9A}/_buildManifest.js +0 -0
  362. /package/dist/web/standalone/.next/static/{YnUwu2WWaT0_hyTLUF4nq → JgU2F-5N9mTyB7kUSSk9A}/_ssgManifest.js +0 -0
@@ -0,0 +1,916 @@
1
+ /**
2
+ * Remote Questions — self-contained MCP-server adapter
3
+ *
4
+ * Mirrors the routing logic from src/resources/extensions/ask-user-questions.ts
5
+ * but without any dependency on @gsd/pi-coding-agent or the main src/ tree.
6
+ * All channel adapters (Discord, Slack, Telegram), config resolution, HTTP
7
+ * calls, and polling are inlined here so packages/mcp-server remains a
8
+ * standalone package.
9
+ *
10
+ * Entry points consumed by server.ts:
11
+ * isRemoteConfigured() — cheap synchronous config check
12
+ * tryRemoteQuestions(...) — dispatch + poll + return result
13
+ */
14
+
15
+ import { readFileSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import { randomUUID } from 'node:crypto';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ type RemoteChannel = 'slack' | 'discord' | 'telegram';
25
+
26
+ interface QuestionOption {
27
+ label: string;
28
+ description: string;
29
+ }
30
+
31
+ export interface RemoteQuestion {
32
+ id: string;
33
+ header: string;
34
+ question: string;
35
+ options: QuestionOption[];
36
+ allowMultiple?: boolean;
37
+ }
38
+
39
+ interface RemotePrompt {
40
+ id: string;
41
+ channel: RemoteChannel;
42
+ createdAt: number;
43
+ timeoutAt: number;
44
+ pollIntervalMs: number;
45
+ questions: RemoteQuestion[];
46
+ context: { source: string };
47
+ }
48
+
49
+ interface RemotePromptRef {
50
+ id: string;
51
+ channel: RemoteChannel;
52
+ messageId: string;
53
+ channelId: string;
54
+ threadTs?: string;
55
+ threadUrl?: string;
56
+ }
57
+
58
+ interface RemoteAnswer {
59
+ answers: Record<string, { answers: string[]; user_note?: string }>;
60
+ }
61
+
62
+ export interface RemoteToolResult {
63
+ content: Array<{ type: 'text'; text: string }>;
64
+ details?: Record<string, unknown>;
65
+ }
66
+
67
+ interface ResolvedConfig {
68
+ channel: RemoteChannel;
69
+ channelId: string;
70
+ timeoutMs: number;
71
+ pollIntervalMs: number;
72
+ token: string;
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Constants
77
+ // ---------------------------------------------------------------------------
78
+
79
+ const PER_REQUEST_TIMEOUT_MS = 15_000;
80
+ const DISCORD_API = 'https://discord.com/api/v10';
81
+ const SLACK_API = 'https://slack.com/api';
82
+ const TELEGRAM_API = 'https://api.telegram.org';
83
+
84
+ const DISCORD_NUMBER_EMOJIS = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣'];
85
+ const SLACK_NUMBER_REACTION_NAMES = ['one', 'two', 'three', 'four', 'five'];
86
+
87
+ const DEFAULT_TIMEOUT_MINUTES = 5;
88
+ const DEFAULT_POLL_INTERVAL_SECONDS = 5;
89
+ const MIN_TIMEOUT_MINUTES = 1;
90
+ const MAX_TIMEOUT_MINUTES = 30;
91
+ const MIN_POLL_INTERVAL_SECONDS = 2;
92
+ const MAX_POLL_INTERVAL_SECONDS = 30;
93
+
94
+ const CHANNEL_ID_PATTERNS: Record<RemoteChannel, RegExp> = {
95
+ slack: /^[A-Z0-9]{9,12}$/,
96
+ discord: /^\d{17,20}$/,
97
+ telegram: /^-?\d{5,20}$/,
98
+ };
99
+
100
+ const ENV_KEYS: Record<RemoteChannel, string> = {
101
+ slack: 'SLACK_BOT_TOKEN',
102
+ discord: 'DISCORD_BOT_TOKEN',
103
+ telegram: 'TELEGRAM_BOT_TOKEN',
104
+ };
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Config resolution — reads ~/.gsd/PREFERENCES.md YAML frontmatter
108
+ // ---------------------------------------------------------------------------
109
+
110
+ function clampNumber(value: unknown, fallback: number, min: number, max: number): number {
111
+ const n = typeof value === 'number' ? value : Number(value);
112
+ if (!Number.isFinite(n)) return fallback;
113
+ return Math.max(min, Math.min(max, n));
114
+ }
115
+
116
+ /**
117
+ * Minimal YAML frontmatter reader. Handles:
118
+ * ---
119
+ * key: value
120
+ * nested_key:
121
+ * child: value
122
+ * ---
123
+ * Sufficient for the flat remote_questions config block.
124
+ */
125
+ function parseSimpleFrontmatter(content: string): Record<string, unknown> {
126
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/m);
127
+ if (!match) return {};
128
+
129
+ const yaml = match[1];
130
+ const result: Record<string, unknown> = {};
131
+ let currentSection: string | null = null;
132
+ const sectionData: Record<string, Record<string, unknown>> = {};
133
+
134
+ for (const rawLine of yaml.split('\n')) {
135
+ const line = rawLine.replace(/\r$/, '');
136
+ if (!line.trim() || line.trim().startsWith('#')) continue;
137
+
138
+ // Top-level key (no indent)
139
+ const topMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)$/);
140
+ if (topMatch) {
141
+ currentSection = topMatch[1];
142
+ const val = topMatch[2].trim();
143
+ if (val) {
144
+ result[currentSection] = parseSimpleScalar(val);
145
+ currentSection = null; // scalar, no children
146
+ } else {
147
+ sectionData[currentSection] = {};
148
+ result[currentSection] = sectionData[currentSection];
149
+ }
150
+ continue;
151
+ }
152
+
153
+ // Indented child key
154
+ const childMatch = line.match(/^\s+([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)$/);
155
+ if (childMatch && currentSection && sectionData[currentSection]) {
156
+ const childKey = childMatch[1];
157
+ const childVal = childMatch[2].trim();
158
+ sectionData[currentSection][childKey] = parseSimpleScalar(childVal);
159
+ }
160
+ }
161
+
162
+ return result;
163
+ }
164
+
165
+ function parseSimpleScalar(raw: string): string | number | boolean | null {
166
+ const s = raw.replace(/^["']|["']$/g, '').trim();
167
+ if (s === 'true') return true;
168
+ if (s === 'false') return false;
169
+ if (s === 'null' || s === '~') return null;
170
+ const n = Number(s);
171
+ if (s !== '' && !Number.isNaN(n)) return n;
172
+ return s;
173
+ }
174
+
175
+ function loadPreferencesFromFile(path: string): Record<string, unknown> | null {
176
+ try {
177
+ const content = readFileSync(path, 'utf-8');
178
+ return parseSimpleFrontmatter(content);
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ function resolveRemoteConfig(): ResolvedConfig | null {
185
+ const gsdHome = process.env['GSD_HOME'] ?? join(homedir(), '.gsd');
186
+ const globalPath = join(gsdHome, 'PREFERENCES.md');
187
+
188
+ const prefs = loadPreferencesFromFile(globalPath);
189
+ if (!prefs) return null;
190
+
191
+ const rq = prefs['remote_questions'] as Record<string, unknown> | undefined;
192
+ if (!rq || !rq['channel'] || !rq['channel_id']) return null;
193
+
194
+ const channel = String(rq['channel']) as RemoteChannel;
195
+ if (channel !== 'slack' && channel !== 'discord' && channel !== 'telegram') return null;
196
+
197
+ const channelId = String(rq['channel_id']);
198
+ if (!CHANNEL_ID_PATTERNS[channel].test(channelId)) return null;
199
+
200
+ const token = process.env[ENV_KEYS[channel]];
201
+ if (!token) return null;
202
+
203
+ const timeoutMs = clampNumber(rq['timeout_minutes'], DEFAULT_TIMEOUT_MINUTES, MIN_TIMEOUT_MINUTES, MAX_TIMEOUT_MINUTES) * 60 * 1000;
204
+ const pollIntervalMs = clampNumber(rq['poll_interval_seconds'], DEFAULT_POLL_INTERVAL_SECONDS, MIN_POLL_INTERVAL_SECONDS, MAX_POLL_INTERVAL_SECONDS) * 1000;
205
+
206
+ return { channel, channelId, timeoutMs, pollIntervalMs, token };
207
+ }
208
+
209
+ /**
210
+ * Cheap synchronous check — does not make any HTTP requests.
211
+ */
212
+ export function isRemoteConfigured(): boolean {
213
+ return resolveRemoteConfig() !== null;
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // HTTP helper
218
+ // ---------------------------------------------------------------------------
219
+
220
+ async function apiRequest(
221
+ url: string,
222
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE',
223
+ body: unknown,
224
+ authScheme: 'Bearer' | 'Bot',
225
+ authToken: string,
226
+ errorLabel: string,
227
+ ): Promise<unknown> {
228
+ const headers: Record<string, string> = {
229
+ Authorization: `${authScheme} ${authToken}`,
230
+ };
231
+
232
+ const init: RequestInit = {
233
+ method,
234
+ headers,
235
+ signal: AbortSignal.timeout(PER_REQUEST_TIMEOUT_MS),
236
+ };
237
+
238
+ if (body !== undefined) {
239
+ headers['Content-Type'] = 'application/json';
240
+ init.body = JSON.stringify(body);
241
+ }
242
+
243
+ const response = await fetch(url, init);
244
+
245
+ if (response.status === 204) return {};
246
+
247
+ if (!response.ok) {
248
+ const text = await response.text().catch(() => '');
249
+ const safeText = text.length > 200 ? text.slice(0, 200) + '\u2026' : text;
250
+ throw new Error(`${errorLabel} HTTP ${response.status}: ${safeText}`);
251
+ }
252
+
253
+ return response.json();
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Payload formatting
258
+ // ---------------------------------------------------------------------------
259
+
260
+ function formatForDiscord(prompt: RemotePrompt): { embeds: unknown[]; reactionEmojis: string[] } {
261
+ const reactionEmojis: string[] = [];
262
+ const embeds = prompt.questions.map((q, questionIndex) => {
263
+ const supportsReactions = prompt.questions.length === 1;
264
+ const optionLines = q.options.map((opt, i) => {
265
+ const emoji = DISCORD_NUMBER_EMOJIS[i] ?? `${i + 1}.`;
266
+ if (supportsReactions && DISCORD_NUMBER_EMOJIS[i]) reactionEmojis.push(DISCORD_NUMBER_EMOJIS[i]);
267
+ return `${emoji} **${opt.label}** — ${opt.description}`;
268
+ });
269
+
270
+ const footerParts: string[] = [];
271
+ if (supportsReactions) {
272
+ footerParts.push(q.allowMultiple
273
+ ? 'Reply with comma-separated choices (`1,3`) or react with matching numbers'
274
+ : 'Reply with a number or react with the matching number');
275
+ } else {
276
+ footerParts.push(`Question ${questionIndex + 1}/${prompt.questions.length} — reply with one line per question or use semicolons`);
277
+ }
278
+ footerParts.push(`Source: ${prompt.context.source}`);
279
+
280
+ return {
281
+ title: q.header,
282
+ description: q.question,
283
+ color: 0x7c3aed,
284
+ fields: [{ name: 'Options', value: optionLines.join('\n') }],
285
+ footer: { text: footerParts.join(' · ') },
286
+ };
287
+ });
288
+
289
+ return { embeds, reactionEmojis };
290
+ }
291
+
292
+ function formatForSlack(prompt: RemotePrompt): unknown[] {
293
+ const blocks: unknown[] = [
294
+ { type: 'header', text: { type: 'plain_text', text: 'GSD needs your input' } },
295
+ ];
296
+
297
+ if (prompt.questions.length > 1) {
298
+ blocks.push({
299
+ type: 'context',
300
+ elements: [{ type: 'mrkdwn', text: 'Reply once in thread using one line per question or semicolons (`1; 2; custom note`).' }],
301
+ });
302
+ }
303
+
304
+ for (const q of prompt.questions) {
305
+ const supportsReactions = prompt.questions.length === 1;
306
+ blocks.push({ type: 'section', text: { type: 'mrkdwn', text: `*${q.header}*\n${q.question}` } });
307
+ blocks.push({
308
+ type: 'section',
309
+ text: { type: 'mrkdwn', text: q.options.map((opt, i) => `${i + 1}. *${opt.label}* — ${opt.description}`).join('\n') },
310
+ });
311
+ blocks.push({
312
+ type: 'context',
313
+ elements: [{
314
+ type: 'mrkdwn',
315
+ text: prompt.questions.length > 1
316
+ ? (q.allowMultiple ? 'For this question, use comma-separated numbers (`1,3`) or free text.' : 'For this question, use one number (`1`) or free text.')
317
+ : (q.allowMultiple
318
+ ? (supportsReactions ? 'Reply in thread with comma-separated numbers (`1,3`) or react with matching number emoji.' : 'Reply in thread with comma-separated numbers (`1,3`) or free text.')
319
+ : (supportsReactions ? 'Reply in thread with a number (`1`) or react with the matching number emoji.' : 'Reply in thread with a number (`1`) or free text.')),
320
+ }],
321
+ });
322
+ blocks.push({ type: 'divider' });
323
+ }
324
+
325
+ return blocks;
326
+ }
327
+
328
+ function formatForTelegram(prompt: RemotePrompt): { text: string; parse_mode: 'HTML'; reply_markup?: unknown } {
329
+ const escape = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
330
+ const lines: string[] = ['<b>GSD needs your input</b>', ''];
331
+
332
+ for (let qi = 0; qi < prompt.questions.length; qi++) {
333
+ const q = prompt.questions[qi];
334
+ lines.push(`<b>${escape(q.header)}</b>`);
335
+ lines.push(escape(q.question));
336
+ lines.push('');
337
+ for (let i = 0; i < q.options.length; i++) {
338
+ lines.push(`${i + 1}. <b>${escape(q.options[i].label)}</b> — ${escape(q.options[i].description)}`);
339
+ }
340
+ lines.push('');
341
+ if (prompt.questions.length === 1) {
342
+ lines.push(q.allowMultiple ? 'Reply with comma-separated numbers (1,3) or free text.' : 'Reply with a number or tap a button below.');
343
+ } else {
344
+ lines.push(`Question ${qi + 1}/${prompt.questions.length} — reply with one line per question or use semicolons.`);
345
+ }
346
+ if (qi < prompt.questions.length - 1) lines.push('');
347
+ }
348
+
349
+ const result: { text: string; parse_mode: 'HTML'; reply_markup?: unknown } = {
350
+ text: lines.join('\n'),
351
+ parse_mode: 'HTML',
352
+ };
353
+
354
+ if (prompt.questions.length === 1 && prompt.questions[0].options.length <= 5) {
355
+ result.reply_markup = {
356
+ inline_keyboard: prompt.questions[0].options.map((opt, i) => [{
357
+ text: `${i + 1}. ${opt.label}`,
358
+ callback_data: `${prompt.id}:${i}`,
359
+ }]),
360
+ };
361
+ }
362
+
363
+ return result;
364
+ }
365
+
366
+ // ---------------------------------------------------------------------------
367
+ // Response parsing
368
+ // ---------------------------------------------------------------------------
369
+
370
+ function parseAnswerForQuestion(text: string, q: RemoteQuestion): { answers: string[]; user_note?: string } {
371
+ if (!text) return { answers: [], user_note: 'No response provided' };
372
+
373
+ if (/^[\d,\s]+$/.test(text)) {
374
+ const nums = text
375
+ .split(',')
376
+ .map((s) => parseInt(s.trim(), 10))
377
+ .filter((n) => !Number.isNaN(n) && n >= 1 && n <= q.options.length);
378
+ if (nums.length > 0) {
379
+ const selected = nums.map((n) => q.options[n - 1].label);
380
+ return { answers: q.allowMultiple ? selected : [selected[0]] };
381
+ }
382
+ }
383
+
384
+ const single = parseInt(text, 10);
385
+ if (!Number.isNaN(single) && single >= 1 && single <= q.options.length) {
386
+ return { answers: [q.options[single - 1].label] };
387
+ }
388
+
389
+ const truncated = text.length > 500 ? text.slice(0, 500) + '\u2026' : text;
390
+ return { answers: [], user_note: truncated };
391
+ }
392
+
393
+ function parseTextReply(text: string, questions: RemoteQuestion[]): RemoteAnswer {
394
+ const answers: RemoteAnswer['answers'] = {};
395
+ const trimmed = text.trim();
396
+
397
+ if (questions.length === 1) {
398
+ answers[questions[0].id] = parseAnswerForQuestion(trimmed, questions[0]);
399
+ return { answers };
400
+ }
401
+
402
+ const parts = trimmed.includes(';')
403
+ ? trimmed.split(';').map((s) => s.trim()).filter(Boolean)
404
+ : trimmed.split('\n').map((s) => s.trim()).filter(Boolean);
405
+
406
+ for (let i = 0; i < questions.length; i++) {
407
+ answers[questions[i].id] = parseAnswerForQuestion(parts[i] ?? '', questions[i]);
408
+ }
409
+
410
+ return { answers };
411
+ }
412
+
413
+ function parseDiscordReactions(
414
+ reactions: Array<{ emoji: string; count: number }>,
415
+ questions: RemoteQuestion[],
416
+ ): RemoteAnswer {
417
+ const answers: RemoteAnswer['answers'] = {};
418
+ if (questions.length !== 1) {
419
+ for (const q of questions) {
420
+ answers[q.id] = { answers: [], user_note: 'Discord reactions are only supported for single-question prompts' };
421
+ }
422
+ return { answers };
423
+ }
424
+
425
+ const q = questions[0];
426
+ const picked = reactions
427
+ .filter((r) => DISCORD_NUMBER_EMOJIS.includes(r.emoji) && r.count > 0)
428
+ .map((r) => q.options[DISCORD_NUMBER_EMOJIS.indexOf(r.emoji)]?.label)
429
+ .filter((l): l is string => Boolean(l));
430
+
431
+ answers[q.id] = picked.length > 0
432
+ ? { answers: q.allowMultiple ? picked : [picked[0]] }
433
+ : { answers: [], user_note: 'No clear response via reactions' };
434
+
435
+ return { answers };
436
+ }
437
+
438
+ function parseSlackReactions(reactionNames: string[], questions: RemoteQuestion[]): RemoteAnswer {
439
+ const answers: RemoteAnswer['answers'] = {};
440
+ if (questions.length !== 1) {
441
+ for (const q of questions) {
442
+ answers[q.id] = { answers: [], user_note: 'Slack reactions are only supported for single-question prompts' };
443
+ }
444
+ return { answers };
445
+ }
446
+
447
+ const q = questions[0];
448
+ const picked = reactionNames
449
+ .filter((name) => SLACK_NUMBER_REACTION_NAMES.includes(name))
450
+ .map((name) => q.options[SLACK_NUMBER_REACTION_NAMES.indexOf(name)]?.label)
451
+ .filter((l): l is string => Boolean(l));
452
+
453
+ answers[q.id] = picked.length > 0
454
+ ? { answers: q.allowMultiple ? picked : [picked[0]] }
455
+ : { answers: [], user_note: 'No clear response via reactions' };
456
+
457
+ return { answers };
458
+ }
459
+
460
+ function parseTelegramCallbackData(callbackData: string, questions: RemoteQuestion[], promptId: string): RemoteAnswer | null {
461
+ const pattern = new RegExp(`^${promptId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}:(\\d+)$`);
462
+ const match = callbackData.match(pattern);
463
+ if (match && questions.length === 1) {
464
+ const idx = parseInt(match[1], 10);
465
+ const q = questions[0];
466
+ if (idx >= 0 && idx < q.options.length) {
467
+ return { answers: { [q.id]: { answers: [q.options[idx].label] } } };
468
+ }
469
+ }
470
+ return null;
471
+ }
472
+
473
+ // ---------------------------------------------------------------------------
474
+ // Channel adapters
475
+ // ---------------------------------------------------------------------------
476
+
477
+ interface DispatchResult {
478
+ ref: RemotePromptRef;
479
+ }
480
+
481
+ // --- Discord ---
482
+
483
+ async function discordValidate(token: string, channelId: string): Promise<{ botUserId: string; guildId: string | null }> {
484
+ const meRes = await apiRequest(`${DISCORD_API}/users/@me`, 'GET', undefined, 'Bot', token, 'Discord API') as Record<string, unknown>;
485
+ if (!meRes['id']) throw new Error('Discord auth failed: invalid token');
486
+ const botUserId = String(meRes['id']);
487
+
488
+ let guildId: string | null = null;
489
+ try {
490
+ const chanRes = await apiRequest(`${DISCORD_API}/channels/${channelId}`, 'GET', undefined, 'Bot', token, 'Discord API') as Record<string, unknown>;
491
+ if (chanRes['guild_id']) guildId = String(chanRes['guild_id']);
492
+ } catch { /* non-fatal */ }
493
+
494
+ return { botUserId, guildId };
495
+ }
496
+
497
+ async function discordSend(prompt: RemotePrompt, token: string, channelId: string, guildId: string | null): Promise<DispatchResult> {
498
+ const { embeds, reactionEmojis } = formatForDiscord(prompt);
499
+ const res = await apiRequest(
500
+ `${DISCORD_API}/channels/${channelId}/messages`,
501
+ 'POST',
502
+ { content: '**GSD needs your input** — reply to this message with your answer', embeds },
503
+ 'Bot', token, 'Discord API',
504
+ ) as Record<string, unknown>;
505
+
506
+ if (!res['id']) throw new Error(`Discord send failed: ${JSON.stringify(res)}`);
507
+ const messageId = String(res['id']);
508
+
509
+ if (prompt.questions.length === 1) {
510
+ for (const emoji of reactionEmojis) {
511
+ try {
512
+ await apiRequest(`${DISCORD_API}/channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`, 'PUT', undefined, 'Bot', token, 'Discord API');
513
+ } catch { /* best-effort */ }
514
+ }
515
+ }
516
+
517
+ const threadUrl = guildId ? `https://discord.com/channels/${guildId}/${channelId}/${messageId}` : undefined;
518
+ return { ref: { id: prompt.id, channel: 'discord', messageId, channelId, threadUrl } };
519
+ }
520
+
521
+ async function discordPoll(prompt: RemotePrompt, ref: RemotePromptRef, token: string, botUserId: string): Promise<RemoteAnswer | null> {
522
+ // Try reactions first for single-question prompts
523
+ if (prompt.questions.length === 1) {
524
+ const reactions: Array<{ emoji: string; count: number }> = [];
525
+ for (const emoji of DISCORD_NUMBER_EMOJIS) {
526
+ try {
527
+ const users = await apiRequest(
528
+ `${DISCORD_API}/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent(emoji)}`,
529
+ 'GET', undefined, 'Bot', token, 'Discord API',
530
+ ) as unknown[];
531
+ if (Array.isArray(users)) {
532
+ const humanUsers = users.filter((u) => (u as Record<string, unknown>)['id'] !== botUserId);
533
+ if (humanUsers.length > 0) reactions.push({ emoji, count: humanUsers.length });
534
+ }
535
+ } catch (err) {
536
+ const msg = String((err as Error).message ?? '');
537
+ if (msg.includes('HTTP 404')) continue;
538
+ if (msg.includes('HTTP 401') || msg.includes('HTTP 403')) throw err;
539
+ }
540
+ }
541
+ if (reactions.length > 0) return parseDiscordReactions(reactions, prompt.questions);
542
+ }
543
+
544
+ // Try text replies
545
+ const messages = await apiRequest(
546
+ `${DISCORD_API}/channels/${ref.channelId}/messages?after=${ref.messageId}&limit=10`,
547
+ 'GET', undefined, 'Bot', token, 'Discord API',
548
+ ) as unknown[];
549
+
550
+ if (!Array.isArray(messages)) return null;
551
+
552
+ const replies = messages.filter((m) => {
553
+ const msg = m as Record<string, unknown>;
554
+ const author = msg['author'] as Record<string, unknown> | undefined;
555
+ const msgRef = msg['message_reference'] as Record<string, unknown> | undefined;
556
+ return author?.['id'] && author['id'] !== botUserId && msgRef?.['message_id'] === ref.messageId && msg['content'];
557
+ });
558
+
559
+ if (replies.length === 0) return null;
560
+ const first = replies[0] as Record<string, unknown>;
561
+ return parseTextReply(String(first['content']), prompt.questions);
562
+ }
563
+
564
+ async function discordAcknowledge(ref: RemotePromptRef, token: string): Promise<void> {
565
+ try {
566
+ await apiRequest(
567
+ `${DISCORD_API}/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent('✅')}/@me`,
568
+ 'PUT', undefined, 'Bot', token, 'Discord API',
569
+ );
570
+ } catch { /* best-effort */ }
571
+ }
572
+
573
+ // --- Slack ---
574
+
575
+ async function slackValidate(token: string): Promise<string> {
576
+ const res = await apiRequest(`${SLACK_API}/auth.test`, 'GET', undefined, 'Bearer', token, 'Slack API') as Record<string, unknown>;
577
+ if (!res['ok']) throw new Error(`Slack auth failed: ${res['error'] ?? 'invalid token'}`);
578
+ return String(res['user_id'] ?? '');
579
+ }
580
+
581
+ async function slackSend(prompt: RemotePrompt, token: string, channelId: string): Promise<DispatchResult> {
582
+ const res = await apiRequest(
583
+ `${SLACK_API}/chat.postMessage`,
584
+ 'POST',
585
+ { channel: channelId, text: 'GSD needs your input', blocks: formatForSlack(prompt) },
586
+ 'Bearer', token, 'Slack API',
587
+ ) as Record<string, unknown>;
588
+
589
+ if (!res['ok']) throw new Error(`Slack postMessage failed: ${res['error'] ?? 'unknown'}`);
590
+
591
+ const ts = String(res['ts']);
592
+ const channel = String(res['channel']);
593
+
594
+ if (prompt.questions.length === 1) {
595
+ const reactionNames = SLACK_NUMBER_REACTION_NAMES.slice(0, prompt.questions[0].options.length);
596
+ for (const name of reactionNames) {
597
+ try {
598
+ await apiRequest(`${SLACK_API}/reactions.add`, 'POST', { channel, timestamp: ts, name }, 'Bearer', token, 'Slack API');
599
+ } catch { /* best-effort */ }
600
+ }
601
+ }
602
+
603
+ return {
604
+ ref: {
605
+ id: prompt.id,
606
+ channel: 'slack',
607
+ messageId: ts,
608
+ threadTs: ts,
609
+ channelId: channel,
610
+ threadUrl: `https://slack.com/archives/${channel}/p${ts.replace('.', '')}`,
611
+ },
612
+ };
613
+ }
614
+
615
+ async function slackPoll(prompt: RemotePrompt, ref: RemotePromptRef, token: string, botUserId: string): Promise<RemoteAnswer | null> {
616
+ // Check reactions for single-question prompts
617
+ if (prompt.questions.length === 1) {
618
+ const qs = new URLSearchParams({ channel: ref.channelId, timestamp: ref.messageId, full: 'true' }).toString();
619
+ const res = await apiRequest(`${SLACK_API}/reactions.get?${qs}`, 'GET', undefined, 'Bearer', token, 'Slack API') as Record<string, unknown>;
620
+
621
+ if (res['ok']) {
622
+ const message = (res['message'] ?? {}) as { reactions?: Array<{ name?: string; count?: number; users?: string[] }> };
623
+ const reactions = Array.isArray(message.reactions) ? message.reactions : [];
624
+ const picked = reactions
625
+ .filter((r) => r.name && SLACK_NUMBER_REACTION_NAMES.includes(r.name))
626
+ .filter((r) => {
627
+ const count = Number(r.count ?? 0);
628
+ const users = Array.isArray(r.users) ? r.users.map(String) : [];
629
+ const botIncluded = botUserId ? users.includes(botUserId) : false;
630
+ return count > (botIncluded ? 1 : 0);
631
+ })
632
+ .map((r) => String(r.name));
633
+
634
+ if (picked.length > 0) return parseSlackReactions(picked, prompt.questions);
635
+ }
636
+ }
637
+
638
+ // Check thread replies
639
+ const qs = new URLSearchParams({ channel: ref.channelId, ts: ref.threadTs!, limit: '20' }).toString();
640
+ const res = await apiRequest(`${SLACK_API}/conversations.replies?${qs}`, 'GET', undefined, 'Bearer', token, 'Slack API') as Record<string, unknown>;
641
+
642
+ if (!res['ok']) return null;
643
+
644
+ const messages = (res['messages'] ?? []) as Array<{ user?: string; text?: string; ts: string }>;
645
+ const userReplies = messages.filter((m) => m.ts !== ref.threadTs && m.user && m.user !== botUserId && m.text);
646
+ if (userReplies.length === 0) return null;
647
+
648
+ return parseTextReply(String(userReplies[0].text), prompt.questions);
649
+ }
650
+
651
+ async function slackAcknowledge(ref: RemotePromptRef, token: string): Promise<void> {
652
+ try {
653
+ await apiRequest(
654
+ `${SLACK_API}/reactions.add`,
655
+ 'POST',
656
+ { channel: ref.channelId, timestamp: ref.messageId, name: 'white_check_mark' },
657
+ 'Bearer', token, 'Slack API',
658
+ );
659
+ } catch { /* best-effort */ }
660
+ }
661
+
662
+ // --- Telegram ---
663
+
664
+ async function telegramValidate(token: string): Promise<number> {
665
+ const res = await apiRequest(`${TELEGRAM_API}/bot${token}/getMe`, 'GET', undefined, 'Bearer', token, 'Telegram API') as Record<string, unknown>;
666
+ const result = res['result'] as Record<string, unknown> | undefined;
667
+ if (!res['ok'] || !result?.['id']) throw new Error('Telegram auth failed: invalid bot token');
668
+ return result['id'] as number;
669
+ }
670
+
671
+ async function telegramSend(prompt: RemotePrompt, token: string, chatId: string): Promise<DispatchResult> {
672
+ const payload = formatForTelegram(prompt);
673
+ const params: Record<string, unknown> = { chat_id: chatId, text: payload.text, parse_mode: payload.parse_mode };
674
+ if (payload.reply_markup) params['reply_markup'] = payload.reply_markup;
675
+
676
+ const res = await apiRequest(`${TELEGRAM_API}/bot${token}/sendMessage`, 'POST', params, 'Bearer', token, 'Telegram API') as Record<string, unknown>;
677
+ const result = res['result'] as Record<string, unknown> | undefined;
678
+ if (!res['ok'] || !result?.['message_id']) throw new Error(`Telegram sendMessage failed: ${JSON.stringify(res)}`);
679
+
680
+ const messageId = String(result['message_id']);
681
+ // Build public URL only for public channels (negative IDs are private groups)
682
+ const isPublic = !chatId.startsWith('-');
683
+ const messageUrl = isPublic ? `https://t.me/${chatId.replace('@', '')}/${messageId}` : undefined;
684
+
685
+ return { ref: { id: prompt.id, channel: 'telegram', messageId, channelId: chatId, threadUrl: messageUrl } };
686
+ }
687
+
688
+ async function telegramPoll(
689
+ prompt: RemotePrompt,
690
+ ref: RemotePromptRef,
691
+ token: string,
692
+ botUserId: number,
693
+ lastUpdateId: { value: number },
694
+ ): Promise<RemoteAnswer | null> {
695
+ const params: Record<string, unknown> = {
696
+ offset: lastUpdateId.value + 1,
697
+ timeout: 0,
698
+ allowed_updates: ['message', 'callback_query'],
699
+ };
700
+
701
+ const res = await apiRequest(`${TELEGRAM_API}/bot${token}/getUpdates`, 'POST', params, 'Bearer', token, 'Telegram API') as Record<string, unknown>;
702
+ if (!res['ok'] || !Array.isArray(res['result'])) return null;
703
+
704
+ for (const update of res['result'] as Record<string, unknown>[]) {
705
+ if ((update['update_id'] as number) > lastUpdateId.value) {
706
+ lastUpdateId.value = update['update_id'] as number;
707
+ }
708
+
709
+ // Callback query (inline keyboard button press)
710
+ if (update['callback_query']) {
711
+ const cq = update['callback_query'] as Record<string, unknown>;
712
+ const msg = cq['message'] as Record<string, unknown> | undefined;
713
+ const from = cq['from'] as Record<string, unknown> | undefined;
714
+ if (msg && String((msg['chat'] as Record<string, unknown>)?.['id']) === ref.channelId &&
715
+ String(msg['message_id']) === ref.messageId && from?.['id'] !== botUserId) {
716
+ // Dismiss loading spinner
717
+ try {
718
+ await apiRequest(`${TELEGRAM_API}/bot${token}/answerCallbackQuery`, 'POST', { callback_query_id: cq['id'] }, 'Bearer', token, 'Telegram API');
719
+ } catch { /* best-effort */ }
720
+ const callbackData = cq['data'] ? String(cq['data']) : null;
721
+ if (callbackData) {
722
+ const parsed = parseTelegramCallbackData(callbackData, prompt.questions, prompt.id);
723
+ if (parsed) return parsed;
724
+ }
725
+ }
726
+ }
727
+
728
+ // Text message reply
729
+ if (update['message']) {
730
+ const msg = update['message'] as Record<string, unknown>;
731
+ const from = msg['from'] as Record<string, unknown> | undefined;
732
+ if (String((msg['chat'] as Record<string, unknown>)?.['id']) === ref.channelId &&
733
+ from?.['id'] !== botUserId && msg['text']) {
734
+ return parseTextReply(String(msg['text']), prompt.questions);
735
+ }
736
+ }
737
+ }
738
+
739
+ return null;
740
+ }
741
+
742
+ // ---------------------------------------------------------------------------
743
+ // Polling loop
744
+ // ---------------------------------------------------------------------------
745
+
746
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
747
+ return new Promise((resolve) => {
748
+ if (signal?.aborted) return resolve();
749
+ const timer = setTimeout(() => {
750
+ signal?.removeEventListener('abort', onAbort);
751
+ resolve();
752
+ }, ms);
753
+ const onAbort = () => { clearTimeout(timer); resolve(); };
754
+ signal?.addEventListener('abort', onAbort, { once: true });
755
+ });
756
+ }
757
+
758
+ interface ChannelState {
759
+ botUserId: string | number;
760
+ guildId?: string | null; // Discord only
761
+ lastUpdateId?: { value: number }; // Telegram only
762
+ }
763
+
764
+ async function pollUntilDone(
765
+ config: ResolvedConfig,
766
+ prompt: RemotePrompt,
767
+ ref: RemotePromptRef,
768
+ state: ChannelState,
769
+ signal?: AbortSignal,
770
+ ): Promise<RemoteAnswer | null> {
771
+ while (Date.now() < prompt.timeoutAt && !signal?.aborted) {
772
+ try {
773
+ let answer: RemoteAnswer | null = null;
774
+
775
+ if (config.channel === 'discord') {
776
+ answer = await discordPoll(prompt, ref, config.token, String(state.botUserId));
777
+ } else if (config.channel === 'slack') {
778
+ answer = await slackPoll(prompt, ref, config.token, String(state.botUserId));
779
+ } else {
780
+ answer = await telegramPoll(prompt, ref, config.token, state.botUserId as number, state.lastUpdateId!);
781
+ }
782
+
783
+ if (answer) return answer;
784
+ } catch {
785
+ // Non-fatal poll error — wait and retry
786
+ }
787
+
788
+ await sleep(prompt.pollIntervalMs, signal);
789
+ }
790
+
791
+ return null;
792
+ }
793
+
794
+ // ---------------------------------------------------------------------------
795
+ // Public entry point
796
+ // ---------------------------------------------------------------------------
797
+
798
+ function buildPrompt(questions: RemoteQuestion[], config: ResolvedConfig): RemotePrompt {
799
+ const createdAt = Date.now();
800
+ return {
801
+ id: randomUUID(),
802
+ channel: config.channel,
803
+ createdAt,
804
+ timeoutAt: createdAt + config.timeoutMs,
805
+ pollIntervalMs: config.pollIntervalMs,
806
+ context: { source: 'ask_user_questions' },
807
+ questions: questions.map((q) => ({
808
+ id: q.id,
809
+ header: q.header,
810
+ question: q.question,
811
+ options: q.options,
812
+ allowMultiple: q.allowMultiple ?? false,
813
+ })),
814
+ };
815
+ }
816
+
817
+ function formatForTool(answer: RemoteAnswer): Record<string, { answers: string[] }> {
818
+ const out: Record<string, { answers: string[] }> = {};
819
+ for (const [id, data] of Object.entries(answer.answers)) {
820
+ const list = [...data.answers];
821
+ if (data.user_note) list.push(`user_note: ${data.user_note}`);
822
+ out[id] = { answers: list };
823
+ }
824
+ return out;
825
+ }
826
+
827
+ /**
828
+ * Dispatch questions to the configured remote channel and wait for a response.
829
+ *
830
+ * Returns null when no remote channel is configured.
831
+ * Returns a tool result shaped like { content, details } on success or
832
+ * timeout — callers should check details.timed_out before trusting the result.
833
+ */
834
+ export async function tryRemoteQuestions(
835
+ questions: RemoteQuestion[],
836
+ signal?: AbortSignal,
837
+ ): Promise<RemoteToolResult | null> {
838
+ const config = resolveRemoteConfig();
839
+ if (!config) return null;
840
+
841
+ const prompt = buildPrompt(questions, config);
842
+
843
+ // Validate auth and send the prompt
844
+ let ref: RemotePromptRef;
845
+ let state: ChannelState;
846
+
847
+ try {
848
+ if (config.channel === 'discord') {
849
+ const { botUserId, guildId } = await discordValidate(config.token, config.channelId);
850
+ state = { botUserId, guildId };
851
+ const dispatch = await discordSend(prompt, config.token, config.channelId, guildId);
852
+ ref = dispatch.ref;
853
+ } else if (config.channel === 'slack') {
854
+ const botUserId = await slackValidate(config.token);
855
+ state = { botUserId };
856
+ const dispatch = await slackSend(prompt, config.token, config.channelId);
857
+ ref = dispatch.ref;
858
+ } else {
859
+ const botUserId = await telegramValidate(config.token);
860
+ state = { botUserId, lastUpdateId: { value: 0 } };
861
+ const dispatch = await telegramSend(prompt, config.token, config.channelId);
862
+ ref = dispatch.ref;
863
+ }
864
+ } catch (err) {
865
+ return {
866
+ content: [{ type: 'text', text: `Remote questions failed (${config.channel}): ${(err as Error).message}` }],
867
+ details: { remote: true, channel: config.channel, error: true, status: 'failed' },
868
+ };
869
+ }
870
+
871
+ const answer = await pollUntilDone(config, prompt, ref, state, signal);
872
+
873
+ if (!answer) {
874
+ const timedOut = !signal?.aborted;
875
+ return {
876
+ content: [{
877
+ type: 'text',
878
+ text: JSON.stringify({
879
+ timed_out: timedOut,
880
+ channel: config.channel,
881
+ prompt_id: prompt.id,
882
+ timeout_minutes: config.timeoutMs / 60000,
883
+ thread_url: ref.threadUrl ?? null,
884
+ message: `User did not respond within ${config.timeoutMs / 60000} minutes.`,
885
+ }),
886
+ }],
887
+ details: {
888
+ remote: true,
889
+ channel: config.channel,
890
+ timed_out: timedOut,
891
+ promptId: prompt.id,
892
+ threadUrl: ref.threadUrl ?? null,
893
+ status: signal?.aborted ? 'cancelled' : 'timed_out',
894
+ },
895
+ };
896
+ }
897
+
898
+ // Best-effort acknowledgement
899
+ try {
900
+ if (config.channel === 'discord') await discordAcknowledge(ref, config.token);
901
+ else if (config.channel === 'slack') await slackAcknowledge(ref, config.token);
902
+ } catch { /* best-effort */ }
903
+
904
+ return {
905
+ content: [{ type: 'text', text: JSON.stringify({ answers: formatForTool(answer) }) }],
906
+ details: {
907
+ remote: true,
908
+ channel: config.channel,
909
+ timed_out: false,
910
+ promptId: prompt.id,
911
+ threadUrl: ref.threadUrl ?? null,
912
+ questions,
913
+ status: 'answered',
914
+ },
915
+ };
916
+ }