@opengsd/gsd-pi 1.4.0 → 1.5.0

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 (329) hide show
  1. package/README.md +11 -11
  2. package/dist/onboarding.js +11 -0
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/GSD-WORKFLOW.md +20 -25
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +118 -53
  6. package/dist/resources/extensions/cursor-cli/index.js +16 -0
  7. package/dist/resources/extensions/cursor-cli/models.js +63 -0
  8. package/dist/resources/extensions/cursor-cli/package.json +11 -0
  9. package/dist/resources/extensions/cursor-cli/readiness.js +128 -0
  10. package/dist/resources/extensions/cursor-cli/stream-adapter.js +353 -0
  11. package/dist/resources/extensions/gsd/activity-log.js +4 -1
  12. package/dist/resources/extensions/gsd/artifact-verification.js +25 -9
  13. package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
  14. package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
  15. package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
  16. package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
  17. package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
  18. package/dist/resources/extensions/gsd/auto-dispatch.js +15 -5
  19. package/dist/resources/extensions/gsd/auto-model-selection.js +36 -18
  20. package/dist/resources/extensions/gsd/auto-prompts.js +76 -2
  21. package/dist/resources/extensions/gsd/auto-start.js +4 -3
  22. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +4 -0
  23. package/dist/resources/extensions/gsd/auto-worktree.js +38 -11
  24. package/dist/resources/extensions/gsd/auto.js +23 -3
  25. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -0
  26. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +114 -5
  27. package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
  28. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +10 -2
  29. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +14 -1
  30. package/dist/resources/extensions/gsd/codebase-generator.js +157 -19
  31. package/dist/resources/extensions/gsd/commands/handlers/core.js +40 -4
  32. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +153 -1
  33. package/dist/resources/extensions/gsd/commands-worktree.js +11 -2
  34. package/dist/resources/extensions/gsd/db/queries.js +39 -0
  35. package/dist/resources/extensions/gsd/discussion-handoff.js +8 -3
  36. package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -2
  37. package/dist/resources/extensions/gsd/doctor-engine-checks.js +206 -14
  38. package/dist/resources/extensions/gsd/doctor-providers.js +38 -10
  39. package/dist/resources/extensions/gsd/doctor.js +93 -3
  40. package/dist/resources/extensions/gsd/exec-sandbox.js +6 -2
  41. package/dist/resources/extensions/gsd/flat-phase-migration.js +74 -23
  42. package/dist/resources/extensions/gsd/git-service.js +14 -18
  43. package/dist/resources/extensions/gsd/gsd-db.js +52 -0
  44. package/dist/resources/extensions/gsd/guidance.js +1 -0
  45. package/dist/resources/extensions/gsd/guided-flow.js +40 -2
  46. package/dist/resources/extensions/gsd/key-manager.js +1 -0
  47. package/dist/resources/extensions/gsd/markdown-renderer.js +25 -8
  48. package/dist/resources/extensions/gsd/migrate-external.js +24 -7
  49. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
  50. package/dist/resources/extensions/gsd/preferences-diagnostics.js +24 -1
  51. package/dist/resources/extensions/gsd/preferences-models.js +6 -4
  52. package/dist/resources/extensions/gsd/preferences-validation.js +25 -2
  53. package/dist/resources/extensions/gsd/preferences.js +29 -2
  54. package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  55. package/dist/resources/extensions/gsd/redact-secrets.js +37 -0
  56. package/dist/resources/extensions/gsd/repo-identity.js +16 -0
  57. package/dist/resources/extensions/gsd/repository-registry.js +6 -1
  58. package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
  59. package/dist/resources/extensions/gsd/safe-fs.js +23 -2
  60. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  61. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
  62. package/dist/resources/extensions/gsd/tools/plan-task.js +76 -8
  63. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -0
  64. package/dist/resources/extensions/gsd/unit-context-manifest.js +5 -4
  65. package/dist/resources/extensions/gsd/unit-registry.js +27 -2
  66. package/dist/resources/extensions/gsd/unit-runtime.js +21 -0
  67. package/dist/resources/extensions/gsd/workspace.js +26 -8
  68. package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
  69. package/dist/resources/extensions/gsd/worktree-manager.js +97 -4
  70. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  71. package/dist/web/standalone/.next/BUILD_ID +1 -1
  72. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  73. package/dist/web/standalone/.next/build-manifest.json +2 -2
  74. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/index.html +1 -1
  92. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  99. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  100. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  102. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  103. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  104. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  105. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  106. package/dist/wizard.d.ts +8 -0
  107. package/dist/wizard.js +8 -1
  108. package/integrations/hermes/CONTEXT.md +47 -0
  109. package/integrations/hermes/README.md +21 -0
  110. package/integrations/hermes/__init__.py +31 -0
  111. package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
  112. package/integrations/hermes/docs/setup.md +4 -0
  113. package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
  114. package/integrations/hermes/open_gsd_hermes/config.py +2 -0
  115. package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
  116. package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
  117. package/integrations/hermes/plugin.yaml +4 -0
  118. package/integrations/hermes/tests/test_config.py +6 -0
  119. package/integrations/hermes/tests/test_directory_loader.py +37 -0
  120. package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
  121. package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
  122. package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
  123. package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
  124. package/integrations/hermes/tests/test_milestone_client.py +447 -0
  125. package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
  126. package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
  127. package/package.json +8 -8
  128. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts +1 -0
  129. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts.map +1 -1
  130. package/packages/cloud-mcp-gateway/dist/auth-store.js +21 -1
  131. package/packages/cloud-mcp-gateway/dist/auth-store.js.map +1 -1
  132. package/packages/cloud-mcp-gateway/dist/auth-store.test.js +34 -0
  133. package/packages/cloud-mcp-gateway/dist/auth-store.test.js.map +1 -1
  134. package/packages/cloud-mcp-gateway/package.json +2 -2
  135. package/packages/contracts/package.json +1 -1
  136. package/packages/daemon/dist/cloud-runtime.d.ts +2 -0
  137. package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -1
  138. package/packages/daemon/dist/cloud-runtime.js +25 -1
  139. package/packages/daemon/dist/cloud-runtime.js.map +1 -1
  140. package/packages/daemon/dist/cloud-runtime.test.js +51 -0
  141. package/packages/daemon/dist/cloud-runtime.test.js.map +1 -1
  142. package/packages/daemon/dist/message-batcher.d.ts +7 -0
  143. package/packages/daemon/dist/message-batcher.d.ts.map +1 -1
  144. package/packages/daemon/dist/message-batcher.js +13 -0
  145. package/packages/daemon/dist/message-batcher.js.map +1 -1
  146. package/packages/daemon/dist/message-batcher.test.js +48 -0
  147. package/packages/daemon/dist/message-batcher.test.js.map +1 -1
  148. package/packages/daemon/dist/session-manager.js +1 -1
  149. package/packages/daemon/dist/session-manager.js.map +1 -1
  150. package/packages/daemon/package.json +5 -5
  151. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts +13 -1
  152. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
  153. package/packages/gsd-agent-core/dist/compaction/compaction.js +15 -1
  154. package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
  155. package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
  156. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
  157. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
  158. package/packages/gsd-agent-core/package.json +5 -5
  159. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.d.ts.map +1 -1
  160. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js +1 -0
  161. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js.map +1 -1
  162. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +1 -1
  163. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
  164. package/packages/gsd-agent-modes/package.json +7 -7
  165. package/packages/mcp-server/dist/server.d.ts +18 -5
  166. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  167. package/packages/mcp-server/dist/server.js +46 -5
  168. package/packages/mcp-server/dist/server.js.map +1 -1
  169. package/packages/mcp-server/dist/session-manager.js +1 -1
  170. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  171. package/packages/mcp-server/package.json +4 -4
  172. package/packages/native/package.json +1 -1
  173. package/packages/pi-agent-core/package.json +1 -1
  174. package/packages/pi-ai/dist/env-api-keys.d.ts.map +1 -1
  175. package/packages/pi-ai/dist/env-api-keys.js +3 -0
  176. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  177. package/packages/pi-ai/dist/types.d.ts +2 -2
  178. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  179. package/packages/pi-ai/dist/types.js.map +1 -1
  180. package/packages/pi-ai/package.json +1 -1
  181. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  182. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  183. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
  185. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
  186. package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
  187. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  188. package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
  189. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  190. package/packages/pi-coding-agent/package.json +7 -7
  191. package/packages/pi-tui/package.json +2 -2
  192. package/packages/rpc-client/dist/rpc-client.js +1 -1
  193. package/packages/rpc-client/dist/rpc-client.js.map +1 -1
  194. package/packages/rpc-client/package.json +2 -2
  195. package/pkg/package.json +1 -1
  196. package/scripts/link-workspace-packages.cjs +14 -4
  197. package/src/resources/GSD-WORKFLOW.md +20 -25
  198. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +130 -51
  199. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +371 -39
  200. package/src/resources/extensions/cursor-cli/index.ts +18 -0
  201. package/src/resources/extensions/cursor-cli/models.ts +65 -0
  202. package/src/resources/extensions/cursor-cli/package.json +11 -0
  203. package/src/resources/extensions/cursor-cli/readiness.ts +143 -0
  204. package/src/resources/extensions/cursor-cli/stream-adapter.ts +440 -0
  205. package/src/resources/extensions/cursor-cli/tests/index.test.ts +54 -0
  206. package/src/resources/extensions/cursor-cli/tests/readiness.test.ts +29 -0
  207. package/src/resources/extensions/cursor-cli/tests/stream-adapter.test.ts +166 -0
  208. package/src/resources/extensions/gsd/activity-log.ts +4 -1
  209. package/src/resources/extensions/gsd/artifact-verification.ts +25 -9
  210. package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
  211. package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
  212. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  213. package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
  214. package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
  215. package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
  216. package/src/resources/extensions/gsd/auto-dispatch.ts +17 -5
  217. package/src/resources/extensions/gsd/auto-model-selection.ts +49 -18
  218. package/src/resources/extensions/gsd/auto-prompts.ts +73 -2
  219. package/src/resources/extensions/gsd/auto-start.ts +4 -3
  220. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +4 -0
  221. package/src/resources/extensions/gsd/auto-worktree.ts +50 -11
  222. package/src/resources/extensions/gsd/auto.ts +22 -3
  223. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -0
  224. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +128 -5
  225. package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
  226. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +12 -2
  227. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +23 -1
  228. package/src/resources/extensions/gsd/codebase-generator.ts +179 -18
  229. package/src/resources/extensions/gsd/commands/handlers/core.ts +39 -3
  230. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +135 -1
  231. package/src/resources/extensions/gsd/commands-worktree.ts +13 -2
  232. package/src/resources/extensions/gsd/db/queries.ts +40 -0
  233. package/src/resources/extensions/gsd/discussion-handoff.ts +10 -2
  234. package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -2
  235. package/src/resources/extensions/gsd/doctor-engine-checks.ts +269 -18
  236. package/src/resources/extensions/gsd/doctor-providers.ts +39 -10
  237. package/src/resources/extensions/gsd/doctor-types.ts +5 -1
  238. package/src/resources/extensions/gsd/doctor.ts +95 -3
  239. package/src/resources/extensions/gsd/exec-sandbox.ts +6 -2
  240. package/src/resources/extensions/gsd/flat-phase-migration.ts +98 -22
  241. package/src/resources/extensions/gsd/git-service.ts +14 -17
  242. package/src/resources/extensions/gsd/gsd-db.ts +72 -0
  243. package/src/resources/extensions/gsd/guidance.ts +2 -0
  244. package/src/resources/extensions/gsd/guided-flow.ts +45 -2
  245. package/src/resources/extensions/gsd/key-manager.ts +1 -0
  246. package/src/resources/extensions/gsd/markdown-renderer.ts +25 -7
  247. package/src/resources/extensions/gsd/migrate-external.ts +23 -6
  248. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
  249. package/src/resources/extensions/gsd/model-router.ts +4 -5
  250. package/src/resources/extensions/gsd/preferences-diagnostics.ts +24 -0
  251. package/src/resources/extensions/gsd/preferences-models.ts +6 -3
  252. package/src/resources/extensions/gsd/preferences-validation.ts +28 -2
  253. package/src/resources/extensions/gsd/preferences.ts +38 -4
  254. package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  255. package/src/resources/extensions/gsd/redact-secrets.ts +40 -0
  256. package/src/resources/extensions/gsd/repo-identity.ts +17 -0
  257. package/src/resources/extensions/gsd/repository-registry.ts +7 -1
  258. package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
  259. package/src/resources/extensions/gsd/safe-fs.ts +22 -2
  260. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  261. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
  262. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
  263. package/src/resources/extensions/gsd/tests/auto-migrating-recovery.test.ts +18 -0
  264. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +178 -0
  265. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
  266. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  267. package/src/resources/extensions/gsd/tests/auto-remote-session-lock-cleanup.test.ts +41 -0
  268. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +41 -0
  269. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +4 -4
  270. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +3 -0
  271. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-harness-abort.test.ts +55 -0
  272. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +8 -0
  273. package/src/resources/extensions/gsd/tests/batch-task-query.test.ts +85 -0
  274. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +29 -0
  275. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +365 -0
  276. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +62 -1
  277. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +138 -1
  278. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +397 -5
  279. package/src/resources/extensions/gsd/tests/doctor-workspace.test.ts +207 -0
  280. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +38 -1
  281. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +87 -19
  282. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +89 -0
  283. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +9 -0
  284. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  285. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +122 -0
  286. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
  287. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
  288. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
  289. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
  290. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +2 -2
  291. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
  292. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +17 -0
  293. package/src/resources/extensions/gsd/tests/plan-task.test.ts +264 -1
  294. package/src/resources/extensions/gsd/tests/preferences.test.ts +150 -0
  295. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +142 -0
  296. package/src/resources/extensions/gsd/tests/recovery-verify-logs.test.ts +4 -4
  297. package/src/resources/extensions/gsd/tests/redact-secrets.test.ts +56 -0
  298. package/src/resources/extensions/gsd/tests/register-hooks-flat-phase-migration.test.ts +73 -0
  299. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +275 -0
  300. package/src/resources/extensions/gsd/tests/repository-registry.test.ts +41 -2
  301. package/src/resources/extensions/gsd/tests/require-slice-discussion-dispatch.test.ts +55 -0
  302. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
  303. package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
  304. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -0
  305. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
  306. package/src/resources/extensions/gsd/tests/status-per-repo.test.ts +94 -0
  307. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +35 -0
  308. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +63 -0
  309. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +66 -18
  310. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
  311. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  312. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +52 -0
  313. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +53 -1
  314. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +208 -1
  315. package/src/resources/extensions/gsd/tests/workspace.test.ts +18 -4
  316. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
  317. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
  318. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +31 -1
  319. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +32 -0
  320. package/src/resources/extensions/gsd/tools/plan-task.ts +107 -9
  321. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +58 -0
  322. package/src/resources/extensions/gsd/unit-context-manifest.ts +11 -5
  323. package/src/resources/extensions/gsd/unit-registry.ts +34 -2
  324. package/src/resources/extensions/gsd/unit-runtime.ts +42 -0
  325. package/src/resources/extensions/gsd/workspace.ts +42 -8
  326. package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
  327. package/src/resources/extensions/gsd/worktree-manager.ts +143 -5
  328. /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → bCg3aYrFN1DKZr2dqgE7L}/_buildManifest.js +0 -0
  329. /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → bCg3aYrFN1DKZr2dqgE7L}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
1
  // gsd-pi - Claude Code stream adapter regression tests
2
- import { describe, test } from "node:test";
2
+ import { describe, mock, test } from "node:test";
3
3
  import { clearGuidedUnitContext, setGuidedUnitContext } from "../../gsd/guided-unit-context.ts";
4
4
  import assert from "node:assert/strict";
5
5
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
@@ -46,6 +46,7 @@ import {
46
46
  buildWorkflowMcpReadinessProgressMessage,
47
47
  pushWorkflowMcpReadinessProgressEvent,
48
48
  resolveWorkflowMcpPreflightServerConfig,
49
+ CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS,
49
50
  } from "../stream-adapter.ts";
50
51
  import { CLAUDE_CODE_MODELS } from "../models.ts";
51
52
  import type { AssistantMessage, Context, Message } from "@gsd/pi-ai";
@@ -107,6 +108,51 @@ function setWorkflowMcpEnv(
107
108
  };
108
109
  }
109
110
 
111
+ // ---------------------------------------------------------------------------
112
+ // Shared MCP elicitation fixture — a representative ask_user_questions
113
+ // elicitation request. Used both by the external-tool-results interview
114
+ // lifecycle tests and by the MCP elicitation bridge tests, so it lives at
115
+ // module scope.
116
+ // ---------------------------------------------------------------------------
117
+
118
+ const askUserQuestionsRequest = {
119
+ serverName: "gsd-workflow",
120
+ message: "Please answer the following question(s).",
121
+ mode: "form" as const,
122
+ requestedSchema: {
123
+ type: "object" as const,
124
+ properties: {
125
+ storage_scope: {
126
+ type: "string",
127
+ title: "Storage",
128
+ description: "Does this app need to sync across devices?",
129
+ oneOf: [
130
+ { const: "Local-only (Recommended)", title: "Local-only (Recommended)" },
131
+ { const: "Cloud-synced", title: "Cloud-synced" },
132
+ { const: "None of the above", title: "None of the above" },
133
+ ],
134
+ },
135
+ storage_scope__note: {
136
+ type: "string",
137
+ title: "Storage Note",
138
+ description: "Optional note for None of the above.",
139
+ },
140
+ platform: {
141
+ type: "array",
142
+ title: "Platform",
143
+ description: "Where should it run?",
144
+ items: {
145
+ anyOf: [
146
+ { const: "Web", title: "Web" },
147
+ { const: "Desktop", title: "Desktop" },
148
+ { const: "Mobile", title: "Mobile" },
149
+ ],
150
+ },
151
+ },
152
+ },
153
+ },
154
+ };
155
+
110
156
  // ---------------------------------------------------------------------------
111
157
  // Existing tests — exhausted stream fallback (#2575)
112
158
  // ---------------------------------------------------------------------------
@@ -661,6 +707,293 @@ describe("stream-adapter — no transcript fabrication (#4102)", () => {
661
707
  });
662
708
 
663
709
  describe("stream-adapter — Claude Code external tool results", () => {
710
+ test("emits one synthetic completion per tool when SDK sends consecutive user result messages (#1153)", async () => {
711
+ // Capture synthetic completions through onExternalToolResult, which the
712
+ // adapter invokes exactly once per tool at emission time. Filtering the
713
+ // collected toolcall_end events afterwards is unreliable here: the streaming
714
+ // toolcall_end (content_block_stop) and the synthetic completion reference
715
+ // the SAME tool-call block, and attachExternalResultsToToolBlocks mutates
716
+ // that block later, so a post-hoc `event.toolCall.externalResult` check also
717
+ // matches the earlier streaming ends and double-counts each tool.
718
+ const syntheticCompletions: Array<{ id: string; text: string | undefined }> = [];
719
+ const stream = streamViaClaudeCode(
720
+ { id: "claude-sonnet-4-6" } as any,
721
+ { messages: [{ role: "user", content: "Read and grep." } as Message] },
722
+ {
723
+ _skipWorkflowMcpPreflightForTest: true,
724
+ onExternalToolResult(event: {
725
+ toolCall: { id: string };
726
+ result: { content?: Array<{ text?: string }> };
727
+ }) {
728
+ syntheticCompletions.push({
729
+ id: event.toolCall.id,
730
+ text: event.result.content?.[0]?.text,
731
+ });
732
+ },
733
+ async *_sdkQueryForTest() {
734
+ yield {
735
+ type: "stream_event",
736
+ event: { type: "message_start", message: { model: "claude-sonnet-4-6" } },
737
+ parent_tool_use_id: null,
738
+ uuid: "partial-1",
739
+ session_id: "session-1",
740
+ };
741
+ yield {
742
+ type: "stream_event",
743
+ event: {
744
+ type: "content_block_start",
745
+ index: 0,
746
+ content_block: { type: "tool_use", id: "tool-read-1", name: "Read", input: {} },
747
+ },
748
+ parent_tool_use_id: null,
749
+ uuid: "partial-1",
750
+ session_id: "session-1",
751
+ };
752
+ yield {
753
+ type: "stream_event",
754
+ event: {
755
+ type: "content_block_delta",
756
+ index: 0,
757
+ delta: { type: "input_json_delta", partial_json: "{\"file_path\":\"a.txt\"}" },
758
+ },
759
+ parent_tool_use_id: null,
760
+ uuid: "partial-1",
761
+ session_id: "session-1",
762
+ };
763
+ yield {
764
+ type: "stream_event",
765
+ event: { type: "content_block_stop", index: 0 },
766
+ parent_tool_use_id: null,
767
+ uuid: "partial-1",
768
+ session_id: "session-1",
769
+ };
770
+ yield {
771
+ type: "stream_event",
772
+ event: {
773
+ type: "content_block_start",
774
+ index: 1,
775
+ content_block: { type: "tool_use", id: "tool-grep-1", name: "Grep", input: {} },
776
+ },
777
+ parent_tool_use_id: null,
778
+ uuid: "partial-1",
779
+ session_id: "session-1",
780
+ };
781
+ yield {
782
+ type: "stream_event",
783
+ event: {
784
+ type: "content_block_delta",
785
+ index: 1,
786
+ delta: { type: "input_json_delta", partial_json: "{\"pattern\":\"needle\"}" },
787
+ },
788
+ parent_tool_use_id: null,
789
+ uuid: "partial-1",
790
+ session_id: "session-1",
791
+ };
792
+ yield {
793
+ type: "stream_event",
794
+ event: { type: "content_block_stop", index: 1 },
795
+ parent_tool_use_id: null,
796
+ uuid: "partial-1",
797
+ session_id: "session-1",
798
+ };
799
+ yield {
800
+ type: "user",
801
+ uuid: "user-result-1",
802
+ session_id: "session-1",
803
+ parent_tool_use_id: "tool-read-1",
804
+ message: {
805
+ role: "user",
806
+ content: [{
807
+ type: "tool_result",
808
+ tool_use_id: "tool-read-1",
809
+ content: "read result",
810
+ is_error: false,
811
+ }],
812
+ },
813
+ };
814
+ yield {
815
+ type: "user",
816
+ uuid: "user-result-2",
817
+ session_id: "session-1",
818
+ parent_tool_use_id: "tool-grep-1",
819
+ message: {
820
+ role: "user",
821
+ content: [{
822
+ type: "tool_result",
823
+ tool_use_id: "tool-grep-1",
824
+ content: "grep result",
825
+ is_error: false,
826
+ }],
827
+ },
828
+ };
829
+ yield {
830
+ type: "result",
831
+ subtype: "success",
832
+ uuid: "result-1",
833
+ session_id: "session-1",
834
+ duration_ms: 1,
835
+ duration_api_ms: 1,
836
+ is_error: false,
837
+ num_turns: 1,
838
+ result: "done",
839
+ stop_reason: "end_turn",
840
+ total_cost_usd: 0,
841
+ usage: {
842
+ input_tokens: 0,
843
+ output_tokens: 0,
844
+ cache_read_input_tokens: 0,
845
+ cache_creation_input_tokens: 0,
846
+ },
847
+ };
848
+ },
849
+ } as any,
850
+ );
851
+
852
+ // Drain the stream so the adapter processes every SDK message and fires
853
+ // onExternalToolResult for each synthetic completion.
854
+ const events: any[] = [];
855
+ for await (const event of stream) {
856
+ events.push(event);
857
+ }
858
+
859
+ assert.deepEqual(
860
+ syntheticCompletions.map((completion) => completion.id),
861
+ ["tool-read-1", "tool-grep-1"],
862
+ );
863
+ assert.deepEqual(
864
+ syntheticCompletions.map((completion) => completion.text),
865
+ ["read result", "grep result"],
866
+ );
867
+ });
868
+
869
+ test("closes an in-flight interview when ask_user_questions reports timed_out first", async (t) => {
870
+ const notifications: Array<{ message: string; type?: string }> = [];
871
+ let elicitationPromise: Promise<unknown> | undefined;
872
+ let elicitationSignalController: AbortController | undefined;
873
+ let customOpened = false;
874
+ const ui = {
875
+ custom: async (factory: any) => new Promise((resolve) => {
876
+ customOpened = true;
877
+ factory({ requestRender() {} } as any, {} as any, {} as any, resolve);
878
+ }),
879
+ notify: (message: string, type?: string) => {
880
+ notifications.push({ message, type });
881
+ },
882
+ };
883
+ t.after(() => {
884
+ elicitationSignalController?.abort();
885
+ });
886
+
887
+ const stream = streamViaClaudeCode(
888
+ { id: "claude-sonnet-4-6" } as any,
889
+ { messages: [{ role: "user", content: "Ask me." } as Message] },
890
+ {
891
+ extensionUIContext: ui as any,
892
+ _skipWorkflowMcpPreflightForTest: true,
893
+ async *_sdkQueryForTest(args: {
894
+ prompt: string | AsyncIterable<unknown>;
895
+ options?: Record<string, unknown>;
896
+ }) {
897
+ const onElicitation = args.options?.onElicitation as
898
+ | ((request: unknown, options: { signal: AbortSignal }) => Promise<unknown>)
899
+ | undefined;
900
+ assert.equal(typeof onElicitation, "function");
901
+ elicitationSignalController = new AbortController();
902
+ elicitationPromise = onElicitation!(askUserQuestionsRequest, {
903
+ signal: elicitationSignalController.signal,
904
+ });
905
+ assert.equal(customOpened, true, "interview overlay should be open before the tool result arrives");
906
+
907
+ // The adapter only assembles tool-call blocks after a
908
+ // message_start has created the partial-message builder. Real
909
+ // SDK streams always emit it before a tool_use content block;
910
+ // without it the timed_out result is never matched to the
911
+ // in-flight ask_user_questions block, so the interview would
912
+ // never be closed.
913
+ yield {
914
+ type: "stream_event",
915
+ event: { type: "message_start", message: { model: "claude-sonnet-4-6" } },
916
+ parent_tool_use_id: null,
917
+ uuid: "partial-1",
918
+ session_id: "session-1",
919
+ };
920
+ yield {
921
+ type: "stream_event",
922
+ event: {
923
+ type: "content_block_start",
924
+ index: 0,
925
+ content_block: {
926
+ type: "tool_use",
927
+ id: "ask-1",
928
+ name: "mcp__gsd-workflow__ask_user_questions",
929
+ input: {},
930
+ },
931
+ },
932
+ parent_tool_use_id: null,
933
+ uuid: "partial-1",
934
+ session_id: "session-1",
935
+ };
936
+ yield {
937
+ type: "user",
938
+ uuid: "user-result-1",
939
+ session_id: "session-1",
940
+ parent_tool_use_id: "ask-1",
941
+ message: {
942
+ role: "user",
943
+ content: [{
944
+ type: "mcp_tool_result",
945
+ tool_use_id: "ask-1",
946
+ content: [{ type: "text", text: "timed out" }],
947
+ structuredContent: { timed_out: true },
948
+ is_error: false,
949
+ }],
950
+ },
951
+ };
952
+ yield {
953
+ type: "result",
954
+ subtype: "success",
955
+ uuid: "result-1",
956
+ session_id: "session-1",
957
+ duration_ms: 1,
958
+ duration_api_ms: 1,
959
+ is_error: false,
960
+ num_turns: 1,
961
+ result: "done",
962
+ stop_reason: "end_turn",
963
+ total_cost_usd: 0,
964
+ usage: {
965
+ input_tokens: 0,
966
+ output_tokens: 0,
967
+ cache_read_input_tokens: 0,
968
+ cache_creation_input_tokens: 0,
969
+ },
970
+ };
971
+ },
972
+ } as any,
973
+ );
974
+
975
+ await stream.result();
976
+ assert.ok(elicitationPromise);
977
+ let guard: ReturnType<typeof setTimeout> | undefined;
978
+ const elicitationResult = await Promise.race([
979
+ elicitationPromise,
980
+ new Promise<never>((_, reject) => {
981
+ guard = setTimeout(
982
+ () => reject(new Error("timed out waiting for in-flight interview to close")),
983
+ 250,
984
+ );
985
+ }),
986
+ ]).finally(() => {
987
+ if (guard) clearTimeout(guard);
988
+ });
989
+
990
+ assert.deepEqual(elicitationResult, { action: "decline" });
991
+ assert.deepEqual(notifications, [{
992
+ message: "Question expired before you answered - the form was closed and no answers were sent.",
993
+ type: "warning",
994
+ }]);
995
+ });
996
+
664
997
  test("serverToolUseToToolCallLike preserves object input for extension tool_result routing", () => {
665
998
  const toolCall = serverToolUseToToolCallLike({
666
999
  id: "srv-1",
@@ -2436,44 +2769,6 @@ describe("stream-adapter — workflow MCP readiness", () => {
2436
2769
  });
2437
2770
 
2438
2771
  describe("stream-adapter — MCP elicitation bridge", () => {
2439
- const askUserQuestionsRequest = {
2440
- serverName: "gsd-workflow",
2441
- message: "Please answer the following question(s).",
2442
- mode: "form" as const,
2443
- requestedSchema: {
2444
- type: "object" as const,
2445
- properties: {
2446
- storage_scope: {
2447
- type: "string",
2448
- title: "Storage",
2449
- description: "Does this app need to sync across devices?",
2450
- oneOf: [
2451
- { const: "Local-only (Recommended)", title: "Local-only (Recommended)" },
2452
- { const: "Cloud-synced", title: "Cloud-synced" },
2453
- { const: "None of the above", title: "None of the above" },
2454
- ],
2455
- },
2456
- storage_scope__note: {
2457
- type: "string",
2458
- title: "Storage Note",
2459
- description: "Optional note for None of the above.",
2460
- },
2461
- platform: {
2462
- type: "array",
2463
- title: "Platform",
2464
- description: "Where should it run?",
2465
- items: {
2466
- anyOf: [
2467
- { const: "Web", title: "Web" },
2468
- { const: "Desktop", title: "Desktop" },
2469
- { const: "Mobile", title: "Mobile" },
2470
- ],
2471
- },
2472
- },
2473
- },
2474
- },
2475
- };
2476
-
2477
2772
  test("parseAskUserQuestionsElicitation rebuilds interview questions from the MCP schema", () => {
2478
2773
  const questions = parseAskUserQuestionsElicitation(askUserQuestionsRequest);
2479
2774
  assert.deepEqual(questions, [
@@ -2882,6 +3177,43 @@ describe("stream-adapter — MCP elicitation bridge", () => {
2882
3177
  assert.deepEqual(result, { action: "decline" });
2883
3178
  });
2884
3179
 
3180
+ test("expires the interview overlay before the MCP elicitation response is dropped", async (t) => {
3181
+ mock.timers.enable({ apis: ["setTimeout"] });
3182
+ t.after(() => {
3183
+ mock.timers.reset();
3184
+ });
3185
+
3186
+ const notifications: Array<{ message: string; type?: string }> = [];
3187
+ let customOpened = false;
3188
+ const handler = createClaudeCodeElicitationHandler({
3189
+ custom: async (factory: any) => new Promise((resolve) => {
3190
+ customOpened = true;
3191
+ factory({ requestRender() {} } as any, {} as any, {} as any, resolve);
3192
+ }),
3193
+ notify: (message: string, type?: string) => {
3194
+ notifications.push({ message, type });
3195
+ },
3196
+ select: async () => {
3197
+ throw new Error("expired interview must not re-open dialog fallback");
3198
+ },
3199
+ } as any);
3200
+ assert.ok(handler);
3201
+
3202
+ const resultPromise = handler!(askUserQuestionsRequest, { signal: new AbortController().signal });
3203
+ await Promise.resolve();
3204
+ assert.equal(customOpened, true);
3205
+
3206
+ mock.timers.tick(CLAUDE_CODE_INTERVIEW_FORM_TIMEOUT_MS);
3207
+ const result = await resultPromise;
3208
+
3209
+ assert.deepEqual(result, { action: "decline" });
3210
+ assert.deepEqual(notifications, [{
3211
+ message: "Question expired before you answered - the form was closed and no answers were sent.",
3212
+ type: "warning",
3213
+ }]);
3214
+ assert.equal(isInteractiveElicitationInFlight(), false, "marker must clear after deadline expiry");
3215
+ });
3216
+
2885
3217
  test("returns cancel (today's semantics) when the user genuinely dismisses", async () => {
2886
3218
  const handler = createClaudeCodeElicitationHandler({
2887
3219
  custom: async () => ({ endInterview: false, answers: {} }),
@@ -0,0 +1,18 @@
1
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
2
+ import { CURSOR_AGENT_MODELS } from "./models.js";
3
+ import { isCursorAgentReady } from "./readiness.js";
4
+ import { streamViaCursorAgent } from "./stream-adapter.js";
5
+
6
+ export default function cursorCli(pi: ExtensionAPI): void {
7
+ if (process.env.GSD_CURSOR_DISABLE === "1") return;
8
+
9
+ pi.registerProvider("cursor-agent", {
10
+ name: "Cursor Agent",
11
+ authMode: "externalCli",
12
+ api: "cursor-stream-json",
13
+ baseUrl: "local://cursor-agent",
14
+ isReady: isCursorAgentReady,
15
+ streamSimple: streamViaCursorAgent,
16
+ models: CURSOR_AGENT_MODELS,
17
+ });
18
+ }
@@ -0,0 +1,65 @@
1
+ const ZERO_COST = {
2
+ input: 0,
3
+ output: 0,
4
+ cacheRead: 0,
5
+ cacheWrite: 0,
6
+ };
7
+
8
+ const CURSOR_INPUT: ("text" | "image")[] = ["text"];
9
+
10
+ export const CURSOR_AGENT_MODELS = [
11
+ {
12
+ id: "composer-2.5",
13
+ name: "Composer 2.5 (via Cursor)",
14
+ reasoning: true,
15
+ input: CURSOR_INPUT,
16
+ cost: ZERO_COST,
17
+ contextWindow: 1_000_000,
18
+ maxTokens: 64_000,
19
+ },
20
+ {
21
+ id: "claude-sonnet-4-6",
22
+ name: "Claude Sonnet 4.6 (via Cursor)",
23
+ reasoning: true,
24
+ input: CURSOR_INPUT,
25
+ cost: ZERO_COST,
26
+ contextWindow: 1_000_000,
27
+ maxTokens: 64_000,
28
+ },
29
+ {
30
+ id: "claude-opus-4-7",
31
+ name: "Claude Opus 4.7 (via Cursor)",
32
+ reasoning: true,
33
+ input: CURSOR_INPUT,
34
+ cost: ZERO_COST,
35
+ contextWindow: 1_000_000,
36
+ maxTokens: 128_000,
37
+ },
38
+ {
39
+ id: "gpt-5.5",
40
+ name: "GPT-5.5 (via Cursor)",
41
+ reasoning: true,
42
+ input: CURSOR_INPUT,
43
+ cost: ZERO_COST,
44
+ contextWindow: 1_000_000,
45
+ maxTokens: 128_000,
46
+ },
47
+ {
48
+ id: "gemini-2.5-pro",
49
+ name: "Gemini 2.5 Pro (via Cursor)",
50
+ reasoning: true,
51
+ input: CURSOR_INPUT,
52
+ cost: ZERO_COST,
53
+ contextWindow: 1_000_000,
54
+ maxTokens: 65_536,
55
+ },
56
+ {
57
+ id: "grok-4",
58
+ name: "Grok 4 (via Cursor)",
59
+ reasoning: true,
60
+ input: CURSOR_INPUT,
61
+ cost: ZERO_COST,
62
+ contextWindow: 256_000,
63
+ maxTokens: 64_000,
64
+ },
65
+ ];
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@gsd/cursor-cli",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "pi": {
7
+ "extensions": [
8
+ "./index.ts"
9
+ ]
10
+ }
11
+ }
@@ -0,0 +1,143 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ const VERSION_TIMEOUT_MS = 5_000;
4
+ const STATUS_TIMEOUT_MS = 10_000;
5
+ const CHECK_INTERVAL_MS = 30_000;
6
+
7
+ let cachedBinaryPresent: boolean | null = null;
8
+ let cachedAuthed: boolean | null = null;
9
+ let lastCheckMs = 0;
10
+
11
+ export function getCursorAgentCommand(platform: NodeJS.Platform = process.platform): string {
12
+ return platform === "win32" ? "cursor-agent.cmd" : "cursor-agent";
13
+ }
14
+
15
+ export function getCursorAgentCommandCandidates(platform: NodeJS.Platform = process.platform): string[] {
16
+ const command = getCursorAgentCommand(platform);
17
+ return platform === "win32" ? [command, "cursor-agent.exe", "cursor-agent"] : [command];
18
+ }
19
+
20
+ export function buildCursorAgentSpawnInvocation(
21
+ command: string,
22
+ args: string[],
23
+ platform: NodeJS.Platform = process.platform,
24
+ ): { command: string; args: string[] } {
25
+ if (platform === "win32") {
26
+ return { command: "cmd", args: ["/c", command, ...args] };
27
+ }
28
+ return { command, args };
29
+ }
30
+
31
+ function debugLog(...parts: unknown[]): void {
32
+ if (process.env.GSD_CURSOR_DEBUG) {
33
+ process.stderr.write(`[cursor-readiness] ${parts.map((part) => String(part)).join(" ")}\n`);
34
+ }
35
+ }
36
+
37
+ function spawnCursorAgent(command: string, args: string[], timeout: number): Buffer {
38
+ const invocation = buildCursorAgentSpawnInvocation(command, args);
39
+ return execFileSync(invocation.command, invocation.args, { timeout, stdio: "pipe" });
40
+ }
41
+
42
+ function findWorkingCommand(): string | null {
43
+ for (const command of getCursorAgentCommandCandidates()) {
44
+ try {
45
+ spawnCursorAgent(command, ["--version"], VERSION_TIMEOUT_MS);
46
+ debugLog("version probe ok via", command);
47
+ return command;
48
+ } catch (error) {
49
+ debugLog("version probe failed for", command, (error as Error).message?.slice(0, 200));
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+
55
+ export function parseCursorAgentStatus(output: string): boolean | null {
56
+ const trimmed = output.trim();
57
+ if (!trimmed) return null;
58
+
59
+ if (trimmed.startsWith("{")) {
60
+ try {
61
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
62
+ for (const key of ["authenticated", "loggedIn", "logged_in", "isAuthenticated"]) {
63
+ if (typeof parsed[key] === "boolean") return parsed[key];
64
+ }
65
+ } catch {
66
+ // Fall through to text heuristics.
67
+ }
68
+ }
69
+
70
+ const lower = trimmed.toLowerCase();
71
+ if (/not authenticated|not logged in|no credentials|logged out|unauthenticated/.test(lower)) return false;
72
+ if (/authenticated|logged in|signed in|cursor account|subscription/.test(lower)) return true;
73
+ return null;
74
+ }
75
+
76
+ export function isCursorAgentApiKeyValue(value: string | undefined): boolean {
77
+ const trimmed = value?.trim();
78
+ return Boolean(trimmed && trimmed !== "cli");
79
+ }
80
+
81
+ function hasCursorApiKey(): boolean {
82
+ return isCursorAgentApiKeyValue(process.env.CURSOR_API_KEY);
83
+ }
84
+
85
+ function probeAuth(command: string): boolean | null {
86
+ if (hasCursorApiKey()) return true;
87
+
88
+ for (const args of [["agent", "status", "--json"], ["status", "--json"], ["agent", "status"], ["status"]]) {
89
+ try {
90
+ const out = spawnCursorAgent(command, args, STATUS_TIMEOUT_MS).toString();
91
+ debugLog("status output", args.join(" "), out.slice(0, 200));
92
+ const parsed = parseCursorAgentStatus(out);
93
+ if (parsed !== null) return parsed;
94
+ } catch (error) {
95
+ debugLog("status failed", args.join(" "), (error as Error).message?.slice(0, 200));
96
+ }
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ export function isCursorAgentReadyUncached(): boolean {
103
+ const command = findWorkingCommand();
104
+ if (!command) return false;
105
+ return probeAuth(command) === true;
106
+ }
107
+
108
+ function refreshCache(): void {
109
+ const now = Date.now();
110
+ if (cachedBinaryPresent !== null && now - lastCheckMs < CHECK_INTERVAL_MS) return;
111
+ lastCheckMs = now;
112
+
113
+ const command = findWorkingCommand();
114
+ if (!command) {
115
+ cachedBinaryPresent = false;
116
+ cachedAuthed = false;
117
+ return;
118
+ }
119
+
120
+ cachedBinaryPresent = true;
121
+ const authed = probeAuth(command);
122
+ if (authed === null) {
123
+ if (cachedAuthed === null) cachedAuthed = false;
124
+ return;
125
+ }
126
+ cachedAuthed = authed;
127
+ }
128
+
129
+ export function isCursorAgentBinaryPresent(): boolean {
130
+ refreshCache();
131
+ return cachedBinaryPresent ?? false;
132
+ }
133
+
134
+ export function isCursorAgentReady(): boolean {
135
+ refreshCache();
136
+ return (cachedBinaryPresent ?? false) && (cachedAuthed ?? false);
137
+ }
138
+
139
+ export function clearCursorAgentReadinessCache(): void {
140
+ cachedBinaryPresent = null;
141
+ cachedAuthed = null;
142
+ lastCheckMs = 0;
143
+ }