gsd-pi 2.18.0 → 2.20.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 (289) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +3 -3
  3. package/dist/onboarding.d.ts +3 -1
  4. package/dist/onboarding.js +77 -3
  5. package/dist/remote-questions-config.d.ts +1 -1
  6. package/dist/resources/extensions/google-search/index.ts +164 -47
  7. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +148 -39
  9. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  10. package/dist/resources/extensions/gsd/auto.ts +690 -39
  11. package/dist/resources/extensions/gsd/captures.ts +384 -0
  12. package/dist/resources/extensions/gsd/commands.ts +654 -36
  13. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  14. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  15. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  16. package/dist/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  17. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  18. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  19. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  20. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  21. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  22. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  23. package/dist/resources/extensions/gsd/export.ts +81 -2
  24. package/dist/resources/extensions/gsd/files.ts +39 -9
  25. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  26. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  27. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  28. package/dist/resources/extensions/gsd/history.ts +0 -1
  29. package/dist/resources/extensions/gsd/index.ts +277 -1
  30. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  31. package/dist/resources/extensions/gsd/metrics.ts +84 -0
  32. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  33. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  34. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  35. package/dist/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  36. package/dist/resources/extensions/gsd/preferences.ts +198 -150
  37. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  39. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  40. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  41. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  42. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  43. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  44. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  45. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  46. package/dist/resources/extensions/gsd/quick.ts +156 -0
  47. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  48. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  49. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  50. package/dist/resources/extensions/gsd/state.ts +30 -0
  51. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  52. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  53. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  54. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  55. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  57. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  58. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  59. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  60. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  61. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  62. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  63. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  64. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  65. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  66. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  67. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  68. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  69. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  70. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  71. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  72. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  73. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  74. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  75. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  76. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  77. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  78. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  79. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  80. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  81. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  82. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  83. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  84. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  85. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  86. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  87. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  88. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  89. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  90. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  91. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  92. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  93. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  94. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  95. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  96. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  97. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  98. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  99. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  100. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  101. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  102. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  103. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  104. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  105. package/dist/resources/extensions/gsd/types.ts +29 -0
  106. package/dist/resources/extensions/gsd/undo.ts +0 -1
  107. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  108. package/dist/resources/extensions/gsd/visualizer-data.ts +505 -0
  109. package/dist/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  110. package/dist/resources/extensions/gsd/visualizer-views.ts +755 -0
  111. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  112. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  113. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  114. package/dist/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  115. package/dist/resources/extensions/remote-questions/format.ts +166 -14
  116. package/dist/resources/extensions/remote-questions/manager.ts +14 -4
  117. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  118. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  119. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  120. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  121. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  122. package/dist/resources/extensions/voice/index.ts +4 -3
  123. package/package.json +1 -1
  124. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  126. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  129. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  131. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  133. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  137. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  139. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  140. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  142. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  144. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  146. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  148. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  150. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  152. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  153. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  154. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  156. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  158. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  160. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  161. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  162. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  163. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  164. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  165. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  166. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  167. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  168. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  169. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  170. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  171. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  172. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  173. package/src/resources/extensions/google-search/index.ts +164 -47
  174. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  175. package/src/resources/extensions/gsd/auto-prompts.ts +148 -39
  176. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  177. package/src/resources/extensions/gsd/auto.ts +690 -39
  178. package/src/resources/extensions/gsd/captures.ts +384 -0
  179. package/src/resources/extensions/gsd/commands.ts +654 -36
  180. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  181. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  182. package/src/resources/extensions/gsd/context-store.ts +195 -0
  183. package/src/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  184. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  185. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  186. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  187. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  188. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  189. package/src/resources/extensions/gsd/doctor.ts +283 -2
  190. package/src/resources/extensions/gsd/export.ts +81 -2
  191. package/src/resources/extensions/gsd/files.ts +39 -9
  192. package/src/resources/extensions/gsd/git-service.ts +6 -0
  193. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  194. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  195. package/src/resources/extensions/gsd/history.ts +0 -1
  196. package/src/resources/extensions/gsd/index.ts +277 -1
  197. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  198. package/src/resources/extensions/gsd/metrics.ts +84 -0
  199. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  200. package/src/resources/extensions/gsd/model-router.ts +256 -0
  201. package/src/resources/extensions/gsd/notifications.ts +0 -1
  202. package/src/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  203. package/src/resources/extensions/gsd/preferences.ts +198 -150
  204. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  205. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  206. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  207. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  208. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  209. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  210. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  211. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  212. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  213. package/src/resources/extensions/gsd/quick.ts +156 -0
  214. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  215. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  216. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  217. package/src/resources/extensions/gsd/state.ts +30 -0
  218. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  219. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  220. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  221. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  222. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  223. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  224. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  225. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  226. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  227. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  228. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  229. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  230. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  231. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  232. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  233. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  234. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  235. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  236. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  237. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  238. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  239. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  240. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  241. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  242. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  243. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  244. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  245. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  246. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  247. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  248. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  249. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  250. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  251. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  252. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  253. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  254. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  255. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  256. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  257. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  258. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  259. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  260. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  261. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  262. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  263. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  264. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  265. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  266. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  267. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  268. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  269. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  270. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  271. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  272. package/src/resources/extensions/gsd/types.ts +29 -0
  273. package/src/resources/extensions/gsd/undo.ts +0 -1
  274. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  275. package/src/resources/extensions/gsd/visualizer-data.ts +505 -0
  276. package/src/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  277. package/src/resources/extensions/gsd/visualizer-views.ts +755 -0
  278. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  279. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  280. package/src/resources/extensions/remote-questions/config.ts +4 -2
  281. package/src/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  282. package/src/resources/extensions/remote-questions/format.ts +166 -14
  283. package/src/resources/extensions/remote-questions/manager.ts +14 -4
  284. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  285. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  286. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  287. package/src/resources/extensions/remote-questions/types.ts +2 -1
  288. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  289. package/src/resources/extensions/voice/index.ts +4 -3
@@ -0,0 +1,462 @@
1
+ import { createTestContext } from './test-helpers.ts';
2
+ import {
3
+ openDatabase,
4
+ closeDatabase,
5
+ isDbAvailable,
6
+ insertDecision,
7
+ insertRequirement,
8
+ insertArtifact,
9
+ } from '../gsd-db.ts';
10
+ import {
11
+ queryDecisions,
12
+ queryRequirements,
13
+ formatDecisionsForPrompt,
14
+ formatRequirementsForPrompt,
15
+ queryArtifact,
16
+ queryProject,
17
+ } from '../context-store.ts';
18
+
19
+ const { assertEq, assertTrue, assertMatch, report } = createTestContext();
20
+
21
+ // ═══════════════════════════════════════════════════════════════════════════
22
+ // context-store: fallback when DB not open
23
+ // ═══════════════════════════════════════════════════════════════════════════
24
+
25
+ console.log('\n=== context-store: fallback returns empty when DB not open ===');
26
+ {
27
+ closeDatabase();
28
+ assertTrue(!isDbAvailable(), 'DB should not be available');
29
+
30
+ const d = queryDecisions();
31
+ assertEq(d, [], 'queryDecisions returns [] when DB closed');
32
+
33
+ const r = queryRequirements();
34
+ assertEq(r, [], 'queryRequirements returns [] when DB closed');
35
+
36
+ const df = queryDecisions({ milestoneId: 'M001' });
37
+ assertEq(df, [], 'queryDecisions with opts returns [] when DB closed');
38
+
39
+ const rf = queryRequirements({ sliceId: 'S01' });
40
+ assertEq(rf, [], 'queryRequirements with opts returns [] when DB closed');
41
+ }
42
+
43
+ // ═══════════════════════════════════════════════════════════════════════════
44
+ // context-store: query decisions
45
+ // ═══════════════════════════════════════════════════════════════════════════
46
+
47
+ console.log('\n=== context-store: query all active decisions ===');
48
+ {
49
+ openDatabase(':memory:');
50
+
51
+ insertDecision({
52
+ id: 'D001', when_context: 'M001/S01', scope: 'architecture',
53
+ decision: 'use SQLite', choice: 'node:sqlite', rationale: 'built-in',
54
+ revisable: 'yes', superseded_by: 'D003', // superseded!
55
+ });
56
+ insertDecision({
57
+ id: 'D002', when_context: 'M001/S01', scope: 'architecture',
58
+ decision: 'use WAL mode', choice: 'WAL', rationale: 'concurrent reads',
59
+ revisable: 'no', superseded_by: null,
60
+ });
61
+ insertDecision({
62
+ id: 'D003', when_context: 'M002/S01', scope: 'performance',
63
+ decision: 'use better-sqlite3', choice: 'better-sqlite3', rationale: 'faster',
64
+ revisable: 'yes', superseded_by: null,
65
+ });
66
+
67
+ const all = queryDecisions();
68
+ assertEq(all.length, 2, 'query all active decisions returns 2 (superseded excluded)');
69
+ const ids = all.map(d => d.id);
70
+ assertTrue(ids.includes('D002'), 'D002 should be in active results');
71
+ assertTrue(ids.includes('D003'), 'D003 should be in active results');
72
+ assertTrue(!ids.includes('D001'), 'D001 (superseded) should NOT be in active results');
73
+
74
+ closeDatabase();
75
+ }
76
+
77
+ console.log('\n=== context-store: query decisions by milestone ===');
78
+ {
79
+ openDatabase(':memory:');
80
+
81
+ insertDecision({
82
+ id: 'D001', when_context: 'M001/S01', scope: 'architecture',
83
+ decision: 'decision A', choice: 'A', rationale: 'r', revisable: 'yes',
84
+ superseded_by: null,
85
+ });
86
+ insertDecision({
87
+ id: 'D002', when_context: 'M002/S02', scope: 'architecture',
88
+ decision: 'decision B', choice: 'B', rationale: 'r', revisable: 'yes',
89
+ superseded_by: null,
90
+ });
91
+
92
+ const m1 = queryDecisions({ milestoneId: 'M001' });
93
+ assertEq(m1.length, 1, 'milestone filter M001 returns 1');
94
+ assertEq(m1[0]?.id, 'D001', 'milestone filter returns D001');
95
+
96
+ const m2 = queryDecisions({ milestoneId: 'M002' });
97
+ assertEq(m2.length, 1, 'milestone filter M002 returns 1');
98
+ assertEq(m2[0]?.id, 'D002', 'milestone filter returns D002');
99
+
100
+ closeDatabase();
101
+ }
102
+
103
+ console.log('\n=== context-store: query decisions by scope ===');
104
+ {
105
+ openDatabase(':memory:');
106
+
107
+ insertDecision({
108
+ id: 'D001', when_context: 'M001/S01', scope: 'architecture',
109
+ decision: 'decision A', choice: 'A', rationale: 'r', revisable: 'yes',
110
+ superseded_by: null,
111
+ });
112
+ insertDecision({
113
+ id: 'D002', when_context: 'M001/S01', scope: 'performance',
114
+ decision: 'decision B', choice: 'B', rationale: 'r', revisable: 'yes',
115
+ superseded_by: null,
116
+ });
117
+
118
+ const arch = queryDecisions({ scope: 'architecture' });
119
+ assertEq(arch.length, 1, 'scope filter architecture returns 1');
120
+ assertEq(arch[0]?.id, 'D001', 'scope filter returns D001');
121
+
122
+ const perf = queryDecisions({ scope: 'performance' });
123
+ assertEq(perf.length, 1, 'scope filter performance returns 1');
124
+ assertEq(perf[0]?.id, 'D002', 'scope filter returns D002');
125
+
126
+ const none = queryDecisions({ scope: 'nonexistent' });
127
+ assertEq(none.length, 0, 'scope filter nonexistent returns 0');
128
+
129
+ closeDatabase();
130
+ }
131
+
132
+ // ═══════════════════════════════════════════════════════════════════════════
133
+ // context-store: query requirements
134
+ // ═══════════════════════════════════════════════════════════════════════════
135
+
136
+ console.log('\n=== context-store: query all active requirements ===');
137
+ {
138
+ openDatabase(':memory:');
139
+
140
+ insertRequirement({
141
+ id: 'R001', class: 'functional', status: 'active',
142
+ description: 'req A', why: 'w', source: 'M001', primary_owner: 'S01',
143
+ supporting_slices: 'S02', validation: 'v', notes: '', full_content: '',
144
+ superseded_by: 'R003', // superseded!
145
+ });
146
+ insertRequirement({
147
+ id: 'R002', class: 'non-functional', status: 'active',
148
+ description: 'req B', why: 'w', source: 'M001', primary_owner: 'S01',
149
+ supporting_slices: '', validation: 'v', notes: '', full_content: '',
150
+ superseded_by: null,
151
+ });
152
+ insertRequirement({
153
+ id: 'R003', class: 'functional', status: 'validated',
154
+ description: 'req C', why: 'w', source: 'M001', primary_owner: 'S02',
155
+ supporting_slices: 'S01', validation: 'v', notes: '', full_content: '',
156
+ superseded_by: null,
157
+ });
158
+
159
+ const all = queryRequirements();
160
+ assertEq(all.length, 2, 'query all active requirements returns 2 (superseded excluded)');
161
+ const ids = all.map(r => r.id);
162
+ assertTrue(ids.includes('R002'), 'R002 should be active');
163
+ assertTrue(ids.includes('R003'), 'R003 should be active');
164
+ assertTrue(!ids.includes('R001'), 'R001 (superseded) should NOT be active');
165
+
166
+ closeDatabase();
167
+ }
168
+
169
+ console.log('\n=== context-store: query requirements by slice ===');
170
+ {
171
+ openDatabase(':memory:');
172
+
173
+ insertRequirement({
174
+ id: 'R001', class: 'functional', status: 'active',
175
+ description: 'req A', why: 'w', source: 'M001', primary_owner: 'S01',
176
+ supporting_slices: '', validation: 'v', notes: '', full_content: '',
177
+ superseded_by: null,
178
+ });
179
+ insertRequirement({
180
+ id: 'R002', class: 'functional', status: 'active',
181
+ description: 'req B', why: 'w', source: 'M001', primary_owner: 'S02',
182
+ supporting_slices: 'S01', validation: 'v', notes: '', full_content: '',
183
+ superseded_by: null,
184
+ });
185
+ insertRequirement({
186
+ id: 'R003', class: 'functional', status: 'active',
187
+ description: 'req C', why: 'w', source: 'M001', primary_owner: 'S03',
188
+ supporting_slices: '', validation: 'v', notes: '', full_content: '',
189
+ superseded_by: null,
190
+ });
191
+
192
+ const s01 = queryRequirements({ sliceId: 'S01' });
193
+ assertEq(s01.length, 2, 'slice filter S01 returns 2 (primary + supporting)');
194
+ const s01ids = s01.map(r => r.id).sort();
195
+ assertEq(s01ids, ['R001', 'R002'], 'S01 owns R001 and supports R002');
196
+
197
+ const s03 = queryRequirements({ sliceId: 'S03' });
198
+ assertEq(s03.length, 1, 'slice filter S03 returns 1');
199
+ assertEq(s03[0]?.id, 'R003', 'S03 owns R003');
200
+
201
+ closeDatabase();
202
+ }
203
+
204
+ console.log('\n=== context-store: query requirements by status ===');
205
+ {
206
+ openDatabase(':memory:');
207
+
208
+ insertRequirement({
209
+ id: 'R001', class: 'functional', status: 'active',
210
+ description: 'req A', why: 'w', source: 'M001', primary_owner: 'S01',
211
+ supporting_slices: '', validation: 'v', notes: '', full_content: '',
212
+ superseded_by: null,
213
+ });
214
+ insertRequirement({
215
+ id: 'R002', class: 'functional', status: 'validated',
216
+ description: 'req B', why: 'w', source: 'M001', primary_owner: 'S01',
217
+ supporting_slices: '', validation: 'v', notes: '', full_content: '',
218
+ superseded_by: null,
219
+ });
220
+ insertRequirement({
221
+ id: 'R003', class: 'functional', status: 'deferred',
222
+ description: 'req C', why: 'w', source: 'M001', primary_owner: 'S01',
223
+ supporting_slices: '', validation: 'v', notes: '', full_content: '',
224
+ superseded_by: null,
225
+ });
226
+
227
+ const active = queryRequirements({ status: 'active' });
228
+ assertEq(active.length, 1, 'status filter active returns 1');
229
+ assertEq(active[0]?.id, 'R001', 'active returns R001');
230
+
231
+ const validated = queryRequirements({ status: 'validated' });
232
+ assertEq(validated.length, 1, 'status filter validated returns 1');
233
+ assertEq(validated[0]?.id, 'R002', 'validated returns R002');
234
+
235
+ closeDatabase();
236
+ }
237
+
238
+ // ═══════════════════════════════════════════════════════════════════════════
239
+ // context-store: format decisions
240
+ // ═══════════════════════════════════════════════════════════════════════════
241
+
242
+ console.log('\n=== context-store: formatDecisionsForPrompt ===');
243
+ {
244
+ const empty = formatDecisionsForPrompt([]);
245
+ assertEq(empty, '', 'empty input returns empty string');
246
+
247
+ const result = formatDecisionsForPrompt([
248
+ {
249
+ seq: 1, id: 'D001', when_context: 'M001/S01', scope: 'architecture',
250
+ decision: 'use SQLite', choice: 'node:sqlite', rationale: 'built-in',
251
+ revisable: 'yes', superseded_by: null,
252
+ },
253
+ {
254
+ seq: 2, id: 'D002', when_context: 'M001/S02', scope: 'performance',
255
+ decision: 'use WAL', choice: 'WAL', rationale: 'concurrent',
256
+ revisable: 'no', superseded_by: null,
257
+ },
258
+ ]);
259
+
260
+ // Should be a markdown table
261
+ assertMatch(result, /^\| # \| When \| Scope/, 'has table header');
262
+ assertMatch(result, /\|---\|/, 'has separator row');
263
+ assertMatch(result, /\| D001 \|/, 'has D001 row');
264
+ assertMatch(result, /\| D002 \|/, 'has D002 row');
265
+ const lines = result.split('\n');
266
+ assertEq(lines.length, 4, 'table has 4 lines (header + separator + 2 rows)');
267
+ }
268
+
269
+ // ═══════════════════════════════════════════════════════════════════════════
270
+ // context-store: format requirements
271
+ // ═══════════════════════════════════════════════════════════════════════════
272
+
273
+ console.log('\n=== context-store: formatRequirementsForPrompt ===');
274
+ {
275
+ const empty = formatRequirementsForPrompt([]);
276
+ assertEq(empty, '', 'empty input returns empty string');
277
+
278
+ const result = formatRequirementsForPrompt([
279
+ {
280
+ id: 'R001', class: 'functional', status: 'active',
281
+ description: 'System must persist decisions', why: 'agent memory',
282
+ source: 'M001', primary_owner: 'S01', supporting_slices: 'S02',
283
+ validation: 'roundtrip test', notes: 'high priority',
284
+ full_content: '', superseded_by: null,
285
+ },
286
+ {
287
+ id: 'R002', class: 'non-functional', status: 'active',
288
+ description: 'Sub-5ms query latency', why: 'prompt injection speed',
289
+ source: 'M001', primary_owner: 'S01', supporting_slices: '',
290
+ validation: 'timing test', notes: '',
291
+ full_content: '', superseded_by: null,
292
+ },
293
+ ]);
294
+
295
+ assertMatch(result, /### R001: System must persist decisions/, 'has R001 section header');
296
+ assertMatch(result, /### R002: Sub-5ms query latency/, 'has R002 section header');
297
+ assertMatch(result, /\*\*Class:\*\* functional/, 'has class field');
298
+ assertMatch(result, /\*\*Status:\*\* active/, 'has status field');
299
+ assertMatch(result, /\*\*Supporting Slices:\*\* S02/, 'has supporting slices when present');
300
+ // R002 has no supporting_slices — should not have that line
301
+ // R002 has no notes — should not have notes line
302
+ const r002Section = result.split('### R002')[1] || '';
303
+ assertTrue(!r002Section.includes('**Supporting Slices:**'), 'no supporting slices line when empty');
304
+ assertTrue(!r002Section.includes('**Notes:**'), 'no notes line when empty');
305
+ }
306
+
307
+ // ═══════════════════════════════════════════════════════════════════════════
308
+ // context-store: sub-5ms timing assertion
309
+ // ═══════════════════════════════════════════════════════════════════════════
310
+
311
+ console.log('\n=== context-store: sub-5ms query timing ===');
312
+ {
313
+ openDatabase(':memory:');
314
+
315
+ // Insert 50 decisions
316
+ for (let i = 1; i <= 50; i++) {
317
+ const id = `D${String(i).padStart(3, '0')}`;
318
+ insertDecision({
319
+ id,
320
+ when_context: `M00${(i % 3) + 1}/S0${(i % 5) + 1}`,
321
+ scope: i % 2 === 0 ? 'architecture' : 'performance',
322
+ decision: `decision ${i}`,
323
+ choice: `choice ${i}`,
324
+ rationale: `rationale ${i}`,
325
+ revisable: i % 3 === 0 ? 'no' : 'yes',
326
+ superseded_by: null,
327
+ });
328
+ }
329
+
330
+ // Insert 50 requirements
331
+ for (let i = 1; i <= 50; i++) {
332
+ const id = `R${String(i).padStart(3, '0')}`;
333
+ insertRequirement({
334
+ id,
335
+ class: i % 2 === 0 ? 'functional' : 'non-functional',
336
+ status: i % 4 === 0 ? 'validated' : 'active',
337
+ description: `requirement ${i}`,
338
+ why: `why ${i}`,
339
+ source: 'M001',
340
+ primary_owner: `S0${(i % 5) + 1}`,
341
+ supporting_slices: i % 3 === 0 ? 'S01, S02' : '',
342
+ validation: `validation ${i}`,
343
+ notes: '',
344
+ full_content: '',
345
+ superseded_by: null,
346
+ });
347
+ }
348
+
349
+ // Time the queries — warm up first
350
+ queryDecisions();
351
+ queryRequirements();
352
+
353
+ const start = performance.now();
354
+ const decisions = queryDecisions();
355
+ const requirements = queryRequirements();
356
+ const elapsed = performance.now() - start;
357
+
358
+ assertTrue(decisions.length === 50, `got ${decisions.length} decisions (expected 50)`);
359
+ assertTrue(requirements.length === 50, `got ${requirements.length} requirements (expected 50)`);
360
+ assertTrue(elapsed < 5, `query latency ${elapsed.toFixed(2)}ms should be < 5ms`);
361
+ console.log(` timing: ${elapsed.toFixed(2)}ms for 50+50 row queries`);
362
+
363
+ closeDatabase();
364
+ }
365
+
366
+ // ═══════════════════════════════════════════════════════════════════════════
367
+ // context-store: queryArtifact
368
+ // ═══════════════════════════════════════════════════════════════════════════
369
+
370
+ console.log('\n=== context-store: queryArtifact returns content for existing path ===');
371
+ {
372
+ openDatabase(':memory:');
373
+
374
+ insertArtifact({
375
+ path: 'PROJECT.md',
376
+ artifact_type: 'project',
377
+ milestone_id: null,
378
+ slice_id: null,
379
+ task_id: null,
380
+ full_content: '# My Project\n\nProject description here.',
381
+ });
382
+ insertArtifact({
383
+ path: '.gsd/milestones/M001/M001-PLAN.md',
384
+ artifact_type: 'milestone_plan',
385
+ milestone_id: 'M001',
386
+ slice_id: null,
387
+ task_id: null,
388
+ full_content: '# M001 Plan\n\nMilestone content.',
389
+ });
390
+
391
+ const project = queryArtifact('PROJECT.md');
392
+ assertEq(project, '# My Project\n\nProject description here.', 'queryArtifact returns full_content for PROJECT.md');
393
+
394
+ const plan = queryArtifact('.gsd/milestones/M001/M001-PLAN.md');
395
+ assertEq(plan, '# M001 Plan\n\nMilestone content.', 'queryArtifact returns full_content for milestone plan');
396
+
397
+ closeDatabase();
398
+ }
399
+
400
+ console.log('\n=== context-store: queryArtifact returns null for missing path ===');
401
+ {
402
+ openDatabase(':memory:');
403
+
404
+ const missing = queryArtifact('nonexistent.md');
405
+ assertEq(missing, null, 'queryArtifact returns null for path not in DB');
406
+
407
+ closeDatabase();
408
+ }
409
+
410
+ console.log('\n=== context-store: queryArtifact returns null when DB unavailable ===');
411
+ {
412
+ closeDatabase();
413
+ assertTrue(!isDbAvailable(), 'DB should not be available');
414
+
415
+ const result = queryArtifact('PROJECT.md');
416
+ assertEq(result, null, 'queryArtifact returns null when DB closed');
417
+ }
418
+
419
+ // ═══════════════════════════════════════════════════════════════════════════
420
+ // context-store: queryProject
421
+ // ═══════════════════════════════════════════════════════════════════════════
422
+
423
+ console.log('\n=== context-store: queryProject returns PROJECT.md content ===');
424
+ {
425
+ openDatabase(':memory:');
426
+
427
+ insertArtifact({
428
+ path: 'PROJECT.md',
429
+ artifact_type: 'project',
430
+ milestone_id: null,
431
+ slice_id: null,
432
+ task_id: null,
433
+ full_content: '# Test Project\n\nThis is the project description.',
434
+ });
435
+
436
+ const content = queryProject();
437
+ assertEq(content, '# Test Project\n\nThis is the project description.', 'queryProject returns PROJECT.md content');
438
+
439
+ closeDatabase();
440
+ }
441
+
442
+ console.log('\n=== context-store: queryProject returns null when no PROJECT.md ===');
443
+ {
444
+ openDatabase(':memory:');
445
+
446
+ const content = queryProject();
447
+ assertEq(content, null, 'queryProject returns null when PROJECT.md not imported');
448
+
449
+ closeDatabase();
450
+ }
451
+
452
+ console.log('\n=== context-store: queryProject returns null when DB unavailable ===');
453
+ {
454
+ closeDatabase();
455
+ assertTrue(!isDbAvailable(), 'DB should not be available');
456
+
457
+ const content = queryProject();
458
+ assertEq(content, null, 'queryProject returns null when DB closed');
459
+ }
460
+
461
+ // ─── Final Report ──────────────────────────────────────────────────────────
462
+ report();
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Tests for the continue-here context-pressure monitor.
3
+ *
4
+ * Verifies:
5
+ * - Threshold comparison: fires when percent >= continueThresholdPercent
6
+ * - Null/undefined safety: no fire on missing or null context usage
7
+ * - One-shot guard: fires exactly once even if percent stays high
8
+ * - Cleanup: interval is cleared after fire and in clearUnitTimeout()
9
+ * - End-to-end pipeline: different model sizes produce correct budgets
10
+ */
11
+
12
+ import { describe, it } from "node:test";
13
+ import assert from "node:assert/strict";
14
+
15
+ import { computeBudgets } from "../context-budget.js";
16
+
17
+ // ─── Pure threshold / pipeline tests ──────────────────────────────────────────
18
+ // These test the budget engine outputs that the continue-here monitor relies on.
19
+
20
+ describe("continue-here", () => {
21
+ describe("threshold comparison", () => {
22
+ it("fires when percent >= continueThresholdPercent (70%)", () => {
23
+ const budget = computeBudgets(128_000);
24
+ const threshold = budget.continueThresholdPercent;
25
+ assert.equal(threshold, 70);
26
+
27
+ // Simulate check: 70% should fire
28
+ assert.ok(70 >= threshold, "exactly at threshold should fire");
29
+ // 71% should fire
30
+ assert.ok(71 >= threshold, "above threshold should fire");
31
+ // 100% should fire
32
+ assert.ok(100 >= threshold, "at maximum should fire");
33
+ });
34
+
35
+ it("does not fire below continueThresholdPercent", () => {
36
+ const budget = computeBudgets(128_000);
37
+ const threshold = budget.continueThresholdPercent;
38
+
39
+ // 69% should not fire
40
+ assert.ok(69 < threshold, "below threshold should not fire");
41
+ // 0% should not fire
42
+ assert.ok(0 < threshold, "zero usage should not fire");
43
+ // 50% should not fire
44
+ assert.ok(50 < threshold, "half usage should not fire");
45
+ });
46
+ });
47
+
48
+ describe("null/undefined safety", () => {
49
+ it("no fire when getContextUsage returns undefined", () => {
50
+ const budget = computeBudgets(128_000);
51
+ const threshold = budget.continueThresholdPercent;
52
+
53
+ // Simulate the guard: usage is undefined → skip
54
+ const usage = undefined as { percent: number | null } | undefined;
55
+ const shouldFire = usage != null && usage.percent != null && usage.percent >= threshold;
56
+ assert.equal(shouldFire, false, "undefined usage must not fire");
57
+ });
58
+
59
+ it("no fire when percent is null", () => {
60
+ const budget = computeBudgets(128_000);
61
+ const threshold = budget.continueThresholdPercent;
62
+
63
+ // Simulate the guard: percent is null → skip
64
+ const usage: { percent: number | null } | undefined = { percent: null };
65
+ const shouldFire = usage != null && usage.percent != null && usage.percent >= threshold;
66
+ assert.equal(shouldFire, false, "null percent must not fire");
67
+ });
68
+ });
69
+
70
+ describe("one-shot guard", () => {
71
+ it("fires exactly once even when percent stays above threshold", () => {
72
+ const budget = computeBudgets(128_000);
73
+ const threshold = budget.continueThresholdPercent;
74
+
75
+ // Simulate repeated polls with percent above threshold
76
+ let fired = false;
77
+ let fireCount = 0;
78
+ const usagePercents = [75, 80, 85, 90, 95];
79
+
80
+ for (const percent of usagePercents) {
81
+ if (fired) continue; // one-shot guard
82
+ if (percent >= threshold) {
83
+ fired = true;
84
+ fireCount++;
85
+ }
86
+ }
87
+
88
+ assert.equal(fireCount, 1, "must fire exactly once");
89
+ assert.equal(fired, true);
90
+ });
91
+ });
92
+
93
+ describe("end-to-end pipeline across model sizes", () => {
94
+ const modelSizes = [
95
+ { name: "128K", contextWindow: 128_000 },
96
+ { name: "200K", contextWindow: 200_000 },
97
+ { name: "1M", contextWindow: 1_000_000 },
98
+ ];
99
+
100
+ it("all model sizes produce continueThresholdPercent of 70", () => {
101
+ for (const { name, contextWindow } of modelSizes) {
102
+ const budget = computeBudgets(contextWindow);
103
+ assert.equal(
104
+ budget.continueThresholdPercent,
105
+ 70,
106
+ `${name} model should have 70% threshold`,
107
+ );
108
+ }
109
+ });
110
+
111
+ it("larger models produce larger verificationBudgetChars", () => {
112
+ const budgets = modelSizes.map(({ contextWindow }) => computeBudgets(contextWindow));
113
+
114
+ // 128K < 200K < 1M
115
+ assert.ok(
116
+ budgets[0].verificationBudgetChars < budgets[1].verificationBudgetChars,
117
+ "128K verification budget should be smaller than 200K",
118
+ );
119
+ assert.ok(
120
+ budgets[1].verificationBudgetChars < budgets[2].verificationBudgetChars,
121
+ "200K verification budget should be smaller than 1M",
122
+ );
123
+ });
124
+
125
+ it("larger models produce larger inlineContextBudgetChars", () => {
126
+ const budgets = modelSizes.map(({ contextWindow }) => computeBudgets(contextWindow));
127
+
128
+ assert.ok(
129
+ budgets[0].inlineContextBudgetChars < budgets[1].inlineContextBudgetChars,
130
+ "128K inline budget should be smaller than 200K",
131
+ );
132
+ assert.ok(
133
+ budgets[1].inlineContextBudgetChars < budgets[2].inlineContextBudgetChars,
134
+ "200K inline budget should be smaller than 1M",
135
+ );
136
+ });
137
+
138
+ it("task count range scales with context window", () => {
139
+ const b128 = computeBudgets(128_000);
140
+ const b200 = computeBudgets(200_000);
141
+ const b1m = computeBudgets(1_000_000);
142
+
143
+ // All have min=2
144
+ assert.equal(b128.taskCountRange.min, 2);
145
+ assert.equal(b200.taskCountRange.min, 2);
146
+ assert.equal(b1m.taskCountRange.min, 2);
147
+
148
+ // Max tasks scale: 128K→5, 200K→6, 1M→8
149
+ assert.equal(b128.taskCountRange.max, 5, "128K max tasks");
150
+ assert.equal(b200.taskCountRange.max, 6, "200K max tasks");
151
+ assert.equal(b1m.taskCountRange.max, 8, "1M max tasks");
152
+ });
153
+
154
+ it("produces deterministic verificationBudgetChars values", () => {
155
+ // 128K: 128000 * 4 * 0.10 = 51200
156
+ assert.equal(computeBudgets(128_000).verificationBudgetChars, 51_200);
157
+ // 200K: 200000 * 4 * 0.10 = 80000
158
+ assert.equal(computeBudgets(200_000).verificationBudgetChars, 80_000);
159
+ // 1M: 1000000 * 4 * 0.10 = 400000
160
+ assert.equal(computeBudgets(1_000_000).verificationBudgetChars, 400_000);
161
+ });
162
+ });
163
+
164
+ describe("continueHereFired runtime record field", () => {
165
+ it("AutoUnitRuntimeRecord includes continueHereFired with default false", async () => {
166
+ // Import writeUnitRuntimeRecord to verify the field is present and defaults
167
+ const { writeUnitRuntimeRecord, readUnitRuntimeRecord, clearUnitRuntimeRecord } = await import("../unit-runtime.js");
168
+ const fs = await import("node:fs");
169
+ const path = await import("node:path");
170
+ const os = await import("node:os");
171
+
172
+ // Use a temp directory as basePath
173
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "continue-here-test-"));
174
+ try {
175
+ const record = writeUnitRuntimeRecord(tmpDir, "execute-task", "M007/S02/T02", Date.now(), {
176
+ phase: "dispatched",
177
+ wrapupWarningSent: false,
178
+ });
179
+
180
+ assert.equal(record.continueHereFired, false, "default continueHereFired should be false");
181
+
182
+ // Verify it persists to disk
183
+ const read = readUnitRuntimeRecord(tmpDir, "execute-task", "M007/S02/T02");
184
+ assert.ok(read, "record should be readable");
185
+ assert.equal(read!.continueHereFired, false);
186
+
187
+ // Update to true
188
+ const updated = writeUnitRuntimeRecord(tmpDir, "execute-task", "M007/S02/T02", Date.now(), {
189
+ continueHereFired: true,
190
+ });
191
+ assert.equal(updated.continueHereFired, true, "updated continueHereFired should be true");
192
+
193
+ // Verify persistence
194
+ const readUpdated = readUnitRuntimeRecord(tmpDir, "execute-task", "M007/S02/T02");
195
+ assert.equal(readUpdated!.continueHereFired, true, "persisted continueHereFired should be true");
196
+
197
+ // Clean up
198
+ clearUnitRuntimeRecord(tmpDir, "execute-task", "M007/S02/T02");
199
+ } finally {
200
+ fs.rmSync(tmpDir, { recursive: true, force: true });
201
+ }
202
+ });
203
+ });
204
+ });