@opengsd/gsd-pi 1.4.0 → 1.5.0-dev.4fb2d460

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 (657) 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/ask-user-questions.js +1 -1
  6. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +118 -53
  7. package/dist/resources/extensions/cursor-cli/index.js +16 -0
  8. package/dist/resources/extensions/cursor-cli/models.js +63 -0
  9. package/dist/resources/extensions/cursor-cli/package.json +11 -0
  10. package/dist/resources/extensions/cursor-cli/readiness.js +128 -0
  11. package/dist/resources/extensions/cursor-cli/stream-adapter.js +353 -0
  12. package/dist/resources/extensions/gsd/activity-log.js +4 -1
  13. package/dist/resources/extensions/gsd/artifact-verification.js +25 -9
  14. package/dist/resources/extensions/gsd/auto/closeout.js +137 -42
  15. package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
  16. package/dist/resources/extensions/gsd/auto/orchestrator.js +4 -3
  17. package/dist/resources/extensions/gsd/auto/pre-dispatch.js +6 -3
  18. package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
  19. package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
  20. package/dist/resources/extensions/gsd/auto/unit-phase.js +5 -9
  21. package/dist/resources/extensions/gsd/auto-artifact-paths.js +12 -3
  22. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +11 -7
  23. package/dist/resources/extensions/gsd/auto-dispatch.js +31 -7
  24. package/dist/resources/extensions/gsd/auto-model-selection.js +36 -18
  25. package/dist/resources/extensions/gsd/auto-prompts.js +76 -2
  26. package/dist/resources/extensions/gsd/auto-recovery.js +50 -6
  27. package/dist/resources/extensions/gsd/auto-start.js +8 -6
  28. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +4 -0
  29. package/dist/resources/extensions/gsd/auto-worktree-branch-lifecycle.js +189 -0
  30. package/dist/resources/extensions/gsd/auto-worktree-cleanup.js +133 -0
  31. package/dist/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.js +35 -0
  32. package/dist/resources/extensions/gsd/auto-worktree-creation.js +130 -0
  33. package/dist/resources/extensions/gsd/auto-worktree-entry.js +118 -0
  34. package/dist/resources/extensions/gsd/auto-worktree-merge-already-merged.js +98 -0
  35. package/dist/resources/extensions/gsd/auto-worktree-merge-branch-head.js +52 -0
  36. package/dist/resources/extensions/gsd/auto-worktree-merge-cleanup.js +87 -0
  37. package/dist/resources/extensions/gsd/auto-worktree-merge-code-changes.js +46 -0
  38. package/dist/resources/extensions/gsd/auto-worktree-merge-db-ready.js +68 -0
  39. package/dist/resources/extensions/gsd/auto-worktree-merge-integration-branch.js +51 -0
  40. package/dist/resources/extensions/gsd/auto-worktree-merge-message.js +106 -0
  41. package/dist/resources/extensions/gsd/auto-worktree-merge-pre-teardown.js +64 -0
  42. package/dist/resources/extensions/gsd/auto-worktree-merge-stash.js +168 -0
  43. package/dist/resources/extensions/gsd/auto-worktree-merge.js +302 -0
  44. package/dist/resources/extensions/gsd/auto-worktree-milestone-shelter.js +119 -0
  45. package/dist/resources/extensions/gsd/auto-worktree-path-resolution.js +5 -0
  46. package/dist/resources/extensions/gsd/auto-worktree-path-utils.js +5 -0
  47. package/dist/resources/extensions/gsd/auto-worktree-resource-version.js +41 -0
  48. package/dist/resources/extensions/gsd/auto-worktree-runtime-cleanup.js +99 -0
  49. package/dist/resources/extensions/gsd/auto-worktree-session-registry.js +57 -0
  50. package/dist/resources/extensions/gsd/auto-worktree-sync.js +249 -0
  51. package/dist/resources/extensions/gsd/auto-worktree-teardown.js +120 -0
  52. package/dist/resources/extensions/gsd/auto-worktree.js +20 -2046
  53. package/dist/resources/extensions/gsd/auto.js +31 -5
  54. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -0
  55. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -1
  56. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +134 -7
  57. package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
  58. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  59. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +194 -28
  60. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +14 -1
  61. package/dist/resources/extensions/gsd/codebase-generator.js +164 -20
  62. package/dist/resources/extensions/gsd/commands/handlers/core.js +40 -4
  63. package/dist/resources/extensions/gsd/commands-handlers.js +1 -1
  64. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +153 -1
  65. package/dist/resources/extensions/gsd/commands-workflow-templates.js +3 -3
  66. package/dist/resources/extensions/gsd/commands-worktree.js +11 -2
  67. package/dist/resources/extensions/gsd/db/queries.js +48 -9
  68. package/dist/resources/extensions/gsd/db/writers/cascades.js +20 -2
  69. package/dist/resources/extensions/gsd/discussion-handoff.js +8 -3
  70. package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -2
  71. package/dist/resources/extensions/gsd/doctor-engine-checks.js +292 -18
  72. package/dist/resources/extensions/gsd/doctor-providers.js +38 -10
  73. package/dist/resources/extensions/gsd/doctor.js +93 -3
  74. package/dist/resources/extensions/gsd/exec-sandbox.js +6 -2
  75. package/dist/resources/extensions/gsd/flat-phase-migration.js +106 -26
  76. package/dist/resources/extensions/gsd/forensics.js +1 -1
  77. package/dist/resources/extensions/gsd/git-conflict-resolve.js +2 -2
  78. package/dist/resources/extensions/gsd/git-service.js +59 -18
  79. package/dist/resources/extensions/gsd/gsd-db.js +75 -11
  80. package/dist/resources/extensions/gsd/guidance.js +1 -0
  81. package/dist/resources/extensions/gsd/guided-flow.js +62 -19
  82. package/dist/resources/extensions/gsd/key-manager.js +1 -0
  83. package/dist/resources/extensions/gsd/markdown-renderer.js +40 -11
  84. package/dist/resources/extensions/gsd/md-importer.js +89 -23
  85. package/dist/resources/extensions/gsd/migrate-external.js +24 -7
  86. package/dist/resources/extensions/gsd/migration-auto-check.js +30 -3
  87. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +14 -3
  88. package/dist/resources/extensions/gsd/milestone-settlement.js +1 -1
  89. package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
  90. package/dist/resources/extensions/gsd/parallel-orchestrator.js +3 -1
  91. package/dist/resources/extensions/gsd/paths.js +17 -3
  92. package/dist/resources/extensions/gsd/preferences-diagnostics.js +24 -1
  93. package/dist/resources/extensions/gsd/preferences-models.js +6 -4
  94. package/dist/resources/extensions/gsd/preferences-skills.js +10 -16
  95. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  96. package/dist/resources/extensions/gsd/preferences-validation.js +107 -2
  97. package/dist/resources/extensions/gsd/preferences.js +49 -2
  98. package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  99. package/dist/resources/extensions/gsd/prompts/system.md +6 -6
  100. package/dist/resources/extensions/gsd/quick.js +12 -3
  101. package/dist/resources/extensions/gsd/redact-secrets.js +37 -0
  102. package/dist/resources/extensions/gsd/repo-identity.js +16 -0
  103. package/dist/resources/extensions/gsd/repository-registry.js +6 -1
  104. package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
  105. package/dist/resources/extensions/gsd/safe-fs.js +23 -2
  106. package/dist/resources/extensions/gsd/skill-discovery.js +36 -23
  107. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  108. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
  109. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +2 -1
  110. package/dist/resources/extensions/gsd/state/derive/from-db.js +88 -153
  111. package/dist/resources/extensions/gsd/tool-contract.js +14 -0
  112. package/dist/resources/extensions/gsd/tools/plan-task.js +76 -8
  113. package/dist/resources/extensions/gsd/tools/reopen-slice.js +5 -0
  114. package/dist/resources/extensions/gsd/tools/reopen-task.js +13 -5
  115. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -0
  116. package/dist/resources/extensions/gsd/unit-context-manifest.js +5 -4
  117. package/dist/resources/extensions/gsd/unit-registry.js +37 -2
  118. package/dist/resources/extensions/gsd/unit-runtime.js +21 -0
  119. package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +1 -1
  120. package/dist/resources/extensions/gsd/validation-block-guard.js +2 -1
  121. package/dist/resources/extensions/gsd/workspace-git-preflight.js +2 -2
  122. package/dist/resources/extensions/gsd/workspace.js +26 -8
  123. package/dist/resources/extensions/gsd/worktree-command.js +1 -1
  124. package/dist/resources/extensions/gsd/worktree-lifecycle.js +81 -14
  125. package/dist/resources/extensions/gsd/worktree-manager.js +97 -4
  126. package/dist/resources/extensions/gsd/worktree-reentry.js +2 -1
  127. package/dist/resources/extensions/shared/interview-ui.js +62 -9
  128. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  129. package/dist/web/standalone/.next/BUILD_ID +1 -1
  130. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  131. package/dist/web/standalone/.next/build-manifest.json +3 -3
  132. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  133. package/dist/web/standalone/.next/required-server-files.json +3 -3
  134. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  135. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  136. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  137. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  138. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  139. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  140. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  141. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  142. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  143. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  144. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  145. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  146. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  147. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  148. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  149. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  150. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  151. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  152. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  153. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  154. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  155. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  156. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  157. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  158. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  159. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  160. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  161. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  162. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  163. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  164. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  165. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  166. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  167. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  168. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  169. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  170. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  171. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  172. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  173. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  174. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  175. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  176. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  177. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  178. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  179. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  180. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  181. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  182. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  183. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  184. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  185. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  186. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  187. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  188. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  189. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  190. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  191. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  192. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
  193. package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
  194. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  195. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  196. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  197. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  198. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  199. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  200. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  201. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  202. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  203. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  204. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  205. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  206. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  207. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  208. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  209. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  210. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  211. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  212. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  213. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  214. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  215. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  216. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  217. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  218. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  219. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  220. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  221. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  222. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  223. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  224. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  225. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  226. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  227. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  228. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  229. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  230. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  231. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  232. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  233. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  234. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  235. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  236. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  237. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  238. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  239. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  240. package/dist/web/standalone/.next/server/app/index.html +1 -1
  241. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  242. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  243. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  244. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  245. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  246. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  247. package/dist/web/standalone/.next/server/app/page.js +2 -2
  248. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  249. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  250. package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
  251. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  252. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  253. package/dist/web/standalone/.next/server/middleware.js +3 -3
  254. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  255. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  256. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  257. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +1 -0
  258. package/dist/web/standalone/.next/static/chunks/app/{layout-b35cbfff38aaf4cf.js → layout-4ae2d68984392bbf.js} +1 -1
  259. package/dist/web/standalone/.next/static/chunks/app/page-72a856634ad14c10.js +1 -0
  260. package/dist/web/standalone/.next/static/chunks/{main-app-590a74400e35f685.js → main-app-90d1d8d5e5d2dc6b.js} +1 -1
  261. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +1 -0
  262. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  263. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  264. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  265. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  266. package/dist/web/standalone/server.js +1 -1
  267. package/dist/wizard.d.ts +8 -0
  268. package/dist/wizard.js +8 -1
  269. package/integrations/hermes/CONTEXT.md +47 -0
  270. package/integrations/hermes/README.md +21 -0
  271. package/integrations/hermes/__init__.py +31 -0
  272. package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
  273. package/integrations/hermes/docs/setup.md +4 -0
  274. package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
  275. package/integrations/hermes/open_gsd_hermes/config.py +2 -0
  276. package/integrations/hermes/open_gsd_hermes/gsd_client.py +591 -39
  277. package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
  278. package/integrations/hermes/plugin.yaml +4 -0
  279. package/integrations/hermes/tests/test_config.py +6 -0
  280. package/integrations/hermes/tests/test_directory_loader.py +37 -0
  281. package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
  282. package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
  283. package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
  284. package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
  285. package/integrations/hermes/tests/test_milestone_client.py +447 -0
  286. package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
  287. package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
  288. package/package.json +8 -8
  289. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts +1 -0
  290. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts.map +1 -1
  291. package/packages/cloud-mcp-gateway/dist/auth-store.js +21 -1
  292. package/packages/cloud-mcp-gateway/dist/auth-store.js.map +1 -1
  293. package/packages/cloud-mcp-gateway/dist/auth-store.test.js +34 -0
  294. package/packages/cloud-mcp-gateway/dist/auth-store.test.js.map +1 -1
  295. package/packages/cloud-mcp-gateway/package.json +2 -2
  296. package/packages/contracts/package.json +1 -1
  297. package/packages/daemon/dist/cloud-runtime.d.ts +2 -0
  298. package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -1
  299. package/packages/daemon/dist/cloud-runtime.js +25 -1
  300. package/packages/daemon/dist/cloud-runtime.js.map +1 -1
  301. package/packages/daemon/dist/cloud-runtime.test.js +51 -0
  302. package/packages/daemon/dist/cloud-runtime.test.js.map +1 -1
  303. package/packages/daemon/dist/message-batcher.d.ts +7 -0
  304. package/packages/daemon/dist/message-batcher.d.ts.map +1 -1
  305. package/packages/daemon/dist/message-batcher.js +13 -0
  306. package/packages/daemon/dist/message-batcher.js.map +1 -1
  307. package/packages/daemon/dist/message-batcher.test.js +48 -0
  308. package/packages/daemon/dist/message-batcher.test.js.map +1 -1
  309. package/packages/daemon/dist/session-manager.js +1 -1
  310. package/packages/daemon/dist/session-manager.js.map +1 -1
  311. package/packages/daemon/package.json +5 -5
  312. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts +13 -1
  313. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
  314. package/packages/gsd-agent-core/dist/compaction/compaction.js +15 -1
  315. package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
  316. package/packages/gsd-agent-core/dist/export-html/template.js +53 -4
  317. package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
  318. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
  319. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
  320. package/packages/gsd-agent-core/package.json +5 -5
  321. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  322. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +3 -2
  323. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  324. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.d.ts.map +1 -1
  325. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js +1 -0
  326. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js.map +1 -1
  327. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +1 -1
  328. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
  329. package/packages/gsd-agent-modes/package.json +7 -7
  330. package/packages/mcp-server/dist/server.d.ts +18 -5
  331. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  332. package/packages/mcp-server/dist/server.js +46 -5
  333. package/packages/mcp-server/dist/server.js.map +1 -1
  334. package/packages/mcp-server/dist/session-manager.js +1 -1
  335. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  336. package/packages/mcp-server/package.json +4 -4
  337. package/packages/native/package.json +1 -1
  338. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  339. package/packages/pi-agent-core/dist/agent-loop.js +14 -9
  340. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  341. package/packages/pi-agent-core/package.json +1 -1
  342. package/packages/pi-ai/dist/env-api-keys.d.ts.map +1 -1
  343. package/packages/pi-ai/dist/env-api-keys.js +3 -0
  344. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  345. package/packages/pi-ai/dist/index.d.ts +1 -0
  346. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  347. package/packages/pi-ai/dist/index.js +1 -0
  348. package/packages/pi-ai/dist/index.js.map +1 -1
  349. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  350. package/packages/pi-ai/dist/providers/transform-messages.js +7 -3
  351. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  352. package/packages/pi-ai/dist/tool-result-content.d.ts +3 -0
  353. package/packages/pi-ai/dist/tool-result-content.d.ts.map +1 -0
  354. package/packages/pi-ai/dist/tool-result-content.js +62 -0
  355. package/packages/pi-ai/dist/tool-result-content.js.map +1 -0
  356. package/packages/pi-ai/dist/types.d.ts +10 -22
  357. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  358. package/packages/pi-ai/dist/types.js.map +1 -1
  359. package/packages/pi-ai/package.json +1 -1
  360. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  361. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  362. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  363. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +9 -0
  364. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  365. package/packages/pi-coding-agent/dist/core/package-manager.js +74 -53
  366. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  367. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +6 -0
  368. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  369. package/packages/pi-coding-agent/dist/core/resource-loader.js +30 -2
  370. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  371. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
  372. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
  373. package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
  374. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  375. package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
  376. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  377. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts +66 -0
  378. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts.map +1 -0
  379. package/packages/pi-coding-agent/dist/core/skill-directories.js +138 -0
  380. package/packages/pi-coding-agent/dist/core/skill-directories.js.map +1 -0
  381. package/packages/pi-coding-agent/dist/core/skills.d.ts +4 -1
  382. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  383. package/packages/pi-coding-agent/dist/core/skills.js +4 -1
  384. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  385. package/packages/pi-coding-agent/dist/index.d.ts +1 -0
  386. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  387. package/packages/pi-coding-agent/dist/index.js +1 -0
  388. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  389. package/packages/pi-coding-agent/package.json +7 -7
  390. package/packages/pi-tui/package.json +2 -2
  391. package/packages/rpc-client/dist/rpc-client.js +1 -1
  392. package/packages/rpc-client/dist/rpc-client.js.map +1 -1
  393. package/packages/rpc-client/package.json +2 -2
  394. package/pkg/dist/core/export-html/template.js +53 -4
  395. package/pkg/package.json +1 -1
  396. package/scripts/link-workspace-packages.cjs +14 -4
  397. package/src/resources/GSD-WORKFLOW.md +20 -25
  398. package/src/resources/extensions/ask-user-questions.ts +1 -1
  399. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +130 -51
  400. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +371 -39
  401. package/src/resources/extensions/cursor-cli/index.ts +18 -0
  402. package/src/resources/extensions/cursor-cli/models.ts +65 -0
  403. package/src/resources/extensions/cursor-cli/package.json +11 -0
  404. package/src/resources/extensions/cursor-cli/readiness.ts +143 -0
  405. package/src/resources/extensions/cursor-cli/stream-adapter.ts +440 -0
  406. package/src/resources/extensions/cursor-cli/tests/index.test.ts +54 -0
  407. package/src/resources/extensions/cursor-cli/tests/readiness.test.ts +29 -0
  408. package/src/resources/extensions/cursor-cli/tests/stream-adapter.test.ts +166 -0
  409. package/src/resources/extensions/gsd/activity-log.ts +4 -1
  410. package/src/resources/extensions/gsd/artifact-verification.ts +25 -9
  411. package/src/resources/extensions/gsd/auto/closeout.ts +163 -58
  412. package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
  413. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  414. package/src/resources/extensions/gsd/auto/orchestrator.ts +4 -3
  415. package/src/resources/extensions/gsd/auto/pre-dispatch.ts +6 -3
  416. package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
  417. package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
  418. package/src/resources/extensions/gsd/auto/unit-phase.ts +5 -9
  419. package/src/resources/extensions/gsd/auto-artifact-paths.ts +12 -3
  420. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +13 -7
  421. package/src/resources/extensions/gsd/auto-dispatch.ts +35 -7
  422. package/src/resources/extensions/gsd/auto-model-selection.ts +49 -18
  423. package/src/resources/extensions/gsd/auto-prompts.ts +73 -2
  424. package/src/resources/extensions/gsd/auto-recovery.ts +50 -6
  425. package/src/resources/extensions/gsd/auto-start.ts +9 -6
  426. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +4 -0
  427. package/src/resources/extensions/gsd/auto-worktree-branch-lifecycle.ts +237 -0
  428. package/src/resources/extensions/gsd/auto-worktree-cleanup.ts +166 -0
  429. package/src/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.ts +37 -0
  430. package/src/resources/extensions/gsd/auto-worktree-creation.ts +171 -0
  431. package/src/resources/extensions/gsd/auto-worktree-entry.ts +154 -0
  432. package/src/resources/extensions/gsd/auto-worktree-merge-already-merged.ts +161 -0
  433. package/src/resources/extensions/gsd/auto-worktree-merge-branch-head.ts +83 -0
  434. package/src/resources/extensions/gsd/auto-worktree-merge-cleanup.ts +132 -0
  435. package/src/resources/extensions/gsd/auto-worktree-merge-code-changes.ts +71 -0
  436. package/src/resources/extensions/gsd/auto-worktree-merge-db-ready.ts +112 -0
  437. package/src/resources/extensions/gsd/auto-worktree-merge-integration-branch.ts +99 -0
  438. package/src/resources/extensions/gsd/auto-worktree-merge-message.ts +166 -0
  439. package/src/resources/extensions/gsd/auto-worktree-merge-pre-teardown.ts +93 -0
  440. package/src/resources/extensions/gsd/auto-worktree-merge-stash.ts +213 -0
  441. package/src/resources/extensions/gsd/auto-worktree-merge.ts +391 -0
  442. package/src/resources/extensions/gsd/auto-worktree-milestone-shelter.ts +159 -0
  443. package/src/resources/extensions/gsd/auto-worktree-path-resolution.ts +6 -0
  444. package/src/resources/extensions/gsd/auto-worktree-path-utils.ts +10 -0
  445. package/src/resources/extensions/gsd/auto-worktree-resource-version.ts +44 -0
  446. package/src/resources/extensions/gsd/auto-worktree-runtime-cleanup.ts +119 -0
  447. package/src/resources/extensions/gsd/auto-worktree-session-registry.ts +71 -0
  448. package/src/resources/extensions/gsd/auto-worktree-sync.ts +370 -0
  449. package/src/resources/extensions/gsd/auto-worktree-teardown.ts +161 -0
  450. package/src/resources/extensions/gsd/auto-worktree.ts +69 -2390
  451. package/src/resources/extensions/gsd/auto.ts +32 -19
  452. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -0
  453. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -1
  454. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +148 -7
  455. package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
  456. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  457. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +229 -28
  458. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +23 -1
  459. package/src/resources/extensions/gsd/codebase-generator.ts +186 -19
  460. package/src/resources/extensions/gsd/commands/handlers/core.ts +39 -3
  461. package/src/resources/extensions/gsd/commands-handlers.ts +1 -1
  462. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +135 -1
  463. package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -3
  464. package/src/resources/extensions/gsd/commands-worktree.ts +13 -2
  465. package/src/resources/extensions/gsd/db/queries.ts +49 -9
  466. package/src/resources/extensions/gsd/db/writers/cascades.ts +23 -1
  467. package/src/resources/extensions/gsd/discussion-handoff.ts +10 -2
  468. package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -2
  469. package/src/resources/extensions/gsd/doctor-engine-checks.ts +372 -22
  470. package/src/resources/extensions/gsd/doctor-providers.ts +39 -10
  471. package/src/resources/extensions/gsd/doctor-types.ts +5 -1
  472. package/src/resources/extensions/gsd/doctor.ts +95 -3
  473. package/src/resources/extensions/gsd/exec-sandbox.ts +6 -2
  474. package/src/resources/extensions/gsd/flat-phase-migration.ts +132 -25
  475. package/src/resources/extensions/gsd/forensics.ts +1 -1
  476. package/src/resources/extensions/gsd/git-conflict-resolve.ts +2 -2
  477. package/src/resources/extensions/gsd/git-service.ts +67 -17
  478. package/src/resources/extensions/gsd/gsd-db.ts +105 -11
  479. package/src/resources/extensions/gsd/guidance.ts +2 -0
  480. package/src/resources/extensions/gsd/guided-flow.ts +71 -20
  481. package/src/resources/extensions/gsd/key-manager.ts +1 -0
  482. package/src/resources/extensions/gsd/markdown-renderer.ts +40 -10
  483. package/src/resources/extensions/gsd/md-importer.ts +93 -22
  484. package/src/resources/extensions/gsd/migrate-external.ts +23 -6
  485. package/src/resources/extensions/gsd/migration-auto-check.ts +32 -3
  486. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +19 -4
  487. package/src/resources/extensions/gsd/milestone-settlement.ts +1 -1
  488. package/src/resources/extensions/gsd/model-router.ts +4 -5
  489. package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
  490. package/src/resources/extensions/gsd/parallel-orchestrator.ts +3 -1
  491. package/src/resources/extensions/gsd/paths.ts +21 -3
  492. package/src/resources/extensions/gsd/preferences-diagnostics.ts +24 -0
  493. package/src/resources/extensions/gsd/preferences-models.ts +6 -3
  494. package/src/resources/extensions/gsd/preferences-skills.ts +10 -16
  495. package/src/resources/extensions/gsd/preferences-types.ts +30 -0
  496. package/src/resources/extensions/gsd/preferences-validation.ts +97 -2
  497. package/src/resources/extensions/gsd/preferences.ts +58 -4
  498. package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  499. package/src/resources/extensions/gsd/prompts/system.md +6 -6
  500. package/src/resources/extensions/gsd/quick.ts +11 -3
  501. package/src/resources/extensions/gsd/redact-secrets.ts +40 -0
  502. package/src/resources/extensions/gsd/repo-identity.ts +17 -0
  503. package/src/resources/extensions/gsd/repository-registry.ts +7 -1
  504. package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
  505. package/src/resources/extensions/gsd/safe-fs.ts +22 -2
  506. package/src/resources/extensions/gsd/skill-discovery.ts +47 -23
  507. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  508. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
  509. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +3 -1
  510. package/src/resources/extensions/gsd/state/derive/from-db.ts +201 -157
  511. package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +169 -1
  512. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +2 -1
  513. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
  514. package/src/resources/extensions/gsd/tests/auto-migrating-recovery.test.ts +18 -0
  515. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +178 -0
  516. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
  517. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -0
  518. package/src/resources/extensions/gsd/tests/auto-remote-session-lock-cleanup.test.ts +41 -0
  519. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +41 -0
  520. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +4 -4
  521. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +3 -0
  522. package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +1 -1
  523. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-harness-abort.test.ts +55 -0
  524. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +10 -1
  525. package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +1 -1
  526. package/src/resources/extensions/gsd/tests/auto-worktree-merge-already-merged.test.ts +177 -0
  527. package/src/resources/extensions/gsd/tests/auto-worktree-merge-branch-head.test.ts +118 -0
  528. package/src/resources/extensions/gsd/tests/auto-worktree-merge-cleanup.test.ts +115 -0
  529. package/src/resources/extensions/gsd/tests/auto-worktree-merge-code-changes.test.ts +76 -0
  530. package/src/resources/extensions/gsd/tests/auto-worktree-merge-db-ready.test.ts +140 -0
  531. package/src/resources/extensions/gsd/tests/auto-worktree-merge-integration-branch.test.ts +123 -0
  532. package/src/resources/extensions/gsd/tests/auto-worktree-merge-message.test.ts +114 -0
  533. package/src/resources/extensions/gsd/tests/auto-worktree-merge-pre-teardown.test.ts +84 -0
  534. package/src/resources/extensions/gsd/tests/auto-worktree-merge-stash.test.ts +70 -0
  535. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-shelter.test.ts +52 -0
  536. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +6 -6
  537. package/src/resources/extensions/gsd/tests/auto-worktree-resource-version.test.ts +59 -0
  538. package/src/resources/extensions/gsd/tests/auto-worktree-untracked-content.test.ts +3 -1
  539. package/src/resources/extensions/gsd/tests/batch-task-query.test.ts +85 -0
  540. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +29 -0
  541. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +387 -0
  542. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +62 -1
  543. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +1 -1
  544. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +4 -0
  545. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +138 -1
  546. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +571 -5
  547. package/src/resources/extensions/gsd/tests/doctor-workspace.test.ts +207 -0
  548. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +1 -1
  549. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +109 -2
  550. package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +25 -1
  551. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +87 -19
  552. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +89 -0
  553. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +40 -0
  554. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +9 -0
  555. package/src/resources/extensions/gsd/tests/import-completion-lost-update.test.ts +196 -0
  556. package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +4 -6
  557. package/src/resources/extensions/gsd/tests/integration/auto-stash-merge.test.ts +2 -1
  558. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +3 -3
  559. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +7 -8
  560. package/src/resources/extensions/gsd/tests/integration/feature-branch-lifecycle-integration.test.ts +2 -5
  561. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +76 -0
  562. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +1 -1
  563. package/src/resources/extensions/gsd/tests/integration/merge-preserve-worktree-on-preteardown-error.test.ts +1 -1
  564. package/src/resources/extensions/gsd/tests/integration/merge-strategy-regular.test.ts +2 -1
  565. package/src/resources/extensions/gsd/tests/integration/milestone-transition-worktree.test.ts +5 -7
  566. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  567. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +2 -4
  568. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +16 -15
  569. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +122 -0
  570. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +1 -1
  571. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
  572. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +1 -1
  573. package/src/resources/extensions/gsd/tests/merge-self-branch-guard.test.ts +1 -1
  574. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
  575. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +91 -0
  576. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +592 -23
  577. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +25 -1
  578. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +1 -1
  579. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -3
  580. package/src/resources/extensions/gsd/tests/parallel-worker-lock-contention.test.ts +1 -1
  581. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +2 -2
  582. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
  583. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +17 -0
  584. package/src/resources/extensions/gsd/tests/plan-task.test.ts +264 -1
  585. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +1 -4
  586. package/src/resources/extensions/gsd/tests/preferences.test.ts +169 -0
  587. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +147 -0
  588. package/src/resources/extensions/gsd/tests/recovery-verify-logs.test.ts +4 -4
  589. package/src/resources/extensions/gsd/tests/redact-secrets.test.ts +56 -0
  590. package/src/resources/extensions/gsd/tests/register-hooks-flat-phase-migration.test.ts +73 -0
  591. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +275 -0
  592. package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +52 -0
  593. package/src/resources/extensions/gsd/tests/repository-registry.test.ts +41 -2
  594. package/src/resources/extensions/gsd/tests/require-slice-discussion-dispatch.test.ts +55 -0
  595. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
  596. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
  597. package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
  598. package/src/resources/extensions/gsd/tests/skill-discovery.test.ts +46 -0
  599. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +3 -5
  600. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -0
  601. package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +2 -1
  602. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +2 -1
  603. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
  604. package/src/resources/extensions/gsd/tests/status-per-repo.test.ts +94 -0
  605. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +1 -1
  606. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +1 -1
  607. package/src/resources/extensions/gsd/tests/system-flat-phase-layout.test.ts +33 -0
  608. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +4 -4
  609. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +2 -1
  610. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +4 -4
  611. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +35 -0
  612. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +259 -0
  613. package/src/resources/extensions/gsd/tests/uat-stuck-loop-orphaned-worktree.test.ts +1 -1
  614. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +66 -18
  615. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +81 -0
  616. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  617. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +52 -0
  618. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +53 -1
  619. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +208 -1
  620. package/src/resources/extensions/gsd/tests/workspace.test.ts +18 -4
  621. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -1
  622. package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +1 -1
  623. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +1 -3
  624. package/src/resources/extensions/gsd/tests/worktree-expected-warnings.test.ts +1 -1
  625. package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +1 -1
  626. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
  627. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
  628. package/src/resources/extensions/gsd/tests/worktree-main-branch.test.ts +1 -1
  629. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +1 -1
  630. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +2 -1
  631. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +36 -3
  632. package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +1 -1
  633. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +1 -1
  634. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +32 -0
  635. package/src/resources/extensions/gsd/tool-contract.ts +29 -0
  636. package/src/resources/extensions/gsd/tools/plan-task.ts +107 -9
  637. package/src/resources/extensions/gsd/tools/reopen-slice.ts +5 -0
  638. package/src/resources/extensions/gsd/tools/reopen-task.ts +13 -5
  639. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +58 -0
  640. package/src/resources/extensions/gsd/unit-context-manifest.ts +11 -5
  641. package/src/resources/extensions/gsd/unit-registry.ts +48 -2
  642. package/src/resources/extensions/gsd/unit-runtime.ts +42 -0
  643. package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +1 -1
  644. package/src/resources/extensions/gsd/validation-block-guard.ts +2 -1
  645. package/src/resources/extensions/gsd/workspace-git-preflight.ts +2 -2
  646. package/src/resources/extensions/gsd/workspace.ts +42 -8
  647. package/src/resources/extensions/gsd/worktree-command.ts +1 -1
  648. package/src/resources/extensions/gsd/worktree-lifecycle.ts +154 -26
  649. package/src/resources/extensions/gsd/worktree-manager.ts +143 -5
  650. package/src/resources/extensions/gsd/worktree-reentry.ts +2 -1
  651. package/src/resources/extensions/shared/interview-ui.ts +58 -11
  652. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +32 -0
  653. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +0 -1
  654. package/dist/web/standalone/.next/static/chunks/app/page-bc4c723c6a19c6f0.js +0 -1
  655. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +0 -1
  656. /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → gXoWec4UNqcR0tkuHwYCr}/_buildManifest.js +0 -0
  657. /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → gXoWec4UNqcR0tkuHwYCr}/_ssgManifest.js +0 -0
@@ -0,0 +1,313 @@
1
+ """Tests for /gsd new-milestone command routing and the cross-command guards.
2
+
3
+ Covers the issue #1162 design:
4
+ - _cmd_new_milestone: input parsing (bare text, --file, empty, --auto rejected),
5
+ co-run guard (reject if auto RUNNING/BLOCKED), spawn + ack, help text.
6
+ - _cmd_auto: symmetric co-run guard (reject if milestone run active).
7
+ - _cmd_cancel: routes to cancel_milestone() when a milestone subprocess is active.
8
+ - _cmd_reply: routes planning-blocker replies to client stdin when milestone
9
+ subprocess is active with a pending blocker.
10
+
11
+ Async handlers are driven with asyncio.run, matching the convention in
12
+ test_register.py.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ from pathlib import Path
19
+ from unittest.mock import MagicMock
20
+
21
+ import pytest
22
+
23
+ from open_gsd_hermes.commands import GsdCommandRouter
24
+ from open_gsd_hermes.config import GsdConfig
25
+ from open_gsd_hermes.supervisor import SupervisorContext, SupervisorState
26
+ from open_gsd_hermes.types import BindingContext
27
+
28
+
29
+ def _make_router(
30
+ *,
31
+ project_dir: str | None = "/proj",
32
+ supervisor_state: SupervisorState = SupervisorState.IDLE,
33
+ milestone_active: bool = False,
34
+ milestone_blocker_id: str | None = None,
35
+ ) -> tuple[GsdCommandRouter, MagicMock]:
36
+ """Build a router with a MagicMock client + bind store for isolated testing."""
37
+ config = GsdConfig(default_project=project_dir)
38
+ client = MagicMock()
39
+ client.milestone_active.return_value = milestone_active
40
+ client.milestone_session_id.return_value = "S-1" if milestone_active else None
41
+ client.milestone_pending_blocker_id.return_value = milestone_blocker_id
42
+ bind_store = MagicMock()
43
+ supervisor = MagicMock()
44
+ notifications = MagicMock()
45
+
46
+ sup_ctx_holder: dict[str, SupervisorContext] = {
47
+ "ctx": SupervisorContext(
48
+ session_id=None,
49
+ project_dir=project_dir,
50
+ state=supervisor_state,
51
+ )
52
+ }
53
+
54
+ def get_session_key() -> str:
55
+ return "agent:main:cli:direct:local"
56
+
57
+ def get_binding_ctx() -> BindingContext:
58
+ return BindingContext(cwd=project_dir)
59
+
60
+ def get_supervisor_ctx() -> SupervisorContext:
61
+ return sup_ctx_holder["ctx"]
62
+
63
+ def set_supervisor_ctx(ctx: SupervisorContext) -> None:
64
+ sup_ctx_holder["ctx"] = ctx
65
+
66
+ def get_platform_channel() -> tuple[str | None, str | None]:
67
+ return "cli", "local"
68
+
69
+ router = GsdCommandRouter(
70
+ config,
71
+ client,
72
+ bind_store,
73
+ supervisor,
74
+ get_session_key,
75
+ get_binding_ctx,
76
+ get_supervisor_ctx,
77
+ set_supervisor_ctx,
78
+ get_platform_channel,
79
+ notifications,
80
+ )
81
+ return router, client
82
+
83
+
84
+ @pytest.fixture
85
+ def project(tmp_path: Path) -> Path:
86
+ """A directory that looks like a GSD project (.gsd/ present)."""
87
+ (tmp_path / ".gsd").mkdir()
88
+ return tmp_path
89
+
90
+
91
+ def run(router: GsdCommandRouter, args: str) -> str:
92
+ return asyncio.run(router.handle(args))
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # _cmd_new_milestone — input parsing & usage
97
+ # ---------------------------------------------------------------------------
98
+
99
+
100
+ def test_new_milestone_bare_text_passes_context_text(project: Path) -> None:
101
+ router, client = _make_router(project_dir=str(project))
102
+ result = run(router, "new-milestone Build a REST API with auth")
103
+ client.create_milestone.assert_called_once()
104
+ kwargs = client.create_milestone.call_args.kwargs
105
+ assert kwargs.get("context_text") == "Build a REST API with auth"
106
+ assert kwargs.get("context_file") is None
107
+ assert "started" in result.lower() or "S-1" in result
108
+
109
+
110
+ def test_new_milestone_terminal_callback_clears_local_session_id(
111
+ project: Path,
112
+ ) -> None:
113
+ router, client = _make_router(project_dir=str(project))
114
+ client.create_milestone.return_value = "milestone-4242"
115
+
116
+ run(router, "new-milestone Build a REST API with auth")
117
+ ctx = router._get_supervisor_ctx() # type: ignore[attr-defined]
118
+ ctx.pending_blocker_id = "blk-1"
119
+ assert ctx.session_id == "milestone-4242"
120
+
121
+ on_terminal = client.create_milestone.call_args.kwargs["on_terminal"]
122
+ on_terminal("complete")
123
+
124
+ ctx = router._get_supervisor_ctx() # type: ignore[attr-defined]
125
+ assert ctx.state == SupervisorState.COMPLETE
126
+ assert ctx.session_id is None
127
+ assert ctx.pending_blocker_id is None
128
+ assert ctx.notified_terminal is True
129
+
130
+ result = run(router, "reply stale response")
131
+ client.resolve_blocker.assert_not_called()
132
+ assert "no active" in result.lower()
133
+
134
+
135
+ def test_new_milestone_file_flag_passes_context_file(project: Path) -> None:
136
+ router, client = _make_router(project_dir=str(project))
137
+ spec = project / "spec.md"
138
+ spec.write_text("milestone spec", encoding="utf-8")
139
+ run(router, f"new-milestone --file {spec}")
140
+ kwargs = client.create_milestone.call_args.kwargs
141
+ assert kwargs.get("context_file") == str(spec)
142
+ assert kwargs.get("context_text") is None
143
+
144
+
145
+ def test_new_milestone_file_flag_resolves_relative_path_from_project(
146
+ project: Path,
147
+ ) -> None:
148
+ router, client = _make_router(project_dir=str(project))
149
+ spec = project / "spec.md"
150
+ spec.write_text("milestone spec", encoding="utf-8")
151
+ run(router, "new-milestone --file spec.md")
152
+ kwargs = client.create_milestone.call_args.kwargs
153
+ assert kwargs.get("context_file") == str(spec)
154
+ assert kwargs.get("context_text") is None
155
+
156
+
157
+ def test_new_milestone_file_flag_rejects_missing_file(project: Path) -> None:
158
+ router, client = _make_router(project_dir=str(project))
159
+ result = run(router, "new-milestone --file missing.md")
160
+ assert "not found" in result.lower()
161
+ client.create_milestone.assert_not_called()
162
+
163
+
164
+ def test_new_milestone_empty_returns_usage(project: Path) -> None:
165
+ router, client = _make_router(project_dir=str(project))
166
+ result = run(router, "new-milestone")
167
+ assert "usage" in result.lower()
168
+ client.create_milestone.assert_not_called()
169
+
170
+
171
+ def test_new_milestone_rejects_auto_flag(project: Path) -> None:
172
+ """--auto chaining is out of scope; execution stays with /gsd auto."""
173
+ router, client = _make_router(project_dir=str(project))
174
+ result = run(router, "new-milestone --auto some spec")
175
+ assert "auto" in result.lower()
176
+ client.create_milestone.assert_not_called()
177
+
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Co-run guards (Q2 + Q9 symmetry)
181
+ # ---------------------------------------------------------------------------
182
+
183
+
184
+ def test_new_milestone_rejects_when_auto_running(project: Path) -> None:
185
+ """Co-run guard: refuse if an auto session is RUNNING or BLOCKED."""
186
+ router, client = _make_router(
187
+ project_dir=str(project),
188
+ supervisor_state=SupervisorState.RUNNING,
189
+ )
190
+ result = run(router, "new-milestone some spec")
191
+ assert "cancel" in result.lower() or "running" in result.lower()
192
+ client.create_milestone.assert_not_called()
193
+
194
+
195
+ def test_new_milestone_rejects_when_auto_blocked(project: Path) -> None:
196
+ router, client = _make_router(
197
+ project_dir=str(project),
198
+ supervisor_state=SupervisorState.BLOCKED,
199
+ )
200
+ result = run(router, "new-milestone some spec")
201
+ assert "cancel" in result.lower() or "running" in result.lower()
202
+ client.create_milestone.assert_not_called()
203
+
204
+
205
+ def test_new_milestone_prioritizes_milestone_guard_over_auto_state(
206
+ project: Path,
207
+ ) -> None:
208
+ """A duplicate milestone command should report the milestone guard."""
209
+ router, client = _make_router(
210
+ project_dir=str(project),
211
+ supervisor_state=SupervisorState.RUNNING,
212
+ milestone_active=True,
213
+ )
214
+ result = run(router, "new-milestone some spec")
215
+ assert "milestone creation is in progress" in result.lower()
216
+ assert "auto is running" not in result.lower()
217
+ client.create_milestone.assert_not_called()
218
+
219
+
220
+ def test_new_milestone_allows_when_auto_complete(project: Path) -> None:
221
+ """A terminal auto state does not block milestone creation."""
222
+ router, client = _make_router(
223
+ project_dir=str(project),
224
+ supervisor_state=SupervisorState.COMPLETE,
225
+ )
226
+ run(router, "new-milestone some spec")
227
+ client.create_milestone.assert_called_once()
228
+
229
+
230
+ def test_auto_rejects_when_milestone_active(project: Path) -> None:
231
+ """Symmetric guard: /gsd auto refuses if a milestone run is in progress."""
232
+ router, client = _make_router(
233
+ project_dir=str(project),
234
+ milestone_active=True,
235
+ )
236
+ result = run(router, "auto")
237
+ assert "cancel" in result.lower() or "milestone" in result.lower()
238
+ client.execute.assert_not_called()
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # _cmd_cancel — milestone branch
243
+ # ---------------------------------------------------------------------------
244
+
245
+
246
+ def test_cancel_routes_to_cancel_milestone_when_milestone_active(
247
+ project: Path,
248
+ ) -> None:
249
+ router, client = _make_router(
250
+ project_dir=str(project),
251
+ milestone_active=True,
252
+ )
253
+ run(router, "cancel")
254
+ client.cancel_milestone.assert_called_once()
255
+
256
+
257
+ def test_cancel_routes_to_mcp_cancel_when_only_auto_active(project: Path) -> None:
258
+ """When no milestone proc is active, cancel falls back to the MCP path."""
259
+ router, client = _make_router(
260
+ project_dir=str(project),
261
+ supervisor_state=SupervisorState.RUNNING,
262
+ )
263
+ # Give the auto session an id so the MCP cancel-by-session path is taken
264
+ run(router, "cancel")
265
+ client.cancel_milestone.assert_not_called()
266
+ assert client.cancel.called or client.cancel_by_project.called
267
+
268
+
269
+ # ---------------------------------------------------------------------------
270
+ # _cmd_reply — milestone-blocker branch
271
+ # ---------------------------------------------------------------------------
272
+
273
+
274
+ def test_reply_routes_to_milestone_blocker_when_milestone_pending(
275
+ project: Path,
276
+ ) -> None:
277
+ router, client = _make_router(
278
+ project_dir=str(project),
279
+ milestone_active=True,
280
+ milestone_blocker_id="blk-9",
281
+ )
282
+ result = run(router, "reply use option B")
283
+ client.respond_to_milestone_blocker.assert_called_once_with("use option B")
284
+ assert "sent" in result.lower()
285
+
286
+
287
+ def test_reply_does_not_fall_through_to_mcp_when_milestone_active_no_blocker(
288
+ project: Path,
289
+ ) -> None:
290
+ """Active milestone without a pending blocker must not hit MCP resolve_blocker."""
291
+ router, client = _make_router(
292
+ project_dir=str(project),
293
+ milestone_active=True,
294
+ milestone_blocker_id=None,
295
+ )
296
+ router._get_supervisor_ctx().session_id = "milestone-1" # type: ignore[attr-defined]
297
+ result = run(router, "reply some answer")
298
+ client.respond_to_milestone_blocker.assert_not_called()
299
+ client.resolve_blocker.assert_not_called()
300
+ assert "no pending blocker" in result.lower()
301
+
302
+
303
+ def test_reply_falls_back_to_mcp_when_no_milestone(project: Path) -> None:
304
+ """No milestone proc → existing MCP resolve_blocker path (needs a session)."""
305
+ router, client = _make_router(
306
+ project_dir=str(project),
307
+ supervisor_state=SupervisorState.BLOCKED,
308
+ )
309
+ # set a session id so the existing reply path doesn't short-circuit
310
+ router._get_supervisor_ctx().session_id = "auto-1" # type: ignore[attr-defined]
311
+ run(router, "reply some answer")
312
+ client.respond_to_milestone_blocker.assert_not_called()
313
+ client.resolve_blocker.assert_called_once()
@@ -138,6 +138,7 @@ def test_auto_resets_session_specific_supervisor_context(tmp_path) -> None:
138
138
  )
139
139
  supervisor = MockSupervisor()
140
140
  client = MagicMock()
141
+ client.milestone_active.return_value = False
141
142
  client.execute.return_value = {"sessionId": "new-session"}
142
143
 
143
144
  router = GsdCommandRouter(
@@ -179,6 +180,7 @@ def test_status_returns_friendly_message_when_mcp_progress_fails(tmp_path) -> No
179
180
  def test_auto_returns_friendly_message_when_mcp_execute_fails(tmp_path) -> None:
180
181
  supervisor = MockSupervisor()
181
182
  client = MagicMock()
183
+ client.milestone_active.return_value = False
182
184
  client.execute.side_effect = McpProtocolError("sidecar unavailable")
183
185
  router, _project_dir, _supervisor, _ctx, _stored = build_router(
184
186
  tmp_path, client, supervisor=supervisor
@@ -193,6 +195,7 @@ def test_auto_returns_friendly_message_when_mcp_execute_fails(tmp_path) -> None:
193
195
  def test_auto_rejects_missing_session_id_in_mcp_response(tmp_path) -> None:
194
196
  supervisor = MockSupervisor()
195
197
  client = MagicMock()
198
+ client.milestone_active.return_value = False
196
199
  client.execute.return_value = {}
197
200
  router, _project_dir, _supervisor, ctx, _stored = build_router(
198
201
  tmp_path, client, supervisor=supervisor
@@ -209,6 +212,7 @@ def test_auto_rejects_missing_session_id_in_mcp_response(tmp_path) -> None:
209
212
  def test_cancel_stops_supervisor_when_mcp_cancel_fails(tmp_path) -> None:
210
213
  supervisor = MockSupervisor()
211
214
  client = MagicMock()
215
+ client.milestone_active.return_value = False
212
216
  client.cancel_by_project.side_effect = McpProtocolError("sidecar unavailable")
213
217
  router, _project_dir, _supervisor, ctx, stored = build_router(
214
218
  tmp_path, client, supervisor=supervisor
@@ -226,6 +230,7 @@ def test_cancel_sends_terminal_notification_after_successful_mcp_cancel(tmp_path
226
230
  supervisor = MockSupervisor()
227
231
  notifications = MagicMock()
228
232
  client = MagicMock()
233
+ client.milestone_active.return_value = False
229
234
  ctx = SupervisorContext(
230
235
  session_id="s1",
231
236
  state=SupervisorState.RUNNING,
@@ -261,6 +266,7 @@ def test_cancel_prefers_supervisor_project_for_active_session(tmp_path) -> None:
261
266
  (running_project / ".gsd").mkdir()
262
267
  supervisor = MockSupervisor()
263
268
  client = MagicMock()
269
+ client.milestone_active.return_value = False
264
270
  ctx = SupervisorContext(
265
271
  session_id="s1",
266
272
  project_dir=str(running_project),
@@ -291,6 +297,7 @@ def test_cancel_prefers_supervisor_project_for_active_session(tmp_path) -> None:
291
297
  def test_cancel_stops_supervisor_when_binding_resolution_fails(tmp_path) -> None:
292
298
  supervisor = MockSupervisor()
293
299
  client = MagicMock()
300
+ client.milestone_active.return_value = False
294
301
  ctx = SupervisorContext(
295
302
  session_id="s1",
296
303
  project_dir=str(tmp_path / "missing"),
@@ -323,6 +330,7 @@ def test_cancel_stops_supervisor_when_binding_resolution_fails(tmp_path) -> None
323
330
 
324
331
  def test_reply_returns_friendly_message_when_mcp_resolve_fails(tmp_path) -> None:
325
332
  client = MagicMock()
333
+ client.milestone_active.return_value = False
326
334
  client.resolve_blocker.side_effect = McpProtocolError("sidecar unavailable")
327
335
  ctx = SupervisorContext(session_id="s1", project_dir="/tmp/project")
328
336
  router, _project_dir, _supervisor, _ctx, _stored = build_router(
@@ -370,6 +378,38 @@ def test_supervisor_detects_blocker() -> None:
370
378
  assert any("blocker" in t.lower() or "Approve" in t for t in sent)
371
379
 
372
380
 
381
+ def test_notify_blocker_prefers_title_over_generic_message() -> None:
382
+ sent: list[str] = []
383
+
384
+ def dispatch(_name: str, args: dict) -> None:
385
+ sent.append(args.get("text", ""))
386
+
387
+ notifications = NotificationService(
388
+ MagicMock(),
389
+ GsdConfig(),
390
+ lambda: DeliveryTarget("slack", "channel", "C123"),
391
+ dispatch=dispatch,
392
+ )
393
+
394
+ notifications.notify_blocker(
395
+ SessionStatus(
396
+ status="blocked",
397
+ pending_blocker={
398
+ "type": "extension_ui_request",
399
+ "method": "select",
400
+ "id": "sel-1",
401
+ "title": "Choose milestone scope",
402
+ "message": "Action required",
403
+ "options": ["A", "B"],
404
+ },
405
+ )
406
+ )
407
+
408
+ assert len(sent) == 1
409
+ assert "GSD blocker: Choose milestone scope" in sent[0]
410
+ assert "GSD blocker: Action required" not in sent[0]
411
+
412
+
373
413
  def test_supervisor_terminal_tick_updates_progress_before_stopping() -> None:
374
414
  ctx = SupervisorContext(
375
415
  session_id="s1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/gsd-pi",
3
- "version": "1.4.0",
3
+ "version": "1.5.0-dev.4fb2d460",
4
4
  "description": "GSD Pi local-first coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -77,7 +77,7 @@
77
77
  "baseline:refactor:phase0": "pnpm run baseline:refactor:gate && node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/derive-state-db.test.ts src/resources/extensions/gsd/tests/single-writer-invariant.test.ts src/resources/extensions/gsd/tests/auto-recovery.test.ts src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts",
78
78
  "legacy:cleanup:gate": "node scripts/legacy-cleanup-gate.mjs",
79
79
  "legacy:cleanup:evidence": "node scripts/legacy-cleanup-evidence.mjs",
80
- "test:unit:compiled": "node --import ./scripts/dist-test-resolve.mjs --experimental-test-isolation=process --test-reporter=./scripts/test-reporter-compact.mjs --test \"dist-test/src/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.mjs\" \"dist-test/src/resources/extensions/shared/tests/*.test.js\" \"dist-test/src/resources/extensions/subagent/tests/*.test.js\" \"dist-test/src/resources/extensions/claude-code-cli/tests/*.test.js\" \"dist-test/src/resources/extensions/github-sync/tests/*.test.js\" \"dist-test/src/resources/extensions/universal-config/tests/*.test.js\" \"dist-test/src/resources/extensions/visual-brief/tests/*.test.js\" \"dist-test/src/resources/extensions/voice/tests/*.test.js\" \"dist-test/src/resources/extensions/mcp-client/tests/*.test.js\" \"dist-test/src/resources/extensions/remote-questions/tests/*.test.js\"",
80
+ "test:unit:compiled": "node --import ./scripts/dist-test-resolve.mjs --experimental-test-isolation=process --test-reporter=./scripts/test-reporter-compact.mjs --test \"dist-test/src/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.mjs\" \"dist-test/src/resources/extensions/shared/tests/*.test.js\" \"dist-test/src/resources/extensions/subagent/tests/*.test.js\" \"dist-test/src/resources/extensions/claude-code-cli/tests/*.test.js\" \"dist-test/src/resources/extensions/cursor-cli/tests/*.test.js\" \"dist-test/src/resources/extensions/github-sync/tests/*.test.js\" \"dist-test/src/resources/extensions/universal-config/tests/*.test.js\" \"dist-test/src/resources/extensions/visual-brief/tests/*.test.js\" \"dist-test/src/resources/extensions/voice/tests/*.test.js\" \"dist-test/src/resources/extensions/mcp-client/tests/*.test.js\" \"dist-test/src/resources/extensions/remote-questions/tests/*.test.js\"",
81
81
  "test:unit": "pnpm run test:compile && pnpm run test:unit:compiled",
82
82
  "test:changed:src": "node scripts/verify-changed-src-tests.mjs",
83
83
  "test:packages": "pnpm run test:compile && pnpm run test:packages:compiled",
@@ -123,7 +123,7 @@
123
123
  "audit:test-gaps": "node scripts/audit-test-gaps.mjs",
124
124
  "audit:test-matrix": "node scripts/audit-test-matrix.mjs",
125
125
  "test:evaluation": "node scripts/run-test-evaluation.mjs",
126
- "test:coverage:unit": "cross-env NODE_OPTIONS=--max-old-space-size=8192 c8 --reporter=lcovonly --report-dir=coverage/unit --temp-directory=coverage/.tmp-unit --exclude=\"**/*.test.*\" --exclude=\"**/tests/**\" --exclude=\"**/test/**\" --all node --import ./scripts/dist-test-resolve.mjs --experimental-test-isolation=process --test \"dist-test/src/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.mjs\" \"dist-test/src/resources/extensions/shared/tests/*.test.js\" \"dist-test/src/resources/extensions/subagent/tests/*.test.js\" \"dist-test/src/resources/extensions/claude-code-cli/tests/*.test.js\" \"dist-test/src/resources/extensions/github-sync/tests/*.test.js\" \"dist-test/src/resources/extensions/universal-config/tests/*.test.js\" \"dist-test/src/resources/extensions/visual-brief/tests/*.test.js\" \"dist-test/src/resources/extensions/voice/tests/*.test.js\" \"dist-test/src/resources/extensions/mcp-client/tests/*.test.js\" \"dist-test/src/resources/extensions/remote-questions/tests/*.test.js\"",
126
+ "test:coverage:unit": "cross-env NODE_OPTIONS=--max-old-space-size=8192 c8 --reporter=lcovonly --report-dir=coverage/unit --temp-directory=coverage/.tmp-unit --exclude=\"**/*.test.*\" --exclude=\"**/tests/**\" --exclude=\"**/test/**\" --all node --import ./scripts/dist-test-resolve.mjs --experimental-test-isolation=process --test \"dist-test/src/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.mjs\" \"dist-test/src/resources/extensions/shared/tests/*.test.js\" \"dist-test/src/resources/extensions/subagent/tests/*.test.js\" \"dist-test/src/resources/extensions/claude-code-cli/tests/*.test.js\" \"dist-test/src/resources/extensions/cursor-cli/tests/*.test.js\" \"dist-test/src/resources/extensions/github-sync/tests/*.test.js\" \"dist-test/src/resources/extensions/universal-config/tests/*.test.js\" \"dist-test/src/resources/extensions/visual-brief/tests/*.test.js\" \"dist-test/src/resources/extensions/voice/tests/*.test.js\" \"dist-test/src/resources/extensions/mcp-client/tests/*.test.js\" \"dist-test/src/resources/extensions/remote-questions/tests/*.test.js\"",
127
127
  "test:coverage:integration": "pnpm exec playwright install chromium && cross-env NODE_OPTIONS=--max-old-space-size=8192 c8 --reporter=lcovonly --report-dir=coverage/integration --temp-directory=coverage/.tmp-integration --exclude=\"**/*.test.*\" --exclude=\"**/tests/**\" --all node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test --test-timeout=180000 \"src/tests/integration/*.test.ts\" \"src/resources/extensions/gsd/tests/integration/*.test.ts\" \"src/resources/extensions/async-jobs/*.test.ts\" \"src/resources/extensions/ollama/**/*.test.ts\" \"src/resources/extensions/ollama/*.test.ts\" \"src/resources/extensions/search-the-web/tests/*.test.ts\" \"src/resources/extensions/bg-shell/tests/*.test.ts\" \"src/resources/extensions/slash-commands/tests/*.test.ts\" \"src/resources/extensions/browser-tools/tests/*.test.mjs\"",
128
128
  "test:coverage:packages": "node scripts/run-coverage-packages.mjs",
129
129
  "test:coverage:merge": "node scripts/merge-coverage-reports.mjs",
@@ -210,11 +210,11 @@
210
210
  },
211
211
  "optionalDependencies": {
212
212
  "@mariozechner/clipboard": "0.3.6",
213
- "@opengsd/engine-darwin-arm64": "1.4.0",
214
- "@opengsd/engine-darwin-x64": "1.4.0",
215
- "@opengsd/engine-linux-arm64-gnu": "1.4.0",
216
- "@opengsd/engine-linux-x64-gnu": "1.4.0",
217
- "@opengsd/engine-win32-x64-msvc": "1.4.0",
213
+ "@opengsd/engine-darwin-arm64": "1.5.0",
214
+ "@opengsd/engine-darwin-x64": "1.5.0",
215
+ "@opengsd/engine-linux-arm64-gnu": "1.5.0",
216
+ "@opengsd/engine-linux-x64-gnu": "1.5.0",
217
+ "@opengsd/engine-win32-x64-msvc": "1.5.0",
218
218
  "fsevents": "~2.3.3",
219
219
  "koffi": "^2.9.0"
220
220
  },
@@ -46,6 +46,7 @@ export declare class InMemoryAuthStore {
46
46
  revokeDeviceToken(deviceToken: string): boolean;
47
47
  snapshot(): AuthStoreSnapshot;
48
48
  protected afterMutation(): void;
49
+ private sweepExpiredPairingCodes;
49
50
  private loadSnapshot;
50
51
  }
51
52
  export declare class FileAuthStore extends InMemoryAuthStore {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../src/auth-store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,KAAK,CAAC,eAAe,GAAG,gBAAgB,CAAC,CAAC;IACtD,YAAY,EAAE,KAAK,CAAC,iBAAiB,GAAG,gBAAgB,CAAC,CAAC;IAC1D,YAAY,EAAE,KAAK,CAAC,iBAAiB,GAAG,gBAAgB,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAiB;IAC5B,SAAS,CAAC,QAAQ,CAAC,UAAU,kDAAyD;IACtF,SAAS,CAAC,QAAQ,CAAC,YAAY,oDAA2D;IAC1F,SAAS,CAAC,QAAQ,CAAC,YAAY,oDAA2D;gBAE9E,aAAa,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,QAAQ,CAAC,EAAE,iBAAiB;IAK3F,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAQjD,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI;IAO1D,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,GAAG,IAAI;IAOvE,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAiB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAS9F,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB;IAkBzE,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAQ/C,QAAQ,IAAI,iBAAiB;IAS7B,SAAS,CAAC,aAAa,IAAI,IAAI;IAI/B,OAAO,CAAC,YAAY;CAarB;AAED,qBAAa,aAAc,SAAQ,iBAAiB;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAGhC,QAAQ,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;cAQhC,aAAa,IAAI,IAAI;IAIxC,OAAO,CAAC,OAAO;CAMhB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAgB5F;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,SAAkC,GAAG,gBAAgB,CAK/G"}
1
+ {"version":3,"file":"auth-store.d.ts","sourceRoot":"","sources":["../src/auth-store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,UAAU,EAAE,KAAK,CAAC,eAAe,GAAG,gBAAgB,CAAC,CAAC;IACtD,YAAY,EAAE,KAAK,CAAC,iBAAiB,GAAG,gBAAgB,CAAC,CAAC;IAC1D,YAAY,EAAE,KAAK,CAAC,iBAAiB,GAAG,gBAAgB,CAAC,CAAC;CAC3D;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAiB;IAC5B,SAAS,CAAC,QAAQ,CAAC,UAAU,kDAAyD;IACtF,SAAS,CAAC,QAAQ,CAAC,YAAY,oDAA2D;IAC1F,SAAS,CAAC,QAAQ,CAAC,YAAY,oDAA2D;gBAE9E,aAAa,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,QAAQ,CAAC,EAAE,iBAAiB;IAK3F,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAQjD,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI;IAO1D,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,iBAAiB,GAAG,IAAI;IAOvE,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAiB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAkB9F,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,gBAAgB;IAmBzE,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;IAQ/C,QAAQ,IAAI,iBAAiB;IAS7B,SAAS,CAAC,aAAa,IAAI,IAAI;IAO/B,OAAO,CAAC,wBAAwB;IAMhC,OAAO,CAAC,YAAY;CAarB;AAED,qBAAa,aAAc,SAAQ,iBAAiB;IAClD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAGhC,QAAQ,EAAE,MAAM,EAChB,aAAa,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;cAQhC,aAAa,IAAI,IAAI;IAIxC,OAAO,CAAC,OAAO;CAMhB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAgB5F;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,SAAkC,GAAG,gBAAgB,CAK/G"}
@@ -36,7 +36,17 @@ export class InMemoryAuthStore {
36
36
  return record;
37
37
  }
38
38
  createPairingCode(userId, ttlMs = 10 * 60 * 1000) {
39
- const code = randomBytes(4).toString("hex").toUpperCase();
39
+ this.sweepExpiredPairingCodes();
40
+ // One live pairing code per user: a new code invalidates any prior un-redeemed
41
+ // one, so the guessable set never grows beyond a single code per user (and the
42
+ // per-exchange scrypt cost stays bounded).
43
+ for (const [hash, record] of this.pairingCodes) {
44
+ if (record.userId === userId)
45
+ this.pairingCodes.delete(hash);
46
+ }
47
+ // 64 bits of entropy (16 hex chars). Endpoint-level request rate limiting is the
48
+ // complementary defense and belongs at the HTTP layer, not here.
49
+ const code = randomBytes(8).toString("hex").toUpperCase();
40
50
  const expiresAt = Date.now() + ttlMs;
41
51
  const key = deriveSecretHash(code);
42
52
  this.pairingCodes.set(key.secretHash, { ...key, userId, expiresAt });
@@ -44,6 +54,7 @@ export class InMemoryAuthStore {
44
54
  return { code, expiresAt };
45
55
  }
46
56
  exchangePairingCode(code, runtimeName) {
57
+ this.sweepExpiredPairingCodes();
47
58
  const normalized = code.trim().toUpperCase();
48
59
  const codeEntry = findSecretEntry(this.pairingCodes, normalized);
49
60
  if (!codeEntry || codeEntry[1].expiresAt < Date.now()) {
@@ -80,6 +91,15 @@ export class InMemoryAuthStore {
80
91
  afterMutation() {
81
92
  // Extension point for persistent stores.
82
93
  }
94
+ // Drop expired pairing codes so a long-lived gateway does not accumulate codes
95
+ // that were generated but never redeemed. Best-effort: callers persist via their
96
+ // own afterMutation() after the create/exchange that triggered the sweep.
97
+ sweepExpiredPairingCodes(now = Date.now()) {
98
+ for (const [hash, record] of this.pairingCodes) {
99
+ if (record.expiresAt < now)
100
+ this.pairingCodes.delete(hash);
101
+ }
102
+ }
83
103
  loadSnapshot(snapshot) {
84
104
  for (const record of snapshot.userTokens ?? []) {
85
105
  this.userTokens.set(record.secretHash, record);
@@ -1 +1 @@
1
- {"version":3,"file":"auth-store.js","sourceRoot":"","sources":["../src/auth-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from \"node:crypto\";\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport interface UserTokenRecord {\n userId: string;\n revoked?: boolean;\n}\n\nexport interface DeviceTokenRecord {\n userId: string;\n runtimeId: string;\n runtimeName?: string;\n revoked?: boolean;\n}\n\nexport interface PairingCodeRecord {\n userId: string;\n expiresAt: number;\n}\n\nexport interface DeviceTokenIssue {\n userId: string;\n runtimeId: string;\n deviceToken: string;\n}\n\nexport interface AuthStoreSnapshot {\n version: 1;\n userTokens: Array<UserTokenRecord & SecretHashRecord>;\n deviceTokens: Array<DeviceTokenRecord & SecretHashRecord>;\n pairingCodes: Array<PairingCodeRecord & SecretHashRecord>;\n}\n\nexport interface SecretHashRecord {\n secretHash: string;\n secretSalt: string;\n}\n\nexport class InMemoryAuthStore {\n protected readonly userTokens = new Map<string, UserTokenRecord & SecretHashRecord>();\n protected readonly deviceTokens = new Map<string, DeviceTokenRecord & SecretHashRecord>();\n protected readonly pairingCodes = new Map<string, PairingCodeRecord & SecretHashRecord>();\n\n constructor(seedUserToken?: { token: string; userId: string }, snapshot?: AuthStoreSnapshot) {\n if (snapshot) this.loadSnapshot(snapshot);\n if (seedUserToken) this.addUserToken(seedUserToken.token, seedUserToken.userId);\n }\n\n addUserToken(token: string, userId: string): void {\n const existing = findSecretRecord(this.userTokens, token);\n if (existing?.userId === userId && !existing.revoked) return;\n const key = deriveSecretHash(token);\n this.userTokens.set(key.secretHash, { ...key, userId });\n this.afterMutation();\n }\n\n authenticateUser(token: string | undefined): string | null {\n if (!token) return null;\n const record = findSecretRecord(this.userTokens, token);\n if (!record || record.revoked) return null;\n return record.userId;\n }\n\n authenticateDevice(token: string | undefined): DeviceTokenRecord | null {\n if (!token) return null;\n const record = findSecretRecord(this.deviceTokens, token);\n if (!record || record.revoked) return null;\n return record;\n }\n\n createPairingCode(userId: string, ttlMs = 10 * 60 * 1000): { code: string; expiresAt: number } {\n const code = randomBytes(4).toString(\"hex\").toUpperCase();\n const expiresAt = Date.now() + ttlMs;\n const key = deriveSecretHash(code);\n this.pairingCodes.set(key.secretHash, { ...key, userId, expiresAt });\n this.afterMutation();\n return { code, expiresAt };\n }\n\n exchangePairingCode(code: string, runtimeName?: string): DeviceTokenIssue {\n const normalized = code.trim().toUpperCase();\n const codeEntry = findSecretEntry(this.pairingCodes, normalized);\n if (!codeEntry || codeEntry[1].expiresAt < Date.now()) {\n if (codeEntry) this.pairingCodes.delete(codeEntry[0]);\n this.afterMutation();\n throw new Error(\"Pairing code is invalid or expired\");\n }\n const [codeHash, record] = codeEntry;\n this.pairingCodes.delete(codeHash);\n const runtimeId = `rt_${randomUUID()}`;\n const deviceToken = `gsd_dev_${randomBytes(32).toString(\"hex\")}`;\n const key = deriveSecretHash(deviceToken);\n this.deviceTokens.set(key.secretHash, { ...key, userId: record.userId, runtimeId, runtimeName });\n this.afterMutation();\n return { userId: record.userId, runtimeId, deviceToken };\n }\n\n revokeDeviceToken(deviceToken: string): boolean {\n const record = findSecretRecord(this.deviceTokens, deviceToken);\n if (!record) return false;\n record.revoked = true;\n this.afterMutation();\n return true;\n }\n\n snapshot(): AuthStoreSnapshot {\n return {\n version: 1,\n userTokens: Array.from(this.userTokens.values()),\n deviceTokens: Array.from(this.deviceTokens.values()),\n pairingCodes: Array.from(this.pairingCodes.values()),\n };\n }\n\n protected afterMutation(): void {\n // Extension point for persistent stores.\n }\n\n private loadSnapshot(snapshot: AuthStoreSnapshot): void {\n for (const record of snapshot.userTokens ?? []) {\n this.userTokens.set(record.secretHash, record);\n }\n for (const record of snapshot.deviceTokens ?? []) {\n this.deviceTokens.set(record.secretHash, record);\n }\n for (const record of snapshot.pairingCodes ?? []) {\n if (record.expiresAt >= Date.now()) {\n this.pairingCodes.set(record.secretHash, record);\n }\n }\n }\n}\n\nexport class FileAuthStore extends InMemoryAuthStore {\n private readonly filePath: string;\n\n constructor(\n filePath: string,\n seedUserToken?: { token: string; userId: string },\n ) {\n super(undefined, readSnapshot(filePath));\n this.filePath = filePath;\n if (seedUserToken) this.addUserToken(seedUserToken.token, seedUserToken.userId);\n this.persist();\n }\n\n protected override afterMutation(): void {\n this.persist();\n }\n\n private persist(): void {\n mkdirSync(dirname(this.filePath), { recursive: true });\n const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;\n writeFileSync(tmp, `${JSON.stringify(this.snapshot(), null, 2)}\\n`, { encoding: \"utf8\", mode: 0o600 });\n renameSync(tmp, this.filePath);\n }\n}\n\nexport function extractBearerToken(header: string | string[] | undefined): string | undefined {\n const value = Array.isArray(header) ? header[0] : header;\n if (!value || value.length <= \"Bearer \".length) return undefined;\n if (value.slice(0, \"Bearer\".length).toLowerCase() !== \"bearer\") return undefined;\n\n const firstSeparator = value.charCodeAt(\"Bearer\".length);\n if (firstSeparator !== 0x20 && firstSeparator !== 0x09) return undefined;\n\n let tokenStart = \"Bearer\".length + 1;\n while (tokenStart < value.length) {\n const char = value.charCodeAt(tokenStart);\n if (char !== 0x20 && char !== 0x09) break;\n tokenStart += 1;\n }\n\n return tokenStart < value.length ? value.slice(tokenStart) : undefined;\n}\n\nexport function deriveSecretHash(secret: string, secretSalt = randomBytes(16).toString(\"hex\")): SecretHashRecord {\n return {\n secretHash: scryptSync(secret, secretSalt, 32).toString(\"hex\"),\n secretSalt,\n };\n}\n\nfunction findSecretRecord<T extends SecretHashRecord>(records: Map<string, T>, secret: string): T | undefined {\n return findSecretEntry(records, secret)?.[1];\n}\n\nfunction findSecretEntry<T extends SecretHashRecord>(records: Map<string, T>, secret: string): [string, T] | undefined {\n for (const entry of records) {\n if (secretMatches(entry[1], secret)) return entry;\n }\n return undefined;\n}\n\nfunction secretMatches(record: SecretHashRecord, secret: string): boolean {\n const candidate = Buffer.from(deriveSecretHash(secret, record.secretSalt).secretHash, \"hex\");\n const expected = Buffer.from(record.secretHash, \"hex\");\n return candidate.length === expected.length && timingSafeEqual(candidate, expected);\n}\n\nfunction readSnapshot(filePath: string): AuthStoreSnapshot | undefined {\n try {\n const parsed = JSON.parse(readFileSync(filePath, \"utf8\")) as Partial<AuthStoreSnapshot>;\n if (parsed.version !== 1) return undefined;\n return {\n version: 1,\n userTokens: Array.isArray(parsed.userTokens) ? parsed.userTokens as AuthStoreSnapshot[\"userTokens\"] : [],\n deviceTokens: Array.isArray(parsed.deviceTokens) ? parsed.deviceTokens as AuthStoreSnapshot[\"deviceTokens\"] : [],\n pairingCodes: Array.isArray(parsed.pairingCodes) ? parsed.pairingCodes as AuthStoreSnapshot[\"pairingCodes\"] : [],\n };\n } catch {\n return undefined;\n }\n}\n"]}
1
+ {"version":3,"file":"auth-store.js","sourceRoot":"","sources":["../src/auth-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from \"node:crypto\";\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport interface UserTokenRecord {\n userId: string;\n revoked?: boolean;\n}\n\nexport interface DeviceTokenRecord {\n userId: string;\n runtimeId: string;\n runtimeName?: string;\n revoked?: boolean;\n}\n\nexport interface PairingCodeRecord {\n userId: string;\n expiresAt: number;\n}\n\nexport interface DeviceTokenIssue {\n userId: string;\n runtimeId: string;\n deviceToken: string;\n}\n\nexport interface AuthStoreSnapshot {\n version: 1;\n userTokens: Array<UserTokenRecord & SecretHashRecord>;\n deviceTokens: Array<DeviceTokenRecord & SecretHashRecord>;\n pairingCodes: Array<PairingCodeRecord & SecretHashRecord>;\n}\n\nexport interface SecretHashRecord {\n secretHash: string;\n secretSalt: string;\n}\n\nexport class InMemoryAuthStore {\n protected readonly userTokens = new Map<string, UserTokenRecord & SecretHashRecord>();\n protected readonly deviceTokens = new Map<string, DeviceTokenRecord & SecretHashRecord>();\n protected readonly pairingCodes = new Map<string, PairingCodeRecord & SecretHashRecord>();\n\n constructor(seedUserToken?: { token: string; userId: string }, snapshot?: AuthStoreSnapshot) {\n if (snapshot) this.loadSnapshot(snapshot);\n if (seedUserToken) this.addUserToken(seedUserToken.token, seedUserToken.userId);\n }\n\n addUserToken(token: string, userId: string): void {\n const existing = findSecretRecord(this.userTokens, token);\n if (existing?.userId === userId && !existing.revoked) return;\n const key = deriveSecretHash(token);\n this.userTokens.set(key.secretHash, { ...key, userId });\n this.afterMutation();\n }\n\n authenticateUser(token: string | undefined): string | null {\n if (!token) return null;\n const record = findSecretRecord(this.userTokens, token);\n if (!record || record.revoked) return null;\n return record.userId;\n }\n\n authenticateDevice(token: string | undefined): DeviceTokenRecord | null {\n if (!token) return null;\n const record = findSecretRecord(this.deviceTokens, token);\n if (!record || record.revoked) return null;\n return record;\n }\n\n createPairingCode(userId: string, ttlMs = 10 * 60 * 1000): { code: string; expiresAt: number } {\n this.sweepExpiredPairingCodes();\n // One live pairing code per user: a new code invalidates any prior un-redeemed\n // one, so the guessable set never grows beyond a single code per user (and the\n // per-exchange scrypt cost stays bounded).\n for (const [hash, record] of this.pairingCodes) {\n if (record.userId === userId) this.pairingCodes.delete(hash);\n }\n // 64 bits of entropy (16 hex chars). Endpoint-level request rate limiting is the\n // complementary defense and belongs at the HTTP layer, not here.\n const code = randomBytes(8).toString(\"hex\").toUpperCase();\n const expiresAt = Date.now() + ttlMs;\n const key = deriveSecretHash(code);\n this.pairingCodes.set(key.secretHash, { ...key, userId, expiresAt });\n this.afterMutation();\n return { code, expiresAt };\n }\n\n exchangePairingCode(code: string, runtimeName?: string): DeviceTokenIssue {\n this.sweepExpiredPairingCodes();\n const normalized = code.trim().toUpperCase();\n const codeEntry = findSecretEntry(this.pairingCodes, normalized);\n if (!codeEntry || codeEntry[1].expiresAt < Date.now()) {\n if (codeEntry) this.pairingCodes.delete(codeEntry[0]);\n this.afterMutation();\n throw new Error(\"Pairing code is invalid or expired\");\n }\n const [codeHash, record] = codeEntry;\n this.pairingCodes.delete(codeHash);\n const runtimeId = `rt_${randomUUID()}`;\n const deviceToken = `gsd_dev_${randomBytes(32).toString(\"hex\")}`;\n const key = deriveSecretHash(deviceToken);\n this.deviceTokens.set(key.secretHash, { ...key, userId: record.userId, runtimeId, runtimeName });\n this.afterMutation();\n return { userId: record.userId, runtimeId, deviceToken };\n }\n\n revokeDeviceToken(deviceToken: string): boolean {\n const record = findSecretRecord(this.deviceTokens, deviceToken);\n if (!record) return false;\n record.revoked = true;\n this.afterMutation();\n return true;\n }\n\n snapshot(): AuthStoreSnapshot {\n return {\n version: 1,\n userTokens: Array.from(this.userTokens.values()),\n deviceTokens: Array.from(this.deviceTokens.values()),\n pairingCodes: Array.from(this.pairingCodes.values()),\n };\n }\n\n protected afterMutation(): void {\n // Extension point for persistent stores.\n }\n\n // Drop expired pairing codes so a long-lived gateway does not accumulate codes\n // that were generated but never redeemed. Best-effort: callers persist via their\n // own afterMutation() after the create/exchange that triggered the sweep.\n private sweepExpiredPairingCodes(now = Date.now()): void {\n for (const [hash, record] of this.pairingCodes) {\n if (record.expiresAt < now) this.pairingCodes.delete(hash);\n }\n }\n\n private loadSnapshot(snapshot: AuthStoreSnapshot): void {\n for (const record of snapshot.userTokens ?? []) {\n this.userTokens.set(record.secretHash, record);\n }\n for (const record of snapshot.deviceTokens ?? []) {\n this.deviceTokens.set(record.secretHash, record);\n }\n for (const record of snapshot.pairingCodes ?? []) {\n if (record.expiresAt >= Date.now()) {\n this.pairingCodes.set(record.secretHash, record);\n }\n }\n }\n}\n\nexport class FileAuthStore extends InMemoryAuthStore {\n private readonly filePath: string;\n\n constructor(\n filePath: string,\n seedUserToken?: { token: string; userId: string },\n ) {\n super(undefined, readSnapshot(filePath));\n this.filePath = filePath;\n if (seedUserToken) this.addUserToken(seedUserToken.token, seedUserToken.userId);\n this.persist();\n }\n\n protected override afterMutation(): void {\n this.persist();\n }\n\n private persist(): void {\n mkdirSync(dirname(this.filePath), { recursive: true });\n const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;\n writeFileSync(tmp, `${JSON.stringify(this.snapshot(), null, 2)}\\n`, { encoding: \"utf8\", mode: 0o600 });\n renameSync(tmp, this.filePath);\n }\n}\n\nexport function extractBearerToken(header: string | string[] | undefined): string | undefined {\n const value = Array.isArray(header) ? header[0] : header;\n if (!value || value.length <= \"Bearer \".length) return undefined;\n if (value.slice(0, \"Bearer\".length).toLowerCase() !== \"bearer\") return undefined;\n\n const firstSeparator = value.charCodeAt(\"Bearer\".length);\n if (firstSeparator !== 0x20 && firstSeparator !== 0x09) return undefined;\n\n let tokenStart = \"Bearer\".length + 1;\n while (tokenStart < value.length) {\n const char = value.charCodeAt(tokenStart);\n if (char !== 0x20 && char !== 0x09) break;\n tokenStart += 1;\n }\n\n return tokenStart < value.length ? value.slice(tokenStart) : undefined;\n}\n\nexport function deriveSecretHash(secret: string, secretSalt = randomBytes(16).toString(\"hex\")): SecretHashRecord {\n return {\n secretHash: scryptSync(secret, secretSalt, 32).toString(\"hex\"),\n secretSalt,\n };\n}\n\nfunction findSecretRecord<T extends SecretHashRecord>(records: Map<string, T>, secret: string): T | undefined {\n return findSecretEntry(records, secret)?.[1];\n}\n\nfunction findSecretEntry<T extends SecretHashRecord>(records: Map<string, T>, secret: string): [string, T] | undefined {\n for (const entry of records) {\n if (secretMatches(entry[1], secret)) return entry;\n }\n return undefined;\n}\n\nfunction secretMatches(record: SecretHashRecord, secret: string): boolean {\n const candidate = Buffer.from(deriveSecretHash(secret, record.secretSalt).secretHash, \"hex\");\n const expected = Buffer.from(record.secretHash, \"hex\");\n return candidate.length === expected.length && timingSafeEqual(candidate, expected);\n}\n\nfunction readSnapshot(filePath: string): AuthStoreSnapshot | undefined {\n try {\n const parsed = JSON.parse(readFileSync(filePath, \"utf8\")) as Partial<AuthStoreSnapshot>;\n if (parsed.version !== 1) return undefined;\n return {\n version: 1,\n userTokens: Array.isArray(parsed.userTokens) ? parsed.userTokens as AuthStoreSnapshot[\"userTokens\"] : [],\n deviceTokens: Array.isArray(parsed.deviceTokens) ? parsed.deviceTokens as AuthStoreSnapshot[\"deviceTokens\"] : [],\n pairingCodes: Array.isArray(parsed.pairingCodes) ? parsed.pairingCodes as AuthStoreSnapshot[\"pairingCodes\"] : [],\n };\n } catch {\n return undefined;\n }\n}\n"]}
@@ -22,6 +22,40 @@ test("pairing code is one-time use", () => {
22
22
  auth.exchangePairingCode(code);
23
23
  assert.throws(() => auth.exchangePairingCode(code), /invalid or expired/);
24
24
  });
25
+ test("pairing code has 64 bits of entropy (16 hex chars)", () => {
26
+ const auth = new InMemoryAuthStore({ token: "user-token", userId: "u1" });
27
+ const { code } = auth.createPairingCode("u1");
28
+ assert.match(code, /^[0-9A-F]{16}$/);
29
+ });
30
+ test("a new pairing code invalidates the user's prior un-redeemed code", () => {
31
+ const auth = new InMemoryAuthStore({ token: "user-token", userId: "u1" });
32
+ const { code: first } = auth.createPairingCode("u1");
33
+ const { code: second } = auth.createPairingCode("u1");
34
+ assert.notEqual(first, second);
35
+ assert.throws(() => auth.exchangePairingCode(first), /invalid or expired/);
36
+ // The newest code still works.
37
+ const issued = auth.exchangePairingCode(second);
38
+ assert.equal(typeof issued.deviceToken, "string");
39
+ });
40
+ test("codes for different users coexist", () => {
41
+ const auth = new InMemoryAuthStore({ token: "user-token", userId: "u1" });
42
+ auth.addUserToken("user-token-2", "u2");
43
+ const { code: c1 } = auth.createPairingCode("u1");
44
+ const { code: c2 } = auth.createPairingCode("u2");
45
+ assert.equal(auth.exchangePairingCode(c1).userId, "u1");
46
+ assert.equal(auth.exchangePairingCode(c2).userId, "u2");
47
+ });
48
+ test("expired pairing codes are swept on the next create/exchange", () => {
49
+ const auth = new InMemoryAuthStore({ token: "user-token", userId: "u1" });
50
+ // Create an already-expired code for u2 (negative ttl), then a fresh code for u1.
51
+ auth.addUserToken("user-token-2", "u2");
52
+ const { code: stale } = auth.createPairingCode("u2", -1);
53
+ auth.createPairingCode("u1"); // triggers the sweep
54
+ const snapshot = auth.snapshot();
55
+ // Only u1's live code remains; the expired u2 code was swept.
56
+ assert.equal(snapshot.pairingCodes.length, 1);
57
+ assert.throws(() => auth.exchangePairingCode(stale), /invalid or expired/);
58
+ });
25
59
  test("extractBearerToken parses bearer auth header", () => {
26
60
  assert.equal(extractBearerToken("Bearer abc"), "abc");
27
61
  assert.equal(extractBearerToken("bearer abc"), "abc");