gsd-pi 2.59.0 → 2.60.0-dev.2580e65

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 (354) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +7 -4
  2. package/dist/resources/extensions/gsd/auto/phases.js +62 -1
  3. package/dist/resources/extensions/gsd/auto-dashboard.js +21 -8
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +6 -3
  5. package/dist/resources/extensions/gsd/auto-model-selection.js +57 -3
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +43 -3
  7. package/dist/resources/extensions/gsd/auto-prompts.js +49 -20
  8. package/dist/resources/extensions/gsd/auto-recovery.js +37 -18
  9. package/dist/resources/extensions/gsd/auto-start.js +9 -5
  10. package/dist/resources/extensions/gsd/auto-timers.js +11 -5
  11. package/dist/resources/extensions/gsd/auto-unit-closeout.js +5 -3
  12. package/dist/resources/extensions/gsd/auto-verification.js +3 -2
  13. package/dist/resources/extensions/gsd/auto-worktree.js +120 -55
  14. package/dist/resources/extensions/gsd/auto.js +39 -17
  15. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +6 -3
  16. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +72 -2
  17. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +4 -10
  18. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +2 -1
  19. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +58 -5
  20. package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -10
  21. package/dist/resources/extensions/gsd/captures.js +54 -1
  22. package/dist/resources/extensions/gsd/commands/catalog.js +2 -0
  23. package/dist/resources/extensions/gsd/commands-codebase.js +48 -21
  24. package/dist/resources/extensions/gsd/commands-inspect.js +2 -1
  25. package/dist/resources/extensions/gsd/commands-maintenance.js +32 -19
  26. package/dist/resources/extensions/gsd/complexity-classifier.js +9 -5
  27. package/dist/resources/extensions/gsd/context-masker.js +68 -0
  28. package/dist/resources/extensions/gsd/custom-verification.js +3 -2
  29. package/dist/resources/extensions/gsd/docs/preferences-reference.md +7 -0
  30. package/dist/resources/extensions/gsd/gsd-db.js +35 -15
  31. package/dist/resources/extensions/gsd/guided-flow.js +19 -9
  32. package/dist/resources/extensions/gsd/init-wizard.js +12 -0
  33. package/dist/resources/extensions/gsd/markdown-renderer.js +11 -9
  34. package/dist/resources/extensions/gsd/md-importer.js +5 -4
  35. package/dist/resources/extensions/gsd/milestone-actions.js +3 -2
  36. package/dist/resources/extensions/gsd/milestone-ids.js +2 -1
  37. package/dist/resources/extensions/gsd/model-router.js +199 -45
  38. package/dist/resources/extensions/gsd/parallel-merge.js +5 -3
  39. package/dist/resources/extensions/gsd/parallel-orchestrator.js +26 -14
  40. package/dist/resources/extensions/gsd/phase-anchor.js +56 -0
  41. package/dist/resources/extensions/gsd/preferences-types.js +2 -0
  42. package/dist/resources/extensions/gsd/preferences-validation.js +91 -0
  43. package/dist/resources/extensions/gsd/preferences.js +15 -3
  44. package/dist/resources/extensions/gsd/prompt-loader.js +3 -2
  45. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
  46. package/dist/resources/extensions/gsd/prompts/rethink.md +7 -0
  47. package/dist/resources/extensions/gsd/prompts/triage-captures.md +6 -1
  48. package/dist/resources/extensions/gsd/rethink.js +5 -2
  49. package/dist/resources/extensions/gsd/rule-registry.js +7 -6
  50. package/dist/resources/extensions/gsd/safe-fs.js +6 -8
  51. package/dist/resources/extensions/gsd/state.js +1 -1
  52. package/dist/resources/extensions/gsd/status-guards.js +4 -3
  53. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -2
  54. package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -2
  55. package/dist/resources/extensions/gsd/tools/complete-task.js +3 -2
  56. package/dist/resources/extensions/gsd/tools/plan-milestone.js +3 -2
  57. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -2
  58. package/dist/resources/extensions/gsd/tools/plan-task.js +2 -1
  59. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +4 -4
  60. package/dist/resources/extensions/gsd/tools/reopen-slice.js +2 -1
  61. package/dist/resources/extensions/gsd/tools/reopen-task.js +2 -1
  62. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -1
  63. package/dist/resources/extensions/gsd/tools/validate-milestone.js +2 -1
  64. package/dist/resources/extensions/gsd/triage-resolution.js +135 -1
  65. package/dist/resources/extensions/gsd/triage-ui.js +12 -3
  66. package/dist/resources/extensions/gsd/workflow-events.js +2 -1
  67. package/dist/resources/extensions/gsd/workflow-logger.js +37 -4
  68. package/dist/resources/extensions/gsd/workflow-migration.js +14 -12
  69. package/dist/resources/extensions/gsd/workflow-projections.js +2 -2
  70. package/dist/resources/extensions/gsd/workflow-reconcile.js +2 -2
  71. package/dist/resources/extensions/gsd/worktree-manager.js +26 -14
  72. package/dist/resources/extensions/shared/interview-ui.js +3 -1
  73. package/dist/resources/skills/btw/SKILL.md +42 -0
  74. package/dist/web/standalone/.next/BUILD_ID +1 -1
  75. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  76. package/dist/web/standalone/.next/build-manifest.json +3 -3
  77. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  78. package/dist/web/standalone/.next/required-server-files.json +3 -3
  79. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  80. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  82. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  90. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  92. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  93. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  94. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  96. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  106. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  118. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  132. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  134. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  136. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  138. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  142. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  143. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  144. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  145. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  146. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  147. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  148. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  149. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  150. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  151. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  152. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  153. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  154. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  155. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  156. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  157. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  158. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  159. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  160. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  161. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  162. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  163. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  164. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  165. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  166. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  167. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  168. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  169. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  170. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  171. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  172. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  173. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  174. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  175. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  176. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  177. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  178. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  179. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  180. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  181. package/dist/web/standalone/.next/server/app/index.html +1 -1
  182. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  183. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  184. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  185. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  186. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  187. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  188. package/dist/web/standalone/.next/server/app/page.js +2 -2
  189. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  190. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  191. package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
  192. package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
  193. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  194. package/dist/web/standalone/.next/server/middleware.js +2 -2
  195. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  196. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  197. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  198. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  199. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  200. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  201. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  202. package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +1 -0
  203. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  204. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  205. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  206. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  207. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  208. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  209. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  210. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  211. package/dist/web/standalone/server.js +1 -1
  212. package/package.json +1 -1
  213. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  214. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  215. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  216. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -1
  217. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  218. package/packages/pi-coding-agent/dist/core/extensions/runner.js +16 -0
  219. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  220. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +26 -0
  221. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  222. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  223. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  224. package/packages/pi-coding-agent/dist/core/lsp/config.js +6 -1
  225. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  226. package/packages/pi-coding-agent/dist/core/lsp/defaults.json +2 -2
  227. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts +2 -0
  228. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts.map +1 -0
  229. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js +47 -0
  230. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js.map +1 -0
  231. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +1 -0
  232. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  233. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +6 -0
  234. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  235. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.d.ts +2 -0
  236. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.d.ts.map +1 -0
  237. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js +122 -0
  238. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js.map +1 -0
  239. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  240. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  241. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +30 -0
  242. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  243. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
  244. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +1 -7
  245. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  246. package/packages/pi-coding-agent/package.json +1 -1
  247. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  248. package/packages/pi-coding-agent/src/core/extensions/runner.ts +19 -0
  249. package/packages/pi-coding-agent/src/core/extensions/types.ts +26 -0
  250. package/packages/pi-coding-agent/src/core/lsp/config.ts +7 -1
  251. package/packages/pi-coding-agent/src/core/lsp/defaults.json +2 -2
  252. package/packages/pi-coding-agent/src/core/lsp/lsp-legacy-alias.test.ts +70 -0
  253. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts +156 -0
  254. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +7 -0
  255. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +38 -0
  256. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +1 -8
  257. package/pkg/package.json +1 -1
  258. package/src/resources/extensions/ask-user-questions.ts +7 -3
  259. package/src/resources/extensions/gsd/auto/phases.ts +70 -1
  260. package/src/resources/extensions/gsd/auto-dashboard.ts +22 -8
  261. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -3
  262. package/src/resources/extensions/gsd/auto-model-selection.ts +77 -6
  263. package/src/resources/extensions/gsd/auto-post-unit.ts +52 -5
  264. package/src/resources/extensions/gsd/auto-prompts.ts +54 -20
  265. package/src/resources/extensions/gsd/auto-recovery.ts +38 -18
  266. package/src/resources/extensions/gsd/auto-start.ts +10 -9
  267. package/src/resources/extensions/gsd/auto-timers.ts +12 -5
  268. package/src/resources/extensions/gsd/auto-unit-closeout.ts +6 -2
  269. package/src/resources/extensions/gsd/auto-verification.ts +3 -6
  270. package/src/resources/extensions/gsd/auto-worktree.ts +121 -55
  271. package/src/resources/extensions/gsd/auto.ts +40 -17
  272. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +4 -3
  273. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +80 -2
  274. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +4 -16
  275. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +2 -1
  276. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +61 -4
  277. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -10
  278. package/src/resources/extensions/gsd/captures.ts +71 -2
  279. package/src/resources/extensions/gsd/commands/catalog.ts +2 -0
  280. package/src/resources/extensions/gsd/commands-codebase.ts +52 -20
  281. package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
  282. package/src/resources/extensions/gsd/commands-maintenance.ts +28 -19
  283. package/src/resources/extensions/gsd/complexity-classifier.ts +10 -5
  284. package/src/resources/extensions/gsd/context-masker.ts +74 -0
  285. package/src/resources/extensions/gsd/custom-verification.ts +3 -2
  286. package/src/resources/extensions/gsd/docs/preferences-reference.md +7 -0
  287. package/src/resources/extensions/gsd/gsd-db.ts +14 -16
  288. package/src/resources/extensions/gsd/guided-flow.ts +9 -8
  289. package/src/resources/extensions/gsd/init-wizard.ts +12 -0
  290. package/src/resources/extensions/gsd/markdown-renderer.ts +11 -17
  291. package/src/resources/extensions/gsd/md-importer.ts +5 -4
  292. package/src/resources/extensions/gsd/milestone-actions.ts +3 -2
  293. package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
  294. package/src/resources/extensions/gsd/model-router.ts +245 -56
  295. package/src/resources/extensions/gsd/parallel-merge.ts +5 -3
  296. package/src/resources/extensions/gsd/parallel-orchestrator.ts +18 -14
  297. package/src/resources/extensions/gsd/phase-anchor.ts +71 -0
  298. package/src/resources/extensions/gsd/preferences-types.ts +22 -0
  299. package/src/resources/extensions/gsd/preferences-validation.ts +83 -0
  300. package/src/resources/extensions/gsd/preferences.ts +16 -3
  301. package/src/resources/extensions/gsd/prompt-loader.ts +3 -2
  302. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
  303. package/src/resources/extensions/gsd/prompts/rethink.md +7 -0
  304. package/src/resources/extensions/gsd/prompts/triage-captures.md +6 -1
  305. package/src/resources/extensions/gsd/rethink.ts +5 -2
  306. package/src/resources/extensions/gsd/rule-registry.ts +7 -6
  307. package/src/resources/extensions/gsd/safe-fs.ts +6 -5
  308. package/src/resources/extensions/gsd/state.ts +1 -1
  309. package/src/resources/extensions/gsd/status-guards.ts +4 -3
  310. package/src/resources/extensions/gsd/tests/capability-router.test.ts +347 -0
  311. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +63 -0
  312. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +27 -2
  313. package/src/resources/extensions/gsd/tests/context-masker.test.ts +122 -0
  314. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  315. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +1188 -0
  316. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +841 -0
  317. package/src/resources/extensions/gsd/tests/model-router.test.ts +488 -2
  318. package/src/resources/extensions/gsd/tests/phase-anchor.test.ts +83 -0
  319. package/src/resources/extensions/gsd/tests/preferences.test.ts +62 -0
  320. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +21 -0
  321. package/src/resources/extensions/gsd/tests/silent-catch-diagnostics.test.ts +284 -0
  322. package/src/resources/extensions/gsd/tests/status-guards.test.ts +4 -0
  323. package/src/resources/extensions/gsd/tests/stop-backtrack.test.ts +216 -0
  324. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +1 -1
  325. package/src/resources/extensions/gsd/tests/workflow-logger-audit.test.ts +120 -0
  326. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +6 -6
  327. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -6
  328. package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -6
  329. package/src/resources/extensions/gsd/tools/complete-task.ts +3 -6
  330. package/src/resources/extensions/gsd/tools/plan-milestone.ts +3 -6
  331. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -6
  332. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -3
  333. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +4 -6
  334. package/src/resources/extensions/gsd/tools/reopen-slice.ts +2 -3
  335. package/src/resources/extensions/gsd/tools/reopen-task.ts +2 -3
  336. package/src/resources/extensions/gsd/tools/replan-slice.ts +2 -3
  337. package/src/resources/extensions/gsd/tools/validate-milestone.ts +2 -3
  338. package/src/resources/extensions/gsd/triage-resolution.ts +151 -1
  339. package/src/resources/extensions/gsd/triage-ui.ts +12 -3
  340. package/src/resources/extensions/gsd/types.ts +1 -0
  341. package/src/resources/extensions/gsd/workflow-events.ts +2 -1
  342. package/src/resources/extensions/gsd/workflow-logger.ts +52 -5
  343. package/src/resources/extensions/gsd/workflow-migration.ts +14 -12
  344. package/src/resources/extensions/gsd/workflow-projections.ts +2 -2
  345. package/src/resources/extensions/gsd/workflow-reconcile.ts +2 -2
  346. package/src/resources/extensions/gsd/worktree-manager.ts +16 -14
  347. package/src/resources/extensions/shared/interview-ui.ts +3 -1
  348. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +144 -0
  349. package/src/resources/skills/btw/SKILL.md +42 -0
  350. package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +0 -1
  351. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  352. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  353. /package/dist/web/standalone/.next/static/{DGvT_c5Vb7Wu3X-fEOVUU → ogyMN7M-3bGGuRY08L5HR}/_buildManifest.js +0 -0
  354. /package/dist/web/standalone/.next/static/{DGvT_c5Vb7Wu3X-fEOVUU → ogyMN7M-3bGGuRY08L5HR}/_ssgManifest.js +0 -0
@@ -1,12 +1,17 @@
1
- import test from "node:test";
1
+ import test, { describe } from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
 
4
4
  import {
5
5
  resolveModelForComplexity,
6
6
  escalateTier,
7
7
  defaultRoutingConfig,
8
+ scoreModel,
9
+ computeTaskRequirements,
10
+ scoreEligibleModels,
11
+ getEligibleModels,
12
+ MODEL_CAPABILITY_PROFILES,
8
13
  } from "../model-router.js";
9
- import type { DynamicRoutingConfig, RoutingDecision } from "../model-router.js";
14
+ import type { DynamicRoutingConfig, RoutingDecision, ModelCapabilities } from "../model-router.js";
10
15
  import type { ClassificationResult } from "../complexity-classifier.js";
11
16
 
12
17
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -206,6 +211,89 @@ test("#2192: known model is still downgraded normally", () => {
206
211
  assert.notEqual(result.modelId, "claude-opus-4-6");
207
212
  });
208
213
 
214
+ // ─── Capability Scoring (ADR-004 Phase 2) ───────────────────────────────────
215
+
216
+ test("defaultRoutingConfig includes capability_routing: true", () => {
217
+ const config = defaultRoutingConfig();
218
+ assert.equal(config.capability_routing, true);
219
+ });
220
+
221
+ test("scoreModel computes weighted average of capability × requirement", () => {
222
+ const caps: ModelCapabilities = {
223
+ coding: 90, debugging: 80, research: 70,
224
+ reasoning: 85, speed: 50, longContext: 60, instruction: 75,
225
+ };
226
+ const reqs = { coding: 0.9, reasoning: 0.5 };
227
+ const score = scoreModel(caps, reqs);
228
+ // Expected: (0.9*90 + 0.5*85) / (0.9 + 0.5) = (81 + 42.5) / 1.4 = 88.21...
229
+ assert.ok(Math.abs(score - 88.21) < 0.1, `score ${score} should be ~88.21`);
230
+ });
231
+
232
+ test("scoreModel returns 50 for empty requirements", () => {
233
+ const caps: ModelCapabilities = {
234
+ coding: 90, debugging: 80, research: 70,
235
+ reasoning: 85, speed: 50, longContext: 60, instruction: 75,
236
+ };
237
+ const score = scoreModel(caps, {});
238
+ assert.equal(score, 50);
239
+ });
240
+
241
+ test("computeTaskRequirements returns base vector for known unit type", () => {
242
+ const reqs = computeTaskRequirements("execute-task");
243
+ assert.ok(reqs.coding !== undefined && reqs.coding > 0);
244
+ });
245
+
246
+ test("computeTaskRequirements boosts instruction for docs-tagged tasks", () => {
247
+ const reqs = computeTaskRequirements("execute-task", { tags: ["docs"] });
248
+ assert.ok((reqs.instruction ?? 0) >= 0.8);
249
+ assert.ok((reqs.coding ?? 1) <= 0.4);
250
+ });
251
+
252
+ test("computeTaskRequirements returns generic vector for unknown unit type", () => {
253
+ const reqs = computeTaskRequirements("unknown-unit");
254
+ assert.ok(reqs.reasoning !== undefined);
255
+ });
256
+
257
+ test("resolveModelForComplexity uses capability scoring when enabled", () => {
258
+ const config: DynamicRoutingConfig = {
259
+ ...defaultRoutingConfig(),
260
+ enabled: true,
261
+ capability_routing: true,
262
+ };
263
+ const result = resolveModelForComplexity(
264
+ makeClassification("light"),
265
+ { primary: "claude-opus-4-6", fallbacks: [] },
266
+ config,
267
+ ["claude-opus-4-6", "claude-haiku-4-5", "gpt-4o-mini"],
268
+ "execute-task",
269
+ );
270
+ assert.equal(result.wasDowngraded, true);
271
+ assert.equal(result.selectionMethod, "capability-scored");
272
+ });
273
+
274
+ test("resolveModelForComplexity falls back to tier-only when capability_routing is false", () => {
275
+ const config: DynamicRoutingConfig = {
276
+ ...defaultRoutingConfig(),
277
+ enabled: true,
278
+ capability_routing: false,
279
+ };
280
+ const result = resolveModelForComplexity(
281
+ makeClassification("light"),
282
+ { primary: "claude-opus-4-6", fallbacks: [] },
283
+ config,
284
+ ["claude-opus-4-6", "claude-haiku-4-5", "gpt-4o-mini"],
285
+ );
286
+ assert.equal(result.wasDowngraded, true);
287
+ assert.ok(!result.selectionMethod || result.selectionMethod === "tier-only");
288
+ });
289
+
290
+ test("MODEL_CAPABILITY_PROFILES has entries for core models", () => {
291
+ const profiledModels = Object.keys(MODEL_CAPABILITY_PROFILES);
292
+ assert.ok(profiledModels.length >= 9, `Expected ≥9 profiles, got ${profiledModels.length}`);
293
+ assert.ok(MODEL_CAPABILITY_PROFILES["claude-opus-4-6"]);
294
+ assert.ok(MODEL_CAPABILITY_PROFILES["claude-haiku-4-5"]);
295
+ });
296
+
209
297
  // ─── #2885: openai-codex and modern OpenAI models in tier map ────────────────
210
298
 
211
299
  test("#2885: openai-codex light-tier models are recognized", () => {
@@ -270,3 +358,401 @@ test("#2885: heavy openai-codex model downgrades to light for light task", () =>
270
358
  // Should pick a light-tier model
271
359
  assert.notEqual(result.modelId, "gpt-5.4", "should not use the heavy model for light task");
272
360
  });
361
+ // ─── scoreModel ──────────────────────────────────────────────────────────────
362
+
363
+ describe("scoreModel", () => {
364
+ const sonnetProfile: ModelCapabilities = MODEL_CAPABILITY_PROFILES["claude-sonnet-4-6"]!;
365
+
366
+ test("produces correct weighted average for two dimensions (coding:0.9, instruction:0.7)", () => {
367
+ // (0.9*85 + 0.7*85) / (0.9+0.7) = (76.5+59.5)/1.6 = 136/1.6 = 85.0
368
+ const score = scoreModel(sonnetProfile, { coding: 0.9, instruction: 0.7 });
369
+ assert.ok(Math.abs(score - 85.0) < 0.01, `Expected ~85.0, got ${score}`);
370
+ });
371
+
372
+ test("returns 50 when requirements is empty", () => {
373
+ const score = scoreModel(sonnetProfile, {});
374
+ assert.equal(score, 50);
375
+ });
376
+
377
+ test("returns correct score for single dimension coding:1.0", () => {
378
+ // coding=90 for claude-opus-4-6
379
+ const opusProfile = MODEL_CAPABILITY_PROFILES["claude-opus-4-6"]!;
380
+ const score = scoreModel(opusProfile, { coding: 1.0 });
381
+ assert.equal(score, 95);
382
+ });
383
+
384
+ test("handles all 7 dimensions correctly", () => {
385
+ // Uniform weight 1.0 on every dim → average of all dim values
386
+ const profile: ModelCapabilities = {
387
+ coding: 60, debugging: 60, research: 60, reasoning: 60,
388
+ speed: 60, longContext: 60, instruction: 60,
389
+ };
390
+ const reqs: Partial<Record<keyof ModelCapabilities, number>> = {
391
+ coding: 1.0, debugging: 1.0, research: 1.0, reasoning: 1.0,
392
+ speed: 1.0, longContext: 1.0, instruction: 1.0,
393
+ };
394
+ const score = scoreModel(profile, reqs);
395
+ assert.equal(score, 60);
396
+ });
397
+ });
398
+
399
+ // ─── computeTaskRequirements ─────────────────────────────────────────────────
400
+
401
+ describe("computeTaskRequirements", () => {
402
+ test("execute-task with no metadata returns base vector", () => {
403
+ const req = computeTaskRequirements("execute-task", undefined);
404
+ assert.deepStrictEqual(req, { coding: 0.9, instruction: 0.7, speed: 0.3 });
405
+ });
406
+
407
+ test("execute-task with tags:['docs'] adjusts requirements", () => {
408
+ const req = computeTaskRequirements("execute-task", { tags: ["docs"] });
409
+ assert.equal(req.instruction, 0.9);
410
+ assert.equal(req.coding, 0.3);
411
+ assert.equal(req.speed, 0.7);
412
+ });
413
+
414
+ test("execute-task with tags:['config'] adjusts requirements", () => {
415
+ const req = computeTaskRequirements("execute-task", { tags: ["config"] });
416
+ assert.equal(req.instruction, 0.9);
417
+ });
418
+
419
+ test("execute-task with complexityKeywords:['concurrency'] boosts debugging and reasoning", () => {
420
+ const req = computeTaskRequirements("execute-task", { complexityKeywords: ["concurrency"] });
421
+ assert.equal(req.debugging, 0.9);
422
+ assert.equal(req.reasoning, 0.8);
423
+ });
424
+
425
+ test("execute-task with complexityKeywords:['migration'] boosts reasoning and coding", () => {
426
+ const req = computeTaskRequirements("execute-task", { complexityKeywords: ["migration"] });
427
+ assert.equal(req.reasoning, 0.9);
428
+ assert.equal(req.coding, 0.8);
429
+ });
430
+
431
+ test("execute-task with fileCount:8 boosts coding and reasoning", () => {
432
+ const req = computeTaskRequirements("execute-task", { fileCount: 8 });
433
+ assert.equal(req.coding, 0.9);
434
+ assert.equal(req.reasoning, 0.7);
435
+ });
436
+
437
+ test("execute-task with estimatedLines:600 boosts coding and reasoning", () => {
438
+ const req = computeTaskRequirements("execute-task", { estimatedLines: 600 });
439
+ assert.equal(req.coding, 0.9);
440
+ assert.equal(req.reasoning, 0.7);
441
+ });
442
+
443
+ test("research-milestone returns correct base vector", () => {
444
+ const req = computeTaskRequirements("research-milestone");
445
+ assert.deepStrictEqual(req, { research: 0.9, longContext: 0.7, reasoning: 0.5 });
446
+ });
447
+
448
+ test("plan-slice returns correct base vector", () => {
449
+ const req = computeTaskRequirements("plan-slice");
450
+ assert.deepStrictEqual(req, { reasoning: 0.9, coding: 0.5 });
451
+ });
452
+
453
+ test("unknown-unit-type returns default reasoning requirement", () => {
454
+ const req = computeTaskRequirements("unknown-unit-type");
455
+ assert.deepStrictEqual(req, { reasoning: 0.5 });
456
+ });
457
+
458
+ test("non-execute-task with metadata ignores metadata refinements", () => {
459
+ // research-milestone should return the same vector regardless of metadata
460
+ const reqWithMeta = computeTaskRequirements("research-milestone", { tags: ["docs"], fileCount: 10 });
461
+ const reqWithout = computeTaskRequirements("research-milestone");
462
+ assert.deepStrictEqual(reqWithMeta, reqWithout);
463
+ });
464
+ });
465
+
466
+ // ─── scoreEligibleModels ─────────────────────────────────────────────────────
467
+
468
+ describe("scoreEligibleModels", () => {
469
+ test("ranks models by score descending when scores differ by more than 2", () => {
470
+ // research: heavily weights research dimension. gemini-2.5-pro has 85 research vs sonnet's 75
471
+ const requirements = { research: 0.9, longContext: 0.7, reasoning: 0.5 };
472
+ const results = scoreEligibleModels(["claude-sonnet-4-6", "gemini-2.5-pro"], requirements);
473
+ assert.equal(results.length, 2);
474
+ assert.ok(results[0].score >= results[1].score, "Should be sorted by score descending");
475
+ });
476
+
477
+ test("within 2-point threshold, prefers cheaper model", () => {
478
+ // Use models without built-in profiles (both get score 50) so tie-break applies
479
+ // Then use known models with equal scores: force this via single unknown model pair
480
+ const requirements = { coding: 1.0 };
481
+ // model-a and model-b are both unknown → score=50, cost=Infinity → lexicographic
482
+ const results = scoreEligibleModels(["model-z", "model-a"], requirements);
483
+ // Both unknown: score=50 (within 2), cost=Infinity (equal) → lex: model-a first
484
+ assert.equal(results[0].modelId, "model-a");
485
+ });
486
+
487
+ test("single model returns array of one", () => {
488
+ const results = scoreEligibleModels(["claude-sonnet-4-6"], { coding: 0.9 });
489
+ assert.equal(results.length, 1);
490
+ assert.equal(results[0].modelId, "claude-sonnet-4-6");
491
+ });
492
+
493
+ test("unknown model with no profile gets score of 50", () => {
494
+ const results = scoreEligibleModels(["totally-unknown-model"], { coding: 1.0 });
495
+ assert.equal(results[0].score, 50);
496
+ });
497
+
498
+ test("capabilityOverrides deep-merges with built-in profile", () => {
499
+ const requirements = { coding: 1.0 };
500
+ // Override sonnet's coding to 30 — gpt-4o (coding=80) should win
501
+ const results = scoreEligibleModels(
502
+ ["claude-sonnet-4-6", "gpt-4o"],
503
+ requirements,
504
+ { "claude-sonnet-4-6": { coding: 30 } },
505
+ );
506
+ assert.equal(results[0].modelId, "gpt-4o", "gpt-4o should rank first after coding override");
507
+ });
508
+ });
509
+
510
+ // ─── getEligibleModels ───────────────────────────────────────────────────────
511
+
512
+ describe("getEligibleModels", () => {
513
+ const ALL_MODELS = [
514
+ "claude-opus-4-6", // heavy
515
+ "claude-sonnet-4-6", // standard
516
+ "claude-haiku-4-5", // light
517
+ "gpt-4o-mini", // light
518
+ "gpt-4o", // standard
519
+ ];
520
+
521
+ test("returns light-tier models from available list sorted by cost", () => {
522
+ const config: DynamicRoutingConfig = defaultRoutingConfig();
523
+ const result = getEligibleModels("light", ALL_MODELS, config);
524
+ assert.ok(result.length >= 1);
525
+ for (const id of result) {
526
+ assert.ok(
527
+ ["claude-haiku-4-5", "gpt-4o-mini"].includes(id),
528
+ `Expected light-tier model, got ${id}`,
529
+ );
530
+ }
531
+ });
532
+
533
+ test("returns standard-tier models from available list sorted by cost", () => {
534
+ const config: DynamicRoutingConfig = defaultRoutingConfig();
535
+ const result = getEligibleModels("standard", ALL_MODELS, config);
536
+ assert.ok(result.length >= 1);
537
+ for (const id of result) {
538
+ assert.ok(
539
+ ["claude-sonnet-4-6", "gpt-4o"].includes(id),
540
+ `Expected standard-tier model, got ${id}`,
541
+ );
542
+ }
543
+ });
544
+
545
+ test("tier_models pinned model returns single-element array", () => {
546
+ const config: DynamicRoutingConfig = {
547
+ ...defaultRoutingConfig(),
548
+ tier_models: { light: "gpt-4o-mini" },
549
+ };
550
+ const result = getEligibleModels("light", ALL_MODELS, config);
551
+ assert.deepStrictEqual(result, ["gpt-4o-mini"]);
552
+ });
553
+
554
+ test("empty available list returns empty array", () => {
555
+ const config: DynamicRoutingConfig = defaultRoutingConfig();
556
+ const result = getEligibleModels("light", [], config);
557
+ assert.equal(result.length, 0);
558
+ });
559
+
560
+ test("unknown models classified as standard appear in standard tier results", () => {
561
+ const config: DynamicRoutingConfig = defaultRoutingConfig();
562
+ // unknown-model-xyz has no entry → defaults to standard tier
563
+ const result = getEligibleModels("standard", ["unknown-model-xyz"], config);
564
+ assert.ok(result.includes("unknown-model-xyz"), "Unknown model should appear in standard tier");
565
+ });
566
+ });
567
+
568
+ // ─── capability-aware routing integration ────────────────────────────────────
569
+
570
+ describe("capability-aware routing integration", () => {
571
+ // All standard-tier models available alongside heavy (opus)
572
+ const MULTI_MODEL_AVAILABLE = [
573
+ "claude-opus-4-6",
574
+ "claude-sonnet-4-6",
575
+ "gpt-4o",
576
+ "gemini-2.5-pro",
577
+ "claude-haiku-4-5",
578
+ "gpt-4o-mini",
579
+ ];
580
+
581
+ // 1. Full pipeline with capability scoring active
582
+ test("full pipeline with capability_routing: true returns capability-scored decision", () => {
583
+ const config: DynamicRoutingConfig = { ...defaultRoutingConfig(), enabled: true, capability_routing: true };
584
+ // Configured primary is opus (heavy) — standard tier should trigger capability scoring
585
+ const result = resolveModelForComplexity(
586
+ { tier: "standard", reason: "test", downgraded: false },
587
+ { primary: "claude-opus-4-6", fallbacks: [] },
588
+ config,
589
+ MULTI_MODEL_AVAILABLE,
590
+ "execute-task",
591
+ { tags: [], complexityKeywords: [], fileCount: 3, estimatedLines: 100, codeBlockCount: 0 },
592
+ );
593
+ assert.equal(result.selectionMethod, "capability-scored", "should use capability scoring when enabled with multiple eligible models");
594
+ assert.ok(result.capabilityScores !== undefined, "capabilityScores should be populated");
595
+ assert.ok(Object.keys(result.capabilityScores!).length > 1, "should have scores for multiple models");
596
+ assert.equal(result.wasDowngraded, true, "should be downgraded from opus");
597
+ });
598
+
599
+ // 2. capability_routing: false falls back to tier-only
600
+ test("capability_routing: false skips scoring and uses tier-only", () => {
601
+ const config: DynamicRoutingConfig = { ...defaultRoutingConfig(), enabled: true, capability_routing: false };
602
+ const result = resolveModelForComplexity(
603
+ { tier: "standard", reason: "test", downgraded: false },
604
+ { primary: "claude-opus-4-6", fallbacks: [] },
605
+ config,
606
+ MULTI_MODEL_AVAILABLE,
607
+ "execute-task",
608
+ undefined,
609
+ );
610
+ assert.equal(result.selectionMethod, "tier-only", "capability_routing: false should use tier-only");
611
+ assert.equal(result.capabilityScores, undefined, "capabilityScores should be undefined for tier-only");
612
+ });
613
+
614
+ // 3. Single eligible model skips scoring
615
+ test("single eligible model skips capability scoring and uses tier-only", () => {
616
+ const config: DynamicRoutingConfig = {
617
+ ...defaultRoutingConfig(),
618
+ enabled: true,
619
+ capability_routing: true,
620
+ tier_models: { standard: "claude-sonnet-4-6" },
621
+ };
622
+ // Pin to single standard model — eligible.length === 1 → skips STEP 2
623
+ const result = resolveModelForComplexity(
624
+ { tier: "standard", reason: "test", downgraded: false },
625
+ { primary: "claude-opus-4-6", fallbacks: [] },
626
+ config,
627
+ MULTI_MODEL_AVAILABLE,
628
+ "execute-task",
629
+ undefined,
630
+ );
631
+ // Single pinned model → tier-only (no scoring needed)
632
+ assert.equal(result.selectionMethod, "tier-only", "single eligible model should use tier-only");
633
+ assert.equal(result.modelId, "claude-sonnet-4-6", "should use the pinned model");
634
+ });
635
+
636
+ // 4. Unknown model with no profile gets uniform 50s and competes
637
+ test("unknown model with no profile gets uniform score of 50 and can compete", () => {
638
+ const unknownModel = "unknown-future-model-xyz";
639
+ const config: DynamicRoutingConfig = { ...defaultRoutingConfig(), enabled: true, capability_routing: true };
640
+ // Add unknown model to available list at standard tier (unknown → standard per D-15)
641
+ // scoring should still work with score=50 for the unknown model
642
+ const requirements = { coding: 0.9, instruction: 0.7, speed: 0.3 };
643
+ const scored = scoreEligibleModels([unknownModel, "claude-sonnet-4-6"], requirements);
644
+ const unknownEntry = scored.find(s => s.modelId === unknownModel);
645
+ assert.ok(unknownEntry !== undefined, "unknown model should be in scored results");
646
+ // Unknown model gets uniform 50s: (0.9*50 + 0.7*50 + 0.3*50) / (0.9+0.7+0.3) ≈ 50
647
+ assert.ok(Math.abs(unknownEntry!.score - 50) < 0.01, `expected score ~50, got ${unknownEntry!.score}`);
648
+ });
649
+
650
+ // 5. Capability overrides change scoring outcome
651
+ test("capabilityOverrides boost a model above another for same task", () => {
652
+ // sonnet: coding=85, gpt-4o: coding=80. Override gpt-4o coding to 99 → gpt-4o should win.
653
+ const requirements = { coding: 1.0 };
654
+ const overrides = { "gpt-4o": { coding: 99 } };
655
+ const scored = scoreEligibleModels(["claude-sonnet-4-6", "gpt-4o"], requirements, overrides);
656
+ assert.equal(scored[0].modelId, "gpt-4o", "overridden model should win for coding-heavy task");
657
+ assert.ok(scored[0].score > 90, `expected score > 90 after override, got ${scored[0].score}`);
658
+ });
659
+
660
+ // 5b. Capability overrides pass through resolveModelForComplexity to scoreEligibleModels
661
+ test("resolveModelForComplexity passes capabilityOverrides to scoring step", () => {
662
+ const config: DynamicRoutingConfig = { ...defaultRoutingConfig(), enabled: true, capability_routing: true };
663
+ // sonnet coding=85, gpt-4o coding=80. Override gpt-4o coding to 99 → gpt-4o should win.
664
+ const overrides: Record<string, Partial<ModelCapabilities>> = { "gpt-4o": { coding: 99 } };
665
+ const result = resolveModelForComplexity(
666
+ { tier: "standard", reason: "test", downgraded: false },
667
+ { primary: "claude-opus-4-6", fallbacks: [] },
668
+ config,
669
+ ["claude-opus-4-6", "claude-sonnet-4-6", "gpt-4o"],
670
+ "execute-task",
671
+ undefined,
672
+ overrides,
673
+ );
674
+ assert.equal(result.selectionMethod, "capability-scored");
675
+ assert.equal(result.modelId, "gpt-4o", "gpt-4o should win with coding override");
676
+ });
677
+
678
+ // 6. Regression: existing routing guards unchanged
679
+ test("regression: routing-disabled passthrough still returns tier-only", () => {
680
+ const config: DynamicRoutingConfig = { ...defaultRoutingConfig(), enabled: false };
681
+ const result = resolveModelForComplexity(
682
+ { tier: "light", reason: "test", downgraded: false },
683
+ { primary: "claude-opus-4-6", fallbacks: [] },
684
+ config,
685
+ MULTI_MODEL_AVAILABLE,
686
+ "execute-task",
687
+ undefined,
688
+ );
689
+ assert.equal(result.selectionMethod, "tier-only");
690
+ assert.equal(result.wasDowngraded, false);
691
+ assert.equal(result.modelId, "claude-opus-4-6");
692
+ });
693
+
694
+ test("regression: unknown-model bypass returns tier-only and does not downgrade", () => {
695
+ const config: DynamicRoutingConfig = { ...defaultRoutingConfig(), enabled: true };
696
+ const result = resolveModelForComplexity(
697
+ { tier: "light", reason: "test", downgraded: false },
698
+ { primary: "totally-unknown-custom-model", fallbacks: [] },
699
+ config,
700
+ ["totally-unknown-custom-model", ...MULTI_MODEL_AVAILABLE],
701
+ "execute-task",
702
+ undefined,
703
+ );
704
+ assert.equal(result.selectionMethod, "tier-only");
705
+ assert.equal(result.wasDowngraded, false);
706
+ assert.equal(result.modelId, "totally-unknown-custom-model");
707
+ });
708
+
709
+ test("regression: no-downgrade-needed path returns tier-only", () => {
710
+ const config: DynamicRoutingConfig = { ...defaultRoutingConfig(), enabled: true, capability_routing: true };
711
+ // Configured model is sonnet (standard), requesting standard → no downgrade needed
712
+ const result = resolveModelForComplexity(
713
+ { tier: "standard", reason: "test", downgraded: false },
714
+ { primary: "claude-sonnet-4-6", fallbacks: [] },
715
+ config,
716
+ MULTI_MODEL_AVAILABLE,
717
+ "execute-task",
718
+ undefined,
719
+ );
720
+ assert.equal(result.selectionMethod, "tier-only");
721
+ assert.equal(result.wasDowngraded, false);
722
+ assert.equal(result.modelId, "claude-sonnet-4-6");
723
+ });
724
+ });
725
+
726
+ // ─── getModelTier unknown default ────────────────────────────────────────────
727
+
728
+ describe("getModelTier unknown default", () => {
729
+ test("unknown model returns standard tier (not heavy) via downgrade behavior", () => {
730
+ // We can verify this indirectly: resolveModelForComplexity for a standard classification
731
+ // with an unknown primary model should NOT downgrade (because unknown → standard, not heavy)
732
+ const config = { ...defaultRoutingConfig(), enabled: true };
733
+ // Use "unknown-model-xyz" as primary — its tier will be "standard" per D-15
734
+ // Classification is "heavy" → tier >= standard → no downgrade
735
+ // But unknown models use the isKnownModel() guard, so they pass through anyway
736
+ // Test the positive: an unknown model is NOT treated as heavy
737
+ const result = resolveModelForComplexity(
738
+ makeClassification("standard"),
739
+ { primary: "claude-sonnet-4-6", fallbacks: [] },
740
+ config,
741
+ ["claude-sonnet-4-6", "claude-haiku-4-5", "gpt-4o-mini"],
742
+ );
743
+ // standard classification with standard model (sonnet) → no downgrade
744
+ assert.equal(result.wasDowngraded, false, "standard model should not downgrade for standard task");
745
+ assert.equal(result.modelId, "claude-sonnet-4-6");
746
+ });
747
+
748
+ test("unknown model in getEligibleModels defaults to standard tier", () => {
749
+ // Per D-15: getModelTier returns "standard" for unknown models
750
+ const config: DynamicRoutingConfig = defaultRoutingConfig();
751
+ const standardModels = getEligibleModels("standard", ["totally-unknown-model-abc"], config);
752
+ const lightModels = getEligibleModels("light", ["totally-unknown-model-abc"], config);
753
+ const heavyModels = getEligibleModels("heavy", ["totally-unknown-model-abc"], config);
754
+ assert.ok(standardModels.includes("totally-unknown-model-abc"), "Unknown model should be in standard tier");
755
+ assert.equal(lightModels.length, 0, "Unknown model should NOT be in light tier");
756
+ assert.equal(heavyModels.length, 0, "Unknown model should NOT be in heavy tier");
757
+ });
758
+ });
@@ -0,0 +1,83 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import { writePhaseAnchor, readPhaseAnchor, formatAnchorForPrompt } from "../phase-anchor.js";
8
+ import type { PhaseAnchor } from "../phase-anchor.js";
9
+
10
+ function makeTempBase(): string {
11
+ const tmp = mkdtempSync(join(tmpdir(), "gsd-anchor-test-"));
12
+ mkdirSync(join(tmp, ".gsd", "milestones", "M001", "anchors"), { recursive: true });
13
+ return tmp;
14
+ }
15
+
16
+ test("writePhaseAnchor creates anchor file in correct location", () => {
17
+ const base = makeTempBase();
18
+ try {
19
+ const anchor: PhaseAnchor = {
20
+ phase: "discuss",
21
+ milestoneId: "M001",
22
+ generatedAt: new Date().toISOString(),
23
+ intent: "Define authentication requirements",
24
+ decisions: ["Use JWT tokens", "Session expiry 24h"],
25
+ blockers: [],
26
+ nextSteps: ["Plan the implementation slices"],
27
+ };
28
+ writePhaseAnchor(base, "M001", anchor);
29
+ assert.ok(existsSync(join(base, ".gsd", "milestones", "M001", "anchors", "discuss.json")));
30
+ } finally {
31
+ rmSync(base, { recursive: true, force: true });
32
+ }
33
+ });
34
+
35
+ test("readPhaseAnchor returns written anchor", () => {
36
+ const base = makeTempBase();
37
+ try {
38
+ const anchor: PhaseAnchor = {
39
+ phase: "plan",
40
+ milestoneId: "M001",
41
+ generatedAt: new Date().toISOString(),
42
+ intent: "Break work into slices",
43
+ decisions: ["3 slices: auth, UI, tests"],
44
+ blockers: ["Need DB schema first"],
45
+ nextSteps: ["Execute S01"],
46
+ };
47
+ writePhaseAnchor(base, "M001", anchor);
48
+ const read = readPhaseAnchor(base, "M001", "plan");
49
+ assert.ok(read);
50
+ assert.equal(read!.intent, "Break work into slices");
51
+ assert.deepEqual(read!.decisions, ["3 slices: auth, UI, tests"]);
52
+ assert.deepEqual(read!.blockers, ["Need DB schema first"]);
53
+ } finally {
54
+ rmSync(base, { recursive: true, force: true });
55
+ }
56
+ });
57
+
58
+ test("readPhaseAnchor returns null when no anchor exists", () => {
59
+ const base = makeTempBase();
60
+ try {
61
+ const read = readPhaseAnchor(base, "M001", "discuss");
62
+ assert.equal(read, null);
63
+ } finally {
64
+ rmSync(base, { recursive: true, force: true });
65
+ }
66
+ });
67
+
68
+ test("formatAnchorForPrompt produces markdown block", () => {
69
+ const anchor: PhaseAnchor = {
70
+ phase: "discuss",
71
+ milestoneId: "M001",
72
+ generatedAt: "2026-04-03T00:00:00.000Z",
73
+ intent: "Define requirements",
74
+ decisions: ["Use JWT"],
75
+ blockers: [],
76
+ nextSteps: ["Plan slices"],
77
+ };
78
+ const md = formatAnchorForPrompt(anchor);
79
+ assert.ok(md.includes("## Handoff from discuss"));
80
+ assert.ok(md.includes("Define requirements"));
81
+ assert.ok(md.includes("Use JWT"));
82
+ assert.ok(md.includes("Plan slices"));
83
+ });
@@ -461,3 +461,65 @@ test("experimental.rtk defaults to off in new project preferences", () => {
461
461
  assert.notEqual(prefs, null);
462
462
  assert.equal(prefs!.experimental?.rtk, undefined);
463
463
  });
464
+
465
+ // ── Codebase Map Preferences ─────────────────────────────────────────────────
466
+
467
+ test("codebase preferences validate and pass through correctly", () => {
468
+ const result = validatePreferences({
469
+ codebase: {
470
+ exclude_patterns: ["docs/", "fixtures/"],
471
+ max_files: 1000,
472
+ collapse_threshold: 15,
473
+ },
474
+ });
475
+ assert.equal(result.errors.length, 0);
476
+ assert.deepEqual(result.preferences.codebase?.exclude_patterns, ["docs/", "fixtures/"]);
477
+ assert.equal(result.preferences.codebase?.max_files, 1000);
478
+ assert.equal(result.preferences.codebase?.collapse_threshold, 15);
479
+ });
480
+
481
+ test("codebase preferences reject invalid types", () => {
482
+ const result = validatePreferences({
483
+ codebase: {
484
+ exclude_patterns: "not-an-array" as any,
485
+ max_files: -5,
486
+ collapse_threshold: 0,
487
+ },
488
+ });
489
+ assert.ok(result.errors.some(e => e.includes("exclude_patterns must be an array")));
490
+ assert.ok(result.errors.some(e => e.includes("max_files must be a positive")));
491
+ assert.ok(result.errors.some(e => e.includes("collapse_threshold must be a positive")));
492
+ });
493
+
494
+ test("codebase preferences warn on unknown keys", () => {
495
+ const result = validatePreferences({
496
+ codebase: {
497
+ exclude_patterns: ["docs/"],
498
+ unknown_key: true,
499
+ } as any,
500
+ });
501
+ assert.equal(result.errors.length, 0);
502
+ assert.ok(result.warnings.some(w => w.includes('unknown codebase key "unknown_key"')));
503
+ assert.deepEqual(result.preferences.codebase?.exclude_patterns, ["docs/"]);
504
+ });
505
+
506
+ test("codebase preferences parse from markdown frontmatter", () => {
507
+ const content = [
508
+ "---",
509
+ "version: 1",
510
+ "codebase:",
511
+ " exclude_patterns:",
512
+ ' - "docs/"',
513
+ ' - ".cache/"',
514
+ " max_files: 800",
515
+ " collapse_threshold: 10",
516
+ "---",
517
+ ].join("\n");
518
+ const prefs = parsePreferencesMarkdown(content);
519
+ assert.notEqual(prefs, null);
520
+ const result = validatePreferences(prefs!);
521
+ assert.equal(result.errors.length, 0);
522
+ assert.deepEqual(result.preferences.codebase?.exclude_patterns, ["docs/", ".cache/"]);
523
+ assert.equal(result.preferences.codebase?.max_files, 800);
524
+ assert.equal(result.preferences.codebase?.collapse_threshold, 10);
525
+ });