gsd-pi 2.76.0 → 2.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (536) hide show
  1. package/README.md +45 -25
  2. package/dist/claude-cli-check.js +32 -3
  3. package/dist/mcp-server.d.ts +7 -0
  4. package/dist/mcp-server.js +35 -1
  5. package/dist/onboarding.js +45 -0
  6. package/dist/resource-loader.d.ts +1 -1
  7. package/dist/resource-loader.js +2 -8
  8. package/dist/resources/agents/researcher.md +1 -1
  9. package/dist/resources/extensions/claude-code-cli/readiness.js +31 -8
  10. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  11. package/dist/resources/extensions/gsd/auto/loop.js +9 -0
  12. package/dist/resources/extensions/gsd/auto/phases.js +104 -11
  13. package/dist/resources/extensions/gsd/auto/run-unit.js +38 -2
  14. package/dist/resources/extensions/gsd/auto/session.js +22 -1
  15. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -3
  16. package/dist/resources/extensions/gsd/auto-model-selection.js +53 -16
  17. package/dist/resources/extensions/gsd/auto-post-unit.js +25 -2
  18. package/dist/resources/extensions/gsd/auto-prompts.js +14 -0
  19. package/dist/resources/extensions/gsd/auto-recovery.js +32 -1
  20. package/dist/resources/extensions/gsd/auto-start.js +58 -57
  21. package/dist/resources/extensions/gsd/auto-verification.js +33 -0
  22. package/dist/resources/extensions/gsd/auto-worktree.js +51 -53
  23. package/dist/resources/extensions/gsd/auto.js +70 -28
  24. package/dist/resources/extensions/gsd/blocked-models.js +68 -0
  25. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +93 -1
  26. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  27. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  28. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +3 -0
  29. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +12 -0
  30. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +52 -6
  31. package/dist/resources/extensions/gsd/bootstrap/system-context.js +84 -23
  32. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +34 -2
  33. package/dist/resources/extensions/gsd/clean-root-preflight.js +93 -0
  34. package/dist/resources/extensions/gsd/commands-extract-learnings.js +54 -89
  35. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  36. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  37. package/dist/resources/extensions/gsd/complexity-classifier.js +5 -3
  38. package/dist/resources/extensions/gsd/db-writer.js +88 -16
  39. package/dist/resources/extensions/gsd/doctor-git-checks.js +23 -29
  40. package/dist/resources/extensions/gsd/doctor-providers.js +51 -5
  41. package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +1 -0
  42. package/dist/resources/extensions/gsd/error-classifier.js +31 -3
  43. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  44. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  45. package/dist/resources/extensions/gsd/gitignore.js +1 -0
  46. package/dist/resources/extensions/gsd/gsd-db.js +168 -23
  47. package/dist/resources/extensions/gsd/guided-flow.js +190 -1
  48. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  49. package/dist/resources/extensions/gsd/hook-emitter.js +108 -0
  50. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  51. package/dist/resources/extensions/gsd/key-manager.js +28 -0
  52. package/dist/resources/extensions/gsd/memory-backfill.js +126 -0
  53. package/dist/resources/extensions/gsd/memory-store.js +19 -0
  54. package/dist/resources/extensions/gsd/model-router.js +36 -3
  55. package/dist/resources/extensions/gsd/pre-execution-checks.js +44 -9
  56. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  57. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  58. package/dist/resources/extensions/gsd/preferences.js +17 -17
  59. package/dist/resources/extensions/gsd/prompt-loader.js +22 -7
  60. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  61. package/dist/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  62. package/dist/resources/extensions/gsd/prompts/debug-diagnose.md +2 -0
  63. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  64. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  65. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -2
  66. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  67. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -0
  68. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -0
  69. package/dist/resources/extensions/gsd/safety/evidence-collector.js +96 -0
  70. package/dist/resources/extensions/gsd/safety/file-change-validator.js +13 -5
  71. package/dist/resources/extensions/gsd/safety/safety-harness.js +5 -1
  72. package/dist/resources/extensions/gsd/state.js +43 -4
  73. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  74. package/dist/resources/extensions/gsd/tools/complete-milestone.js +16 -10
  75. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  76. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  77. package/dist/resources/extensions/gsd/tools/memory-tools.js +26 -1
  78. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  79. package/dist/resources/extensions/gsd/uok/plan-v2.js +20 -3
  80. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  81. package/dist/resources/extensions/gsd/workflow-templates/spike.md +6 -0
  82. package/dist/resources/extensions/gsd/worktree-resolver.js +50 -10
  83. package/dist/resources/extensions/search-the-web/command-search-provider.js +5 -4
  84. package/dist/resources/extensions/search-the-web/native-search.js +45 -13
  85. package/dist/resources/skills/api-design/SKILL.md +190 -0
  86. package/dist/resources/skills/create-mcp-server/SKILL.md +121 -0
  87. package/dist/resources/skills/decompose-into-slices/SKILL.md +139 -0
  88. package/dist/resources/skills/dependency-upgrade/SKILL.md +158 -0
  89. package/dist/resources/skills/design-an-interface/SKILL.md +102 -0
  90. package/dist/resources/skills/forensics/SKILL.md +153 -0
  91. package/dist/resources/skills/grill-me/SKILL.md +93 -0
  92. package/dist/resources/skills/handoff/SKILL.md +121 -0
  93. package/dist/resources/skills/observability/SKILL.md +174 -0
  94. package/dist/resources/skills/security-review/SKILL.md +181 -0
  95. package/dist/resources/skills/spike-wrap-up/SKILL.md +138 -0
  96. package/dist/resources/skills/tdd/SKILL.md +112 -0
  97. package/dist/resources/skills/verify-before-complete/SKILL.md +98 -0
  98. package/dist/resources/skills/write-docs/SKILL.md +82 -0
  99. package/dist/resources/skills/write-milestone-brief/SKILL.md +135 -0
  100. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  101. package/dist/web/standalone/.next/BUILD_ID +1 -1
  102. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  103. package/dist/web/standalone/.next/build-manifest.json +2 -2
  104. package/dist/web/standalone/.next/required-server-files.json +1 -1
  105. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  106. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  107. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  108. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  109. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  110. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  111. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  112. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  113. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  114. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  115. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  116. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  117. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  118. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  119. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  120. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  121. package/dist/web/standalone/.next/server/app/index.html +1 -1
  122. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  123. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  124. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  125. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  126. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  127. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  128. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  129. package/dist/web/standalone/.next/server/chunks/6897.js +2 -2
  130. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/middleware-manifest.json +1 -1
  132. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  133. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  134. package/dist/web/standalone/server.js +1 -1
  135. package/dist/welcome-screen.js +6 -1
  136. package/dist/wizard.js +2 -0
  137. package/package.json +1 -1
  138. package/packages/daemon/package.json +2 -2
  139. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  140. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  141. package/packages/mcp-server/dist/remote-questions.js +732 -0
  142. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  143. package/packages/mcp-server/dist/server.d.ts +7 -0
  144. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  145. package/packages/mcp-server/dist/server.js +70 -8
  146. package/packages/mcp-server/dist/server.js.map +1 -1
  147. package/packages/mcp-server/dist/session-manager.d.ts +14 -0
  148. package/packages/mcp-server/dist/session-manager.d.ts.map +1 -1
  149. package/packages/mcp-server/dist/session-manager.js +49 -1
  150. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  151. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
  152. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  153. package/packages/mcp-server/dist/workflow-tools.js +163 -25
  154. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  155. package/packages/mcp-server/package.json +4 -3
  156. package/packages/mcp-server/src/mcp-server.test.ts +67 -0
  157. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  158. package/packages/mcp-server/src/remote-questions.ts +916 -0
  159. package/packages/mcp-server/src/server.ts +89 -14
  160. package/packages/mcp-server/src/session-manager.ts +43 -1
  161. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  162. package/packages/mcp-server/src/workflow-tools.ts +215 -43
  163. package/packages/mcp-server/tsconfig.test.json +19 -0
  164. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  165. package/packages/native/package.json +1 -1
  166. package/packages/pi-agent-core/dist/agent-loop.js +12 -0
  167. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  168. package/packages/pi-agent-core/dist/types.d.ts +30 -0
  169. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  170. package/packages/pi-agent-core/dist/types.js.map +1 -1
  171. package/packages/pi-agent-core/package.json +1 -1
  172. package/packages/pi-agent-core/src/agent-loop.ts +14 -0
  173. package/packages/pi-agent-core/src/types.ts +34 -0
  174. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  175. package/packages/pi-ai/dist/models/custom.d.ts +38 -0
  176. package/packages/pi-ai/dist/models/custom.d.ts.map +1 -1
  177. package/packages/pi-ai/dist/models/custom.js +41 -0
  178. package/packages/pi-ai/dist/models/custom.js.map +1 -1
  179. package/packages/pi-ai/dist/providers/anthropic-auth.test.js +1 -1
  180. package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -1
  181. package/packages/pi-ai/dist/providers/anthropic-bearer-auth.test.d.ts +2 -0
  182. package/packages/pi-ai/dist/providers/anthropic-bearer-auth.test.d.ts.map +1 -0
  183. package/packages/pi-ai/dist/providers/anthropic-bearer-auth.test.js +13 -0
  184. package/packages/pi-ai/dist/providers/anthropic-bearer-auth.test.js.map +1 -0
  185. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  186. package/packages/pi-ai/dist/providers/anthropic-shared.js +27 -4
  187. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  188. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  189. package/packages/pi-ai/dist/providers/anthropic.js +13 -4
  190. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  191. package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts +2 -0
  192. package/packages/pi-ai/dist/providers/minimax-tool-name.test.d.ts.map +1 -0
  193. package/packages/pi-ai/dist/providers/minimax-tool-name.test.js +80 -0
  194. package/packages/pi-ai/dist/providers/minimax-tool-name.test.js.map +1 -0
  195. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  196. package/packages/pi-ai/dist/providers/openai-completions.js +60 -15
  197. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  198. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  199. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  200. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  201. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  202. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts +17 -0
  203. package/packages/pi-ai/dist/providers/think-tag-parser.d.ts.map +1 -0
  204. package/packages/pi-ai/dist/providers/think-tag-parser.js +75 -0
  205. package/packages/pi-ai/dist/providers/think-tag-parser.js.map +1 -0
  206. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts +2 -0
  207. package/packages/pi-ai/dist/providers/think-tag-parser.test.d.ts.map +1 -0
  208. package/packages/pi-ai/dist/providers/think-tag-parser.test.js +41 -0
  209. package/packages/pi-ai/dist/providers/think-tag-parser.test.js.map +1 -0
  210. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  211. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +12 -2
  212. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  213. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.js +164 -14
  214. package/packages/pi-ai/dist/utils/oauth/github-copilot.test.js.map +1 -1
  215. package/packages/pi-ai/dist/utils/oauth/google-antigravity.d.ts.map +1 -1
  216. package/packages/pi-ai/dist/utils/oauth/google-antigravity.js +15 -3
  217. package/packages/pi-ai/dist/utils/oauth/google-antigravity.js.map +1 -1
  218. package/packages/pi-ai/dist/utils/oauth/google-antigravity.test.d.ts +2 -0
  219. package/packages/pi-ai/dist/utils/oauth/google-antigravity.test.d.ts.map +1 -0
  220. package/packages/pi-ai/dist/utils/oauth/google-antigravity.test.js +67 -0
  221. package/packages/pi-ai/dist/utils/oauth/google-antigravity.test.js.map +1 -0
  222. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.d.ts.map +1 -1
  223. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js +16 -3
  224. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.js.map +1 -1
  225. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.test.d.ts +2 -0
  226. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.test.d.ts.map +1 -0
  227. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.test.js +67 -0
  228. package/packages/pi-ai/dist/utils/oauth/google-gemini-cli.test.js.map +1 -0
  229. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.d.ts +2 -0
  230. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.d.ts.map +1 -0
  231. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.js +289 -0
  232. package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.js.map +1 -0
  233. package/packages/pi-ai/package.json +1 -1
  234. package/packages/pi-ai/src/models/custom.ts +42 -0
  235. package/packages/pi-ai/src/providers/anthropic-auth.test.ts +1 -1
  236. package/packages/pi-ai/src/providers/anthropic-bearer-auth.test.ts +26 -0
  237. package/packages/pi-ai/src/providers/anthropic-shared.ts +26 -5
  238. package/packages/pi-ai/src/providers/anthropic.ts +15 -4
  239. package/packages/pi-ai/src/providers/minimax-tool-name.test.ts +98 -0
  240. package/packages/pi-ai/src/providers/openai-completions.ts +57 -16
  241. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  242. package/packages/pi-ai/src/providers/think-tag-parser.test.ts +44 -0
  243. package/packages/pi-ai/src/providers/think-tag-parser.ts +94 -0
  244. package/packages/pi-ai/src/utils/oauth/github-copilot.test.ts +200 -23
  245. package/packages/pi-ai/src/utils/oauth/github-copilot.ts +12 -2
  246. package/packages/pi-ai/src/utils/oauth/google-antigravity.test.ts +84 -0
  247. package/packages/pi-ai/src/utils/oauth/google-antigravity.ts +15 -5
  248. package/packages/pi-ai/src/utils/oauth/google-gemini-cli.test.ts +84 -0
  249. package/packages/pi-ai/src/utils/oauth/google-gemini-cli.ts +16 -5
  250. package/packages/pi-ai/src/utils/oauth/oauth-providers.test.ts +363 -0
  251. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  252. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +3 -2
  253. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  254. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +2 -0
  255. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  256. package/packages/pi-coding-agent/dist/core/agent-session.js +32 -2
  257. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  258. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts +1 -1
  259. package/packages/pi-coding-agent/dist/core/extensions/index.d.ts.map +1 -1
  260. package/packages/pi-coding-agent/dist/core/extensions/index.js.map +1 -1
  261. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  262. package/packages/pi-coding-agent/dist/core/extensions/loader.js +4 -0
  263. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  264. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +35 -2
  265. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  266. package/packages/pi-coding-agent/dist/core/extensions/runner.js +233 -0
  267. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  268. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +205 -2
  269. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  270. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  271. package/packages/pi-coding-agent/dist/core/hooks-runner.d.ts +53 -0
  272. package/packages/pi-coding-agent/dist/core/hooks-runner.d.ts.map +1 -0
  273. package/packages/pi-coding-agent/dist/core/hooks-runner.js +337 -0
  274. package/packages/pi-coding-agent/dist/core/hooks-runner.js.map +1 -0
  275. package/packages/pi-coding-agent/dist/core/hooks-runner.test.d.ts +2 -0
  276. package/packages/pi-coding-agent/dist/core/hooks-runner.test.d.ts.map +1 -0
  277. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +234 -0
  278. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -0
  279. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  280. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  281. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  282. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  283. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +3 -1
  284. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -1
  285. package/packages/pi-coding-agent/dist/core/model-discovery.js +92 -12
  286. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -1
  287. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +16 -1
  288. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -1
  289. package/packages/pi-coding-agent/dist/core/model-registry-auth-header.test.d.ts +2 -0
  290. package/packages/pi-coding-agent/dist/core/model-registry-auth-header.test.d.ts.map +1 -0
  291. package/packages/pi-coding-agent/dist/core/model-registry-auth-header.test.js +40 -0
  292. package/packages/pi-coding-agent/dist/core/model-registry-auth-header.test.js.map +1 -0
  293. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  294. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  295. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  296. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  297. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +61 -1
  298. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -1
  299. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +5 -0
  300. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  301. package/packages/pi-coding-agent/dist/core/model-registry.js +90 -10
  302. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  303. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  304. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  305. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  306. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  307. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  308. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  309. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  310. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  311. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  312. package/packages/pi-coding-agent/dist/core/session-manager.js +10 -6
  313. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  314. package/packages/pi-coding-agent/dist/core/session-manager.test.js +45 -1
  315. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  316. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +55 -0
  317. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  318. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  319. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  320. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  321. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  322. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  323. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  324. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  325. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  326. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -1
  327. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +13 -7
  328. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -1
  329. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  330. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  331. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  332. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  333. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  334. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  335. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  336. package/packages/pi-coding-agent/package.json +1 -1
  337. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +3 -2
  338. package/packages/pi-coding-agent/src/core/agent-session.ts +38 -2
  339. package/packages/pi-coding-agent/src/core/extensions/index.ts +16 -0
  340. package/packages/pi-coding-agent/src/core/extensions/loader.ts +5 -0
  341. package/packages/pi-coding-agent/src/core/extensions/runner.ts +351 -0
  342. package/packages/pi-coding-agent/src/core/extensions/types.ts +258 -0
  343. package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +269 -0
  344. package/packages/pi-coding-agent/src/core/hooks-runner.ts +460 -0
  345. package/packages/pi-coding-agent/src/core/index.ts +10 -0
  346. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +19 -0
  347. package/packages/pi-coding-agent/src/core/model-discovery.ts +99 -12
  348. package/packages/pi-coding-agent/src/core/model-registry-auth-header.test.ts +44 -0
  349. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  350. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +75 -0
  351. package/packages/pi-coding-agent/src/core/model-registry.ts +102 -10
  352. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  353. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  354. package/packages/pi-coding-agent/src/core/session-manager.test.ts +65 -1
  355. package/packages/pi-coding-agent/src/core/session-manager.ts +10 -6
  356. package/packages/pi-coding-agent/src/core/settings-manager.ts +57 -0
  357. package/packages/pi-coding-agent/src/index.ts +16 -0
  358. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  359. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +16 -7
  360. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  361. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  362. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  363. package/packages/pi-tui/package.json +1 -1
  364. package/packages/rpc-client/package.json +1 -1
  365. package/pkg/package.json +1 -1
  366. package/scripts/link-workspace-packages.cjs +1 -0
  367. package/src/resources/agents/researcher.md +1 -1
  368. package/src/resources/extensions/claude-code-cli/readiness.ts +32 -8
  369. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  370. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  371. package/src/resources/extensions/gsd/auto/loop-deps.ts +14 -0
  372. package/src/resources/extensions/gsd/auto/loop.ts +9 -0
  373. package/src/resources/extensions/gsd/auto/phases.ts +131 -10
  374. package/src/resources/extensions/gsd/auto/run-unit.ts +40 -2
  375. package/src/resources/extensions/gsd/auto/session.ts +35 -2
  376. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -3
  377. package/src/resources/extensions/gsd/auto-model-selection.ts +71 -15
  378. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  379. package/src/resources/extensions/gsd/auto-prompts.ts +28 -1
  380. package/src/resources/extensions/gsd/auto-recovery.ts +26 -1
  381. package/src/resources/extensions/gsd/auto-start.ts +60 -68
  382. package/src/resources/extensions/gsd/auto-verification.ts +33 -0
  383. package/src/resources/extensions/gsd/auto-worktree.ts +62 -63
  384. package/src/resources/extensions/gsd/auto.ts +73 -28
  385. package/src/resources/extensions/gsd/blocked-models.ts +98 -0
  386. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +120 -1
  387. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  388. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  389. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +5 -0
  390. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +15 -0
  391. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +54 -6
  392. package/src/resources/extensions/gsd/bootstrap/system-context.ts +89 -26
  393. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +35 -2
  394. package/src/resources/extensions/gsd/clean-root-preflight.ts +111 -0
  395. package/src/resources/extensions/gsd/commands-extract-learnings.ts +55 -90
  396. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  397. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  398. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -3
  399. package/src/resources/extensions/gsd/db-writer.ts +88 -17
  400. package/src/resources/extensions/gsd/doctor-git-checks.ts +23 -27
  401. package/src/resources/extensions/gsd/doctor-providers.ts +59 -6
  402. package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +2 -0
  403. package/src/resources/extensions/gsd/error-classifier.ts +36 -3
  404. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  405. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  406. package/src/resources/extensions/gsd/gitignore.ts +1 -1
  407. package/src/resources/extensions/gsd/gsd-db.ts +186 -23
  408. package/src/resources/extensions/gsd/guided-flow.ts +222 -1
  409. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  410. package/src/resources/extensions/gsd/hook-emitter.ts +188 -0
  411. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  412. package/src/resources/extensions/gsd/journal.ts +2 -1
  413. package/src/resources/extensions/gsd/key-manager.ts +28 -0
  414. package/src/resources/extensions/gsd/memory-backfill.ts +140 -0
  415. package/src/resources/extensions/gsd/memory-store.ts +26 -0
  416. package/src/resources/extensions/gsd/model-router.ts +42 -1
  417. package/src/resources/extensions/gsd/pre-execution-checks.ts +46 -10
  418. package/src/resources/extensions/gsd/preferences-types.ts +46 -0
  419. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  420. package/src/resources/extensions/gsd/preferences.ts +17 -17
  421. package/src/resources/extensions/gsd/prompt-loader.ts +30 -7
  422. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  423. package/src/resources/extensions/gsd/prompts/complete-slice.md +2 -2
  424. package/src/resources/extensions/gsd/prompts/debug-diagnose.md +2 -0
  425. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  426. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  427. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -2
  428. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  429. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -0
  430. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -0
  431. package/src/resources/extensions/gsd/safety/evidence-collector.ts +119 -0
  432. package/src/resources/extensions/gsd/safety/file-change-validator.ts +17 -4
  433. package/src/resources/extensions/gsd/safety/safety-harness.ts +9 -0
  434. package/src/resources/extensions/gsd/state.ts +45 -4
  435. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +188 -2
  436. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +95 -1
  437. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +12 -0
  438. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
  439. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +141 -0
  440. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +33 -3
  441. package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +38 -0
  442. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +23 -0
  443. package/src/resources/extensions/gsd/tests/blocked-models.test.ts +98 -0
  444. package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +54 -0
  445. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +186 -0
  446. package/src/resources/extensions/gsd/tests/commands-extract-learnings.test.ts +68 -66
  447. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  448. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +61 -1
  449. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  450. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  451. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +3 -3
  452. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  453. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +42 -0
  454. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +8 -4
  455. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +148 -3
  456. package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +1 -1
  457. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +306 -1
  458. package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
  459. package/src/resources/extensions/gsd/tests/exec-history.test.ts +237 -0
  460. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  461. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +58 -0
  462. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +40 -9
  463. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +62 -0
  464. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +447 -1
  465. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  466. package/src/resources/extensions/gsd/tests/integration/doctor-git-symlink-cwd.test.ts +11 -0
  467. package/src/resources/extensions/gsd/tests/integration/doctor-git.test.ts +78 -0
  468. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +1 -0
  469. package/src/resources/extensions/gsd/tests/integration/gitignore-tracked-gsd.test.ts +1 -0
  470. package/src/resources/extensions/gsd/tests/integration/idle-recovery.test.ts +30 -0
  471. package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +1 -1
  472. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  473. package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
  474. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +37 -0
  475. package/src/resources/extensions/gsd/tests/key-manager.test.ts +9 -0
  476. package/src/resources/extensions/gsd/tests/load-memory-block.test.ts +36 -0
  477. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  478. package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +12 -0
  479. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  480. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
  481. package/src/resources/extensions/gsd/tests/plan-gate-failed-doctor-heal-hint.test.ts +37 -0
  482. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  483. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +272 -0
  484. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +356 -0
  485. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  486. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  487. package/src/resources/extensions/gsd/tests/prompt-loader-extension-dir.test.ts +49 -0
  488. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +103 -4
  489. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  490. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  491. package/src/resources/extensions/gsd/tests/resume-dispatch-worktree.test.ts +230 -0
  492. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +205 -0
  493. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  494. package/src/resources/extensions/gsd/tests/schema-v21-sequence.test.ts +413 -0
  495. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  496. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
  497. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  498. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  499. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +23 -0
  500. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +9 -3
  501. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  502. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +35 -0
  503. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +6 -1
  504. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -5
  505. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  506. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  507. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  508. package/src/resources/extensions/gsd/tools/complete-milestone.ts +15 -9
  509. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  510. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  511. package/src/resources/extensions/gsd/tools/memory-tools.ts +31 -1
  512. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  513. package/src/resources/extensions/gsd/uok/plan-v2.ts +26 -3
  514. package/src/resources/extensions/gsd/workflow-logger.ts +4 -1
  515. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  516. package/src/resources/extensions/gsd/workflow-templates/spike.md +6 -0
  517. package/src/resources/extensions/gsd/worktree-resolver.ts +54 -9
  518. package/src/resources/extensions/search-the-web/command-search-provider.ts +5 -4
  519. package/src/resources/extensions/search-the-web/native-search.ts +48 -12
  520. package/src/resources/skills/api-design/SKILL.md +190 -0
  521. package/src/resources/skills/create-mcp-server/SKILL.md +121 -0
  522. package/src/resources/skills/decompose-into-slices/SKILL.md +139 -0
  523. package/src/resources/skills/dependency-upgrade/SKILL.md +158 -0
  524. package/src/resources/skills/design-an-interface/SKILL.md +102 -0
  525. package/src/resources/skills/forensics/SKILL.md +153 -0
  526. package/src/resources/skills/grill-me/SKILL.md +93 -0
  527. package/src/resources/skills/handoff/SKILL.md +121 -0
  528. package/src/resources/skills/observability/SKILL.md +174 -0
  529. package/src/resources/skills/security-review/SKILL.md +181 -0
  530. package/src/resources/skills/spike-wrap-up/SKILL.md +138 -0
  531. package/src/resources/skills/tdd/SKILL.md +112 -0
  532. package/src/resources/skills/verify-before-complete/SKILL.md +98 -0
  533. package/src/resources/skills/write-docs/SKILL.md +82 -0
  534. package/src/resources/skills/write-milestone-brief/SKILL.md +135 -0
  535. /package/dist/web/standalone/.next/static/{ssX7BLv3Dw9Fb4CtrCGeR → pV-mPo7rYGb5JBC09C8GG}/_buildManifest.js +0 -0
  536. /package/dist/web/standalone/.next/static/{ssX7BLv3Dw9Fb4CtrCGeR → pV-mPo7rYGb5JBC09C8GG}/_ssgManifest.js +0 -0
@@ -102,7 +102,8 @@ describe("token-counter: estimateTokensForProvider", () => {
102
102
 
103
103
  describe("token-counter: backward compatibility", () => {
104
104
  it("countTokensSync returns heuristic estimate when tiktoken is not loaded", () => {
105
- // Without tiktoken loaded, countTokensSync falls back to ceil(len/4)
105
+ // Without tiktoken loaded, countTokensSync falls back to estimateTokensForProvider.
106
+ // With no provider (defaults to "unknown", ratio 4.0): ceil(100/4) = 25.
106
107
  const text = "A".repeat(100);
107
108
  const result = countTokensSync(text);
108
109
  // Either tiktoken is loaded (exact count) or heuristic (ceil(100/4) = 25)
@@ -127,3 +128,106 @@ describe("token-counter: backward compatibility", () => {
127
128
  assert.equal(result, 0);
128
129
  });
129
130
  });
131
+
132
+ // ─── provider-aware fallback (issue #4529) ───────────────────────────────────
133
+ // Regression tests: countTokens/countTokensSync must use provider-specific
134
+ // ratios for their heuristic fallback, not a hardcoded GPT-4o / 4 divisor.
135
+
136
+ describe("token-counter: provider-aware heuristic fallback", () => {
137
+ // These tests exercise the heuristic path (no tiktoken or before init).
138
+ // We call estimateTokensForProvider directly to validate expected values,
139
+ // then verify countTokens/countTokensSync return the same values when
140
+ // tiktoken is unavailable.
141
+
142
+ it("countTokensSync uses anthropic ratio (3.5) when provider is 'anthropic'", () => {
143
+ const text = "A".repeat(350);
144
+ // anthropic: ceil(350 / 3.5) = 100
145
+ // openai/unknown: ceil(350 / 4.0) = 88
146
+ // These are different — the provider must matter.
147
+ const anthropicEstimate = estimateTokensForProvider(text, "anthropic");
148
+ const unknownEstimate = estimateTokensForProvider(text, "unknown");
149
+ assert.equal(anthropicEstimate, 100, "anthropic ratio should give 100 tokens for 350 chars");
150
+ assert.equal(unknownEstimate, 88, "unknown ratio should give 88 tokens for 350 chars");
151
+ assert.notEqual(
152
+ anthropicEstimate,
153
+ unknownEstimate,
154
+ "anthropic and unknown estimates must differ — if they are equal the provider is being ignored",
155
+ );
156
+
157
+ // Actually call countTokensSync with the anthropic provider.
158
+ // When tiktoken is not loaded, this must return the provider-aware estimate (100).
159
+ // When tiktoken is loaded, it returns the tiktoken count (which is also > 0 and
160
+ // will be in the range [88, 120] for 350 "A" characters with cl100k_base).
161
+ const syncResult = countTokensSync(text, "anthropic");
162
+ assert.ok(typeof syncResult === "number", "countTokensSync must return a number");
163
+ assert.ok(syncResult > 0, "countTokensSync must return a positive count");
164
+ // If tiktoken is unavailable the result must exactly match the anthropic heuristic.
165
+ // If tiktoken is available we cannot assert the exact value, but we know it will
166
+ // not equal the unknown-provider heuristic (88) for 350 identical characters.
167
+ const tiktokenAvailable = syncResult !== anthropicEstimate;
168
+ if (!tiktokenAvailable) {
169
+ assert.equal(
170
+ syncResult,
171
+ anthropicEstimate,
172
+ `countTokensSync with 'anthropic' provider must return ${anthropicEstimate} (not the unknown-provider value ${unknownEstimate}) when tiktoken is unavailable`,
173
+ );
174
+ }
175
+ });
176
+
177
+ it("countTokens uses anthropic ratio when provider='anthropic' and tiktoken unavailable", async () => {
178
+ const text = "A".repeat(350);
179
+ // anthropic heuristic: ceil(350 / 3.5) = 100
180
+ // unknown/hardcoded-4 heuristic: ceil(350 / 4.0) = 88
181
+ const anthropicEstimate = estimateTokensForProvider(text, "anthropic"); // 100
182
+ const unknownEstimate = estimateTokensForProvider(text, "unknown"); // 88
183
+ const hardcodedFallback = Math.ceil(text.length / 4); // 88
184
+
185
+ // The anthropic heuristic must produce more tokens than the old /4 fallback.
186
+ assert.equal(anthropicEstimate, 100, "anthropic heuristic should give 100 tokens for 350 chars");
187
+ assert.ok(
188
+ anthropicEstimate > hardcodedFallback,
189
+ `anthropic estimate (${anthropicEstimate}) must exceed the hardcoded /4 fallback (${hardcodedFallback})`,
190
+ );
191
+ assert.notEqual(
192
+ anthropicEstimate,
193
+ unknownEstimate,
194
+ "anthropic and unknown estimates must differ — provider is being ignored if equal",
195
+ );
196
+
197
+ // Call countTokens with the anthropic provider and verify the result.
198
+ const result = await countTokens(text, "anthropic");
199
+ assert.ok(typeof result === "number", "should return a number");
200
+ assert.ok(result > 0, "should return a positive token count");
201
+
202
+ // When tiktoken is unavailable: result must equal the anthropic heuristic (100),
203
+ // NOT the unknown-provider heuristic (88). This is the core regression guard for #4529.
204
+ // When tiktoken IS available: result is the tiktoken count, which will differ from
205
+ // the heuristic — but it must never equal the wrong (unknown) heuristic for this text.
206
+ if (result === anthropicEstimate || result === unknownEstimate) {
207
+ // We are on the heuristic path — assert the correct provider ratio was used.
208
+ assert.equal(
209
+ result,
210
+ anthropicEstimate,
211
+ `countTokens with 'anthropic' provider returned ${result} but expected ${anthropicEstimate} (anthropic heuristic) not ${unknownEstimate} (unknown/hardcoded heuristic)`,
212
+ );
213
+ } else {
214
+ // tiktoken is active — result is an exact BPE count.
215
+ // For 350 identical "A" characters cl100k_base produces a count in [80, 130].
216
+ assert.ok(
217
+ result >= 80 && result <= 130,
218
+ `tiktoken count ${result} for 350 "A" chars is outside expected range [80, 130]`,
219
+ );
220
+ }
221
+ });
222
+
223
+ it("countTokens with provider='anthropic' yields more tokens than provider='openai' (heuristic)", () => {
224
+ const text = "A".repeat(400);
225
+ // anthropic: ceil(400/3.5) = 115, openai: ceil(400/4.0) = 100
226
+ const anthropic = estimateTokensForProvider(text, "anthropic");
227
+ const openai = estimateTokensForProvider(text, "openai");
228
+ assert.ok(
229
+ anthropic > openai,
230
+ `anthropic estimate (${anthropic}) must exceed openai estimate (${openai}) for same text`,
231
+ );
232
+ });
233
+ });
@@ -14,6 +14,7 @@ import {
14
14
  isToolCompatibleWithProvider,
15
15
  filterToolsForProvider,
16
16
  adjustToolSet,
17
+ GROQ_MAX_TOOLS,
17
18
  } from "../model-router.js";
18
19
 
19
20
  import {
@@ -197,3 +198,109 @@ describe("adjustToolSet", () => {
197
198
  assert.deepEqual(removedTools, ["mcp_complex"]);
198
199
  });
199
200
  });
201
+
202
+ // ─── GROQ_MAX_TOOLS constant ─────────────────────────────────────────────────
203
+
204
+ describe("GROQ_MAX_TOOLS", () => {
205
+ test("equals 128", () => {
206
+ assert.equal(GROQ_MAX_TOOLS, 128);
207
+ });
208
+ });
209
+
210
+ // ─── Groq tool-count cap (#4376) ────────────────────────────────────────────
211
+
212
+ describe("filterToolsForProvider — Groq 128-tool cap", () => {
213
+ beforeEach(() => {
214
+ resetToolCompatibilityRegistry();
215
+ });
216
+
217
+ test("does not cap when provider is not groq", () => {
218
+ const toolNames = Array.from({ length: 200 }, (_, i) => `tool_${i}`);
219
+ const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-completions");
220
+ assert.equal(compatible.length, 200);
221
+ assert.equal(filtered.length, 0);
222
+ });
223
+
224
+ test("does not cap when <= 128 tools with groq provider", () => {
225
+ const toolNames = Array.from({ length: 128 }, (_, i) => `tool_${i}`);
226
+ const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-completions", "groq");
227
+ assert.equal(compatible.length, 128);
228
+ assert.equal(filtered.length, 0);
229
+ });
230
+
231
+ test("caps to 128 when >128 tools with groq provider", () => {
232
+ const toolNames = Array.from({ length: 200 }, (_, i) => `tool_${i}`);
233
+ const { compatible, filtered } = filterToolsForProvider(toolNames, "openai-completions", "groq");
234
+ assert.equal(compatible.length, 128);
235
+ assert.equal(filtered.length, 72);
236
+ });
237
+
238
+ test("keeps the first 128 tools when capping", () => {
239
+ const toolNames = Array.from({ length: 200 }, (_, i) => `tool_${i}`);
240
+ const { compatible } = filterToolsForProvider(toolNames, "openai-completions", "groq");
241
+ assert.equal(compatible[0], "tool_0");
242
+ assert.equal(compatible[127], "tool_127");
243
+ });
244
+
245
+ test("trimmed tools appear in filtered list", () => {
246
+ const toolNames = Array.from({ length: 130 }, (_, i) => `tool_${i}`);
247
+ const { filtered } = filterToolsForProvider(toolNames, "openai-completions", "groq");
248
+ assert.equal(filtered.length, 2);
249
+ assert.equal(filtered[0], "tool_128");
250
+ assert.equal(filtered[1], "tool_129");
251
+ });
252
+
253
+ test("emits a warning when tools are trimmed", () => {
254
+ const warnings: string[] = [];
255
+ const original = console.warn;
256
+ console.warn = (...args: unknown[]) => { warnings.push(String(args[0])); };
257
+ try {
258
+ const toolNames = Array.from({ length: 129 }, (_, i) => `tool_${i}`);
259
+ filterToolsForProvider(toolNames, "openai-completions", "groq");
260
+ assert.equal(warnings.length, 1);
261
+ assert.ok(warnings[0].includes("128"), "warning mentions Groq limit");
262
+ } finally {
263
+ console.warn = original;
264
+ }
265
+ });
266
+
267
+ test("does not warn when tools are at the limit", () => {
268
+ const warnings: string[] = [];
269
+ const original = console.warn;
270
+ console.warn = (...args: unknown[]) => { warnings.push(String(args[0])); };
271
+ try {
272
+ const toolNames = Array.from({ length: 128 }, (_, i) => `tool_${i}`);
273
+ filterToolsForProvider(toolNames, "openai-completions", "groq");
274
+ assert.equal(warnings.length, 0);
275
+ } finally {
276
+ console.warn = original;
277
+ }
278
+ });
279
+ });
280
+
281
+ describe("adjustToolSet — Groq 128-tool cap", () => {
282
+ beforeEach(() => {
283
+ resetToolCompatibilityRegistry();
284
+ });
285
+
286
+ test("caps to 128 tools when provider is groq and >128 tools active", () => {
287
+ const toolNames = Array.from({ length: 150 }, (_, i) => `tool_${i}`);
288
+ const { toolNames: result, removedTools } = adjustToolSet(toolNames, "openai-completions", "groq");
289
+ assert.equal(result.length, 128);
290
+ assert.equal(removedTools.length, 22);
291
+ });
292
+
293
+ test("does not cap for non-groq providers even with >128 tools", () => {
294
+ const toolNames = Array.from({ length: 150 }, (_, i) => `tool_${i}`);
295
+ const { toolNames: result, removedTools } = adjustToolSet(toolNames, "openai-completions", "openai");
296
+ assert.equal(result.length, 150);
297
+ assert.equal(removedTools.length, 0);
298
+ });
299
+
300
+ test("does not cap when provider is omitted", () => {
301
+ const toolNames = Array.from({ length: 150 }, (_, i) => `tool_${i}`);
302
+ const { toolNames: result, removedTools } = adjustToolSet(toolNames, "openai-completions");
303
+ assert.equal(result.length, 150);
304
+ assert.equal(removedTools.length, 0);
305
+ });
306
+ });
@@ -109,6 +109,29 @@ test("plan-v2 gate fails closed for execution phase when finalized context is mi
109
109
  assert.match(compiled.reason ?? "", /CONTEXT\.md/i);
110
110
  });
111
111
 
112
+ test("plan-v2 gate accepts finalized context from project-root fallback", () => {
113
+ const projectRoot = createBasePath();
114
+ const worktreeBase = createBasePath();
115
+ seedGraphRows();
116
+
117
+ writeMilestoneFile(projectRoot, "CONTEXT", "Finalized context in project root.");
118
+ writeMilestoneFile(worktreeBase, "CONTEXT-DRAFT", "Draft context in worktree.");
119
+
120
+ const prevProjectRoot = process.env.GSD_PROJECT_ROOT;
121
+ process.env.GSD_PROJECT_ROOT = projectRoot;
122
+ try {
123
+ const compiled = ensurePlanV2Graph(worktreeBase, buildState("executing"));
124
+ assert.equal(compiled.ok, true);
125
+ assert.equal(compiled.finalizedContextIncluded, true);
126
+ } finally {
127
+ if (prevProjectRoot === undefined) {
128
+ delete process.env.GSD_PROJECT_ROOT;
129
+ } else {
130
+ process.env.GSD_PROJECT_ROOT = prevProjectRoot;
131
+ }
132
+ }
133
+ });
134
+
112
135
  test("plan-v2 compiler writes pipeline metadata for clarify/research/draft stages", () => {
113
136
  const basePath = createBasePath();
114
137
  seedGraphRows();
@@ -177,16 +177,22 @@ test("deriveState returns completing-milestone when VALIDATION exists with termi
177
177
  }
178
178
  });
179
179
 
180
- test("deriveState treats needs-remediation as non-terminal re-enters validating-milestone (#832)", async () => {
180
+ test("deriveState returns blocked when needs-remediation has no incomplete slices (#4506)", async () => {
181
181
  const base = makeTmpBase();
182
182
  try {
183
183
  writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
184
184
  writeValidation(base, "M001", "---\nverdict: needs-remediation\nremediation_round: 0\n---\n\n# Validation\nNeeds fixes.");
185
185
 
186
186
  const state = await deriveState(base);
187
- // needs-remediation routes back to validating-milestone for re-validation
188
- assert.equal(state.phase, "validating-milestone");
187
+ // All slices done + needs-remediation blocked (prevents infinite
188
+ // validate-milestone dispatch loop). Previously returned
189
+ // validating-milestone, which caused #4506.
190
+ assert.equal(state.phase, "blocked");
189
191
  assert.equal(state.activeMilestone?.id, "M001");
192
+ assert.ok(
193
+ state.blockers.some(b => b.includes("needs-remediation") && b.includes("M001")),
194
+ "blocker message should mention milestone and verdict",
195
+ );
190
196
  } finally {
191
197
  cleanup(base);
192
198
  }
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from "node:fs";
3
+ import { mkdirSync, rmSync, readFileSync, existsSync, writeFileSync, unlinkSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
@@ -11,7 +11,7 @@ import {
11
11
  _getAdapter,
12
12
  insertGateRow,
13
13
  } from "../gsd-db.ts";
14
- import { markDepthVerified, clearDiscussionFlowState } from "../bootstrap/write-gate.ts";
14
+ import { markDepthVerified, clearDiscussionFlowState, loadWriteGateSnapshot } from "../bootstrap/write-gate.ts";
15
15
  import {
16
16
  executeCompleteMilestone,
17
17
  executePlanMilestone,
@@ -742,3 +742,66 @@ test("executeSummarySave leaves sibling CONTEXT-DRAFT intact for non-CONTEXT art
742
742
  cleanup(base);
743
743
  }
744
744
  });
745
+
746
+ test("executeSummarySave CONTEXT HARD BLOCK clears after write-gate state file is deleted (#4343)", async () => {
747
+ const base = makeTmpBase();
748
+ const originalEnv = process.env.GSD_PERSIST_WRITE_GATE_STATE;
749
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
750
+ try {
751
+ openTestDb(base);
752
+ clearDiscussionFlowState();
753
+
754
+ // First call: CONTEXT artifact without depth verification → HARD BLOCK
755
+ const blocked = await inProjectDir(base, () => executeSummarySave({
756
+ milestone_id: "M001",
757
+ artifact_type: "CONTEXT",
758
+ content: "# Context\n\ncontent",
759
+ }, base));
760
+ assert.equal(blocked.isError, true, "should be blocked without depth verification");
761
+ assert.match(
762
+ blocked.content[0].text,
763
+ /HARD BLOCK/,
764
+ "blocked result should mention HARD BLOCK",
765
+ );
766
+
767
+ // Verify the state file was written (persist mode is active)
768
+ const stateFilePath = join(base, ".gsd", "runtime", "write-gate-state.json");
769
+ // The state file may or may not exist at this point (block doesn't write state).
770
+ // Write a fake state file simulating stale persisted block state.
771
+ mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
772
+ writeFileSync(stateFilePath, JSON.stringify({
773
+ verifiedDepthMilestones: [],
774
+ activeQueuePhase: false,
775
+ pendingGateId: "depth_verification_M001",
776
+ }));
777
+
778
+ // User deletes the state file to reset the block
779
+ unlinkSync(stateFilePath);
780
+ assert.ok(!existsSync(stateFilePath), "state file deleted");
781
+
782
+ // The snapshot loaded after deletion should be clean (no pending gate, no block)
783
+ const snapshot = loadWriteGateSnapshot(base);
784
+ assert.equal(snapshot.pendingGateId, null, "pendingGateId should be null after file deletion");
785
+ assert.deepEqual(snapshot.verifiedDepthMilestones, [], "verifiedDepthMilestones should be empty after file deletion");
786
+
787
+ // Depth-verify and re-attempt: should succeed after deletion clears stale state
788
+ markDepthVerified("M001", base);
789
+
790
+ const unblocked = await inProjectDir(base, () => executeSummarySave({
791
+ milestone_id: "M001",
792
+ artifact_type: "CONTEXT",
793
+ content: "# Context\n\nfinal content",
794
+ }, base));
795
+ assert.equal(unblocked.isError, undefined, "should not be blocked after depth verification");
796
+ assert.equal(unblocked.details.operation, "save_summary");
797
+ } finally {
798
+ if (originalEnv === undefined) {
799
+ delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
800
+ } else {
801
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
802
+ }
803
+ clearDiscussionFlowState();
804
+ closeDatabase();
805
+ cleanup(base);
806
+ }
807
+ });
@@ -10,6 +10,8 @@ import {
10
10
  insertDecision,
11
11
  insertRequirement,
12
12
  insertArtifact,
13
+ insertMilestone,
14
+ getMilestone,
13
15
  getDecisionById,
14
16
  getRequirementById,
15
17
  _getAdapter,
@@ -442,4 +444,37 @@ console.log('\n=== worktree-db: reconcileWorktreeDb ===');
442
444
  cleanup(mainDir, wtDir);
443
445
  }
444
446
 
447
+ // Test: reconcileWorktreeDb must NOT downgrade milestone status complete→active (#4372)
448
+ {
449
+ const mainDir = tempDir();
450
+ const wtDir = tempDir();
451
+ const mainDb = path.join(mainDir, 'gsd.db');
452
+ const wtDb = path.join(wtDir, 'gsd.db');
453
+
454
+ // Seed main with a milestone already marked complete
455
+ seedMainDb(mainDb);
456
+ const mainAdapter = _getAdapter()!;
457
+ insertMilestone({ id: 'M-COMP', title: 'Completed Milestone', status: 'complete' });
458
+ // Manually mark completed_at so it's a realistic complete record
459
+ mainAdapter.prepare(`UPDATE milestones SET completed_at = '2025-06-01T00:00:00.000Z' WHERE id = 'M-COMP'`).run();
460
+ closeDatabase();
461
+
462
+ // Copy to worktree — the worktree has the milestone as 'active' (stale / older snapshot)
463
+ copyWorktreeDb(mainDb, wtDb);
464
+ openDatabase(wtDb);
465
+ const wtAdapter = _getAdapter()!;
466
+ wtAdapter.prepare(`UPDATE milestones SET status = 'active', completed_at = NULL WHERE id = 'M-COMP'`).run();
467
+ closeDatabase();
468
+
469
+ // Reconcile: main should win and keep 'complete'
470
+ openDatabase(mainDb);
471
+ reconcileWorktreeDb(mainDb, wtDb);
472
+
473
+ const m = getMilestone('M-COMP');
474
+ assert.ok(m !== null, 'milestone M-COMP still exists after reconcile');
475
+ assert.strictEqual(m!.status, 'complete', 'complete milestone must not be downgraded to active by stale worktree');
476
+
477
+ cleanup(mainDir, wtDir);
478
+ }
479
+
445
480
  // ─── Final Report ──────────────────────────────────────────────────────────
@@ -190,7 +190,12 @@ describe("worktree journal events", () => {
190
190
  });
191
191
  const resolver = new WorktreeResolver(s, deps);
192
192
 
193
- resolver.mergeAndExit("M001", makeNotifyCtx());
193
+ // Since #4380, mergeAndExit re-throws all errors after emitting the journal
194
+ // event and restoring state — callers must handle the throw.
195
+ assert.throws(
196
+ () => resolver.mergeAndExit("M001", makeNotifyCtx()),
197
+ /conflict in main/,
198
+ );
194
199
 
195
200
  const entries = readJournalEntries(tmp);
196
201
  const failed = entries.find(e => e.eventType === "worktree-merge-failed");
@@ -313,6 +313,43 @@ test("enterMilestone uses originalBasePath as base for worktree ops", () => {
313
313
  assert.equal(createdFrom, "/project"); // uses originalBasePath, not current basePath
314
314
  });
315
315
 
316
+ test("enterMilestone does not create double-nested worktree when originalBasePath is empty and basePath is a worktree path", () => {
317
+ // Regression test for #3729: when s.originalBasePath is "" (falsy) and
318
+ // s.basePath is already a worktree path, the expression
319
+ // `this.s.originalBasePath || this.s.basePath` evaluates to the worktree
320
+ // path. Passing that to createAutoWorktree produces a doubly-nested path
321
+ // like /project/.gsd/worktrees/M001/.gsd/worktrees/M002.
322
+ const wtPath = "/project/.gsd/worktrees/M001";
323
+ const s = makeSession({
324
+ basePath: wtPath,
325
+ originalBasePath: "/project", // will be overwritten below to simulate the bug
326
+ });
327
+ // Simulate the real bug: originalBasePath is "" (falsy) as it is when AutoSession
328
+ // is constructed fresh or reset() is called without auto-start re-setting it.
329
+ s.originalBasePath = "";
330
+
331
+ let createdFromPath = "";
332
+ const deps = makeDeps({
333
+ getAutoWorktreePath: () => null,
334
+ createAutoWorktree: (basePath: string, _mid: string) => {
335
+ createdFromPath = basePath;
336
+ return `/project/.gsd/worktrees/M002`;
337
+ },
338
+ });
339
+ const ctx = makeNotifyCtx();
340
+ const resolver = new WorktreeResolver(s, deps);
341
+
342
+ resolver.enterMilestone("M002", ctx);
343
+
344
+ // The path passed to createAutoWorktree must be the project root, NOT the
345
+ // worktree path. If it equals wtPath the worktree would be created at
346
+ // /project/.gsd/worktrees/M001/.gsd/worktrees/M002 (double-nesting).
347
+ assert.ok(
348
+ !createdFromPath.includes("/.gsd/worktrees/"),
349
+ `createAutoWorktree must be called with project root, got: "${createdFromPath}"`,
350
+ );
351
+ });
352
+
316
353
  // ─── enterMilestone Tests (branch mode) ──────────────────────────────────────
317
354
 
318
355
  test("enterMilestone in branch mode calls enterBranchModeForMilestone and rebuilds GitService", () => {
@@ -577,9 +614,11 @@ test("mergeAndExit in worktree mode restores to project root on merge failure",
577
614
  const ctx = makeNotifyCtx();
578
615
  const resolver = new WorktreeResolver(s, deps);
579
616
 
580
- resolver.mergeAndExit("M001", ctx);
617
+ // Error propagates (#4380) — callers handle recovery. restoreToProjectRoot()
618
+ // still runs before re-throw so state is consistent for the caller.
619
+ assert.throws(() => resolver.mergeAndExit("M001", ctx), /conflict in main/);
581
620
 
582
- assert.equal(s.basePath, "/project"); // error recovery — restored
621
+ assert.equal(s.basePath, "/project"); // error recovery — restored before re-throw
583
622
  assert.ok(
584
623
  ctx.messages.some(
585
624
  (m) => m.level === "warning" && m.msg.includes("conflict in main"),
@@ -607,7 +646,8 @@ test("mergeAndExit failure message tells user worktree and branch are preserved
607
646
  const ctx = makeNotifyCtx();
608
647
  const resolver = new WorktreeResolver(s, deps);
609
648
 
610
- resolver.mergeAndExit("M001", ctx);
649
+ // Error propagates (#4380) — notification is still emitted before re-throw
650
+ assert.throws(() => resolver.mergeAndExit("M001", ctx), /pathspec 'main' did not match/);
611
651
 
612
652
  const warning = ctx.messages.find((m) => m.level === "warning");
613
653
  assert.ok(warning, "a warning message is emitted");
@@ -643,7 +683,8 @@ test("mergeAndExit failure message references /gsd dispatch complete-milestone,
643
683
  const ctx = makeNotifyCtx();
644
684
  const resolver = new WorktreeResolver(s, deps);
645
685
 
646
- resolver.mergeAndExit("M001", ctx);
686
+ // Error propagates (#4380) — notification is still emitted before re-throw
687
+ assert.throws(() => resolver.mergeAndExit("M001", ctx), /dirty working tree/);
647
688
 
648
689
  const warning = ctx.messages.find((m) => m.level === "warning");
649
690
  assert.ok(warning, "a warning message is emitted");
@@ -709,7 +750,8 @@ test("mergeAndExit in branch mode handles merge failure gracefully", () => {
709
750
  const ctx = makeNotifyCtx();
710
751
  const resolver = new WorktreeResolver(s, deps);
711
752
 
712
- resolver.mergeAndExit("M001", ctx);
753
+ // Error propagates (#4380) — notification is still emitted before re-throw
754
+ assert.throws(() => resolver.mergeAndExit("M001", ctx), /branch merge conflict/);
713
755
 
714
756
  assert.ok(
715
757
  ctx.messages.some(
@@ -1069,3 +1111,34 @@ test("mergeAndExit in none mode remains a no-op when NOT in a worktree (#2625)",
1069
1111
  assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0,
1070
1112
  "must NOT merge when not in a worktree and mode is none");
1071
1113
  });
1114
+
1115
+ // ─── #4380 — Non-MergeConflictError must not be swallowed ────────────────────
1116
+
1117
+ test("mergeAndExit propagates non-MergeConflictError to caller (#4380)", () => {
1118
+ // Regression test: previously the catch block in _mergeWorktreeMode only
1119
+ // re-threw MergeConflictError. Permission errors, filesystem errors, and other
1120
+ // non-conflict failures were swallowed silently, making broken states impossible
1121
+ // to diagnose and preventing callers (phases.ts) from applying their own
1122
+ // error-recovery logic.
1123
+ const permissionError = new Error("EACCES: permission denied, open '/project/.git/SQUASH_MSG'");
1124
+ const s = makeSession({
1125
+ basePath: "/project/.gsd/worktrees/M001",
1126
+ originalBasePath: "/project",
1127
+ });
1128
+ const deps = makeDeps({
1129
+ isInAutoWorktree: () => true,
1130
+ getIsolationMode: () => "worktree",
1131
+ mergeMilestoneToMain: () => {
1132
+ throw permissionError;
1133
+ },
1134
+ });
1135
+ const ctx = makeNotifyCtx();
1136
+ const resolver = new WorktreeResolver(s, deps);
1137
+
1138
+ // The error must propagate — callers need it to apply their own recovery logic
1139
+ assert.throws(
1140
+ () => resolver.mergeAndExit("M001", ctx),
1141
+ (err: unknown) => err === permissionError,
1142
+ "non-MergeConflictError must propagate to the caller, not be swallowed",
1143
+ );
1144
+ });
@@ -11,6 +11,10 @@
11
11
 
12
12
  import test from 'node:test';
13
13
  import assert from 'node:assert/strict';
14
+ import { mkdirSync, writeFileSync, unlinkSync, existsSync, rmSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { tmpdir } from 'node:os';
17
+ import { randomUUID } from 'node:crypto';
14
18
  import {
15
19
  isDepthConfirmationAnswer,
16
20
  shouldBlockContextWrite,
@@ -20,8 +24,10 @@ import {
20
24
  markDepthVerified,
21
25
  isMilestoneDepthVerified,
22
26
  shouldBlockContextArtifactSave,
27
+ shouldBlockContextArtifactSaveInSnapshot,
23
28
  clearDiscussionFlowState,
24
29
  resetWriteGateState,
30
+ loadWriteGateSnapshot,
25
31
  } from '../bootstrap/write-gate.ts';
26
32
 
27
33
  // ─── Scenario 1: Blocks CONTEXT.md write during discussion without depth verification (absolute path) ──
@@ -488,3 +494,61 @@ test('write-gate: isDepthConfirmationAnswer falls back to (Recommended) match wi
488
494
  'should reject non-Recommended via fallback',
489
495
  );
490
496
  });
497
+
498
+ // ─── Scenario 29: loadWriteGateSnapshot returns clean state when persist file deleted (#4343) ──
499
+
500
+ test('write-gate: loadWriteGateSnapshot returns empty default when persist file is deleted (#4343)', () => {
501
+ const base = join(tmpdir(), `gsd-write-gate-4343-${randomUUID()}`);
502
+ mkdirSync(join(base, '.gsd', 'runtime'), { recursive: true });
503
+ const stateFilePath = join(base, '.gsd', 'runtime', 'write-gate-state.json');
504
+ const originalEnv = process.env.GSD_PERSIST_WRITE_GATE_STATE;
505
+
506
+ try {
507
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = '1';
508
+
509
+ // Write a state file with a pending gate and verified milestone
510
+ writeFileSync(stateFilePath, JSON.stringify({
511
+ verifiedDepthMilestones: ['M001'],
512
+ activeQueuePhase: false,
513
+ pendingGateId: 'depth_verification_M001',
514
+ }));
515
+ assert.ok(existsSync(stateFilePath), 'precondition: state file exists');
516
+
517
+ // While file exists, snapshot reflects its contents
518
+ const beforeDeletion = loadWriteGateSnapshot(base);
519
+ assert.strictEqual(beforeDeletion.pendingGateId, 'depth_verification_M001', 'pending gate from file');
520
+ assert.deepEqual(beforeDeletion.verifiedDepthMilestones, ['M001'], 'verified milestones from file');
521
+
522
+ // User deletes the state file to clear the HARD BLOCK
523
+ unlinkSync(stateFilePath);
524
+ assert.ok(!existsSync(stateFilePath), 'state file deleted');
525
+
526
+ // After deletion in persist mode, snapshot should be clean (not stale in-memory)
527
+ const afterDeletion = loadWriteGateSnapshot(base);
528
+ assert.strictEqual(afterDeletion.pendingGateId, null, 'pendingGateId cleared after file deletion');
529
+ assert.deepEqual(afterDeletion.verifiedDepthMilestones, [], 'verifiedDepthMilestones cleared after file deletion');
530
+ assert.strictEqual(afterDeletion.activeQueuePhase, false, 'activeQueuePhase cleared after file deletion');
531
+
532
+ // The CONTEXT artifact block check must also resolve to unblocked after deletion+verification
533
+ // (simulate the re-verify flow users would do: delete → depth verify → save)
534
+ const stillBlocked = shouldBlockContextArtifactSaveInSnapshot(afterDeletion, 'CONTEXT', 'M001', null);
535
+ assert.strictEqual(stillBlocked.block, true, 'still blocked without new depth verification');
536
+
537
+ const verifiedSnapshot = {
538
+ ...afterDeletion,
539
+ verifiedDepthMilestones: ['M001'],
540
+ };
541
+ const unblocked = shouldBlockContextArtifactSaveInSnapshot(verifiedSnapshot, 'CONTEXT', 'M001', null);
542
+ assert.strictEqual(unblocked.block, false, 'unblocked after fresh depth verification');
543
+ } finally {
544
+ if (originalEnv === undefined) {
545
+ delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
546
+ } else {
547
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
548
+ }
549
+ clearDiscussionFlowState();
550
+ try {
551
+ rmSync(base, { recursive: true, force: true });
552
+ } catch { /* swallow */ }
553
+ }
554
+ });
@@ -72,7 +72,9 @@ const autoStartSrc = readFileSync(
72
72
  const symlinkIdx = autoStartSrc.indexOf("ensureGsdSymlink(base)");
73
73
  assertTrue(symlinkIdx >= 0, "auto-start.ts calls ensureGsdSymlink(base)");
74
74
 
75
- const afterSymlink = symlinkIdx >= 0 ? autoStartSrc.slice(symlinkIdx, symlinkIdx + 800) : "";
75
+ const afterSymlink = symlinkIdx >= 0
76
+ ? autoStartSrc.slice(symlinkIdx, autoStartSrc.indexOf("Initialize GitServiceImpl", symlinkIdx))
77
+ : "";
76
78
 
77
79
  // The milestones bootstrap must check milestones path, not gsdDir
78
80
  // Old (dead) code: if (!existsSync(gsdDir)) { mkdirSync(join(gsdDir, "milestones"), ...) }