pi-usereq 0.4.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 (361) hide show
  1. package/.g.conf +20 -0
  2. package/.github/workflows/release-npm.yml +131 -0
  3. package/.gitignore +270 -0
  4. package/CHANGELOG.md +221 -0
  5. package/LICENSE +674 -0
  6. package/README.md +260 -0
  7. package/TODO.md +20 -0
  8. package/docs/pi.dev/agent-document-manifest.json +6399 -0
  9. package/docs/pi.dev/coding-agent-docs/compaction.md +394 -0
  10. package/docs/pi.dev/coding-agent-docs/custom-provider.md +596 -0
  11. package/docs/pi.dev/coding-agent-docs/development.md +71 -0
  12. package/docs/pi.dev/coding-agent-docs/extensions.md +2262 -0
  13. package/docs/pi.dev/coding-agent-docs/images/doom-extension.png +0 -0
  14. package/docs/pi.dev/coding-agent-docs/images/exy.png +0 -0
  15. package/docs/pi.dev/coding-agent-docs/images/interactive-mode.png +0 -0
  16. package/docs/pi.dev/coding-agent-docs/images/tree-view.png +0 -0
  17. package/docs/pi.dev/coding-agent-docs/json.md +82 -0
  18. package/docs/pi.dev/coding-agent-docs/keybindings.md +175 -0
  19. package/docs/pi.dev/coding-agent-docs/models.md +392 -0
  20. package/docs/pi.dev/coding-agent-docs/packages.md +218 -0
  21. package/docs/pi.dev/coding-agent-docs/prompt-templates.md +67 -0
  22. package/docs/pi.dev/coding-agent-docs/providers.md +195 -0
  23. package/docs/pi.dev/coding-agent-docs/rpc.md +1377 -0
  24. package/docs/pi.dev/coding-agent-docs/sdk.md +1124 -0
  25. package/docs/pi.dev/coding-agent-docs/session.md +412 -0
  26. package/docs/pi.dev/coding-agent-docs/settings.md +247 -0
  27. package/docs/pi.dev/coding-agent-docs/shell-aliases.md +13 -0
  28. package/docs/pi.dev/coding-agent-docs/skills.md +232 -0
  29. package/docs/pi.dev/coding-agent-docs/terminal-setup.md +106 -0
  30. package/docs/pi.dev/coding-agent-docs/termux.md +127 -0
  31. package/docs/pi.dev/coding-agent-docs/themes.md +295 -0
  32. package/docs/pi.dev/coding-agent-docs/tmux.md +61 -0
  33. package/docs/pi.dev/coding-agent-docs/tree.md +231 -0
  34. package/docs/pi.dev/coding-agent-docs/tui.md +887 -0
  35. package/docs/pi.dev/coding-agent-docs/windows.md +17 -0
  36. package/docs/pi.dev/mom-docs/artifacts-server.md +475 -0
  37. package/docs/pi.dev/mom-docs/events.md +307 -0
  38. package/docs/pi.dev/mom-docs/new.md +970 -0
  39. package/docs/pi.dev/mom-docs/sandbox.md +153 -0
  40. package/docs/pi.dev/mom-docs/slack-bot-minimal-guide.md +399 -0
  41. package/docs/pi.dev/mom-docs/v86.md +319 -0
  42. package/docs/pi.dev/pods-docs/gml-4.5.md +189 -0
  43. package/docs/pi.dev/pods-docs/gpt-oss.md +233 -0
  44. package/docs/pi.dev/pods-docs/implementation-plan.md +183 -0
  45. package/docs/pi.dev/pods-docs/kimi-k2.md +197 -0
  46. package/docs/pi.dev/pods-docs/models.md +116 -0
  47. package/docs/pi.dev/pods-docs/plan.md +166 -0
  48. package/docs/pi.dev/pods-docs/qwen3-coder.md +132 -0
  49. package/images/flowchart-bw.png +0 -0
  50. package/images/flowchart-bw.svg +102 -0
  51. package/images/flowchart.md +100 -0
  52. package/images/flowchart.png +0 -0
  53. package/images/flowchart.svg +3 -0
  54. package/package.json +46 -0
  55. package/req/docs/REFERENCES.md +4554 -0
  56. package/req/docs/REQUIREMENTS.md +475 -0
  57. package/req/docs/WORKFLOW.md +1059 -0
  58. package/scripts/debug-extension.ts +497 -0
  59. package/scripts/lib/extension-debug-harness.ts +450 -0
  60. package/scripts/lib/recording-extension-api.ts +786 -0
  61. package/scripts/lib/sdk-smoke.ts +503 -0
  62. package/scripts/pi-usereq-debug.sh +330 -0
  63. package/scripts/tool-args-to-params.ts +208 -0
  64. package/src/cli.ts +349 -0
  65. package/src/core/agent-tool-json.ts +621 -0
  66. package/src/core/compress-files.ts +72 -0
  67. package/src/core/compress-payload.ts +648 -0
  68. package/src/core/compress.ts +464 -0
  69. package/src/core/config.ts +272 -0
  70. package/src/core/doxygen-parser.ts +318 -0
  71. package/src/core/errors.ts +31 -0
  72. package/src/core/extension-status.ts +660 -0
  73. package/src/core/find-constructs.ts +319 -0
  74. package/src/core/find-payload.ts +915 -0
  75. package/src/core/generate-markdown.ts +120 -0
  76. package/src/core/path-context.ts +196 -0
  77. package/src/core/pi-notify.ts +430 -0
  78. package/src/core/pi-usereq-tools.ts +140 -0
  79. package/src/core/prompts.ts +184 -0
  80. package/src/core/reference-payload.ts +818 -0
  81. package/src/core/resources.ts +63 -0
  82. package/src/core/runtime-project-paths.ts +99 -0
  83. package/src/core/settings-menu.ts +233 -0
  84. package/src/core/source-analyzer.ts +1721 -0
  85. package/src/core/static-check.ts +674 -0
  86. package/src/core/token-counter.ts +729 -0
  87. package/src/core/tool-runner.ts +717 -0
  88. package/src/core/utils.ts +185 -0
  89. package/src/index.ts +2209 -0
  90. package/src/resources/guidelines/Google_C++_Style_Guide.md +3711 -0
  91. package/src/resources/guidelines/Google_Python_Style_Guide.md +3709 -0
  92. package/src/resources/prompts/analyze.md +130 -0
  93. package/src/resources/prompts/change.md +227 -0
  94. package/src/resources/prompts/check.md +139 -0
  95. package/src/resources/prompts/cover.md +219 -0
  96. package/src/resources/prompts/create.md +104 -0
  97. package/src/resources/prompts/fix.md +221 -0
  98. package/src/resources/prompts/flowchart.md +220 -0
  99. package/src/resources/prompts/implement.md +163 -0
  100. package/src/resources/prompts/new.md +226 -0
  101. package/src/resources/prompts/readme.md +182 -0
  102. package/src/resources/prompts/recreate.md +213 -0
  103. package/src/resources/prompts/refactor.md +213 -0
  104. package/src/resources/prompts/references.md +100 -0
  105. package/src/resources/prompts/renumber.md +119 -0
  106. package/src/resources/prompts/workflow.md +202 -0
  107. package/src/resources/prompts/write.md +99 -0
  108. package/src/resources/sounds/Machine-alert-beep-sound-effect.mp3 +0 -0
  109. package/src/resources/sounds/Soft-high-tech-notification-sound-effect.mp3 +0 -0
  110. package/src/resources/templates/Document_Source_Code_in_Doxygen_Style.md +130 -0
  111. package/src/resources/templates/HDT_Test_Authoring_Guide.md +318 -0
  112. package/src/resources/templates/Requirements_Template.md +78 -0
  113. package/tests/attended-results-scenarios.ts +758 -0
  114. package/tests/attended-results.test.ts +39 -0
  115. package/tests/cli-command-option-parity.test.ts +815 -0
  116. package/tests/debug-extension-harness.test.ts +463 -0
  117. package/tests/extension-registration.test.ts +2006 -0
  118. package/tests/fixtures/fixture_c.c +361 -0
  119. package/tests/fixtures/fixture_cpp.cpp +407 -0
  120. package/tests/fixtures/fixture_csharp.cs +411 -0
  121. package/tests/fixtures/fixture_elixir.ex +409 -0
  122. package/tests/fixtures/fixture_go.go +341 -0
  123. package/tests/fixtures/fixture_haskell.hs +250 -0
  124. package/tests/fixtures/fixture_java.java +430 -0
  125. package/tests/fixtures/fixture_javascript.js +383 -0
  126. package/tests/fixtures/fixture_kotlin.kt +451 -0
  127. package/tests/fixtures/fixture_lua.lua +276 -0
  128. package/tests/fixtures/fixture_perl.pl +310 -0
  129. package/tests/fixtures/fixture_php.php +433 -0
  130. package/tests/fixtures/fixture_python.py +502 -0
  131. package/tests/fixtures/fixture_ruby.rb +345 -0
  132. package/tests/fixtures/fixture_rust.rs +380 -0
  133. package/tests/fixtures/fixture_scala.scala +398 -0
  134. package/tests/fixtures/fixture_shell.sh +276 -0
  135. package/tests/fixtures/fixture_swift.swift +397 -0
  136. package/tests/fixtures/fixture_typescript.ts +434 -0
  137. package/tests/fixtures/fixture_zig.zig +295 -0
  138. package/tests/fixtures_attended_results/project/compress-line-numbers.json +5 -0
  139. package/tests/fixtures_attended_results/project/compress.json +5 -0
  140. package/tests/fixtures_attended_results/project/enable-static-check-invalid-command.json +5 -0
  141. package/tests/fixtures_attended_results/project/enable-static-check-valid.json +5 -0
  142. package/tests/fixtures_attended_results/project/files-static-check.json +5 -0
  143. package/tests/fixtures_attended_results/project/find-line-numbers.json +5 -0
  144. package/tests/fixtures_attended_results/project/find.json +5 -0
  145. package/tests/fixtures_attended_results/project/get-base-path.json +5 -0
  146. package/tests/fixtures_attended_results/project/git-check-clean.json +5 -0
  147. package/tests/fixtures_attended_results/project/git-check-dirty.json +5 -0
  148. package/tests/fixtures_attended_results/project/git-path.json +5 -0
  149. package/tests/fixtures_attended_results/project/git-wt-create-invalid.json +5 -0
  150. package/tests/fixtures_attended_results/project/git-wt-create-valid.json +5 -0
  151. package/tests/fixtures_attended_results/project/git-wt-delete-nonexistent.json +5 -0
  152. package/tests/fixtures_attended_results/project/git-wt-delete-valid.json +5 -0
  153. package/tests/fixtures_attended_results/project/git-wt-name.json +5 -0
  154. package/tests/fixtures_attended_results/project/references.json +5 -0
  155. package/tests/fixtures_attended_results/project/static-check.json +5 -0
  156. package/tests/fixtures_attended_results/project/tokens.json +5 -0
  157. package/tests/fixtures_attended_results/standalone/files-compress/fixture_c.c.json +5 -0
  158. package/tests/fixtures_attended_results/standalone/files-compress/fixture_cpp.cpp.json +5 -0
  159. package/tests/fixtures_attended_results/standalone/files-compress/fixture_csharp.cs.json +5 -0
  160. package/tests/fixtures_attended_results/standalone/files-compress/fixture_elixir.ex.json +5 -0
  161. package/tests/fixtures_attended_results/standalone/files-compress/fixture_go.go.json +5 -0
  162. package/tests/fixtures_attended_results/standalone/files-compress/fixture_haskell.hs.json +5 -0
  163. package/tests/fixtures_attended_results/standalone/files-compress/fixture_java.java.json +5 -0
  164. package/tests/fixtures_attended_results/standalone/files-compress/fixture_javascript.js.json +5 -0
  165. package/tests/fixtures_attended_results/standalone/files-compress/fixture_kotlin.kt.json +5 -0
  166. package/tests/fixtures_attended_results/standalone/files-compress/fixture_lua.lua.json +5 -0
  167. package/tests/fixtures_attended_results/standalone/files-compress/fixture_perl.pl.json +5 -0
  168. package/tests/fixtures_attended_results/standalone/files-compress/fixture_php.php.json +5 -0
  169. package/tests/fixtures_attended_results/standalone/files-compress/fixture_python.py.json +5 -0
  170. package/tests/fixtures_attended_results/standalone/files-compress/fixture_ruby.rb.json +5 -0
  171. package/tests/fixtures_attended_results/standalone/files-compress/fixture_rust.rs.json +5 -0
  172. package/tests/fixtures_attended_results/standalone/files-compress/fixture_scala.scala.json +5 -0
  173. package/tests/fixtures_attended_results/standalone/files-compress/fixture_shell.sh.json +5 -0
  174. package/tests/fixtures_attended_results/standalone/files-compress/fixture_swift.swift.json +5 -0
  175. package/tests/fixtures_attended_results/standalone/files-compress/fixture_typescript.ts.json +5 -0
  176. package/tests/fixtures_attended_results/standalone/files-compress/fixture_zig.zig.json +5 -0
  177. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_c.c.json +5 -0
  178. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_cpp.cpp.json +5 -0
  179. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_csharp.cs.json +5 -0
  180. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_elixir.ex.json +5 -0
  181. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_go.go.json +5 -0
  182. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_haskell.hs.json +5 -0
  183. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_java.java.json +5 -0
  184. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_javascript.js.json +5 -0
  185. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_kotlin.kt.json +5 -0
  186. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_lua.lua.json +5 -0
  187. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_perl.pl.json +5 -0
  188. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_php.php.json +5 -0
  189. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_python.py.json +5 -0
  190. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_ruby.rb.json +5 -0
  191. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_rust.rs.json +5 -0
  192. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_scala.scala.json +5 -0
  193. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_shell.sh.json +5 -0
  194. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_swift.swift.json +5 -0
  195. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_typescript.ts.json +5 -0
  196. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_zig.zig.json +5 -0
  197. package/tests/fixtures_attended_results/standalone/files-find/fixture_c.c.json +5 -0
  198. package/tests/fixtures_attended_results/standalone/files-find/fixture_cpp.cpp.json +5 -0
  199. package/tests/fixtures_attended_results/standalone/files-find/fixture_csharp.cs.json +5 -0
  200. package/tests/fixtures_attended_results/standalone/files-find/fixture_elixir.ex.json +5 -0
  201. package/tests/fixtures_attended_results/standalone/files-find/fixture_go.go.json +5 -0
  202. package/tests/fixtures_attended_results/standalone/files-find/fixture_haskell.hs.json +5 -0
  203. package/tests/fixtures_attended_results/standalone/files-find/fixture_java.java.json +5 -0
  204. package/tests/fixtures_attended_results/standalone/files-find/fixture_javascript.js.json +5 -0
  205. package/tests/fixtures_attended_results/standalone/files-find/fixture_kotlin.kt.json +5 -0
  206. package/tests/fixtures_attended_results/standalone/files-find/fixture_lua.lua.json +5 -0
  207. package/tests/fixtures_attended_results/standalone/files-find/fixture_perl.pl.json +5 -0
  208. package/tests/fixtures_attended_results/standalone/files-find/fixture_php.php.json +5 -0
  209. package/tests/fixtures_attended_results/standalone/files-find/fixture_python.py.json +5 -0
  210. package/tests/fixtures_attended_results/standalone/files-find/fixture_ruby.rb.json +5 -0
  211. package/tests/fixtures_attended_results/standalone/files-find/fixture_rust.rs.json +5 -0
  212. package/tests/fixtures_attended_results/standalone/files-find/fixture_scala.scala.json +5 -0
  213. package/tests/fixtures_attended_results/standalone/files-find/fixture_shell.sh.json +5 -0
  214. package/tests/fixtures_attended_results/standalone/files-find/fixture_swift.swift.json +5 -0
  215. package/tests/fixtures_attended_results/standalone/files-find/fixture_typescript.ts.json +5 -0
  216. package/tests/fixtures_attended_results/standalone/files-find/fixture_zig.zig.json +5 -0
  217. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_c.c.json +5 -0
  218. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_cpp.cpp.json +5 -0
  219. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_csharp.cs.json +5 -0
  220. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_elixir.ex.json +5 -0
  221. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_go.go.json +5 -0
  222. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_haskell.hs.json +5 -0
  223. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_java.java.json +5 -0
  224. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_javascript.js.json +5 -0
  225. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_kotlin.kt.json +5 -0
  226. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_lua.lua.json +5 -0
  227. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_perl.pl.json +5 -0
  228. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_php.php.json +5 -0
  229. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_python.py.json +5 -0
  230. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_ruby.rb.json +5 -0
  231. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_rust.rs.json +5 -0
  232. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_scala.scala.json +5 -0
  233. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_shell.sh.json +5 -0
  234. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_swift.swift.json +5 -0
  235. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_typescript.ts.json +5 -0
  236. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_zig.zig.json +5 -0
  237. package/tests/fixtures_attended_results/standalone/files-references/fixture_c.c.json +5 -0
  238. package/tests/fixtures_attended_results/standalone/files-references/fixture_cpp.cpp.json +5 -0
  239. package/tests/fixtures_attended_results/standalone/files-references/fixture_csharp.cs.json +5 -0
  240. package/tests/fixtures_attended_results/standalone/files-references/fixture_elixir.ex.json +5 -0
  241. package/tests/fixtures_attended_results/standalone/files-references/fixture_go.go.json +5 -0
  242. package/tests/fixtures_attended_results/standalone/files-references/fixture_haskell.hs.json +5 -0
  243. package/tests/fixtures_attended_results/standalone/files-references/fixture_java.java.json +5 -0
  244. package/tests/fixtures_attended_results/standalone/files-references/fixture_javascript.js.json +5 -0
  245. package/tests/fixtures_attended_results/standalone/files-references/fixture_kotlin.kt.json +5 -0
  246. package/tests/fixtures_attended_results/standalone/files-references/fixture_lua.lua.json +5 -0
  247. package/tests/fixtures_attended_results/standalone/files-references/fixture_perl.pl.json +5 -0
  248. package/tests/fixtures_attended_results/standalone/files-references/fixture_php.php.json +5 -0
  249. package/tests/fixtures_attended_results/standalone/files-references/fixture_python.py.json +5 -0
  250. package/tests/fixtures_attended_results/standalone/files-references/fixture_ruby.rb.json +5 -0
  251. package/tests/fixtures_attended_results/standalone/files-references/fixture_rust.rs.json +5 -0
  252. package/tests/fixtures_attended_results/standalone/files-references/fixture_scala.scala.json +5 -0
  253. package/tests/fixtures_attended_results/standalone/files-references/fixture_shell.sh.json +5 -0
  254. package/tests/fixtures_attended_results/standalone/files-references/fixture_swift.swift.json +5 -0
  255. package/tests/fixtures_attended_results/standalone/files-references/fixture_typescript.ts.json +5 -0
  256. package/tests/fixtures_attended_results/standalone/files-references/fixture_zig.zig.json +5 -0
  257. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_c.c.json +5 -0
  258. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_cpp.cpp.json +5 -0
  259. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_csharp.cs.json +5 -0
  260. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_elixir.ex.json +5 -0
  261. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_go.go.json +5 -0
  262. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_haskell.hs.json +5 -0
  263. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_java.java.json +5 -0
  264. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_javascript.js.json +5 -0
  265. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_kotlin.kt.json +5 -0
  266. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_lua.lua.json +5 -0
  267. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_perl.pl.json +5 -0
  268. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_php.php.json +5 -0
  269. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_python.py.json +5 -0
  270. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_ruby.rb.json +5 -0
  271. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_rust.rs.json +5 -0
  272. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_scala.scala.json +5 -0
  273. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_shell.sh.json +5 -0
  274. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_swift.swift.json +5 -0
  275. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_typescript.ts.json +5 -0
  276. package/tests/fixtures_attended_results/standalone/files-tokens/fixture_zig.zig.json +5 -0
  277. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_c.c.json +5 -0
  278. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_cpp.cpp.json +5 -0
  279. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_csharp.cs.json +5 -0
  280. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_elixir.ex.json +5 -0
  281. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_go.go.json +5 -0
  282. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_haskell.hs.json +5 -0
  283. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_java.java.json +5 -0
  284. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_javascript.js.json +5 -0
  285. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_kotlin.kt.json +5 -0
  286. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_lua.lua.json +5 -0
  287. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_perl.pl.json +5 -0
  288. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_php.php.json +5 -0
  289. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_python.py.json +5 -0
  290. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_ruby.rb.json +5 -0
  291. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_rust.rs.json +5 -0
  292. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_scala.scala.json +5 -0
  293. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_shell.sh.json +5 -0
  294. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_swift.swift.json +5 -0
  295. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_typescript.ts.json +5 -0
  296. package/tests/fixtures_attended_results/standalone/test-static-check-command/fixture_zig.zig.json +5 -0
  297. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_c.c.json +5 -0
  298. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_cpp.cpp.json +5 -0
  299. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_csharp.cs.json +5 -0
  300. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_elixir.ex.json +5 -0
  301. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_go.go.json +5 -0
  302. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_haskell.hs.json +5 -0
  303. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_java.java.json +5 -0
  304. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_javascript.js.json +5 -0
  305. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_kotlin.kt.json +5 -0
  306. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_lua.lua.json +5 -0
  307. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_perl.pl.json +5 -0
  308. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_php.php.json +5 -0
  309. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_python.py.json +5 -0
  310. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_ruby.rb.json +5 -0
  311. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_rust.rs.json +5 -0
  312. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_scala.scala.json +5 -0
  313. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_shell.sh.json +5 -0
  314. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_swift.swift.json +5 -0
  315. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_typescript.ts.json +5 -0
  316. package/tests/fixtures_attended_results/standalone/test-static-check-dummy/fixture_zig.zig.json +5 -0
  317. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_c.c.json +5 -0
  318. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_cpp.cpp.json +5 -0
  319. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_csharp.cs.json +5 -0
  320. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_elixir.ex.json +5 -0
  321. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_go.go.json +5 -0
  322. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_haskell.hs.json +5 -0
  323. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_java.java.json +5 -0
  324. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_javascript.js.json +5 -0
  325. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_kotlin.kt.json +5 -0
  326. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_lua.lua.json +5 -0
  327. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_perl.pl.json +5 -0
  328. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_php.php.json +5 -0
  329. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_python.py.json +5 -0
  330. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_ruby.rb.json +5 -0
  331. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_rust.rs.json +5 -0
  332. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_scala.scala.json +5 -0
  333. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_shell.sh.json +5 -0
  334. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_swift.swift.json +5 -0
  335. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_typescript.ts.json +5 -0
  336. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_zig.zig.json +5 -0
  337. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_c.c.json +5 -0
  338. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_cpp.cpp.json +5 -0
  339. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_csharp.cs.json +5 -0
  340. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_elixir.ex.json +5 -0
  341. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_go.go.json +5 -0
  342. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_haskell.hs.json +5 -0
  343. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_java.java.json +5 -0
  344. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_javascript.js.json +5 -0
  345. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_kotlin.kt.json +5 -0
  346. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_lua.lua.json +5 -0
  347. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_perl.pl.json +5 -0
  348. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_php.php.json +5 -0
  349. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_python.py.json +5 -0
  350. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_ruby.rb.json +5 -0
  351. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_rust.rs.json +5 -0
  352. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_scala.scala.json +5 -0
  353. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_shell.sh.json +5 -0
  354. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_swift.swift.json +5 -0
  355. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_typescript.ts.json +5 -0
  356. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_zig.zig.json +5 -0
  357. package/tests/helpers.ts +204 -0
  358. package/tests/oracle-project.test.ts +63 -0
  359. package/tests/oracle-standalone.test.ts +48 -0
  360. package/tests/prompt-rendering.test.ts +66 -0
  361. package/tests/release-workflow.test.ts +133 -0
@@ -0,0 +1,2006 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import piUsereqExtension from "../src/index.ts";
7
+ import { DEFAULT_DOCS_DIR, getProjectConfigPath } from "../src/core/config.js";
8
+ import {
9
+ PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES,
10
+ PI_USEREQ_EMBEDDED_TOOL_NAMES,
11
+ PI_USEREQ_STARTUP_TOOL_NAMES,
12
+ } from "../src/core/pi-usereq-tools.js";
13
+ import { getSupportedStaticCheckLanguageSupport } from "../src/core/static-check.js";
14
+ import { createTempDir, initFixtureRepo } from "./helpers.js";
15
+
16
+ /**
17
+ * @brief Describes one fake extension command registration captured during tests.
18
+ * @details Mirrors the minimal command shape consumed by this suite so assertions can inspect descriptions and invoke handlers deterministically. The alias is compile-time only and introduces no runtime side effects.
19
+ */
20
+ type RegisteredCommand = { description?: string; handler: (args: string, ctx: any) => Promise<void> | void };
21
+
22
+ /**
23
+ * @brief Describes one fake extension tool registration captured during tests.
24
+ * @details Mirrors the minimal custom-tool definition surface used by the suite, including prompt metadata, parameter schemas, and the optional execute callback required for direct tool invocation assertions. The alias is compile-time only and introduces no runtime side effects.
25
+ */
26
+ type RegisteredTool = {
27
+ name: string;
28
+ label?: string;
29
+ description?: string;
30
+ promptSnippet?: string;
31
+ promptGuidelines?: string[];
32
+ parameters?: unknown;
33
+ sourceInfo?: Record<string, unknown>;
34
+ execute?: (toolCallId: string, params: any, signal?: AbortSignal, onUpdate?: unknown, ctx?: any) => Promise<any> | any;
35
+ };
36
+
37
+ /**
38
+ * @brief Describes one fake shortcut registration captured during tests.
39
+ * @details Mirrors the minimal shortcut-registration shape exercised by the
40
+ * suite so tests can trigger the configured sound toggle without a live TUI.
41
+ * The alias is compile-time only and introduces no runtime side effects.
42
+ */
43
+ type RegisteredShortcut = { description?: string; handler: (ctx: any) => Promise<void> | void };
44
+
45
+ /**
46
+ * @brief Represents the fake pi runtime returned by `createFakePi`.
47
+ * @details Captures registered commands, tools, shortcuts, event handlers, active-tool state, and prompt-delivery payloads for deterministic extension-registration assertions. The alias is compile-time only and introduces no runtime side effects.
48
+ */
49
+ type FakePi = ReturnType<typeof createFakePi>;
50
+
51
+ /**
52
+ * @brief Describes the scripted UI responses consumed by `createFakeCtx`.
53
+ * @details Supplies queued select and input values so interactive command tests remain deterministic and free from real TUI dependencies. The alias is compile-time only and introduces no runtime side effects.
54
+ */
55
+ type FakeCtxPlan = { selects: string[]; inputs?: string[] };
56
+
57
+ /**
58
+ * @brief Describes one fake session entry stored by session-aware tests.
59
+ * @details Mirrors the minimal subset of session-manager entry shapes consumed by the suite so setup messages and custom entries remain inspectable across simulated runtime interactions. The alias is compile-time only and introduces no runtime side effects.
60
+ */
61
+ type FakeSessionEntry =
62
+ | { id: string; type: "custom"; customType: string; data?: unknown }
63
+ | { id: string; type: "message"; message: unknown };
64
+
65
+ /**
66
+ * @brief Describes the fake session-manager surface consumed by session-aware tests.
67
+ * @details Exposes only the mutation and read APIs required by `ctx.newSession(...setup)`, `ctx.sessionManager`, and `pi.appendEntry(...)`. The interface is compile-time only and introduces no runtime cost.
68
+ */
69
+ interface FakeSessionManager {
70
+ appendCustomEntry: (customType: string, data?: unknown) => string;
71
+ appendMessage: (message: unknown) => string;
72
+ getBranch: () => FakeSessionEntry[];
73
+ getEntries: () => FakeSessionEntry[];
74
+ }
75
+
76
+ /**
77
+ * @brief Describes optional fake pi runtime capabilities used by specific tests.
78
+ * @details Allows tests to connect extension-side session-entry writes to a shared fake session manager without affecting call sites that only require command and tool registration capture. The alias is compile-time only and introduces no runtime side effects.
79
+ */
80
+ type FakePiOptions = { sessionManager?: FakeSessionManager };
81
+
82
+ /**
83
+ * @brief Describes one fake context-usage snapshot returned by test doubles.
84
+ * @details Mirrors the subset of pi `ContextUsage` fields consumed by the
85
+ * extension status controller so tests can script deterministic token and
86
+ * percentage updates across lifecycle hooks. The alias is compile-time only and
87
+ * introduces no runtime side effects.
88
+ */
89
+ type FakeContextUsage = {
90
+ tokens: number | null;
91
+ contextWindow: number;
92
+ percent: number | null;
93
+ };
94
+
95
+ /**
96
+ * @brief Describes the fake theme surface used by test command contexts.
97
+ * @details Constrains tests to the subset of theme callbacks consumed by the
98
+ * extension and allows strict token-validation doubles for render-contract
99
+ * assertions. The alias is compile-time only and introduces no runtime side
100
+ * effects.
101
+ */
102
+ type FakeThemeAdapter = {
103
+ fg: (color: string, text: string) => string;
104
+ bgFromFg: (color: string, text: string) => string;
105
+ bold: (text: string) => string;
106
+ };
107
+
108
+ /**
109
+ * @brief Describes optional fake command-context capabilities used by session-aware tests.
110
+ * @details Allows tests to inject a shared fake session manager, a callback that simulates runtime replacement after `ctx.newSession(...)` completes, a scripted `getContextUsage()` provider for lifecycle-status assertions, and a custom theme adapter for render-contract validation. The alias is compile-time only and introduces no runtime side effects.
111
+ */
112
+ type FakeCtxOptions = {
113
+ sessionManager?: FakeSessionManager;
114
+ onNewSession?: (options?: { parentSession?: string }) => Promise<{ cancelled: boolean } | void> | { cancelled: boolean } | void;
115
+ getContextUsage?: () => FakeContextUsage | undefined;
116
+ theme?: FakeThemeAdapter;
117
+ };
118
+
119
+ /**
120
+ * @brief Extracts text content from a recorded session-manager user message.
121
+ * @details Collapses text-part arrays appended during fake `ctx.newSession(...setup)` execution into one comparable string. Non-text payloads yield an empty string. Runtime is O(n) in part count. No external state is mutated.
122
+ * @param[in] message {unknown} Session-manager message payload.
123
+ * @return {string} Collapsed text content.
124
+ */
125
+ function extractSessionMessageText(message: unknown): string {
126
+ if (!message || typeof message !== "object") {
127
+ return "";
128
+ }
129
+ const content = (message as { content?: unknown }).content;
130
+ if (!Array.isArray(content)) {
131
+ return "";
132
+ }
133
+ return content
134
+ .map((item) => (item && typeof item === "object" && typeof (item as { text?: unknown }).text === "string"
135
+ ? (item as { text: string }).text
136
+ : ""))
137
+ .join("");
138
+ }
139
+
140
+ /**
141
+ * @brief Creates a fake session manager for session-aware tests.
142
+ * @details Stores appended custom entries and setup messages in deterministic insertion order, returns stable synthetic entry identifiers, and exposes branch reads used by `session_start` handlers. Runtime is O(1) per mutation plus O(n) per snapshot copy. Side effects are limited to in-memory state mutation.
143
+ * @return {FakeSessionManager} Fake session manager.
144
+ */
145
+ function createFakeSessionManager(): FakeSessionManager {
146
+ const entries: FakeSessionEntry[] = [];
147
+ let nextEntryId = 0;
148
+ const buildEntryId = (): string => `entry-${nextEntryId++}`;
149
+ return {
150
+ appendCustomEntry(customType: string, data?: unknown): string {
151
+ const entryId = buildEntryId();
152
+ entries.push({ id: entryId, type: "custom", customType, data });
153
+ return entryId;
154
+ },
155
+ appendMessage(message: unknown): string {
156
+ const entryId = buildEntryId();
157
+ entries.push({ id: entryId, type: "message", message });
158
+ return entryId;
159
+ },
160
+ getBranch(): FakeSessionEntry[] {
161
+ return entries.map((entry) => ({ ...entry }));
162
+ },
163
+ getEntries(): FakeSessionEntry[] {
164
+ return entries.map((entry) => ({ ...entry }));
165
+ },
166
+ };
167
+ }
168
+
169
+ /**
170
+ * @brief Creates a fake pi runtime that records command, tool, and shortcut registrations.
171
+ * @details Provides deterministic in-memory implementations for the subset of `ExtensionAPI` methods exercised by this suite, including command registration, tool registration, builtin-tool discovery, shortcut registration, event dispatch, active-tool mutation, prompt delivery, and optional session-entry persistence. Runtime is O(1) per registration plus delegated handler cost. Side effects are limited to in-memory state mutation.
172
+ * @param[in] options {FakePiOptions} Optional fake runtime integrations.
173
+ * @return {FakePi} Fake extension runtime.
174
+ */
175
+ function createFakePi(options: FakePiOptions = {}) {
176
+ const commands = new Map<string, RegisteredCommand>();
177
+ const tools: RegisteredTool[] = [];
178
+ const shortcuts = new Map<string, RegisteredShortcut>();
179
+ const sentUserMessages: Array<{ content: unknown; options?: unknown }> = [];
180
+ const eventHandlers = new Map<string, Array<(event: any, ctx: any) => Promise<void> | void>>();
181
+ const embeddedToolNames = new Set<string>(PI_USEREQ_EMBEDDED_TOOL_NAMES);
182
+ const builtinTools: RegisteredTool[] = PI_USEREQ_EMBEDDED_TOOL_NAMES.map((name) => ({
183
+ name,
184
+ label: name,
185
+ description: `Built-in pi CLI tool ${name}.`,
186
+ sourceInfo: {
187
+ path: `<builtin:${name}>`,
188
+ source: "builtin",
189
+ scope: "temporary",
190
+ origin: "top-level",
191
+ },
192
+ }));
193
+ let activeTools = PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.filter((name) => embeddedToolNames.has(name));
194
+ const getVisibleTools = () => {
195
+ const overridden = new Set(tools.map((tool) => tool.name));
196
+ return [
197
+ ...builtinTools.filter((tool) => !overridden.has(tool.name)),
198
+ ...tools,
199
+ ].map((tool) => ({
200
+ ...tool,
201
+ sourceInfo: tool.sourceInfo ?? {
202
+ path: "<extension:pi-usereq>",
203
+ source: "extension",
204
+ scope: "project",
205
+ origin: "top-level",
206
+ },
207
+ }));
208
+ };
209
+ return {
210
+ commands,
211
+ tools,
212
+ shortcuts,
213
+ sentUserMessages,
214
+ eventHandlers,
215
+ getActiveTools() {
216
+ return [...activeTools];
217
+ },
218
+ getAllTools() {
219
+ return getVisibleTools();
220
+ },
221
+ setActiveTools(names: string[]) {
222
+ const available = new Set(getVisibleTools().map((tool) => tool.name));
223
+ activeTools = names.filter((name, index) => available.has(name) && names.indexOf(name) === index);
224
+ },
225
+ on(name: string, handler: (event: any, ctx: any) => Promise<void> | void) {
226
+ const handlers = eventHandlers.get(name) ?? [];
227
+ handlers.push(handler);
228
+ eventHandlers.set(name, handlers);
229
+ },
230
+ async emit(name: string, event: any, ctx: any) {
231
+ for (const handler of eventHandlers.get(name) ?? []) {
232
+ await handler(event, ctx);
233
+ }
234
+ },
235
+ registerCommand(name: string, options: RegisteredCommand) {
236
+ commands.set(name, options);
237
+ },
238
+ registerTool(definition: RegisteredTool) {
239
+ const existingIndex = tools.findIndex((tool) => tool.name === definition.name);
240
+ if (existingIndex >= 0) {
241
+ tools[existingIndex] = definition;
242
+ } else {
243
+ tools.push(definition);
244
+ }
245
+ if (!activeTools.includes(definition.name)) {
246
+ activeTools = [...activeTools, definition.name];
247
+ }
248
+ },
249
+ registerShortcut(shortcut: string, options: RegisteredShortcut) {
250
+ shortcuts.set(shortcut, options);
251
+ },
252
+ sendUserMessage(content: unknown, options?: unknown) {
253
+ sentUserMessages.push({ content, options });
254
+ },
255
+ appendEntry(customType: string, data?: unknown) {
256
+ return options.sessionManager?.appendCustomEntry(customType, data);
257
+ },
258
+ } as any;
259
+ }
260
+
261
+ /**
262
+ * @brief Encodes one fake themed fragment for deterministic status assertions.
263
+ * @details Wraps the requested color and text in stable XML-like markers so tests can validate color intent without terminal escape sequences. Runtime is O(n) in text length. No external state is mutated.
264
+ * @param[in] color {string} Requested theme color token.
265
+ * @param[in] text {string} Raw text payload.
266
+ * @return {string} Encoded themed fragment.
267
+ */
268
+ function formatFakeThemeForeground(color: string, text: string): string {
269
+ return `<${color}>${text}</${color}>`;
270
+ }
271
+
272
+ /**
273
+ * @brief Encodes one fake background fragment derived from a foreground color.
274
+ * @details Wraps the provided text in stable XML-like markers so tests can
275
+ * assert the context-bar background contract without terminal escape sequences.
276
+ * Runtime is O(n) in text length. No external state is mutated.
277
+ * @param[in] color {string} Foreground color reused as synthetic background.
278
+ * @param[in] text {string} Raw text payload.
279
+ * @return {string} Encoded background fragment.
280
+ */
281
+ function formatFakeThemeBackgroundFromForeground(color: string, text: string): string {
282
+ return `<bg-from-fg-${color}>${text}</bg-from-fg-${color}>`;
283
+ }
284
+
285
+ /**
286
+ * @brief Builds the expected fake context-bar payload for assertions.
287
+ * @details Resolves the empty-state `CLEAR` overlay for unavailable or
288
+ * non-positive percent, resolves the high-water `FULL!` overlay for percent
289
+ * above 90, and otherwise reconstructs the 5-cell bar. Runtime is O(1). No
290
+ * external state is mutated.
291
+ * @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-bar facts.
292
+ * @return {string} Encoded context-bar string.
293
+ */
294
+ function buildExpectedFakeContextBar(options: {
295
+ filledCells: number;
296
+ percent?: number | null;
297
+ }): string {
298
+ if (options.percent === undefined || options.percent === null || options.percent <= 0) {
299
+ return formatFakeThemeBackgroundFromForeground(
300
+ "accent",
301
+ formatFakeThemeForeground("warning", "CLEAR"),
302
+ );
303
+ }
304
+ if (options.percent > 90) {
305
+ return formatFakeThemeBackgroundFromForeground(
306
+ "warning",
307
+ formatFakeThemeForeground("error", "FULL!"),
308
+ );
309
+ }
310
+ return Array.from({ length: 5 }, (_value, index) =>
311
+ formatFakeThemeBackgroundFromForeground(
312
+ "accent",
313
+ formatFakeThemeForeground(index < options.filledCells ? "warning" : "dim", "▓"),
314
+ ),
315
+ ).join("");
316
+ }
317
+
318
+ /**
319
+ * @brief Builds the expected fake pi-usereq status-bar string for assertions.
320
+ * @details Reconstructs the field order, separators, context bar, and timing fields emitted by the extension using deterministic fake theme markers. Runtime is O(s) in source-path count. No external state is mutated.
321
+ * @param[in] options {{ gitPath?: string; basePath?: string; docsDir: string; testsDir: string; srcDir: string[]; toolCount: number; contextFilledCells: number; contextPercent?: number | null; elapsed: string; last: string; beep?: string; sound?: string }} Expected status facts.
322
+ * @return {string} Encoded status-bar string.
323
+ */
324
+ function buildExpectedFakeStatusText(options: {
325
+ gitPath?: string;
326
+ basePath?: string;
327
+ docsDir: string;
328
+ testsDir: string;
329
+ srcDir: string[];
330
+ toolCount: number;
331
+ contextFilledCells: number;
332
+ contextPercent?: number | null;
333
+ elapsed: string;
334
+ last: string;
335
+ beep?: string;
336
+ sound?: string;
337
+ }): string {
338
+ const buildField = (fieldName: string, value: string): string =>
339
+ `${formatFakeThemeForeground("accent", `${fieldName}:`)}${formatFakeThemeForeground("warning", value)}`;
340
+ const contextBar = buildExpectedFakeContextBar({
341
+ filledCells: options.contextFilledCells,
342
+ percent: options.contextPercent,
343
+ });
344
+ return [
345
+ buildField("git", options.gitPath ?? ""),
346
+ buildField("base", options.basePath ?? "."),
347
+ buildField("docs", options.docsDir),
348
+ buildField("tests", options.testsDir),
349
+ buildField("src", options.srcDir.join(",")),
350
+ buildField("tools", String(options.toolCount)),
351
+ `${formatFakeThemeForeground("accent", "context:")}${contextBar}`,
352
+ buildField("elapsed", options.elapsed),
353
+ buildField("last", options.last),
354
+ buildField("beep", options.beep ?? "none"),
355
+ buildField("sound", options.sound ?? "none"),
356
+ ].join(formatFakeThemeForeground("dim", " • "));
357
+ }
358
+
359
+ /**
360
+ * @brief Creates a fake extension command context with scripted UI interactions.
361
+ * @details Materializes deterministic `select`, shared settings-menu `custom`, `input`, `notify`, `setStatus`, `setEditorText`, `waitForIdle`, `newSession`, `sessionManager`, `getContextUsage`, theme-color behaviors, and captured custom-menu render output backed by in-memory state so menu-oriented and prompt-command tests can assert side effects without a real UI. Optional `onNewSession` and theme overrides let tests emulate runtime replacement and strict token validation. Runtime is O(1) plus queued interaction count and delegated new-session setup cost. Side effects are limited to in-memory state mutation.
362
+ * @param[in] cwd {string} Working directory exposed to command handlers.
363
+ * @param[in] plan {FakeCtxPlan} Scripted select and input responses.
364
+ * @param[in] options {FakeCtxOptions} Optional fake session-manager, context-usage, and runtime-replacement integrations.
365
+ * @return {any} Fake command context compatible with the tested handlers.
366
+ */
367
+ function createFakeCtx(cwd: string, plan: FakeCtxPlan = { selects: [] }, options: FakeCtxOptions = {}) {
368
+ const selects = [...plan.selects];
369
+ const inputs = [...(plan.inputs ?? [])];
370
+ const getContextUsage = options.getContextUsage ?? (() => undefined);
371
+ const sessionManager = options.sessionManager ?? createFakeSessionManager();
372
+ const theme = options.theme ?? {
373
+ fg: (color: string, text: string) => formatFakeThemeForeground(color, text),
374
+ bgFromFg: (color: string, text: string) => formatFakeThemeBackgroundFromForeground(color, text),
375
+ bold: (text: string) => `<bold>${text}</bold>`,
376
+ };
377
+ const state = {
378
+ editorText: "",
379
+ statuses: new Map<string, string>(),
380
+ notifications: [] as Array<{ message: string; level: string }>,
381
+ selectCalls: [] as Array<{ title: string; items: string[] }>,
382
+ customRenderLines: [] as string[][],
383
+ waitForIdleCalls: 0,
384
+ newSessions: [] as Array<{ messages: string[] }>,
385
+ };
386
+ return {
387
+ cwd,
388
+ __state: state,
389
+ sessionManager,
390
+ async waitForIdle() {
391
+ state.waitForIdleCalls += 1;
392
+ },
393
+ async newSession(newSessionOptions?: {
394
+ parentSession?: string;
395
+ setup?: (sessionManager: FakeSessionManager) => Promise<void>;
396
+ }) {
397
+ const session = { messages: [] as string[] };
398
+ const setupSessionManager: FakeSessionManager = {
399
+ appendCustomEntry(customType: string, data?: unknown): string {
400
+ return sessionManager.appendCustomEntry(customType, data);
401
+ },
402
+ appendMessage(message: unknown): string {
403
+ if ((message as { role?: unknown })?.role === "user") {
404
+ session.messages.push(extractSessionMessageText(message));
405
+ }
406
+ return sessionManager.appendMessage(message);
407
+ },
408
+ getBranch(): FakeSessionEntry[] {
409
+ return sessionManager.getBranch();
410
+ },
411
+ getEntries(): FakeSessionEntry[] {
412
+ return sessionManager.getEntries();
413
+ },
414
+ };
415
+ if (newSessionOptions?.setup) {
416
+ await newSessionOptions.setup(setupSessionManager);
417
+ }
418
+ state.newSessions.push(session);
419
+ if (options.onNewSession) {
420
+ const result = await options.onNewSession({ parentSession: newSessionOptions?.parentSession });
421
+ return result ?? { cancelled: false };
422
+ }
423
+ return { cancelled: false };
424
+ },
425
+ getContextUsage() {
426
+ return getContextUsage();
427
+ },
428
+ ui: {
429
+ theme,
430
+ async select(title: string, items: string[]) {
431
+ state.selectCalls.push({ title, items: [...items] });
432
+ return selects.shift();
433
+ },
434
+ async input(_title: string, _placeholder?: string) {
435
+ return inputs.shift();
436
+ },
437
+ async custom<T>(factory: (tui: { requestRender: () => void }, theme: { fg: (color: string, text: string) => string; bgFromFg: (color: string, text: string) => string; bold: (text: string) => string }, keybindings: Record<string, never>, done: (value?: T) => void) => unknown) {
438
+ let resolveResult!: (value: T | undefined) => void;
439
+ const resultPromise = new Promise<T | undefined>((resolve) => {
440
+ resolveResult = resolve;
441
+ });
442
+ const component = factory(
443
+ { requestRender: () => undefined },
444
+ theme,
445
+ {},
446
+ (value?: T) => resolveResult(value),
447
+ ) as {
448
+ render?: (width: number) => string[];
449
+ __piUsereqSettingsMenu?: {
450
+ title: string;
451
+ choices: Array<{ label: string }>;
452
+ selectByLabel: (label: string) => boolean;
453
+ cancel: () => void;
454
+ };
455
+ };
456
+ const bridge = component.__piUsereqSettingsMenu;
457
+ assert.ok(bridge, "unsupported custom UI component");
458
+ if (typeof component.render === "function") {
459
+ state.customRenderLines.push(component.render(120));
460
+ }
461
+ const response = selects.shift();
462
+ state.selectCalls.push({ title: bridge.title, items: bridge.choices.map((choice) => choice.label) });
463
+ if (response === undefined) {
464
+ bridge.cancel();
465
+ } else {
466
+ assert.ok(bridge.selectByLabel(response), `unsupported custom selection ${response}`);
467
+ }
468
+ return resultPromise;
469
+ },
470
+ notify(message: string, level: string = "info") {
471
+ state.notifications.push({ message, level });
472
+ },
473
+ setStatus(key: string, value?: string) {
474
+ if (value === undefined) {
475
+ state.statuses.delete(key);
476
+ } else {
477
+ state.statuses.set(key, value);
478
+ }
479
+ },
480
+ setEditorText(value: string) {
481
+ state.editorText = value;
482
+ },
483
+ },
484
+ };
485
+ }
486
+
487
+ /**
488
+ * @brief Executes one registered fake tool while temporarily overriding `process.cwd()`.
489
+ * @details Locates the named tool, validates the presence of its execute callback, switches the process working directory for the duration of the call, invokes the tool, and restores the previous cwd in a `finally` block. Runtime is dominated by the delegated tool logic. Side effects transiently mutate process cwd.
490
+ * @param[in] pi {FakePi} Fake extension runtime containing registered tools.
491
+ * @param[in] toolName {string} Registered tool name.
492
+ * @param[in] cwd {string} Working directory to expose to tool code through `process.cwd()`.
493
+ * @param[in] params {Record<string, unknown>} Tool parameter object.
494
+ * @return {Promise<any>} Tool execution result payload.
495
+ */
496
+ async function executeRegisteredTool(
497
+ pi: FakePi,
498
+ toolName: string,
499
+ cwd: string,
500
+ params: Record<string, unknown> = {},
501
+ ): Promise<any> {
502
+ const tool = pi.tools.find((candidate) => candidate.name === toolName);
503
+ assert.ok(tool, `missing tool ${toolName}`);
504
+ assert.ok(tool.execute, `missing execute handler for tool ${toolName}`);
505
+ const previousCwd = process.cwd();
506
+ process.chdir(cwd);
507
+ try {
508
+ return await tool.execute!("tool-call-id", params, undefined, undefined, { cwd });
509
+ } finally {
510
+ process.chdir(previousCwd);
511
+ }
512
+ }
513
+
514
+ test("extension registers prompt and config commands while exposing tool capabilities only as tools", () => {
515
+ const pi = createFakePi();
516
+ piUsereqExtension(pi);
517
+
518
+ const commandNames = [...pi.commands.keys()].sort();
519
+ for (const name of [
520
+ "pi-usereq",
521
+ "req-analyze",
522
+ "req-change",
523
+ "req-check",
524
+ "req-cover",
525
+ "req-create",
526
+ "req-fix",
527
+ "req-flowchart",
528
+ "req-implement",
529
+ "req-new",
530
+ "req-readme",
531
+ "req-recreate",
532
+ "req-refactor",
533
+ "req-references",
534
+ "req-renumber",
535
+ "req-workflow",
536
+ "req-write",
537
+ ]) {
538
+ assert.ok(commandNames.includes(name), `missing command ${name}`);
539
+ }
540
+
541
+ for (const name of [
542
+ "git-path",
543
+ "get-base-path",
544
+ "files-tokens",
545
+ "files-references",
546
+ "files-compress",
547
+ "files-find",
548
+ "references",
549
+ "compress",
550
+ "find",
551
+ "tokens",
552
+ "files-static-check",
553
+ "static-check",
554
+ "git-check",
555
+ "docs-check",
556
+ "git-wt-name",
557
+ "git-wt-create",
558
+ "git-wt-delete",
559
+ "pi-usereq-show-config",
560
+ "test-static-check",
561
+ ]) {
562
+ assert.ok(!commandNames.includes(name), `unexpected command ${name}`);
563
+ }
564
+
565
+ const toolNames = pi.tools.map((tool: RegisteredTool) => tool.name).sort();
566
+ for (const name of [
567
+ "git-path",
568
+ "get-base-path",
569
+ "files-tokens",
570
+ "files-references",
571
+ "files-compress",
572
+ "files-find",
573
+ "references",
574
+ "compress",
575
+ "find",
576
+ "tokens",
577
+ "files-static-check",
578
+ "static-check",
579
+ "git-check",
580
+ "docs-check",
581
+ "git-wt-name",
582
+ "git-wt-create",
583
+ "git-wt-delete",
584
+ ]) {
585
+ assert.ok(toolNames.includes(name), `missing tool ${name}`);
586
+ }
587
+ });
588
+
589
+ test("token tools register agent-oriented descriptions and schema details", () => {
590
+ const pi = createFakePi();
591
+ piUsereqExtension(pi);
592
+
593
+ const filesTokens = pi.tools.find((tool: RegisteredTool) => tool.name === "files-tokens");
594
+ const tokens = pi.tools.find((tool: RegisteredTool) => tool.name === "tokens");
595
+ assert.ok(filesTokens, "missing files-tokens tool");
596
+ assert.ok(tokens, "missing tokens tool");
597
+
598
+ assert.match(filesTokens.description ?? "", /LLM-oriented JSON payload/);
599
+ assert.ok(filesTokens.promptGuidelines?.some((line) => line.includes("Output contract:")));
600
+ assert.match(String((filesTokens.parameters as { description?: string } | undefined)?.description ?? ""), /line ranges/);
601
+
602
+ assert.match(tokens.description ?? "", /canonical docs/);
603
+ assert.ok(tokens.promptGuidelines?.some((line) => line.includes("docs_dir_path")));
604
+ assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /canonical_doc_names/);
605
+ });
606
+
607
+ test("reference tools register agent-oriented descriptions and schema details", () => {
608
+ const pi = createFakePi();
609
+ piUsereqExtension(pi);
610
+
611
+ const filesReferences = pi.tools.find((tool: RegisteredTool) => tool.name === "files-references");
612
+ const references = pi.tools.find((tool: RegisteredTool) => tool.name === "references");
613
+ assert.ok(filesReferences, "missing files-references tool");
614
+ assert.ok(references, "missing references tool");
615
+
616
+ assert.match(filesReferences.description ?? "", /request, summary, repository, files, and execution sections/);
617
+ assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Numeric contract:")));
618
+ assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Behavior contract:")));
619
+ assert.match(String((filesReferences.parameters as { description?: string } | undefined)?.description ?? ""), /structured Doxygen fields/);
620
+
621
+ assert.match(references.description ?? "", /configured project source directories/);
622
+ assert.ok(references.promptGuidelines?.some((line) => line.includes("source_directory_paths")));
623
+ assert.ok(references.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
624
+ assert.match(String((references.parameters as { description?: string } | undefined)?.description ?? ""), /directory tree/);
625
+ });
626
+
627
+ test("find tools register agent-oriented descriptions and schema details", () => {
628
+ const pi = createFakePi();
629
+ piUsereqExtension(pi);
630
+
631
+ const filesFind = pi.tools.find((tool: RegisteredTool) => tool.name === "files-find");
632
+ const find = pi.tools.find((tool: RegisteredTool) => tool.name === "find");
633
+ assert.ok(filesFind, "missing files-find tool");
634
+ assert.ok(find, "missing find tool");
635
+
636
+ assert.match(filesFind.description ?? "", /LLM-oriented JSON payload/);
637
+ assert.ok(filesFind.promptGuidelines?.some((line) => line.includes("Regex rule:")));
638
+ assert.ok(filesFind.promptGuidelines?.some((line) => line.includes("Supported tags [Typescript]:")));
639
+ assert.ok(filesFind.promptGuidelines?.some((line) => line.includes("Supported tags [Python]:")));
640
+ assert.match(String((filesFind.parameters as { description?: string } | undefined)?.description ?? ""), /supported_tags_by_language/);
641
+
642
+ assert.match(find.description ?? "", /configured project source directories/);
643
+ assert.ok(find.promptGuidelines?.some((line) => line.includes("source_directory_paths")));
644
+ assert.ok(find.promptGuidelines?.some((line) => line.includes("Tag rule:")));
645
+ assert.match(String((find.parameters as { description?: string } | undefined)?.description ?? ""), /Regex matches construct names only/);
646
+ });
647
+
648
+ test("compression tools register agent-oriented descriptions and schema details", () => {
649
+ const pi = createFakePi();
650
+ piUsereqExtension(pi);
651
+
652
+ const filesCompress = pi.tools.find((tool: RegisteredTool) => tool.name === "files-compress");
653
+ const compress = pi.tools.find((tool: RegisteredTool) => tool.name === "compress");
654
+ assert.ok(filesCompress, "missing files-compress tool");
655
+ assert.ok(compress, "missing compress tool");
656
+
657
+ assert.match(filesCompress.description ?? "", /request, summary, repository, files, and execution sections/);
658
+ assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("Line-number behavior:")));
659
+ assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("Behavior contract:")));
660
+ assert.match(String((filesCompress.parameters as { description?: string } | undefined)?.description ?? ""), /structured compressed lines/);
661
+
662
+ assert.match(compress.description ?? "", /configured project source directories/);
663
+ assert.ok(compress.promptGuidelines?.some((line) => line.includes("Configuration contract:")));
664
+ assert.ok(compress.promptGuidelines?.some((line) => line.includes("Behavior contract:")));
665
+ assert.match(String((compress.parameters as { description?: string } | undefined)?.description ?? ""), /structured compressed lines/);
666
+ });
667
+
668
+ test("files-tokens returns structured source facts and separated guidance", async () => {
669
+ const { projectBase } = initFixtureRepo({
670
+ fixtures: [],
671
+ docs: {
672
+ "REQUIREMENTS.md": "---\ntitle: Requirements\n---\n# Requirements\nAlpha\nBeta\n",
673
+ "WORKFLOW.md": "# Workflow\nStep one\n",
674
+ "REFERENCES.md": "# References\nEntry one\n",
675
+ },
676
+ });
677
+ try {
678
+ const pi = createFakePi();
679
+ piUsereqExtension(pi);
680
+ const result = await executeRegisteredTool(pi, "files-tokens", projectBase, {
681
+ files: [`${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`, `${DEFAULT_DOCS_DIR}/MISSING.md`],
682
+ }) as {
683
+ content?: Array<{ type: string; text?: string }>;
684
+ details?: {
685
+ request: {
686
+ base_dir_path: string;
687
+ requested_input_paths: string[];
688
+ };
689
+ summary: {
690
+ counted_file_count: number;
691
+ skipped_file_count: number;
692
+ };
693
+ files: Array<{
694
+ canonical_path: string;
695
+ status: string;
696
+ start_line_number: number;
697
+ end_line_number: number;
698
+ line_count: number;
699
+ byte_count: number;
700
+ primary_heading_text?: string;
701
+ }>;
702
+ guidance: {
703
+ source_observations: {
704
+ skipped_inputs: Array<{ input_path: string; canonical_path: string; reason: string }>;
705
+ };
706
+ derived_recommendations: Array<{ kind: string; ordered_paths: string[] }>;
707
+ actionable_next_steps: Array<{ kind: string; ordered_paths: string[] }>;
708
+ };
709
+ execution: {
710
+ stderr: string;
711
+ };
712
+ };
713
+ };
714
+
715
+ const payload = result.details!;
716
+ assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
717
+ assert.equal(payload.request.base_dir_path, projectBase);
718
+ assert.deepEqual(payload.request.requested_input_paths, [`${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`, `${DEFAULT_DOCS_DIR}/MISSING.md`]);
719
+ assert.equal(payload.summary.counted_file_count, 1);
720
+ assert.equal(payload.summary.skipped_file_count, 1);
721
+ assert.equal(payload.files[0]?.canonical_path, `${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`);
722
+ assert.equal(payload.files[0]?.start_line_number, 1);
723
+ assert.equal(payload.files[0]?.end_line_number, payload.files[0]?.line_count);
724
+ assert.ok((payload.files[0]?.byte_count ?? 0) > 0);
725
+ assert.equal(payload.files[0]?.primary_heading_text, "Requirements");
726
+ assert.deepEqual(payload.guidance.source_observations.skipped_inputs, [
727
+ { input_path: `${DEFAULT_DOCS_DIR}/MISSING.md`, canonical_path: `${DEFAULT_DOCS_DIR}/MISSING.md`, reason: "not found" },
728
+ ]);
729
+ assert.equal(payload.guidance.derived_recommendations[0]?.kind, "prioritize_high_token_paths");
730
+ assert.equal(payload.guidance.actionable_next_steps[0]?.kind, "read_top_token_paths_first");
731
+ assert.match(payload.execution.stderr, new RegExp(`skipped: ${DEFAULT_DOCS_DIR.replace("/", "\\/")}/MISSING\\.md: not found`));
732
+ } finally {
733
+ fs.rmSync(projectBase, { recursive: true, force: true });
734
+ }
735
+ });
736
+
737
+ test("files-references returns structured repository, symbol, and Doxygen facts", async () => {
738
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
739
+ const samplePath = path.join(projectBase, "src", "sample.ts");
740
+ fs.writeFileSync(samplePath, `/**
741
+ * @file
742
+ * @brief Sample source file.
743
+ */
744
+
745
+ /**
746
+ * @brief Builds the sample value.
747
+ * @param[in] input {string} Caller-supplied value.
748
+ * @return {string} Upper-cased result.
749
+ * @satisfies REQ-011
750
+ */
751
+ export function buildSample(input: string): string {
752
+ return input.toUpperCase();
753
+ }
754
+ `, "utf8");
755
+ try {
756
+ const pi = createFakePi();
757
+ piUsereqExtension(pi);
758
+ const result = await executeRegisteredTool(pi, "files-references", projectBase, {
759
+ files: ["src/sample.ts", "src/missing.ts"],
760
+ }) as {
761
+ content?: Array<{ type: string; text?: string }>;
762
+ details?: {
763
+ request: {
764
+ requested_input_paths: string[];
765
+ };
766
+ summary: {
767
+ analyzed_file_count: number;
768
+ skipped_file_count: number;
769
+ };
770
+ repository: {
771
+ file_canonical_paths: string[];
772
+ };
773
+ files: Array<{
774
+ canonical_path: string;
775
+ status: string;
776
+ symbol_count: number;
777
+ doxygen_field_count: number;
778
+ file_doxygen?: { brief?: string[] };
779
+ symbols: Array<{
780
+ symbol_name: string;
781
+ qualified_name: string;
782
+ symbol_kind: string;
783
+ line_range: [number, number];
784
+ doxygen?: {
785
+ brief?: string[];
786
+ params?: Array<{ direction: string; parameter_name?: string; value_type?: string; description?: string }>;
787
+ returns?: string[];
788
+ satisfies_requirement_ids?: string[];
789
+ };
790
+ }>;
791
+ }>;
792
+ execution: {
793
+ stderr: string;
794
+ };
795
+ };
796
+ };
797
+
798
+ const payload = result.details!;
799
+ assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
800
+ assert.deepEqual(payload.request.requested_input_paths, ["src/sample.ts", "src/missing.ts"]);
801
+ assert.equal(payload.summary.analyzed_file_count, 1);
802
+ assert.equal(payload.summary.skipped_file_count, 1);
803
+ assert.deepEqual(payload.repository.file_canonical_paths, ["src/sample.ts"]);
804
+ assert.deepEqual(payload.files.map((file) => file.status), ["analyzed", "skipped"]);
805
+ assert.equal(payload.files[0]?.canonical_path, "src/sample.ts");
806
+ assert.equal(payload.files[0]?.symbol_count, 1);
807
+ assert.ok((payload.files[0]?.doxygen_field_count ?? 0) >= 2);
808
+ assert.deepEqual(payload.files[0]?.file_doxygen?.brief, ["Sample source file."]);
809
+ assert.equal(payload.files[0]?.symbols[0]?.symbol_name, "buildSample");
810
+ assert.equal(payload.files[0]?.symbols[0]?.qualified_name, "buildSample");
811
+ assert.equal(payload.files[0]?.symbols[0]?.symbol_kind, "FUNCTION");
812
+ assert.deepEqual(payload.files[0]?.symbols[0]?.line_range, [12, 14]);
813
+ assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.brief, ["Builds the sample value."]);
814
+ assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.params, [
815
+ { direction: "in", parameter_name: "input", value_type: "string", description: "Caller-supplied value." },
816
+ ]);
817
+ assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.returns, ["{string} Upper-cased result."]);
818
+ assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.satisfies_requirement_ids, ["REQ-011"]);
819
+ assert.match(payload.execution.stderr, /skipped: src\/missing\.ts: not found/);
820
+ } finally {
821
+ fs.rmSync(projectBase, { recursive: true, force: true });
822
+ }
823
+ });
824
+
825
+ test("references returns a structured repository tree for configured source directories", async () => {
826
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
827
+ fs.writeFileSync(path.join(projectBase, "src", "alpha.ts"), "export const ALPHA = 1;\n", "utf8");
828
+ fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
829
+ fs.writeFileSync(path.join(projectBase, "src", "nested", "beta.ts"), "export function beta(): number {\n return 2;\n}\n", "utf8");
830
+ try {
831
+ const pi = createFakePi();
832
+ piUsereqExtension(pi);
833
+ const result = await executeRegisteredTool(pi, "references", projectBase, {}) as {
834
+ content?: Array<{ type: string; text?: string }>;
835
+ details?: {
836
+ summary: {
837
+ analyzed_file_count: number;
838
+ total_symbol_count: number;
839
+ };
840
+ repository: {
841
+ source_directory_paths: string[];
842
+ file_canonical_paths: string[];
843
+ directory_tree: {
844
+ node_name: string;
845
+ children: Array<{ node_name: string; node_kind: string; children: Array<{ relative_path: string; node_kind: string }> }>;
846
+ };
847
+ };
848
+ files: Array<{
849
+ canonical_path: string;
850
+ line_range: [number, number];
851
+ symbols: Array<{ symbol_name: string; line_range: [number, number] }>;
852
+ }>;
853
+ };
854
+ };
855
+
856
+ const payload = result.details!;
857
+ assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
858
+ assert.equal(payload.summary.analyzed_file_count, 2);
859
+ assert.equal(payload.summary.total_symbol_count, 1);
860
+ assert.deepEqual(payload.repository.source_directory_paths, ["src"]);
861
+ assert.deepEqual(payload.repository.file_canonical_paths, ["src/alpha.ts", "src/nested/beta.ts"]);
862
+ assert.equal(payload.repository.directory_tree.node_name, ".");
863
+ assert.equal(payload.repository.directory_tree.children[0]?.node_name, "src");
864
+ assert.equal(payload.repository.directory_tree.children[0]?.node_kind, "directory");
865
+ assert.ok(payload.repository.directory_tree.children[0]?.children.some((child) => child.relative_path === "src/alpha.ts"));
866
+ assert.ok(payload.repository.directory_tree.children[0]?.children.some((child) => child.relative_path === "src/nested"));
867
+ assert.deepEqual(payload.files.map((file) => file.canonical_path), ["src/alpha.ts", "src/nested/beta.ts"]);
868
+ assert.deepEqual(payload.files[0]?.line_range, [1, 1]);
869
+ assert.equal(payload.files[0]?.symbols.length, 0);
870
+ assert.equal(payload.files[1]?.symbols[0]?.symbol_name, "beta");
871
+ assert.deepEqual(payload.files[1]?.symbols[0]?.line_range, [1, 3]);
872
+ } finally {
873
+ fs.rmSync(projectBase, { recursive: true, force: true });
874
+ }
875
+ });
876
+
877
+ test("files-compress returns structured line, symbol, and Doxygen facts", async () => {
878
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
879
+ const samplePath = path.join(projectBase, "src", "sample.ts");
880
+ fs.writeFileSync(samplePath, `/**
881
+ * @file
882
+ * @brief Sample source file.
883
+ */
884
+
885
+ /**
886
+ * @brief Builds the sample value.
887
+ * @param[in] input {string} Caller-supplied value.
888
+ * @return {string} Upper-cased result.
889
+ */
890
+ export function buildSample(input: string): string {
891
+ // stripped comment
892
+ return input.toUpperCase();
893
+ }
894
+ `, "utf8");
895
+ try {
896
+ const pi = createFakePi();
897
+ piUsereqExtension(pi);
898
+ const result = await executeRegisteredTool(pi, "files-compress", projectBase, {
899
+ files: ["src/sample.ts", "src/missing.ts"],
900
+ enableLineNumbers: true,
901
+ }) as {
902
+ content?: Array<{ type: string; text?: string }>;
903
+ details?: {
904
+ request: {
905
+ line_number_mode: string;
906
+ requested_input_paths: string[];
907
+ };
908
+ summary: {
909
+ compressed_file_count: number;
910
+ skipped_file_count: number;
911
+ total_symbol_count: number;
912
+ };
913
+ repository: {
914
+ file_canonical_paths: string[];
915
+ };
916
+ files: Array<{
917
+ canonical_path: string;
918
+ status: string;
919
+ line_number_mode: string;
920
+ source_line_count: number;
921
+ compressed_line_count: number;
922
+ file_doxygen?: { brief?: string[] };
923
+ symbols: Array<{
924
+ symbol_name: string;
925
+ symbol_kind: string;
926
+ line_range: [number, number];
927
+ doxygen?: { brief?: string[] };
928
+ }>;
929
+ compressed_lines: Array<{ source_line_number: number; display_text: string; text: string }>;
930
+ compressed_source_text?: string;
931
+ error_reason?: string;
932
+ }>;
933
+ execution: {
934
+ code: number;
935
+ stderr: string;
936
+ };
937
+ };
938
+ };
939
+
940
+ const payload = result.details!;
941
+ assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
942
+ assert.equal(payload.request.line_number_mode, "enabled");
943
+ assert.deepEqual(payload.request.requested_input_paths, ["src/sample.ts", "src/missing.ts"]);
944
+ assert.equal(payload.summary.compressed_file_count, 1);
945
+ assert.equal(payload.summary.skipped_file_count, 1);
946
+ assert.equal(payload.summary.total_symbol_count, 1);
947
+ assert.ok(payload.repository.file_canonical_paths.includes("src/sample.ts"));
948
+ assert.equal(payload.files[0]?.canonical_path, "src/sample.ts");
949
+ assert.equal(payload.files[0]?.status, "compressed");
950
+ assert.equal(payload.files[0]?.line_number_mode, "enabled");
951
+ assert.ok((payload.files[0]?.source_line_count ?? 0) > 0);
952
+ assert.ok((payload.files[0]?.compressed_line_count ?? 0) > 0);
953
+ assert.deepEqual(payload.files[0]?.file_doxygen?.brief, ["Sample source file."]);
954
+ assert.equal(payload.files[0]?.symbols[0]?.symbol_name, "buildSample");
955
+ assert.equal(payload.files[0]?.symbols[0]?.symbol_kind, "FUNCTION");
956
+ assert.deepEqual(payload.files[0]?.symbols[0]?.line_range, [11, 14]);
957
+ assert.deepEqual(payload.files[0]?.symbols[0]?.doxygen?.brief, ["Builds the sample value."]);
958
+ assert.equal(payload.files[0]?.compressed_lines[0]?.source_line_number, 11);
959
+ assert.match(payload.files[0]?.compressed_lines[0]?.display_text ?? "", /^11: export function buildSample/);
960
+ assert.match(payload.files[0]?.compressed_source_text ?? "", /^11: export function buildSample/m);
961
+ assert.equal(payload.files[1]?.status, "skipped");
962
+ assert.equal(payload.files[1]?.error_reason, "not_found");
963
+ assert.equal(payload.execution.code, 0);
964
+ assert.match(payload.execution.stderr, /skipped: src\/missing\.ts: not_found/);
965
+ } finally {
966
+ fs.rmSync(projectBase, { recursive: true, force: true });
967
+ }
968
+ });
969
+
970
+ test("compress returns a structured repository-scoped compression payload", async () => {
971
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
972
+ fs.writeFileSync(
973
+ path.join(projectBase, "src", "alpha.ts"),
974
+ "export const ALPHA = 1;\n",
975
+ "utf8",
976
+ );
977
+ fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
978
+ fs.writeFileSync(
979
+ path.join(projectBase, "src", "nested", "beta.ts"),
980
+ "export function buildBeta(): number {\n return 2;\n}\n",
981
+ "utf8",
982
+ );
983
+ try {
984
+ const pi = createFakePi();
985
+ piUsereqExtension(pi);
986
+ const result = await executeRegisteredTool(pi, "compress", projectBase, {
987
+ enableLineNumbers: false,
988
+ }) as {
989
+ content?: Array<{ type: string; text?: string }>;
990
+ details?: {
991
+ request: {
992
+ scope: string;
993
+ line_number_mode: string;
994
+ };
995
+ summary: {
996
+ compressed_file_count: number;
997
+ total_compressed_line_count: number;
998
+ };
999
+ repository: {
1000
+ source_directory_paths: string[];
1001
+ file_canonical_paths: string[];
1002
+ };
1003
+ files: Array<{
1004
+ canonical_path: string;
1005
+ line_number_mode: string;
1006
+ compressed_source_text?: string;
1007
+ compressed_lines: Array<{ display_text: string }>;
1008
+ }>;
1009
+ execution: {
1010
+ code: number;
1011
+ stderr: string;
1012
+ };
1013
+ };
1014
+ };
1015
+
1016
+ const payload = result.details!;
1017
+ assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
1018
+ assert.equal(payload.request.scope, "configured-source-directories");
1019
+ assert.equal(payload.request.line_number_mode, "disabled");
1020
+ assert.equal(payload.summary.compressed_file_count, 2);
1021
+ assert.ok((payload.summary.total_compressed_line_count ?? 0) >= 2);
1022
+ assert.deepEqual(payload.repository.source_directory_paths, ["src"]);
1023
+ assert.deepEqual(payload.repository.file_canonical_paths, ["src/alpha.ts", "src/nested/beta.ts"]);
1024
+ assert.equal(payload.files[0]?.line_number_mode, "disabled");
1025
+ assert.doesNotMatch(payload.files[1]?.compressed_source_text ?? "", /^\d+:/m);
1026
+ assert.match(payload.files[1]?.compressed_lines[0]?.display_text ?? "", /^export function buildBeta/);
1027
+ assert.equal(payload.execution.code, 0);
1028
+ assert.equal(payload.execution.stderr, "");
1029
+ } finally {
1030
+ fs.rmSync(projectBase, { recursive: true, force: true });
1031
+ }
1032
+ });
1033
+
1034
+ test("files-find returns structured match, code-line, and Doxygen facts", async () => {
1035
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
1036
+ const samplePath = path.join(projectBase, "src", "sample.ts");
1037
+ fs.writeFileSync(samplePath, `/**
1038
+ * @file
1039
+ * @brief Sample source file.
1040
+ */
1041
+
1042
+ /**
1043
+ * @brief Builds the sample value.
1044
+ * @param[in] input {string} Caller-supplied value.
1045
+ * @return {string} Upper-cased result.
1046
+ * @satisfies REQ-089
1047
+ */
1048
+ export function buildSample(input: string): string {
1049
+ return input.toUpperCase();
1050
+ }
1051
+ `, "utf8");
1052
+ try {
1053
+ const pi = createFakePi();
1054
+ piUsereqExtension(pi);
1055
+ const result = await executeRegisteredTool(pi, "files-find", projectBase, {
1056
+ tag: "FUNCTION",
1057
+ pattern: "^buildSample$",
1058
+ files: ["src/sample.ts", "src/missing.ts"],
1059
+ enableLineNumbers: true,
1060
+ }) as {
1061
+ content?: Array<{ type: string; text?: string }>;
1062
+ details?: {
1063
+ request: {
1064
+ tag_filter_values: string[];
1065
+ tag_filter_status: string;
1066
+ line_number_mode: string;
1067
+ requested_input_paths: string[];
1068
+ };
1069
+ summary: {
1070
+ search_status: string;
1071
+ matched_file_count: number;
1072
+ skipped_file_count: number;
1073
+ total_match_count: number;
1074
+ };
1075
+ repository: {
1076
+ supported_tags_by_language: Record<string, string[]>;
1077
+ };
1078
+ files: Array<{
1079
+ canonical_path: string;
1080
+ status: string;
1081
+ match_count: number;
1082
+ supported_tags: string[];
1083
+ file_doxygen?: { brief?: string[] };
1084
+ matches: Array<{
1085
+ symbol_name: string;
1086
+ qualified_name: string;
1087
+ symbol_kind: string;
1088
+ line_range: [number, number];
1089
+ code_line_count: number;
1090
+ code_lines: Array<{ source_line_number: number; display_text: string; text: string }>;
1091
+ stripped_source_text?: string;
1092
+ doxygen?: {
1093
+ brief?: string[];
1094
+ params?: Array<{ direction: string; parameter_name?: string; value_type?: string; description?: string }>;
1095
+ returns?: string[];
1096
+ satisfies_requirement_ids?: string[];
1097
+ };
1098
+ }>;
1099
+ }>;
1100
+ execution: {
1101
+ code: number;
1102
+ stderr: string;
1103
+ };
1104
+ };
1105
+ };
1106
+
1107
+ const payload = result.details!;
1108
+ assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
1109
+ assert.deepEqual(payload.request.tag_filter_values, ["FUNCTION"]);
1110
+ assert.equal(payload.request.tag_filter_status, "valid");
1111
+ assert.equal(payload.request.line_number_mode, "enabled");
1112
+ assert.deepEqual(payload.request.requested_input_paths, ["src/sample.ts", "src/missing.ts"]);
1113
+ assert.equal(payload.summary.search_status, "matched");
1114
+ assert.equal(payload.summary.matched_file_count, 1);
1115
+ assert.equal(payload.summary.skipped_file_count, 1);
1116
+ assert.equal(payload.summary.total_match_count, 1);
1117
+ assert.ok(payload.repository.supported_tags_by_language.typescript?.includes("FUNCTION"));
1118
+ assert.deepEqual(payload.files.map((file) => file.status), ["matched", "skipped"]);
1119
+ assert.equal(payload.files[0]?.canonical_path, "src/sample.ts");
1120
+ assert.equal(payload.files[0]?.match_count, 1);
1121
+ assert.ok(payload.files[0]?.supported_tags.includes("FUNCTION"));
1122
+ assert.deepEqual(payload.files[0]?.file_doxygen?.brief, ["Sample source file."]);
1123
+ assert.equal(payload.files[0]?.matches[0]?.symbol_name, "buildSample");
1124
+ assert.equal(payload.files[0]?.matches[0]?.qualified_name, "buildSample");
1125
+ assert.equal(payload.files[0]?.matches[0]?.symbol_kind, "FUNCTION");
1126
+ assert.deepEqual(payload.files[0]?.matches[0]?.line_range, [12, 14]);
1127
+ assert.ok((payload.files[0]?.matches[0]?.code_line_count ?? 0) > 0);
1128
+ assert.equal(payload.files[0]?.matches[0]?.code_lines[0]?.source_line_number, 12);
1129
+ assert.match(payload.files[0]?.matches[0]?.code_lines[0]?.display_text ?? "", /^12: export function buildSample/);
1130
+ assert.match(payload.files[0]?.matches[0]?.stripped_source_text ?? "", /^12: export function buildSample/m);
1131
+ assert.deepEqual(payload.files[0]?.matches[0]?.doxygen?.brief, ["Builds the sample value."]);
1132
+ assert.deepEqual(payload.files[0]?.matches[0]?.doxygen?.params, [
1133
+ { direction: "in", parameter_name: "input", value_type: "string", description: "Caller-supplied value." },
1134
+ ]);
1135
+ assert.deepEqual(payload.files[0]?.matches[0]?.doxygen?.returns, ["{string} Upper-cased result."]);
1136
+ assert.deepEqual(payload.files[0]?.matches[0]?.doxygen?.satisfies_requirement_ids, ["REQ-089"]);
1137
+ assert.equal(payload.execution.code, 0);
1138
+ assert.match(payload.execution.stderr, /skipped: src\/missing\.ts: not_found/);
1139
+ } finally {
1140
+ fs.rmSync(projectBase, { recursive: true, force: true });
1141
+ }
1142
+ });
1143
+
1144
+ test("find returns a structured repository-scoped construct-search payload", async () => {
1145
+ const { projectBase } = initFixtureRepo({ fixtures: [] });
1146
+ fs.writeFileSync(
1147
+ path.join(projectBase, "src", "alpha.ts"),
1148
+ `/**
1149
+ * @brief Builds alpha.
1150
+ * @return {number} Constant one.
1151
+ */
1152
+ export function buildAlpha(): number {
1153
+ return 1;
1154
+ }
1155
+ `,
1156
+ "utf8",
1157
+ );
1158
+ fs.mkdirSync(path.join(projectBase, "src", "nested"), { recursive: true });
1159
+ fs.writeFileSync(
1160
+ path.join(projectBase, "src", "nested", "beta.ts"),
1161
+ "export function helperBeta(): number {\n return 2;\n}\n",
1162
+ "utf8",
1163
+ );
1164
+ try {
1165
+ const pi = createFakePi();
1166
+ piUsereqExtension(pi);
1167
+ const result = await executeRegisteredTool(pi, "find", projectBase, {
1168
+ tag: "FUNCTION",
1169
+ pattern: "^build",
1170
+ enableLineNumbers: false,
1171
+ }) as {
1172
+ content?: Array<{ type: string; text?: string }>;
1173
+ details?: {
1174
+ request: {
1175
+ scope: string;
1176
+ regex_status: string;
1177
+ requested_file_count: number;
1178
+ };
1179
+ summary: {
1180
+ search_status: string;
1181
+ matched_file_count: number;
1182
+ no_match_file_count: number;
1183
+ total_match_count: number;
1184
+ };
1185
+ repository: {
1186
+ source_directory_paths: string[];
1187
+ file_canonical_paths: string[];
1188
+ supported_tags_by_language: Record<string, string[]>;
1189
+ };
1190
+ files: Array<{
1191
+ canonical_path: string;
1192
+ status: string;
1193
+ matches: Array<{
1194
+ symbol_name: string;
1195
+ code_lines: Array<{ display_text: string }>;
1196
+ stripped_source_text?: string;
1197
+ }>;
1198
+ }>;
1199
+ execution: {
1200
+ code: number;
1201
+ stderr: string;
1202
+ };
1203
+ };
1204
+ };
1205
+
1206
+ const payload = result.details!;
1207
+ assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
1208
+ assert.equal(payload.request.scope, "configured-source-directories");
1209
+ assert.equal(payload.request.regex_status, "valid");
1210
+ assert.equal(payload.request.requested_file_count, 2);
1211
+ assert.equal(payload.summary.search_status, "matched");
1212
+ assert.equal(payload.summary.matched_file_count, 1);
1213
+ assert.equal(payload.summary.no_match_file_count, 1);
1214
+ assert.equal(payload.summary.total_match_count, 1);
1215
+ assert.deepEqual(payload.repository.source_directory_paths, ["src"]);
1216
+ assert.deepEqual(payload.repository.file_canonical_paths, ["src/alpha.ts", "src/nested/beta.ts"]);
1217
+ assert.ok(payload.repository.supported_tags_by_language.typescript?.includes("FUNCTION"));
1218
+ assert.deepEqual(payload.files.map((file) => file.status), ["matched", "no_match"]);
1219
+ assert.equal(payload.files[0]?.matches[0]?.symbol_name, "buildAlpha");
1220
+ assert.match(payload.files[0]?.matches[0]?.code_lines[0]?.display_text ?? "", /^export function buildAlpha/);
1221
+ assert.doesNotMatch(payload.files[0]?.matches[0]?.stripped_source_text ?? "", /^\d+:/m);
1222
+ assert.equal(payload.execution.code, 0);
1223
+ assert.match(payload.execution.stderr, /info: no_match: src\/nested\/beta\.ts/);
1224
+ } finally {
1225
+ fs.rmSync(projectBase, { recursive: true, force: true });
1226
+ }
1227
+ });
1228
+
1229
+ test("path, static-check, git, docs, and worktree tools return structured JSON payloads", async () => {
1230
+ const { projectBase } = initFixtureRepo({
1231
+ fixtures: [],
1232
+ staticCheck: {
1233
+ TypeScript: [{ module: "Dummy" }],
1234
+ },
1235
+ });
1236
+ fs.writeFileSync(path.join(projectBase, "src", "check.ts"), "export const VALUE = 1;\n", "utf8");
1237
+ const add = spawnSync("git", ["add", "."], { cwd: projectBase, encoding: "utf8" });
1238
+ assert.equal(add.status, 0, add.stderr);
1239
+ const commit = spawnSync("git", ["commit", "-m", "add static-check input"], { cwd: projectBase, encoding: "utf8" });
1240
+ assert.equal(commit.status, 0, commit.stderr);
1241
+ try {
1242
+ const pi = createFakePi();
1243
+ piUsereqExtension(pi);
1244
+
1245
+ const gitPathResult = await executeRegisteredTool(pi, "git-path", projectBase) as {
1246
+ content?: Array<{ text?: string }>;
1247
+ details?: {
1248
+ result: { path_value: string; path_present: boolean };
1249
+ runtime_paths: { git_path: string };
1250
+ execution: { code: number };
1251
+ };
1252
+ };
1253
+ assert.deepEqual(JSON.parse(gitPathResult.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(gitPathResult.details)));
1254
+ assert.equal(gitPathResult.details?.result.path_value, gitPathResult.details?.runtime_paths.git_path);
1255
+ assert.equal(gitPathResult.details?.result.path_present, true);
1256
+ assert.equal(gitPathResult.details?.execution.code, 0);
1257
+
1258
+ const basePathResult = await executeRegisteredTool(pi, "get-base-path", projectBase) as {
1259
+ details?: {
1260
+ result: { path_value: string };
1261
+ runtime_paths: { base_path: string };
1262
+ execution: { code: number };
1263
+ };
1264
+ };
1265
+ assert.equal(basePathResult.details?.result.path_value, basePathResult.details?.runtime_paths.base_path);
1266
+ assert.equal(basePathResult.details?.execution.code, 0);
1267
+
1268
+ const gitCheckResult = await executeRegisteredTool(pi, "git-check", projectBase) as {
1269
+ details?: { result: { status: string; worktree_status: string; head_status: string }; execution: { code: number } };
1270
+ };
1271
+ assert.equal(gitCheckResult.details?.result.status, "clean");
1272
+ assert.equal(gitCheckResult.details?.result.worktree_status, "clean");
1273
+ assert.equal(gitCheckResult.details?.result.head_status, "valid");
1274
+ assert.equal(gitCheckResult.details?.execution.code, 0);
1275
+
1276
+ const docsCheckResult = await executeRegisteredTool(pi, "docs-check", projectBase) as {
1277
+ details?: {
1278
+ summary: { missing_file_count: number; present_file_count: number };
1279
+ files: Array<{ file_name: string; status: string; prompt_command: string }>;
1280
+ execution: { code: number };
1281
+ };
1282
+ };
1283
+ assert.equal(docsCheckResult.details?.summary.missing_file_count, 0);
1284
+ assert.equal(docsCheckResult.details?.summary.present_file_count, 3);
1285
+ assert.deepEqual(docsCheckResult.details?.files.map((file) => file.file_name), ["REQUIREMENTS.md", "WORKFLOW.md", "REFERENCES.md"]);
1286
+ assert.ok(docsCheckResult.details?.files.every((file) => file.status === "present"));
1287
+ assert.equal(docsCheckResult.details?.execution.code, 0);
1288
+
1289
+ const filesStaticCheckResult = await executeRegisteredTool(pi, "files-static-check", projectBase, {
1290
+ files: ["src/check.ts", "src/missing.ts"],
1291
+ }) as {
1292
+ details?: {
1293
+ summary: { selected_file_count: number; skipped_file_count: number; total_configured_checker_count: number };
1294
+ files: Array<{ canonical_path: string; language_name?: string; status: string; configured_checker_modules: string[] }>;
1295
+ execution: { code: number };
1296
+ };
1297
+ };
1298
+ assert.equal(filesStaticCheckResult.details?.summary.selected_file_count, 1);
1299
+ assert.equal(filesStaticCheckResult.details?.summary.skipped_file_count, 1);
1300
+ assert.equal(filesStaticCheckResult.details?.summary.total_configured_checker_count, 1);
1301
+ assert.equal(filesStaticCheckResult.details?.files[0]?.canonical_path, "src/check.ts");
1302
+ assert.equal(filesStaticCheckResult.details?.files[0]?.language_name, "TypeScript");
1303
+ assert.deepEqual(filesStaticCheckResult.details?.files[0]?.configured_checker_modules, ["Dummy"]);
1304
+ assert.equal(filesStaticCheckResult.details?.files[1]?.status, "skipped");
1305
+ assert.equal(filesStaticCheckResult.details?.execution.code, 0);
1306
+
1307
+ const staticCheckResult = await executeRegisteredTool(pi, "static-check", projectBase) as {
1308
+ details?: {
1309
+ request: { selection_directory_paths: string[]; excluded_directory_paths: string[] };
1310
+ summary: { selected_file_count: number };
1311
+ files: Array<{ canonical_path: string; status: string }>;
1312
+ execution: { code: number };
1313
+ };
1314
+ };
1315
+ assert.deepEqual(staticCheckResult.details?.request.selection_directory_paths, ["src", "tests"]);
1316
+ assert.ok(staticCheckResult.details?.request.excluded_directory_paths.includes("tests/fixtures"));
1317
+ assert.equal(staticCheckResult.details?.summary.selected_file_count, 1);
1318
+ assert.equal(staticCheckResult.details?.files[0]?.canonical_path, "src/check.ts");
1319
+ assert.equal(staticCheckResult.details?.files[0]?.status, "selected");
1320
+ assert.equal(staticCheckResult.details?.execution.code, 0);
1321
+
1322
+ const gitWtNameResult = await executeRegisteredTool(pi, "git-wt-name", projectBase) as {
1323
+ details?: { result: { worktree_name?: string; format_text: string; status: string }; execution: { code: number } };
1324
+ };
1325
+ const wtName = gitWtNameResult.details?.result.worktree_name ?? "";
1326
+ assert.equal(gitWtNameResult.details?.result.status, "generated");
1327
+ assert.equal(gitWtNameResult.details?.result.format_text, "useReq-<project>-<sanitized-branch>-<YYYYMMDDHHMMSS>");
1328
+ assert.match(wtName, /^useReq-/);
1329
+ assert.equal(gitWtNameResult.details?.execution.code, 0);
1330
+
1331
+ const gitWtCreateResult = await executeRegisteredTool(pi, "git-wt-create", projectBase, { wtName }) as {
1332
+ details?: { result: { status: string; worktree_name: string; branch_name: string; worktree_path: string }; execution: { code: number } };
1333
+ };
1334
+ assert.equal(gitWtCreateResult.details?.result.status, "created");
1335
+ assert.equal(gitWtCreateResult.details?.result.worktree_name, wtName);
1336
+ assert.equal(gitWtCreateResult.details?.result.branch_name, wtName);
1337
+ assert.match(gitWtCreateResult.details?.result.worktree_path ?? "", new RegExp(`${wtName}$`));
1338
+ assert.equal(gitWtCreateResult.details?.execution.code, 0);
1339
+
1340
+ const gitWtDeleteResult = await executeRegisteredTool(pi, "git-wt-delete", projectBase, { wtName }) as {
1341
+ details?: { result: { status: string; worktree_name: string; branch_name: string; worktree_path: string }; execution: { code: number } };
1342
+ };
1343
+ assert.equal(gitWtDeleteResult.details?.result.status, "deleted");
1344
+ assert.equal(gitWtDeleteResult.details?.result.worktree_name, wtName);
1345
+ assert.equal(gitWtDeleteResult.details?.result.branch_name, wtName);
1346
+ assert.match(gitWtDeleteResult.details?.result.worktree_path ?? "", new RegExp(`${wtName}$`));
1347
+ assert.equal(gitWtDeleteResult.details?.execution.code, 0);
1348
+ } finally {
1349
+ fs.rmSync(projectBase, { recursive: true, force: true });
1350
+ }
1351
+ });
1352
+
1353
+ test("configuration menu saves updated docs-dir in project config", async () => {
1354
+ const cwd = createTempDir("pi-usereq-menu-");
1355
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1356
+ const pi = createFakePi();
1357
+ piUsereqExtension(pi);
1358
+ const command = pi.commands.get("pi-usereq");
1359
+ assert.ok(command);
1360
+
1361
+ await command!.handler("", createFakeCtx(cwd, { selects: ["docs-dir", "Save and close"], inputs: ["docs/custom"] }));
1362
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1363
+ assert.equal(config["docs-dir"], "docs/custom");
1364
+ });
1365
+
1366
+ test("configuration menu omits prompt-delivery controls and persisted config omits removed runtime fields", async () => {
1367
+ const cwd = createTempDir("pi-usereq-menu-current-session-");
1368
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1369
+ const pi = createFakePi();
1370
+ piUsereqExtension(pi);
1371
+ const command = pi.commands.get("pi-usereq");
1372
+ assert.ok(command);
1373
+ const ctx = createFakeCtx(cwd, { selects: ["Save and close"] });
1374
+
1375
+ await command!.handler("", ctx);
1376
+
1377
+ const menuItems = ctx.__state.selectCalls[0]?.items ?? [];
1378
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1379
+ assert.equal(menuItems.some((item) => item.includes("reset-context")), false);
1380
+ assert.equal(Object.prototype.hasOwnProperty.call(config, "reset-context"), false);
1381
+ assert.equal(Object.prototype.hasOwnProperty.call(config, "base-path"), false);
1382
+ assert.equal(Object.prototype.hasOwnProperty.call(config, "git-path"), false);
1383
+ });
1384
+
1385
+ test("configuration menus expose show-config ordering and omit overview or notification-reference rows", async () => {
1386
+ const cwd = createTempDir("pi-usereq-menu-structure-");
1387
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1388
+ const pi = createFakePi();
1389
+ piUsereqExtension(pi);
1390
+ const command = pi.commands.get("pi-usereq");
1391
+ assert.ok(command);
1392
+ const ctx = createFakeCtx(cwd, { selects: ["show-config", "notifications", "Back", "Save and close"] });
1393
+
1394
+ await command!.handler("", ctx);
1395
+
1396
+ assert.deepEqual(ctx.__state.selectCalls[0]?.items ?? [], [
1397
+ "docs-dir",
1398
+ "tests-dir",
1399
+ "src-dir",
1400
+ "static-check",
1401
+ "startup tools",
1402
+ "notifications",
1403
+ "Reset defaults",
1404
+ "show-config",
1405
+ "Save and close",
1406
+ ]);
1407
+ assert.deepEqual(ctx.__state.selectCalls[2]?.items ?? [], [
1408
+ "Toggle beep on success",
1409
+ "Toggle beep on escape",
1410
+ "Toggle beep on error",
1411
+ "Selected notify command",
1412
+ "Sound toggle hotkey bind",
1413
+ "Notify command (low vol.)",
1414
+ "Notify command (mid vol.)",
1415
+ "Notify command (high vol.)",
1416
+ "Back",
1417
+ ]);
1418
+ });
1419
+
1420
+ test("configuration menus reuse CLI settings theme semantics", async () => {
1421
+ const cwd = createTempDir("pi-usereq-menu-theme-");
1422
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1423
+ const usedColors = new Set<string>();
1424
+ const pi = createFakePi();
1425
+ piUsereqExtension(pi);
1426
+ const command = pi.commands.get("pi-usereq");
1427
+ assert.ok(command);
1428
+ const ctx = createFakeCtx(cwd, { selects: [] }, {
1429
+ theme: {
1430
+ fg(color: string, text: string) {
1431
+ usedColors.add(color);
1432
+ return formatFakeThemeForeground(color, text);
1433
+ },
1434
+ bgFromFg(color: string, text: string) {
1435
+ assert.ok(["accent", "muted", "dim"].includes(color));
1436
+ return formatFakeThemeBackgroundFromForeground(color, text);
1437
+ },
1438
+ bold(text: string) {
1439
+ return `<bold>${text}</bold>`;
1440
+ },
1441
+ },
1442
+ });
1443
+
1444
+ await command!.handler("", ctx);
1445
+
1446
+ const renderedMenu = (ctx.__state.customRenderLines[0] ?? []).join("\n");
1447
+ assert.match(renderedMenu, /<accent><bold>pi-usereq<\/bold><\/accent>/);
1448
+ assert.match(renderedMenu, /<accent>→ <\/accent>/);
1449
+ assert.match(renderedMenu, /<accent>pi-usereq\/docs<\/accent>/);
1450
+ assert.match(renderedMenu, /<muted>tests<\/muted>/);
1451
+ assert.match(renderedMenu, /<dim>/);
1452
+ assert.deepEqual([...usedColors].sort(), ["accent", "dim", "muted", "warning"]);
1453
+ });
1454
+
1455
+ test("prompt commands use the current session by default", async () => {
1456
+ const cwd = createTempDir("pi-usereq-prompt-current-");
1457
+ const pi = createFakePi();
1458
+ piUsereqExtension(pi);
1459
+ const command = pi.commands.get("req-analyze");
1460
+ assert.ok(command);
1461
+ const ctx = createFakeCtx(cwd);
1462
+
1463
+ await command!.handler("Inspect src/index.ts for prompt coverage", ctx);
1464
+
1465
+ assert.equal(ctx.__state.waitForIdleCalls, 0);
1466
+ assert.equal(ctx.__state.newSessions.length, 0);
1467
+ assert.equal(pi.sentUserMessages.length, 1);
1468
+ assert.match(String(pi.sentUserMessages[0]?.content), /Inspect src\/index\.ts for prompt coverage/);
1469
+ });
1470
+
1471
+ test("session_start applies configured pi-usereq startup tools", async () => {
1472
+ const cwd = createTempDir("pi-usereq-tools-startup-");
1473
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1474
+ fs.writeFileSync(
1475
+ getProjectConfigPath(cwd),
1476
+ `${JSON.stringify({
1477
+ "docs-dir": DEFAULT_DOCS_DIR,
1478
+ "tests-dir": "tests",
1479
+ "src-dir": ["src", "foobar"],
1480
+ "static-check": {},
1481
+ "enabled-tools": ["git-path", "static-check"],
1482
+ }, null, 2)}\n`,
1483
+ "utf8",
1484
+ );
1485
+
1486
+ const pi = createFakePi();
1487
+ piUsereqExtension(pi);
1488
+ const ctx = createFakeCtx(cwd);
1489
+ await pi.emit("session_start", { reason: "startup" }, ctx);
1490
+
1491
+ const activeTools = new Set(pi.getActiveTools());
1492
+ assert.deepEqual([...activeTools].sort(), ["git-path", "static-check"]);
1493
+ assert.equal(
1494
+ ctx.__state.statuses.get("pi-usereq"),
1495
+ buildExpectedFakeStatusText({
1496
+ docsDir: DEFAULT_DOCS_DIR,
1497
+ testsDir: "tests",
1498
+ srcDir: ["src", "foobar"],
1499
+ toolCount: 2,
1500
+ contextFilledCells: 0,
1501
+ elapsed: "idle",
1502
+ last: "N/A",
1503
+ }),
1504
+ );
1505
+ });
1506
+
1507
+ test("session_start renders the CLEAR overlay when context usage is empty", async () => {
1508
+ const cwd = createTempDir("pi-usereq-context-empty-status-");
1509
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1510
+ const pi = createFakePi();
1511
+ piUsereqExtension(pi);
1512
+ const ctx = createFakeCtx(cwd, { selects: [] }, {
1513
+ getContextUsage: () => ({ tokens: 0, contextWindow: 1000, percent: 0 }),
1514
+ });
1515
+
1516
+ await pi.emit("session_start", { reason: "startup" }, ctx);
1517
+
1518
+ assert.equal(
1519
+ ctx.__state.statuses.get("pi-usereq"),
1520
+ buildExpectedFakeStatusText({
1521
+ docsDir: DEFAULT_DOCS_DIR,
1522
+ testsDir: "tests",
1523
+ srcDir: ["src"],
1524
+ toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1525
+ contextFilledCells: 0,
1526
+ contextPercent: 0,
1527
+ elapsed: "idle",
1528
+ last: "N/A",
1529
+ }),
1530
+ );
1531
+ });
1532
+
1533
+ test("extension registers wrappers for all pi-usereq status hooks", () => {
1534
+ const pi = createFakePi();
1535
+ piUsereqExtension(pi);
1536
+
1537
+ assert.deepEqual(
1538
+ [...pi.eventHandlers.keys()].sort(),
1539
+ [
1540
+ "agent_end",
1541
+ "agent_start",
1542
+ "before_agent_start",
1543
+ "before_provider_request",
1544
+ "context",
1545
+ "input",
1546
+ "message_end",
1547
+ "message_start",
1548
+ "message_update",
1549
+ "model_select",
1550
+ "resources_discover",
1551
+ "session_before_compact",
1552
+ "session_before_fork",
1553
+ "session_before_switch",
1554
+ "session_before_tree",
1555
+ "session_compact",
1556
+ "session_shutdown",
1557
+ "session_start",
1558
+ "session_tree",
1559
+ "tool_call",
1560
+ "tool_execution_end",
1561
+ "tool_execution_start",
1562
+ "tool_execution_update",
1563
+ "tool_result",
1564
+ "turn_end",
1565
+ "turn_start",
1566
+ "user_bash",
1567
+ ],
1568
+ );
1569
+ });
1570
+
1571
+ test("context hook refreshes context usage and rounds progress cells upward", async () => {
1572
+ const cwd = createTempDir("pi-usereq-context-status-");
1573
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1574
+ const contextUsage = { tokens: 0, contextWindow: 1000, percent: 0 };
1575
+ const pi = createFakePi();
1576
+ piUsereqExtension(pi);
1577
+ const ctx = createFakeCtx(cwd, { selects: [] }, {
1578
+ getContextUsage: () => ({ ...contextUsage }),
1579
+ });
1580
+
1581
+ await pi.emit("session_start", { reason: "startup" }, ctx);
1582
+ contextUsage.tokens = 191;
1583
+ contextUsage.percent = 19.1;
1584
+ await pi.emit("context", { messages: [] }, ctx);
1585
+
1586
+ assert.equal(
1587
+ ctx.__state.statuses.get("pi-usereq"),
1588
+ buildExpectedFakeStatusText({
1589
+ docsDir: DEFAULT_DOCS_DIR,
1590
+ testsDir: "tests",
1591
+ srcDir: ["src"],
1592
+ toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1593
+ contextFilledCells: 1,
1594
+ contextPercent: 19.1,
1595
+ elapsed: "idle",
1596
+ last: "N/A",
1597
+ }),
1598
+ );
1599
+ });
1600
+
1601
+ test("context hook renders the FULL! overlay when usage exceeds ninety percent", async () => {
1602
+ const cwd = createTempDir("pi-usereq-context-full-status-");
1603
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1604
+ const contextUsage = { tokens: 910, contextWindow: 1000, percent: 91 };
1605
+ const pi = createFakePi();
1606
+ piUsereqExtension(pi);
1607
+ const ctx = createFakeCtx(cwd, { selects: [] }, {
1608
+ getContextUsage: () => ({ ...contextUsage }),
1609
+ });
1610
+
1611
+ await pi.emit("session_start", { reason: "startup" }, ctx);
1612
+ await pi.emit("context", { messages: [] }, ctx);
1613
+
1614
+ assert.equal(
1615
+ ctx.__state.statuses.get("pi-usereq"),
1616
+ buildExpectedFakeStatusText({
1617
+ docsDir: DEFAULT_DOCS_DIR,
1618
+ testsDir: "tests",
1619
+ srcDir: ["src"],
1620
+ toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1621
+ contextFilledCells: 5,
1622
+ contextPercent: 91,
1623
+ elapsed: "idle",
1624
+ last: "N/A",
1625
+ }),
1626
+ );
1627
+ });
1628
+
1629
+ test("status FULL overlay uses only CLI-supported theme tokens", async () => {
1630
+ const cwd = createTempDir("pi-usereq-context-theme-contract-");
1631
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1632
+ const usedColors = new Set<string>();
1633
+ const pi = createFakePi();
1634
+ piUsereqExtension(pi);
1635
+ const ctx = createFakeCtx(cwd, { selects: [] }, {
1636
+ getContextUsage: () => ({ tokens: 910, contextWindow: 1000, percent: 91 }),
1637
+ theme: {
1638
+ fg(color: string, text: string) {
1639
+ assert.ok(["accent", "warning", "dim", "error"].includes(color));
1640
+ usedColors.add(color);
1641
+ return formatFakeThemeForeground(color, text);
1642
+ },
1643
+ bgFromFg(color: string, text: string) {
1644
+ assert.ok(["accent", "warning"].includes(color));
1645
+ return formatFakeThemeBackgroundFromForeground(color, text);
1646
+ },
1647
+ bold(text: string) {
1648
+ return `<bold>${text}</bold>`;
1649
+ },
1650
+ },
1651
+ });
1652
+
1653
+ await pi.emit("session_start", { reason: "startup" }, ctx);
1654
+ await pi.emit("context", { messages: [] }, ctx);
1655
+
1656
+ assert.ok(usedColors.has("error"));
1657
+ assert.equal(
1658
+ ctx.__state.statuses.get("pi-usereq"),
1659
+ buildExpectedFakeStatusText({
1660
+ docsDir: DEFAULT_DOCS_DIR,
1661
+ testsDir: "tests",
1662
+ srcDir: ["src"],
1663
+ toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1664
+ contextFilledCells: 5,
1665
+ contextPercent: 91,
1666
+ elapsed: "idle",
1667
+ last: "N/A",
1668
+ }),
1669
+ );
1670
+ });
1671
+
1672
+ test("agent timing status updates elapsed and last while preserving aborted last values", async () => {
1673
+ const cwd = createTempDir("pi-usereq-timing-status-");
1674
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1675
+
1676
+ const originalDateNow = Date.now;
1677
+ const originalSetInterval = globalThis.setInterval;
1678
+ const originalClearInterval = globalThis.clearInterval;
1679
+ let nowMs = 0;
1680
+ const intervalCallbacks: Array<() => void> = [];
1681
+ const clearedHandles: Array<number> = [];
1682
+ Date.now = () => nowMs;
1683
+ globalThis.setInterval = ((callback: TimerHandler) => {
1684
+ intervalCallbacks.push(callback as () => void);
1685
+ return intervalCallbacks.length as ReturnType<typeof setInterval>;
1686
+ }) as typeof setInterval;
1687
+ globalThis.clearInterval = ((handle: ReturnType<typeof setInterval>) => {
1688
+ clearedHandles.push(Number(handle));
1689
+ }) as typeof clearInterval;
1690
+
1691
+ try {
1692
+ const pi = createFakePi();
1693
+ piUsereqExtension(pi);
1694
+ const ctx = createFakeCtx(cwd);
1695
+
1696
+ await pi.emit("session_start", { reason: "startup" }, ctx);
1697
+ await pi.emit("agent_start", {}, ctx);
1698
+ assert.equal(
1699
+ ctx.__state.statuses.get("pi-usereq"),
1700
+ buildExpectedFakeStatusText({
1701
+ docsDir: DEFAULT_DOCS_DIR,
1702
+ testsDir: "tests",
1703
+ srcDir: ["src"],
1704
+ toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1705
+ contextFilledCells: 0,
1706
+ elapsed: "0:00",
1707
+ last: "N/A",
1708
+ }),
1709
+ );
1710
+
1711
+ nowMs = 61_000;
1712
+ intervalCallbacks[0]!();
1713
+ assert.equal(
1714
+ ctx.__state.statuses.get("pi-usereq"),
1715
+ buildExpectedFakeStatusText({
1716
+ docsDir: DEFAULT_DOCS_DIR,
1717
+ testsDir: "tests",
1718
+ srcDir: ["src"],
1719
+ toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1720
+ contextFilledCells: 0,
1721
+ elapsed: "1:01",
1722
+ last: "N/A",
1723
+ }),
1724
+ );
1725
+
1726
+ await pi.emit("agent_end", {
1727
+ messages: [
1728
+ {
1729
+ role: "assistant",
1730
+ stopReason: "stop",
1731
+ content: [],
1732
+ },
1733
+ ],
1734
+ }, ctx);
1735
+ assert.equal(
1736
+ ctx.__state.statuses.get("pi-usereq"),
1737
+ buildExpectedFakeStatusText({
1738
+ docsDir: DEFAULT_DOCS_DIR,
1739
+ testsDir: "tests",
1740
+ srcDir: ["src"],
1741
+ toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1742
+ contextFilledCells: 0,
1743
+ elapsed: "idle",
1744
+ last: "1:01",
1745
+ }),
1746
+ );
1747
+
1748
+ nowMs = 120_000;
1749
+ await pi.emit("agent_start", {}, ctx);
1750
+ nowMs = 125_000;
1751
+ await pi.emit("agent_end", {
1752
+ messages: [
1753
+ {
1754
+ role: "assistant",
1755
+ stopReason: "aborted",
1756
+ content: [],
1757
+ },
1758
+ ],
1759
+ }, ctx);
1760
+ assert.equal(
1761
+ ctx.__state.statuses.get("pi-usereq"),
1762
+ buildExpectedFakeStatusText({
1763
+ docsDir: DEFAULT_DOCS_DIR,
1764
+ testsDir: "tests",
1765
+ srcDir: ["src"],
1766
+ toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1767
+ contextFilledCells: 0,
1768
+ elapsed: "idle",
1769
+ last: "1:01",
1770
+ }),
1771
+ );
1772
+ assert.ok(clearedHandles.length >= 2);
1773
+ } finally {
1774
+ Date.now = originalDateNow;
1775
+ globalThis.setInterval = originalSetInterval;
1776
+ globalThis.clearInterval = originalClearInterval;
1777
+ }
1778
+ });
1779
+
1780
+ test("session_start enables default custom tools and default embedded tools only", async () => {
1781
+ const cwd = createTempDir("pi-usereq-default-tools-");
1782
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1783
+ const pi = createFakePi();
1784
+ piUsereqExtension(pi);
1785
+
1786
+ await pi.emit("session_start", { reason: "startup" }, createFakeCtx(cwd));
1787
+
1788
+ const activeTools = new Set(pi.getActiveTools());
1789
+ for (const toolName of PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES) {
1790
+ assert.ok(activeTools.has(toolName), `missing default active tool ${toolName}`);
1791
+ }
1792
+ assert.equal(activeTools.has("find"), false);
1793
+ assert.equal(activeTools.has("grep"), false);
1794
+ assert.equal(activeTools.has("ls"), false);
1795
+ });
1796
+
1797
+ test("configuration menu can enable embedded builtin tools", async () => {
1798
+ const cwd = createTempDir("pi-usereq-menu-embedded-");
1799
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1800
+ const pi = createFakePi();
1801
+ piUsereqExtension(pi);
1802
+ const command = pi.commands.get("pi-usereq");
1803
+ assert.ok(command);
1804
+
1805
+ await command!.handler(
1806
+ "",
1807
+ createFakeCtx(cwd, {
1808
+ selects: ["startup tools", "Toggle tool", "grep", "Back", "Save and close"],
1809
+ }),
1810
+ );
1811
+
1812
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1813
+ assert.ok(config["enabled-tools"].includes("grep"));
1814
+ assert.ok(pi.getActiveTools().includes("grep"));
1815
+ });
1816
+
1817
+ test("configuration menu can disable configurable active tools", async () => {
1818
+ const cwd = createTempDir("pi-usereq-menu-tools-");
1819
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1820
+ const pi = createFakePi();
1821
+ piUsereqExtension(pi);
1822
+ const command = pi.commands.get("pi-usereq");
1823
+ assert.ok(command);
1824
+
1825
+ await command!.handler(
1826
+ "",
1827
+ createFakeCtx(cwd, {
1828
+ selects: ["startup tools", "Disable all configurable tools", "Back", "Save and close"],
1829
+ }),
1830
+ );
1831
+
1832
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1833
+ assert.deepEqual(config["enabled-tools"], []);
1834
+ assert.deepEqual(pi.getActiveTools().filter((toolName: string) => PI_USEREQ_STARTUP_TOOL_NAMES.includes(toolName as never)), []);
1835
+ });
1836
+
1837
+ test("configuration menu can persist pi-notify settings", async () => {
1838
+ const cwd = createTempDir("pi-usereq-menu-notify-");
1839
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1840
+ const pi = createFakePi();
1841
+ piUsereqExtension(pi);
1842
+ const command = pi.commands.get("pi-usereq");
1843
+ assert.ok(command);
1844
+ const ctx = createFakeCtx(cwd, {
1845
+ selects: [
1846
+ "notifications",
1847
+ "Toggle beep on success",
1848
+ "Toggle beep on error",
1849
+ "Selected notify command",
1850
+ "high",
1851
+ "Sound toggle hotkey bind",
1852
+ "Notify command (low vol.)",
1853
+ "Notify command (mid vol.)",
1854
+ "Notify command (high vol.)",
1855
+ "Back",
1856
+ "Save and close",
1857
+ ],
1858
+ inputs: [
1859
+ "alt+shift+s",
1860
+ "echo low %%INSTALLATION_PATH%%",
1861
+ "echo mid %%INSTALLATION_PATH%%",
1862
+ "echo high %%INSTALLATION_PATH%%",
1863
+ ],
1864
+ });
1865
+
1866
+ await command!.handler("", ctx);
1867
+
1868
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1869
+ assert.equal(config["notify-beep-on-end"], true);
1870
+ assert.equal(config["notify-beep-on-error"], true);
1871
+ assert.equal(config["notify-beep-on-esc"], false);
1872
+ assert.equal(config["notify-sound"], "high");
1873
+ assert.equal(config["notify-sound-toggle-shortcut"], "alt+shift+s");
1874
+ assert.equal(config.PI_NOTIFY_SOUND_LOW_CMD, "echo low %%INSTALLATION_PATH%%");
1875
+ assert.equal(config.PI_NOTIFY_SOUND_MID_CMD, "echo mid %%INSTALLATION_PATH%%");
1876
+ assert.equal(config.PI_NOTIFY_SOUND_HIGH_CMD, "echo high %%INSTALLATION_PATH%%");
1877
+ assert.equal(
1878
+ ctx.__state.statuses.get("pi-usereq"),
1879
+ buildExpectedFakeStatusText({
1880
+ docsDir: DEFAULT_DOCS_DIR,
1881
+ testsDir: "tests",
1882
+ srcDir: ["src"],
1883
+ toolCount: pi.getActiveTools().length,
1884
+ contextFilledCells: 0,
1885
+ elapsed: "idle",
1886
+ last: "N/A",
1887
+ beep: "end,err",
1888
+ sound: "high",
1889
+ }),
1890
+ );
1891
+ });
1892
+
1893
+ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () => {
1894
+ const cwd = createTempDir("pi-usereq-shortcut-notify-");
1895
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1896
+ const previousCwd = process.cwd();
1897
+ const pi = createFakePi();
1898
+ process.chdir(cwd);
1899
+ try {
1900
+ piUsereqExtension(pi);
1901
+ } finally {
1902
+ process.chdir(previousCwd);
1903
+ }
1904
+
1905
+ const shortcut = pi.shortcuts.get("alt+s");
1906
+ assert.ok(shortcut);
1907
+ const ctx = createFakeCtx(cwd);
1908
+ await pi.emit("session_start", { reason: "startup" }, ctx);
1909
+
1910
+ for (const expectedSound of ["low", "mid", "high", "none"]) {
1911
+ await shortcut!.handler(ctx);
1912
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1913
+ assert.equal(config["notify-sound"], expectedSound);
1914
+ assert.equal(
1915
+ ctx.__state.statuses.get("pi-usereq"),
1916
+ buildExpectedFakeStatusText({
1917
+ docsDir: DEFAULT_DOCS_DIR,
1918
+ testsDir: "tests",
1919
+ srcDir: ["src"],
1920
+ toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1921
+ contextFilledCells: 0,
1922
+ elapsed: "idle",
1923
+ last: "N/A",
1924
+ sound: expectedSound,
1925
+ }),
1926
+ );
1927
+ }
1928
+ });
1929
+
1930
+ test("git-path tool derives the repository root at runtime", async () => {
1931
+ const { projectBase } = initFixtureRepo();
1932
+ try {
1933
+ const configPath = getProjectConfigPath(projectBase);
1934
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
1935
+ config["git-path"] = "/tmp/wrong-git-root";
1936
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
1937
+
1938
+ const pi = createFakePi();
1939
+ piUsereqExtension(pi);
1940
+ const result = await executeRegisteredTool(pi, "git-path", projectBase) as {
1941
+ content?: Array<{ text?: string }>;
1942
+ details?: { result: { path_value: string }; runtime_paths: { git_path: string } };
1943
+ };
1944
+
1945
+ assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(result.details)));
1946
+ assert.equal(result.details?.result.path_value, result.details?.runtime_paths.git_path);
1947
+ } finally {
1948
+ fs.rmSync(projectBase, { recursive: true, force: true });
1949
+ }
1950
+ });
1951
+
1952
+ test("static-check exposes the supported programming languages", () => {
1953
+ const supported = getSupportedStaticCheckLanguageSupport();
1954
+ assert.equal(supported.length, 20);
1955
+ assert.deepEqual(supported.find((entry) => entry.language === "Python"), { language: "Python", extensions: [".py"] });
1956
+ assert.deepEqual(supported.find((entry) => entry.language === "JavaScript"), {
1957
+ language: "JavaScript",
1958
+ extensions: [".js", ".mjs"],
1959
+ });
1960
+ });
1961
+
1962
+ test("configuration menu can add static-check entries from raw specs", async () => {
1963
+ const cwd = createTempDir("pi-usereq-menu-sc-");
1964
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1965
+ const pi = createFakePi();
1966
+ piUsereqExtension(pi);
1967
+ const command = pi.commands.get("pi-usereq");
1968
+ assert.ok(command);
1969
+
1970
+ await command!.handler(
1971
+ "",
1972
+ createFakeCtx(cwd, {
1973
+ selects: ["static-check", "Add entry from LANG=MODULE[,CMD[,PARAM...]]", "Back", "Save and close"],
1974
+ inputs: ["Python=Command,true"],
1975
+ }),
1976
+ );
1977
+
1978
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1979
+ assert.deepEqual(config["static-check"].Python, [{ module: "Command", cmd: "true" }]);
1980
+ });
1981
+
1982
+ test("configuration menu can add guided static-check entries for explicit supported languages", async () => {
1983
+ const cwd = createTempDir("pi-usereq-menu-sc-guided-");
1984
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1985
+ const pi = createFakePi();
1986
+ piUsereqExtension(pi);
1987
+ const command = pi.commands.get("pi-usereq");
1988
+ assert.ok(command);
1989
+
1990
+ await command!.handler(
1991
+ "",
1992
+ createFakeCtx(cwd, {
1993
+ selects: [
1994
+ "static-check",
1995
+ "Add entry for supported language",
1996
+ "Python",
1997
+ "Ruff",
1998
+ "Back",
1999
+ "Save and close",
2000
+ ],
2001
+ }),
2002
+ );
2003
+
2004
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
2005
+ assert.deepEqual(config["static-check"].Python, [{ module: "Ruff" }]);
2006
+ });