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
package/src/index.ts ADDED
@@ -0,0 +1,2209 @@
1
+ /**
2
+ * @file
3
+ * @brief Registers the pi-usereq extension commands, tools, and configuration UI.
4
+ * @details Bridges the standalone tool-runner layer into the pi extension API by registering prompt commands, agent tools, and interactive configuration menus. Runtime at module load is O(1); later behavior depends on the selected command or tool. Side effects include extension registration, UI updates, filesystem reads/writes, and delegated tool execution.
5
+ */
6
+
7
+ /**
8
+ * @brief Declares the extension version string.
9
+ * @details The value is exported for external inspection and packaging metadata alignment. Access complexity is O(1).
10
+ */
11
+ export const VERSION = "0.4.0"
12
+
13
+ import path from "node:path";
14
+ import type {
15
+ AgentEndEvent,
16
+ ExtensionAPI,
17
+ ExtensionCommandContext,
18
+ ExtensionContext,
19
+ ToolInfo,
20
+ } from "@mariozechner/pi-coding-agent";
21
+ import { Type } from "@sinclair/typebox";
22
+ import {
23
+ buildTokenToolPayload,
24
+ TOKEN_COUNTER_ENCODING,
25
+ type TokenToolPayload,
26
+ } from "./core/token-counter.js";
27
+ import {
28
+ buildDocsCheckToolPayload,
29
+ buildGitCheckToolPayload,
30
+ buildPathQueryToolPayload,
31
+ buildStaticCheckToolPayload,
32
+ buildStructuredToolExecuteResult,
33
+ buildToolExecutionSection,
34
+ buildWorktreeMutationToolPayload,
35
+ buildWorktreeNameToolPayload,
36
+ normalizeToolFailure,
37
+ } from "./core/agent-tool-json.js";
38
+ import {
39
+ buildReferenceToolExecutionStderr,
40
+ buildReferenceToolPayload,
41
+ type ReferenceToolPayload,
42
+ } from "./core/reference-payload.js";
43
+ import {
44
+ buildCompressToolExecutionStderr,
45
+ buildCompressToolPayload,
46
+ type CompressToolPayload,
47
+ } from "./core/compress-payload.js";
48
+ import {
49
+ buildFindToolExecutionStderr,
50
+ buildFindToolPayload,
51
+ type FindToolPayload,
52
+ type FindToolScope,
53
+ } from "./core/find-payload.js";
54
+ import {
55
+ getDefaultConfig,
56
+ loadConfig,
57
+ normalizeConfigPaths,
58
+ saveConfig,
59
+ type StaticCheckEntry,
60
+ type UseReqConfig,
61
+ } from "./core/config.js";
62
+ import {
63
+ cyclePiNotifySoundLevel,
64
+ formatPiNotifyBeepStatus,
65
+ runPiNotifyEffects,
66
+ type PiNotifySoundLevel,
67
+ } from "./core/pi-notify.js";
68
+ import { buildRuntimePathContext, buildRuntimePathFacts } from "./core/path-context.js";
69
+ import { resolveRuntimeGitPath } from "./core/runtime-project-paths.js";
70
+ import { showPiUsereqSettingsMenu, type PiUsereqSettingsMenuChoice } from "./core/settings-menu.js";
71
+ import {
72
+ PI_USEREQ_STARTUP_TOOL_SET,
73
+ isPiUsereqEmbeddedToolName,
74
+ normalizeEnabledPiUsereqTools,
75
+ } from "./core/pi-usereq-tools.js";
76
+ import { renderPrompt } from "./core/prompts.js";
77
+ import { ensureBundledResourcesAccessible } from "./core/resources.js";
78
+ import {
79
+ PI_USEREQ_STATUS_HOOK_NAMES,
80
+ createPiUsereqStatusController,
81
+ disposePiUsereqStatusController,
82
+ renderPiUsereqStatus,
83
+ setPiUsereqStatusConfig,
84
+ updateExtensionStatus,
85
+ type PiUsereqStatusController,
86
+ type PiUsereqStatusHookName,
87
+ } from "./core/extension-status.js";
88
+ import {
89
+ collectSourceFiles,
90
+ runFilesStaticCheck,
91
+ runGetBasePath,
92
+ runGitCheck,
93
+ runGitPath,
94
+ runGitWtCreate,
95
+ runGitWtDelete,
96
+ runGitWtName,
97
+ runProjectStaticCheck,
98
+ } from "./core/tool-runner.js";
99
+ import { LANGUAGE_TAGS } from "./core/find-constructs.js";
100
+ import {
101
+ STATIC_CHECK_MODULES,
102
+ getSupportedStaticCheckLanguageSupport,
103
+ parseEnableStaticCheck,
104
+ } from "./core/static-check.js";
105
+ import { makeRelativeIfContainsProject, shellSplit } from "./core/utils.js";
106
+
107
+ /**
108
+ * @brief Lists bundled prompt commands exposed by the extension.
109
+ * @details Each entry maps to a `req-<name>` command that renders a bundled prompt and sends it to the active session. Access complexity is O(1).
110
+ */
111
+ const PROMPT_NAMES = [
112
+ "analyze",
113
+ "change",
114
+ "check",
115
+ "cover",
116
+ "create",
117
+ "fix",
118
+ "flowchart",
119
+ "implement",
120
+ "new",
121
+ "readme",
122
+ "recreate",
123
+ "refactor",
124
+ "references",
125
+ "renumber",
126
+ "workflow",
127
+ "write",
128
+ ] as const;
129
+
130
+ /**
131
+ * @brief Describes the optional shortcut-registration surface used by pi-usereq.
132
+ * @details Narrows the runtime API to the documented `registerShortcut(...)`
133
+ * method so the extension can remain compatible with offline harnesses that do
134
+ * not implement shortcut capture. Compile-time only and introduces no runtime
135
+ * cost.
136
+ */
137
+ interface PiShortcutRegistrar {
138
+ registerShortcut?: (
139
+ shortcut: string,
140
+ options: {
141
+ description?: string;
142
+ handler: (ctx: ExtensionCommandContext) => Promise<void> | void;
143
+ },
144
+ ) => void;
145
+ }
146
+
147
+ /**
148
+ * @brief Resolves the effective project base from a working directory.
149
+ * @details Normalizes the provided cwd into an absolute path without consulting configuration. Time complexity is O(1). No I/O side effects occur.
150
+ * @param[in] cwd {string} Current working directory.
151
+ * @return {string} Absolute project base path.
152
+ */
153
+ function getProjectBase(cwd: string): string {
154
+ return path.resolve(cwd);
155
+ }
156
+
157
+ /**
158
+ * @brief Builds the shared runtime path facts for the current command or tool context.
159
+ * @details Derives installation, execution, base, config, resource, docs, test, source, and optional git paths from the cwd-derived project configuration plus runtime-only repository probing, then converts them into prompt/tool-facing strings. Runtime is O(s + p) where s is configured source-directory count and p is aggregate path length. Side effects are limited to git subprocess execution.
160
+ * @param[in] cwd {string} Current working directory.
161
+ * @param[in] config {UseReqConfig} Effective project configuration.
162
+ * @return {import("./core/path-context.js").RuntimePathFacts} Shared runtime path facts.
163
+ * @satisfies REQ-145, REQ-146
164
+ */
165
+ function buildSharedRuntimePathFacts(cwd: string, config: UseReqConfig): import("./core/path-context.js").RuntimePathFacts {
166
+ const projectBase = getProjectBase(cwd);
167
+ const gitPath = resolveRuntimeGitPath(projectBase);
168
+ return buildRuntimePathFacts(buildRuntimePathContext(projectBase, config, { gitPath }));
169
+ }
170
+
171
+ /**
172
+ * @brief Loads project configuration for the extension runtime.
173
+ * @details Resolves the project base, loads persisted config, and normalizes configured directory paths without reading or persisting runtime-derived `base-path` or `git-path` metadata. Runtime is dominated by config I/O. Side effects are limited to filesystem reads.
174
+ * @param[in] cwd {string} Current working directory.
175
+ * @return {UseReqConfig} Effective project configuration.
176
+ * @satisfies REQ-030, REQ-145, REQ-146
177
+ */
178
+ function loadProjectConfig(cwd: string): UseReqConfig {
179
+ const projectBase = getProjectBase(cwd);
180
+ return normalizeConfigPaths(projectBase, loadConfig(projectBase));
181
+ }
182
+
183
+ /**
184
+ * @brief Persists project configuration from the extension runtime.
185
+ * @details Resolves the project base, normalizes configured directory paths into project-relative form, and delegates persistence to `saveConfig` without serializing runtime-derived path metadata. Runtime is O(n) in config size. Side effects include config-file writes.
186
+ * @param[in] cwd {string} Current working directory.
187
+ * @param[in] config {UseReqConfig} Configuration to persist.
188
+ * @return {void} No return value.
189
+ * @satisfies REQ-146
190
+ */
191
+ function saveProjectConfig(cwd: string, config: UseReqConfig): void {
192
+ const projectBase = getProjectBase(cwd);
193
+ saveConfig(projectBase, normalizeConfigPaths(projectBase, config));
194
+ }
195
+
196
+ /**
197
+ * @brief Collects the project-scoped static-check selection used by the agent tool.
198
+ * @details Resolves configured source plus test directories, reuses the same fixture-root exclusions as `runProjectStaticCheck`, and returns canonical relative file paths for structured payload emission. Runtime is O(F) plus project file-discovery cost. Side effects are limited to filesystem reads and git subprocesses delegated through `collectSourceFiles`.
199
+ * @param[in] projectBase {string} Resolved project base path.
200
+ * @param[in] config {UseReqConfig} Effective project configuration.
201
+ * @return {{ selectionDirectoryPaths: string[]; excludedDirectoryPaths: string[]; selectedPaths: string[] }} Structured static-check selection facts.
202
+ */
203
+ function collectProjectStaticCheckSelection(
204
+ projectBase: string,
205
+ config: UseReqConfig,
206
+ ): {
207
+ selectionDirectoryPaths: string[];
208
+ excludedDirectoryPaths: string[];
209
+ selectedPaths: string[];
210
+ } {
211
+ const selectionDirectoryPaths = [...config["src-dir"], config["tests-dir"]];
212
+ const testsDirRel = makeRelativeIfContainsProject(config["tests-dir"], projectBase)
213
+ .split(path.sep)
214
+ .join("/")
215
+ .replace(/^\.?\/?/, "")
216
+ .replace(/\/+$/, "");
217
+ const excludedDirectoryPaths = [...new Set([
218
+ "tests/fixtures",
219
+ testsDirRel ? `${testsDirRel}/fixtures` : "fixtures",
220
+ ])];
221
+ const selectedPaths = collectSourceFiles(selectionDirectoryPaths, projectBase)
222
+ .filter((filePath) => {
223
+ const relativePath = path.relative(projectBase, filePath).split(path.sep).join("/");
224
+ return !excludedDirectoryPaths.some((excludedDirectoryPath) => {
225
+ return relativePath === excludedDirectoryPath || relativePath.startsWith(`${excludedDirectoryPath}/`);
226
+ });
227
+ })
228
+ .map((filePath) => path.relative(projectBase, filePath).split(path.sep).join("/"));
229
+ return {
230
+ selectionDirectoryPaths,
231
+ excludedDirectoryPaths,
232
+ selectedPaths,
233
+ };
234
+ }
235
+
236
+ /**
237
+ * @brief Builds execution diagnostics for one token-tool payload.
238
+ * @details Serializes skipped-input and read-error observations into stable stderr lines while leaving successful counted files silent. Runtime is O(n) in issue count. No side effects occur.
239
+ * @param[in] payload {TokenToolPayload} Structured token payload.
240
+ * @return {string} Newline-delimited execution diagnostics.
241
+ */
242
+ function buildTokenToolExecutionStderr(payload: TokenToolPayload): string {
243
+ const skippedLines = payload.guidance.source_observations.skipped_inputs
244
+ .map((entry) => `skipped: ${entry.canonical_path}: ${entry.reason}`);
245
+ const errorLines = payload.guidance.source_observations.error_inputs
246
+ .map((entry) => `error: ${entry.canonical_path}: ${entry.reason}`);
247
+ return [...skippedLines, ...errorLines].join("\n");
248
+ }
249
+
250
+ /**
251
+ * @brief Builds the agent-oriented execute result returned by token-count tools.
252
+ * @details Mirrors the structured token payload into both the text `content` channel and the machine-readable `details` channel while isolating execution metadata under `execution`. Runtime is O(n) in payload size. No side effects occur.
253
+ * @param[in] payload {TokenToolPayload} Structured token payload.
254
+ * @return {{ content: Array<{ type: "text"; text: string }>; details: TokenToolPayload & { execution: { code: number; stderr: string } } }} Token-tool execute result.
255
+ * @satisfies REQ-069, REQ-070, REQ-071, REQ-073, REQ-074, REQ-075, REQ-099, REQ-102
256
+ */
257
+ function buildTokenToolExecuteResult(
258
+ payload: TokenToolPayload,
259
+ ): {
260
+ content: Array<{ type: "text"; text: string }>;
261
+ details: TokenToolPayload & { execution: { code: number; stderr: string } };
262
+ } {
263
+ const details = {
264
+ request: payload.request,
265
+ summary: payload.summary,
266
+ files: payload.files,
267
+ guidance: payload.guidance,
268
+ execution: {
269
+ code: payload.summary.counted_file_count > 0 ? 0 : 1,
270
+ stderr: buildTokenToolExecutionStderr(payload),
271
+ },
272
+ };
273
+ return buildStructuredToolExecuteResult(details);
274
+ }
275
+
276
+ /**
277
+ * @brief Builds the agent-oriented execute result returned by references tools.
278
+ * @details Mirrors the structured references payload into both the text `content` channel and the machine-readable `details` channel while isolating execution metadata under `execution`. Runtime is O(n) in payload size. No side effects occur.
279
+ * @param[in] payload {ReferenceToolPayload} Structured references payload.
280
+ * @return {{ content: Array<{ type: "text"; text: string }>; details: ReferenceToolPayload & { execution: { code: number; stderr: string } } }} References-tool execute result.
281
+ * @satisfies REQ-076, REQ-077, REQ-078, REQ-079, REQ-099, REQ-102
282
+ */
283
+ function buildReferenceToolExecuteResult(
284
+ payload: ReferenceToolPayload,
285
+ ): {
286
+ content: Array<{ type: "text"; text: string }>;
287
+ details: ReferenceToolPayload & { execution: { code: number; stderr: string } };
288
+ } {
289
+ const details = {
290
+ request: payload.request,
291
+ summary: payload.summary,
292
+ repository: payload.repository,
293
+ files: payload.files,
294
+ execution: {
295
+ code: payload.summary.analyzed_file_count > 0 ? 0 : 1,
296
+ stderr: buildReferenceToolExecutionStderr(payload),
297
+ },
298
+ };
299
+ return buildStructuredToolExecuteResult(details);
300
+ }
301
+
302
+ /**
303
+ * @brief Builds the agent-oriented execute result returned by compression tools.
304
+ * @details Mirrors the structured compression payload into both the text `content` channel and the machine-readable `details` channel while isolating execution metadata under `execution`. Runtime is O(n) in payload size. No side effects occur.
305
+ * @param[in] payload {CompressToolPayload} Structured compression payload.
306
+ * @return {{ content: Array<{ type: "text"; text: string }>; details: CompressToolPayload & { execution: { code: number; stderr: string } } }} Compression-tool execute result.
307
+ * @satisfies REQ-081, REQ-082, REQ-083, REQ-084, REQ-085, REQ-087, REQ-088, REQ-099, REQ-102
308
+ */
309
+ function buildCompressionToolExecuteResult(
310
+ payload: CompressToolPayload,
311
+ ): {
312
+ content: Array<{ type: "text"; text: string }>;
313
+ details: CompressToolPayload & { execution: { code: number; stderr: string } };
314
+ } {
315
+ const details = {
316
+ request: payload.request,
317
+ summary: payload.summary,
318
+ repository: payload.repository,
319
+ files: payload.files,
320
+ execution: {
321
+ code: payload.summary.compressed_file_count > 0 ? 0 : 1,
322
+ stderr: buildCompressToolExecutionStderr(payload),
323
+ },
324
+ };
325
+ return buildStructuredToolExecuteResult(details);
326
+ }
327
+
328
+ /**
329
+ * @brief Maps find-payload language identifiers to stable registration labels.
330
+ * @details Preserves the canonical capitalization used by tool descriptions so supported-tag guidance remains deterministic across inspection snapshots. Access complexity is O(1).
331
+ */
332
+ const FIND_TOOL_LANGUAGE_LABELS: Record<string, string> = {
333
+ c: "C",
334
+ cpp: "Cpp",
335
+ csharp: "Csharp",
336
+ elixir: "Elixir",
337
+ go: "Go",
338
+ haskell: "Haskell",
339
+ java: "Java",
340
+ javascript: "Javascript",
341
+ kotlin: "Kotlin",
342
+ lua: "Lua",
343
+ perl: "Perl",
344
+ php: "Php",
345
+ python: "Python",
346
+ ruby: "Ruby",
347
+ rust: "Rust",
348
+ scala: "Scala",
349
+ shell: "Shell",
350
+ swift: "Swift",
351
+ typescript: "Typescript",
352
+ zig: "Zig",
353
+ };
354
+
355
+ /**
356
+ * @brief Defines the stable language order used by find-tool supported-tag guidance.
357
+ * @details Keeps registration descriptions aligned with the repository-supported language matrix and preserves deterministic inspection snapshots. Access complexity is O(1).
358
+ */
359
+ const FIND_TOOL_LANGUAGE_ORDER = [
360
+ "c",
361
+ "cpp",
362
+ "csharp",
363
+ "elixir",
364
+ "go",
365
+ "haskell",
366
+ "java",
367
+ "javascript",
368
+ "kotlin",
369
+ "lua",
370
+ "perl",
371
+ "php",
372
+ "python",
373
+ "ruby",
374
+ "rust",
375
+ "scala",
376
+ "shell",
377
+ "swift",
378
+ "typescript",
379
+ "zig",
380
+ ] as const;
381
+
382
+ /**
383
+ * @brief Builds the supported-tag guidance lines embedded in find-tool registrations.
384
+ * @details Emits one deterministic line per supported language containing its canonical registration label and sorted tag list so downstream agents can specialize requests without invoking the tool first. Runtime is O(l * t log t). No side effects occur.
385
+ * @return {string[]} Supported-tag guidance lines.
386
+ */
387
+ function buildFindToolSupportedTagGuidelines(): string[] {
388
+ return FIND_TOOL_LANGUAGE_ORDER
389
+ .filter((language) => language in LANGUAGE_TAGS)
390
+ .map((language) => `Supported tags [${FIND_TOOL_LANGUAGE_LABELS[language]}]: ${[...LANGUAGE_TAGS[language]!].sort().join(", ")}`);
391
+ }
392
+
393
+ /**
394
+ * @brief Builds the schema description for one find-tool registration.
395
+ * @details Specializes the input-scope sentence for explicit-file or configured-directory searches while keeping the JSON output contract stable and fully machine-readable. Runtime is O(1). No side effects occur.
396
+ * @param[in] scope {FindToolScope} Find-tool scope.
397
+ * @return {string} Parameter-schema description.
398
+ */
399
+ function buildFindToolSchemaDescription(scope: FindToolScope): string {
400
+ const inputContract = scope === "explicit-files"
401
+ ? "Input contract: tag + pattern + files[] + optional enableLineNumbers."
402
+ : "Input contract: tag + pattern + optional enableLineNumbers. Scope is the configured src-dir list resolved from the current project configuration.";
403
+ return `${inputContract} Output contract: JSON object with request, summary, repository, files, and execution. Repository exposes file_canonical_paths and supported_tags_by_language. File entries expose path facts, supported_tags, structured statuses, file_doxygen, and match records with typed line ranges, stripped code lines, and structured Doxygen fields. Regex matches construct names only.`;
404
+ }
405
+
406
+ /**
407
+ * @brief Builds the prompt-guideline set for one find-tool registration.
408
+ * @details Encodes scope selection, output schema, regex semantics, line-number behavior, tag-filter rules, and the full language-to-tag matrix as stable agent-oriented strings. Runtime is O(l * t log t). No side effects occur.
409
+ * @param[in] scope {FindToolScope} Find-tool scope.
410
+ * @return {string[]} Prompt-guideline strings.
411
+ */
412
+ function buildFindToolPromptGuidelines(scope: FindToolScope): string[] {
413
+ const scopeLine = scope === "explicit-files"
414
+ ? "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute."
415
+ : "Scope: resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.";
416
+ const outputLine = scope === "explicit-files"
417
+ ? "Output contract: request + summary + repository + files + execution. Repository exposes requested file scope and supported_tags_by_language; file entries expose status, supported_tags, line ranges, file_doxygen, and matches; match entries expose symbol_kind, signature_text, line ranges, code_lines, stripped_source_text, and structured Doxygen fields."
418
+ : "Output contract: request + summary + repository + files + execution. Repository exposes source_directory_paths, file_canonical_paths, and supported_tags_by_language; file entries expose status, supported_tags, line ranges, file_doxygen, and matches; match entries expose symbol_kind, signature_text, line ranges, code_lines, stripped_source_text, and structured Doxygen fields.";
419
+ return [
420
+ scopeLine,
421
+ outputLine,
422
+ "Regex rule: pattern is applied to construct names only with JavaScript RegExp search semantics; it never matches construct bodies; use ^...$ for exact-name matching.",
423
+ "Tag rule: tag is pipe-separated and case-insensitive; unsupported tags are ignored; if no valid tag remains, request.tag_filter_status becomes invalid.",
424
+ "Line-number behavior: enableLineNumbers changes only display_text and stripped_source_text rendering; numeric source_line_number and line_range facts remain dedicated fields.",
425
+ "Failure contract: invalid tag filters, invalid regex patterns, unsupported extensions, unsupported tag-language combinations, no-match files, and analysis failures are surfaced as structured statuses plus optional execution.stderr diagnostics.",
426
+ ...buildFindToolSupportedTagGuidelines(),
427
+ ];
428
+ }
429
+
430
+ /**
431
+ * @brief Builds the agent-oriented execute result returned by find tools.
432
+ * @details Mirrors the structured find payload into both the text `content` channel and the machine-readable `details` channel while isolating execution metadata under `execution`. Runtime is O(n) in payload size. No side effects occur.
433
+ * @param[in] payload {FindToolPayload} Structured find payload.
434
+ * @return {{ content: Array<{ type: "text"; text: string }>; details: FindToolPayload & { execution: { code: number; stderr: string } } }} Find-tool execute result.
435
+ * @satisfies REQ-089, REQ-090, REQ-091, REQ-092, REQ-093, REQ-094, REQ-097, REQ-098, REQ-099, REQ-102
436
+ */
437
+ function buildFindToolExecuteResult(
438
+ payload: FindToolPayload,
439
+ ): {
440
+ content: Array<{ type: "text"; text: string }>;
441
+ details: FindToolPayload & { execution: { code: number; stderr: string } };
442
+ } {
443
+ const stderr = buildFindToolExecutionStderr(payload);
444
+ const details = {
445
+ request: payload.request,
446
+ summary: payload.summary,
447
+ repository: payload.repository,
448
+ files: payload.files,
449
+ execution: {
450
+ code: payload.summary.search_status === "matched" ? 0 : 1,
451
+ stderr,
452
+ },
453
+ };
454
+ return buildStructuredToolExecuteResult(details);
455
+ }
456
+
457
+ /**
458
+ * @brief Delivers one rendered prompt into the active session.
459
+ * @details Writes the rendered prompt directly through `pi.sendUserMessage(...)` without creating replacement sessions or pre-reset flows. Runtime is O(n) in prompt length. Side effects are limited to user-message delivery.
460
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
461
+ * @param[in] content {string} Rendered prompt markdown.
462
+ * @return {Promise<void>} Promise resolved after the prompt is queued for delivery.
463
+ * @satisfies REQ-004, REQ-067, REQ-068
464
+ */
465
+ async function deliverPromptCommand(pi: ExtensionAPI, content: string): Promise<void> {
466
+ pi.sendUserMessage(content);
467
+ }
468
+
469
+ /**
470
+ * @brief Returns the configurable active-tool inventory visible to the extension.
471
+ * @details Filters runtime tools against the canonical configurable-tool set, thereby combining extension-owned tools with supported embedded pi CLI tools. Output order is sorted by tool name. Runtime is O(t log t). No external state is mutated.
472
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
473
+ * @return {ToolInfo[]} Sorted configurable tool descriptors.
474
+ * @satisfies REQ-007, REQ-063
475
+ */
476
+ function getPiUsereqStartupTools(pi: ExtensionAPI): ToolInfo[] {
477
+ return pi.getAllTools()
478
+ .filter((tool) => PI_USEREQ_STARTUP_TOOL_SET.has(tool.name))
479
+ .filter((tool) => !isPiUsereqEmbeddedToolName(tool.name) || tool.sourceInfo?.source === "builtin")
480
+ .sort((left, right) => left.name.localeCompare(right.name));
481
+ }
482
+
483
+ /**
484
+ * @brief Normalizes and returns the configured enabled active tools.
485
+ * @details Reuses repository normalization rules, updates the config object in place, and returns the normalized array. Runtime is O(n) in configured tool count. Side effect: mutates `config["enabled-tools"]`.
486
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
487
+ * @return {string[]} Normalized enabled tool names.
488
+ */
489
+ function getConfiguredEnabledPiUsereqTools(config: UseReqConfig): string[] {
490
+ const enabledTools = normalizeEnabledPiUsereqTools(config["enabled-tools"]);
491
+ config["enabled-tools"] = [...enabledTools];
492
+ return enabledTools;
493
+ }
494
+
495
+ /**
496
+ * @brief Classifies one configurable tool as embedded or extension-owned.
497
+ * @details Uses the runtime `sourceInfo.source` field plus the supported embedded-name subset to produce one stable UI label. Runtime is O(1). No external state is mutated.
498
+ * @param[in] tool {ToolInfo} Runtime tool descriptor.
499
+ * @return {"builtin" | "extension"} Stable tool-kind label.
500
+ */
501
+ function getPiUsereqToolKind(tool: ToolInfo): "builtin" | "extension" {
502
+ if (tool.sourceInfo?.source === "builtin" && isPiUsereqEmbeddedToolName(tool.name)) {
503
+ return "builtin";
504
+ }
505
+ return "extension";
506
+ }
507
+
508
+ /**
509
+ * @brief Applies the configured active-tool enablement to the current session.
510
+ * @details Preserves non-configurable active tools, removes every configurable tool from the active set, then re-adds only configured tools that exist in the current runtime inventory. Runtime is O(t). Side effects include `pi.setActiveTools(...)`.
511
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
512
+ * @param[in] config {UseReqConfig} Effective project configuration.
513
+ * @return {void} No return value.
514
+ * @satisfies REQ-009, REQ-064
515
+ */
516
+ function applyConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig): void {
517
+ const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
518
+ const allTools = pi.getAllTools();
519
+ const configurableTools = getPiUsereqStartupTools(pi);
520
+ const allToolNames = new Set(allTools.map((tool) => tool.name));
521
+ const nextActive = new Set(pi.getActiveTools().filter((toolName) => allToolNames.has(toolName)));
522
+
523
+ for (const tool of configurableTools) {
524
+ nextActive.delete(tool.name);
525
+ }
526
+ for (const tool of configurableTools) {
527
+ if (enabledTools.has(tool.name)) {
528
+ nextActive.add(tool.name);
529
+ }
530
+ }
531
+
532
+ pi.setActiveTools(allTools.map((tool) => tool.name).filter((toolName) => nextActive.has(toolName)));
533
+ }
534
+
535
+ /**
536
+ * @brief Handles one intercepted pi lifecycle hook for pi-usereq status updates.
537
+ * @details Applies session-start-specific resource validation, project-config
538
+ * refresh, and startup-tool enablement before forwarding the originating hook
539
+ * name and payload into the shared `updateExtensionStatus(...)` pipeline.
540
+ * On `agent_end`, also dispatches configured pi-notify beep and sound effects.
541
+ * Runtime is dominated by configuration loading during `session_start`; all
542
+ * other hooks are O(1). Side effects include resource checks, active-tool
543
+ * mutation, status updates, live-ticker disposal on shutdown, stdout writes,
544
+ * and optional child-process spawning.
545
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
546
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
547
+ * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
548
+ * @param[in] event {unknown} Hook payload forwarded by pi.
549
+ * @param[in] ctx {ExtensionContext} Active extension context.
550
+ * @return {Promise<void>} Promise resolved when hook processing completes.
551
+ * @satisfies REQ-117, REQ-118, REQ-119, REQ-129, REQ-130, REQ-131, REQ-132, REQ-133
552
+ */
553
+ async function handleExtensionStatusEvent(
554
+ pi: ExtensionAPI,
555
+ statusController: PiUsereqStatusController,
556
+ hookName: PiUsereqStatusHookName,
557
+ event: unknown,
558
+ ctx: ExtensionContext,
559
+ ): Promise<void> {
560
+ if (hookName === "session_start") {
561
+ ensureBundledResourcesAccessible();
562
+ const config = loadProjectConfig(ctx.cwd);
563
+ applyConfiguredPiUsereqTools(pi, config);
564
+ setPiUsereqStatusConfig(statusController, config);
565
+ }
566
+ updateExtensionStatus(statusController, hookName, event, ctx);
567
+ if (hookName === "agent_end" && statusController.config) {
568
+ runPiNotifyEffects(statusController.config, event as { messages: AgentEndEvent["messages"] });
569
+ }
570
+ if (hookName === "session_shutdown") {
571
+ disposePiUsereqStatusController(statusController);
572
+ }
573
+ }
574
+
575
+ /**
576
+ * @brief Registers shared wrappers for every supported pi lifecycle hook.
577
+ * @details Installs one generic wrapper per intercepted hook so every resource,
578
+ * session, agent, model, tool, bash, and input event is routed through the
579
+ * same extension-status update pipeline. Runtime is O(h) in registered hook
580
+ * count. Side effects include hook registration.
581
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
582
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
583
+ * @return {void} No return value.
584
+ * @satisfies DES-002, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117
585
+ */
586
+ function registerExtensionStatusHooks(
587
+ pi: ExtensionAPI,
588
+ statusController: PiUsereqStatusController,
589
+ ): void {
590
+ const registerHook = pi.on.bind(pi) as (
591
+ event: PiUsereqStatusHookName,
592
+ handler: (event: unknown, ctx: ExtensionContext) => Promise<void>,
593
+ ) => void;
594
+ for (const hookName of PI_USEREQ_STATUS_HOOK_NAMES) {
595
+ registerHook(hookName, async (event, ctx) => {
596
+ await handleExtensionStatusEvent(pi, statusController, hookName, event, ctx);
597
+ });
598
+ }
599
+ }
600
+
601
+ /**
602
+ * @brief Replaces the configured active-tool selection and applies it immediately.
603
+ * @details Normalizes the requested tool names, stores them in config, and synchronizes the active tool set with runtime registration state. Runtime is O(n + t). Side effect: mutates config and active tools.
604
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
605
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
606
+ * @param[in] enabledTools {string[]} Requested enabled tool names.
607
+ * @return {void} No return value.
608
+ */
609
+ function setConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig, enabledTools: string[]): void {
610
+ config["enabled-tools"] = normalizeEnabledPiUsereqTools(enabledTools);
611
+ applyConfiguredPiUsereqTools(pi, config);
612
+ }
613
+
614
+ /**
615
+ * @brief Renders a textual reference for configurable-tool configuration and runtime state.
616
+ * @details Lists every configurable tool with configured enablement, runtime activation, builtin-versus-extension classification, source metadata, and optional descriptions. Runtime is O(t). No side effects occur.
617
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
618
+ * @param[in] config {UseReqConfig} Effective project configuration.
619
+ * @return {string} Multiline tool-status report.
620
+ */
621
+ function renderPiUsereqToolsReference(pi: ExtensionAPI, config: UseReqConfig): string {
622
+ const tools = getPiUsereqStartupTools(pi);
623
+ const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
624
+ const activeTools = new Set(pi.getActiveTools());
625
+ const lines = [
626
+ "# configurable active tools",
627
+ "",
628
+ `Configured enabled tools: ${enabledTools.size}/${tools.length}`,
629
+ `Currently active tools: ${tools.filter((tool) => activeTools.has(tool.name)).length}/${tools.length}`,
630
+ "",
631
+ "Tools:",
632
+ ];
633
+
634
+ for (const tool of tools) {
635
+ const configured = enabledTools.has(tool.name) ? "enabled" : "disabled";
636
+ const active = activeTools.has(tool.name) ? "active" : "inactive";
637
+ const source = tool.sourceInfo ? `${tool.sourceInfo.source}:${tool.sourceInfo.path}` : "unknown";
638
+ lines.push(`- ${tool.name}`);
639
+ lines.push(` kind: ${getPiUsereqToolKind(tool)}`);
640
+ lines.push(` configured: ${configured}`);
641
+ lines.push(` runtime: ${active}`);
642
+ lines.push(` source: ${source}`);
643
+ if (tool.description) {
644
+ lines.push(` description: ${tool.description}`);
645
+ }
646
+ }
647
+
648
+ return `${lines.join("\n")}\n`;
649
+ }
650
+
651
+ /**
652
+ * @brief Represents one persisted pi-notify beep flag key.
653
+ * @details Restricts menu toggles to the three independent prompt-end beep
654
+ * flags stored in project configuration. Compile-time only and introduces no
655
+ * runtime cost.
656
+ */
657
+ type PiNotifyBeepConfigKey =
658
+ | "notify-beep-on-end"
659
+ | "notify-beep-on-esc"
660
+ | "notify-beep-on-error";
661
+
662
+ /**
663
+ * @brief Flips one persisted pi-notify beep flag.
664
+ * @details Negates the selected prompt-end beep flag in place and returns the resulting boolean value so callers can emit deterministic UI feedback. Runtime is O(1). Side effect: mutates `config`.
665
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
666
+ * @param[in] key {PiNotifyBeepConfigKey} Beep flag key to toggle.
667
+ * @return {boolean} Next enabled state.
668
+ */
669
+ function togglePiNotifyBeepFlag(config: UseReqConfig, key: PiNotifyBeepConfigKey): boolean {
670
+ config[key] = !config[key];
671
+ return config[key];
672
+ }
673
+
674
+ /**
675
+ * @brief Builds the shared settings-menu choices for notification configuration.
676
+ * @details Serializes the current beep flags, selected notify command, hotkey bind, and per-level notify commands into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(1) plus command-length formatting. No external state is mutated.
677
+ * @param[in] config {UseReqConfig} Effective project configuration.
678
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered notification-menu choice vector.
679
+ * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152
680
+ */
681
+ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
682
+ return [
683
+ {
684
+ id: "toggle-beep-on-success",
685
+ label: "Toggle beep on success",
686
+ value: config["notify-beep-on-end"] ? "on" : "off",
687
+ description: "Toggle terminal beep delivery for successful prompt completion.",
688
+ },
689
+ {
690
+ id: "toggle-beep-on-escape",
691
+ label: "Toggle beep on escape",
692
+ value: config["notify-beep-on-esc"] ? "on" : "off",
693
+ description: "Toggle terminal beep delivery for escape-triggered prompt abortion.",
694
+ },
695
+ {
696
+ id: "toggle-beep-on-error",
697
+ label: "Toggle beep on error",
698
+ value: config["notify-beep-on-error"] ? "on" : "off",
699
+ description: "Toggle terminal beep delivery for error-terminated prompt completion.",
700
+ },
701
+ {
702
+ id: "selected-notify-command",
703
+ label: "Selected notify command",
704
+ value: config["notify-sound"],
705
+ description: "Select which notify command runs after successful prompt completion.",
706
+ },
707
+ {
708
+ id: "sound-toggle-hotkey-bind",
709
+ label: "Sound toggle hotkey bind",
710
+ value: config["notify-sound-toggle-shortcut"],
711
+ description: "Edit the keyboard shortcut that cycles the selected notify command.",
712
+ },
713
+ {
714
+ id: "notify-command-low",
715
+ label: "Notify command (low vol.)",
716
+ value: config.PI_NOTIFY_SOUND_LOW_CMD,
717
+ description: "Edit the shell command used when the selected notify command is `low`.",
718
+ },
719
+ {
720
+ id: "notify-command-mid",
721
+ label: "Notify command (mid vol.)",
722
+ value: config.PI_NOTIFY_SOUND_MID_CMD,
723
+ description: "Edit the shell command used when the selected notify command is `mid`.",
724
+ },
725
+ {
726
+ id: "notify-command-high",
727
+ label: "Notify command (high vol.)",
728
+ value: config.PI_NOTIFY_SOUND_HIGH_CMD,
729
+ description: "Edit the shell command used when the selected notify command is `high`.",
730
+ },
731
+ {
732
+ id: "back",
733
+ label: "Back",
734
+ value: "",
735
+ description: "Return to the parent configuration menu.",
736
+ },
737
+ ];
738
+ }
739
+
740
+ /**
741
+ * @brief Opens the shared settings-menu selector for the selected notify command.
742
+ * @details Reuses the pi-usereq settings-menu renderer so notify-command selection remains stylistically aligned with the main configuration UI and returns the chosen sound level or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
743
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
744
+ * @param[in] currentLevel {PiNotifySoundLevel} Currently selected notify command.
745
+ * @return {Promise<PiNotifySoundLevel | undefined>} Selected sound level or `undefined` when cancelled.
746
+ * @satisfies REQ-131, REQ-137, REQ-149, REQ-151, REQ-152, REQ-153, REQ-154
747
+ */
748
+ async function selectPiNotifySoundLevel(
749
+ ctx: ExtensionCommandContext,
750
+ currentLevel: PiNotifySoundLevel,
751
+ ): Promise<PiNotifySoundLevel | undefined> {
752
+ const choice = await showPiUsereqSettingsMenu(ctx, "Selected notify command", [
753
+ {
754
+ id: "none",
755
+ label: "none",
756
+ value: currentLevel === "none" ? "selected" : "",
757
+ description: "Disable external notify-command execution.",
758
+ },
759
+ {
760
+ id: "low",
761
+ label: "low",
762
+ value: currentLevel === "low" ? "selected" : "",
763
+ description: "Run the low-volume notify command after successful completion.",
764
+ },
765
+ {
766
+ id: "mid",
767
+ label: "mid",
768
+ value: currentLevel === "mid" ? "selected" : "",
769
+ description: "Run the mid-volume notify command after successful completion.",
770
+ },
771
+ {
772
+ id: "high",
773
+ label: "high",
774
+ value: currentLevel === "high" ? "selected" : "",
775
+ description: "Run the high-volume notify command after successful completion.",
776
+ },
777
+ ]);
778
+ return choice ? choice as PiNotifySoundLevel : undefined;
779
+ }
780
+
781
+ /**
782
+ * @brief Runs the interactive notification-configuration menu.
783
+ * @details Exposes prompt-end beep toggles, selected notify-command selection, hotkey-bind editing, and per-level notify-command editors through the shared settings-menu renderer. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
784
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
785
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
786
+ * @return {Promise<boolean>} `true` when the sound-toggle shortcut changed.
787
+ * @satisfies REQ-129, REQ-131, REQ-133, REQ-134, REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154
788
+ */
789
+ async function configurePiNotifyMenu(
790
+ ctx: ExtensionCommandContext,
791
+ config: UseReqConfig,
792
+ ): Promise<boolean> {
793
+ const originalShortcut = config["notify-sound-toggle-shortcut"];
794
+ while (true) {
795
+ const choice = await showPiUsereqSettingsMenu(ctx, "notifications", buildPiNotifyMenuChoices(config));
796
+ if (!choice || choice === "back") {
797
+ return config["notify-sound-toggle-shortcut"] !== originalShortcut;
798
+ }
799
+ if (choice === "toggle-beep-on-success") {
800
+ const enabled = togglePiNotifyBeepFlag(config, "notify-beep-on-end");
801
+ ctx.ui.notify(`Beep on success ${enabled ? "enabled" : "disabled"}`, "info");
802
+ continue;
803
+ }
804
+ if (choice === "toggle-beep-on-escape") {
805
+ const enabled = togglePiNotifyBeepFlag(config, "notify-beep-on-esc");
806
+ ctx.ui.notify(`Beep on escape ${enabled ? "enabled" : "disabled"}`, "info");
807
+ continue;
808
+ }
809
+ if (choice === "toggle-beep-on-error") {
810
+ const enabled = togglePiNotifyBeepFlag(config, "notify-beep-on-error");
811
+ ctx.ui.notify(`Beep on error ${enabled ? "enabled" : "disabled"}`, "info");
812
+ continue;
813
+ }
814
+ if (choice === "selected-notify-command") {
815
+ const nextLevel = await selectPiNotifySoundLevel(ctx, config["notify-sound"]);
816
+ if (nextLevel) {
817
+ config["notify-sound"] = nextLevel;
818
+ ctx.ui.notify(`Selected notify command set to ${nextLevel}`, "info");
819
+ }
820
+ continue;
821
+ }
822
+ if (choice === "sound-toggle-hotkey-bind") {
823
+ const value = await ctx.ui.input("Sound toggle hotkey bind", config["notify-sound-toggle-shortcut"]);
824
+ if (value?.trim()) {
825
+ config["notify-sound-toggle-shortcut"] = value.trim();
826
+ ctx.ui.notify(`Sound toggle hotkey bind set to ${config["notify-sound-toggle-shortcut"]}`, "info");
827
+ }
828
+ continue;
829
+ }
830
+ if (choice === "notify-command-low") {
831
+ const value = await ctx.ui.input("Notify command (low vol.)", config.PI_NOTIFY_SOUND_LOW_CMD);
832
+ if (value?.trim()) {
833
+ config.PI_NOTIFY_SOUND_LOW_CMD = value.trim();
834
+ ctx.ui.notify("Updated notify command (low vol.)", "info");
835
+ }
836
+ continue;
837
+ }
838
+ if (choice === "notify-command-mid") {
839
+ const value = await ctx.ui.input("Notify command (mid vol.)", config.PI_NOTIFY_SOUND_MID_CMD);
840
+ if (value?.trim()) {
841
+ config.PI_NOTIFY_SOUND_MID_CMD = value.trim();
842
+ ctx.ui.notify("Updated notify command (mid vol.)", "info");
843
+ }
844
+ continue;
845
+ }
846
+ if (choice === "notify-command-high") {
847
+ const value = await ctx.ui.input("Notify command (high vol.)", config.PI_NOTIFY_SOUND_HIGH_CMD);
848
+ if (value?.trim()) {
849
+ config.PI_NOTIFY_SOUND_HIGH_CMD = value.trim();
850
+ ctx.ui.notify("Updated notify command (high vol.)", "info");
851
+ }
852
+ }
853
+ }
854
+ }
855
+
856
+ /**
857
+ * @brief Registers the configurable successful-run sound shortcut when supported.
858
+ * @details Loads the current project config, registers one raw pi shortcut when
859
+ * the runtime exposes `registerShortcut(...)`, cycles persisted sound state on
860
+ * invocation, saves the config, refreshes the status bar, and emits one info
861
+ * notification. Runtime is O(1) for registration plus config I/O per shortcut
862
+ * use. Side effects include shortcut registration, config writes, and status
863
+ * updates.
864
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
865
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
866
+ * @return {void} No return value.
867
+ * @satisfies REQ-131, REQ-134, REQ-136
868
+ */
869
+ function registerPiNotifyShortcut(
870
+ pi: ExtensionAPI,
871
+ statusController: PiUsereqStatusController,
872
+ ): void {
873
+ const shortcutRegistrar = pi as ExtensionAPI & PiShortcutRegistrar;
874
+ if (typeof shortcutRegistrar.registerShortcut !== "function") {
875
+ return;
876
+ }
877
+ const config = loadProjectConfig(process.cwd());
878
+ shortcutRegistrar.registerShortcut(config["notify-sound-toggle-shortcut"], {
879
+ description: "Cycle pi-usereq prompt-success sound level",
880
+ handler: async (ctx) => {
881
+ const nextConfig = loadProjectConfig(ctx.cwd);
882
+ nextConfig["notify-sound"] = cyclePiNotifySoundLevel(nextConfig["notify-sound"]);
883
+ saveProjectConfig(ctx.cwd, nextConfig);
884
+ setPiUsereqStatusConfig(statusController, nextConfig);
885
+ renderPiUsereqStatus(statusController, ctx);
886
+ ctx.ui.notify(`pi-usereq sound:${nextConfig["notify-sound"]}`, "info");
887
+ },
888
+ });
889
+ }
890
+
891
+ /**
892
+ * @brief Registers bundled prompt commands with the extension.
893
+ * @details Creates one `req-<prompt>` command per bundled prompt name. Each handler ensures resources exist, renders the prompt, and sends it into the current active session. Runtime is O(p) for registration; handler cost depends on prompt rendering plus prompt dispatch. Side effects include command registration and user-message delivery during execution.
894
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
895
+ * @return {void} No return value.
896
+ * @satisfies REQ-004, REQ-067, REQ-068
897
+ */
898
+ function registerPromptCommands(pi: ExtensionAPI): void {
899
+ PROMPT_NAMES.forEach((promptName) => {
900
+ pi.registerCommand(`req-${promptName}`, {
901
+ description: `Run pi-usereq prompt ${promptName}`,
902
+ handler: async (args, ctx) => {
903
+ ensureBundledResourcesAccessible();
904
+ const projectBase = getProjectBase(ctx.cwd);
905
+ const config = loadProjectConfig(ctx.cwd);
906
+ const content = renderPrompt(promptName, args, projectBase, config);
907
+ await deliverPromptCommand(pi, content);
908
+ },
909
+ });
910
+ });
911
+ }
912
+
913
+
914
+ /**
915
+ * @brief Registers pi-usereq agent tools exposed to the model.
916
+ * @details Defines the tool schemas, prompt metadata, and execution handlers that bridge extension tool calls into tool-runner operations without registering duplicate custom slash commands for the same capabilities. Runtime is O(t) for registration; execution cost depends on the selected tool. Side effects include tool registration.
917
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
918
+ * @return {void} No return value.
919
+ * @satisfies REQ-005, REQ-010, REQ-011, REQ-014, REQ-017, REQ-044, REQ-045, REQ-069, REQ-070, REQ-071, REQ-072, REQ-073, REQ-074, REQ-075, REQ-076, REQ-077, REQ-078, REQ-079, REQ-080, REQ-089, REQ-090, REQ-091, REQ-092, REQ-093, REQ-094, REQ-095, REQ-096, REQ-097, REQ-098, REQ-099, REQ-100, REQ-101, REQ-102
920
+ */
921
+ function registerAgentTools(pi: ExtensionAPI): void {
922
+ const gitPathSchema = Type.Object(
923
+ {},
924
+ {
925
+ description: "Input contract: no params. Output contract: JSON object with request, result, and execution. Result exposes path_key, path_value, and path_present for the cwd-derived runtime git root.",
926
+ },
927
+ );
928
+ pi.registerTool({
929
+ name: "git-path",
930
+ label: "git-path",
931
+ description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the resolved `git-path` value through direct-access fields instead of text-only output.",
932
+ promptSnippet: "Return the structured runtime git-root payload for the current project.",
933
+ promptGuidelines: [
934
+ "Input contract: no params. Scope is the cwd-derived runtime path context.",
935
+ "Output contract: request + result + execution. Result exposes path_key, path_value, and path_present.",
936
+ "Behavior contract: git-path is derived at runtime from the current working directory and repository ancestry rules.",
937
+ "Failure contract: configuration-loading failures surface through execution.code and execution.stderr_lines.",
938
+ ],
939
+ parameters: gitPathSchema,
940
+ async execute() {
941
+ ensureBundledResourcesAccessible();
942
+ const projectBase = getProjectBase(process.cwd());
943
+ const config = loadProjectConfig(process.cwd());
944
+ const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
945
+ const result = runGitPath(projectBase, config);
946
+ const payload = buildPathQueryToolPayload(
947
+ "git-path",
948
+ process.cwd(),
949
+ projectBase,
950
+ result.stdout.trimEnd(),
951
+ runtimePaths,
952
+ buildToolExecutionSection(result),
953
+ );
954
+ return buildStructuredToolExecuteResult(payload);
955
+ },
956
+ });
957
+
958
+ const basePathSchema = Type.Object(
959
+ {},
960
+ {
961
+ description: "Input contract: no params. Output contract: JSON object with request, result, and execution. Result exposes path_key, path_value, and path_present for the cwd-derived runtime base path.",
962
+ },
963
+ );
964
+ pi.registerTool({
965
+ name: "get-base-path",
966
+ label: "get-base-path",
967
+ description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the resolved `base-path` value through direct-access fields instead of text-only output.",
968
+ promptSnippet: "Return the structured runtime project-base payload.",
969
+ promptGuidelines: [
970
+ "Input contract: no params. Scope is the cwd-derived runtime path context.",
971
+ "Output contract: request + result + execution. Result exposes path_key, path_value, and path_present.",
972
+ "Behavior contract: base-path equals the current working directory used by the extension command or tool.",
973
+ "Failure contract: configuration-loading failures surface through execution.code and execution.stderr_lines.",
974
+ ],
975
+ parameters: basePathSchema,
976
+ async execute() {
977
+ const projectBase = getProjectBase(process.cwd());
978
+ const config = loadProjectConfig(process.cwd());
979
+ const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
980
+ const result = runGetBasePath(projectBase, config);
981
+ const payload = buildPathQueryToolPayload(
982
+ "get-base-path",
983
+ process.cwd(),
984
+ projectBase,
985
+ result.stdout.trimEnd(),
986
+ runtimePaths,
987
+ buildToolExecutionSection(result),
988
+ );
989
+ return buildStructuredToolExecuteResult(payload);
990
+ },
991
+ });
992
+
993
+ const filesReferencesSchema = Type.Object(
994
+ {
995
+ files: Type.Array(
996
+ Type.String({ description: "Project-relative or absolute source file path resolved from the current working directory when not already absolute" }),
997
+ { description: "Explicit source-file list preserved in caller order" },
998
+ ),
999
+ },
1000
+ {
1001
+ description: "Input contract: files[]. Output contract: JSON object with request, summary, repository, files, and execution. File entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, standalone comments, and structured status facts. Missing or unsupported inputs become skipped entries. The tool fails when no source file can be analyzed.",
1002
+ },
1003
+ );
1004
+ const multiFileSchema = Type.Object(
1005
+ {
1006
+ files: Type.Array(
1007
+ Type.String({ description: "Project-relative or absolute file path resolved from the current working directory when not already absolute" }),
1008
+ { description: "Explicit file list preserved in caller order" },
1009
+ ),
1010
+ },
1011
+ {
1012
+ description: "Input contract: files[]. Output contract: JSON object with request, summary, files, and execution. File entries expose canonical paths, detected language, configured checker modules, selection status, and error facts.",
1013
+ },
1014
+ );
1015
+ const filesTokensSchema = Type.Object(
1016
+ {
1017
+ files: Type.Array(
1018
+ Type.String({ description: "Project-relative or absolute file path resolved from the current working directory when not already absolute" }),
1019
+ { description: "Input file list preserved in caller order" },
1020
+ ),
1021
+ },
1022
+ {
1023
+ description: "Input contract: files[]. Output contract: JSON object with request, summary, files, guidance, and execution. File entries expose path identifiers, access facts, line ranges, sizes, token metrics, and optional heading or Doxygen metadata. Missing or non-file inputs become skipped entries. The tool fails when no processable files remain.",
1024
+ },
1025
+ );
1026
+
1027
+ pi.registerTool({
1028
+ name: "files-tokens",
1029
+ label: "files-tokens",
1030
+ description: "Scope: explicit files. Return an LLM-oriented JSON payload with request, summary, files, guidance, and execution sections. File entries expose direct-access path facts, status, line ranges, sizes, token metrics, and optional heading or Doxygen metadata.",
1031
+ promptSnippet: "Return the structured token-analysis payload for caller-selected files.",
1032
+ promptGuidelines: [
1033
+ "Scope: explicit files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
1034
+ "Output contract: request + summary + files + guidance + execution. File entries expose canonical paths, absolute paths, existence, file status, line range, line count, byte count, character count, token count, shares, and optional primary-heading or Doxygen file metadata.",
1035
+ "Numeric contract: counts, sizes, shares, and line ranges remain in dedicated numeric fields; descriptive text is limited to stable reasons and guidance labels.",
1036
+ "Behavior contract: missing or non-file inputs become skipped entries; read failures become error entries; the tool fails only when no processable files remain.",
1037
+ ],
1038
+ parameters: filesTokensSchema,
1039
+ async execute(_toolCallId, params) {
1040
+ const payload = buildTokenToolPayload({
1041
+ toolName: "files-tokens",
1042
+ scope: "explicit-files",
1043
+ baseDir: process.cwd(),
1044
+ requestedPaths: params.files,
1045
+ encodingName: TOKEN_COUNTER_ENCODING,
1046
+ });
1047
+ return buildTokenToolExecuteResult(payload);
1048
+ },
1049
+ });
1050
+
1051
+ pi.registerTool({
1052
+ name: "files-references",
1053
+ label: "files-references",
1054
+ description: "Scope: explicit source files. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, standalone comments, and structured status facts.",
1055
+ promptSnippet: "Return the structured references payload for caller-selected source files.",
1056
+ promptGuidelines: [
1057
+ "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
1058
+ "Output contract: request + summary + repository + files + execution. File entries expose canonical paths, absolute paths, file status, line counts, line ranges, imports, symbols, child relationships, standalone comments, and structured Doxygen metadata.",
1059
+ "Numeric contract: line counts, line ranges, symbol counts, import counts, comment counts, and Doxygen counts remain in dedicated numeric fields; text is limited to residual comment or signature content that cannot be split safely.",
1060
+ "Behavior contract: missing inputs, non-file inputs, and unsupported extensions become structured skipped entries; analysis failures become structured error entries; the tool fails only when no source file can be analyzed.",
1061
+ ],
1062
+ parameters: filesReferencesSchema,
1063
+ async execute(_toolCallId, params) {
1064
+ const payload = buildReferenceToolPayload({
1065
+ toolName: "files-references",
1066
+ scope: "explicit-files",
1067
+ baseDir: process.cwd(),
1068
+ requestedPaths: params.files,
1069
+ });
1070
+ return buildReferenceToolExecuteResult(payload);
1071
+ },
1072
+ });
1073
+
1074
+ const filesCompressSchema = Type.Object(
1075
+ {
1076
+ files: Type.Array(
1077
+ Type.String({ description: "Project-relative or absolute source file path resolved from the current working directory when not already absolute" }),
1078
+ { description: "Explicit source-file list preserved in caller order" },
1079
+ ),
1080
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `compressed_source_text` and `compressed_lines[].display_text` include original source line-number prefixes" })),
1081
+ },
1082
+ {
1083
+ description: "Input contract: files[] plus optional enableLineNumbers. Output contract: JSON object with request, summary, repository, files, and execution. File entries expose path identifiers, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts. Missing, unsupported, or invalid inputs become structured skipped entries. The tool fails when no file is compressed.",
1084
+ },
1085
+ );
1086
+
1087
+ pi.registerTool({
1088
+ name: "files-compress",
1089
+ label: "files-compress",
1090
+ description: "Scope: explicit files. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose canonical paths, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts.",
1091
+ promptSnippet: "Return the structured compression payload for caller-selected source files.",
1092
+ promptGuidelines: [
1093
+ "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
1094
+ "Output contract: request + summary + repository + files + execution. File entries expose canonical paths, absolute paths, line_number_mode, source line counts, source line ranges, compressed line counts, removed line counts, compressed_lines, compressed_source_text, symbols, and file_doxygen.",
1095
+ "Line-number behavior: enableLineNumbers changes only rendered display strings; numeric source_line_number facts remain dedicated fields on compressed_lines for direct access.",
1096
+ "Behavior contract: missing inputs, non-file inputs, and unsupported extensions become structured skipped entries; compression failures become structured error entries; symbol-analysis failures retain compressed output with symbol_analysis_status=error; the tool fails only when no file is compressed.",
1097
+ ],
1098
+ parameters: filesCompressSchema,
1099
+ async execute(_toolCallId, params) {
1100
+ const payload = buildCompressToolPayload({
1101
+ toolName: "files-compress",
1102
+ scope: "explicit-files",
1103
+ baseDir: process.cwd(),
1104
+ requestedPaths: params.files,
1105
+ includeLineNumbers: params.enableLineNumbers ?? false,
1106
+ });
1107
+ return buildCompressionToolExecuteResult(payload);
1108
+ },
1109
+ });
1110
+
1111
+ const filesFindSchema = Type.Object(
1112
+ {
1113
+ tag: Type.String({ description: "Pipe-separated construct-tag filter applied case-insensitively; unsupported tags are ignored" }),
1114
+ pattern: Type.String({ description: "JavaScript RegExp applied to construct names only; use ^...$ for exact-name matching" }),
1115
+ files: Type.Array(
1116
+ Type.String({ description: "Project-relative or absolute source file path resolved from the current working directory when not already absolute" }),
1117
+ { description: "Explicit source-file list preserved in caller order" },
1118
+ ),
1119
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `code_lines[].display_text` and `stripped_source_text` include original source line-number prefixes" })),
1120
+ },
1121
+ {
1122
+ description: buildFindToolSchemaDescription("explicit-files"),
1123
+ },
1124
+ );
1125
+
1126
+ pi.registerTool({
1127
+ name: "files-find",
1128
+ label: "files-find",
1129
+ description: "Scope: explicit source files. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose structured statuses and match records with typed location, symbol, stripped-code, and Doxygen facts.",
1130
+ promptSnippet: "Return the structured construct-search payload for caller-selected source files.",
1131
+ promptGuidelines: buildFindToolPromptGuidelines("explicit-files"),
1132
+ parameters: filesFindSchema,
1133
+ async execute(_toolCallId, params) {
1134
+ const payload = buildFindToolPayload({
1135
+ toolName: "files-find",
1136
+ scope: "explicit-files",
1137
+ baseDir: process.cwd(),
1138
+ tagFilter: params.tag,
1139
+ pattern: params.pattern,
1140
+ requestedPaths: params.files,
1141
+ includeLineNumbers: params.enableLineNumbers ?? false,
1142
+ });
1143
+ return buildFindToolExecuteResult(payload);
1144
+ },
1145
+ });
1146
+
1147
+ const referencesSchema = Type.Object(
1148
+ {},
1149
+ {
1150
+ description: "Input contract: no params. Scope is the configured src-dir list resolved from the current project configuration. Output contract: JSON object with request, summary, repository, files, and execution. Repository exposes the structured directory tree; file entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, and status facts. The tool fails when no configured source file can be analyzed.",
1151
+ },
1152
+ );
1153
+ const tokensSchema = Type.Object(
1154
+ {},
1155
+ {
1156
+ description: "Input contract: no params. Scope is the configured docs-dir plus canonical docs REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md. Output contract: same structured JSON shape as files-tokens, plus docs_dir_path and canonical_doc_names in request. Missing canonical docs become skipped entries. The tool fails when no processable canonical docs remain.",
1157
+ },
1158
+ );
1159
+
1160
+ pi.registerTool({
1161
+ name: "references",
1162
+ label: "references",
1163
+ description: "Scope: configured project source directories. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. The repository section exposes the structured directory tree; file entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, standalone comments, and status facts.",
1164
+ promptSnippet: "Return the structured project references payload from the configured source directories.",
1165
+ promptGuidelines: [
1166
+ "Scope: no params; resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.",
1167
+ "Output contract: request + summary + repository + files + execution. Repository exposes source_directory_paths, file_canonical_paths, and directory_tree; file entries expose canonical paths, line counts, line ranges, imports, symbols, hierarchy, standalone comments, and structured Doxygen metadata.",
1168
+ "Configuration contract: output changes with cwd-derived project config, src-dir values, and repository source discovery; the tool does not accept explicit file overrides.",
1169
+ "Behavior contract: configured source files are analyzed in deterministic order, analysis failures become structured error entries, and the tool fails when no configured source file can be analyzed.",
1170
+ ],
1171
+ parameters: referencesSchema,
1172
+ async execute() {
1173
+ const projectBase = getProjectBase(process.cwd());
1174
+ const config = loadProjectConfig(process.cwd());
1175
+ const payload = buildReferenceToolPayload({
1176
+ toolName: "references",
1177
+ scope: "configured-source-directories",
1178
+ baseDir: projectBase,
1179
+ requestedPaths: collectSourceFiles(config["src-dir"], projectBase),
1180
+ sourceDirectoryPaths: config["src-dir"],
1181
+ });
1182
+ return buildReferenceToolExecuteResult(payload);
1183
+ },
1184
+ });
1185
+
1186
+ const compressSchema = Type.Object(
1187
+ {
1188
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `compressed_source_text` and `compressed_lines[].display_text` include original source line-number prefixes" })),
1189
+ },
1190
+ {
1191
+ description: "Input contract: optional enableLineNumbers boolean. Scope is the configured src-dir list resolved from the current project configuration. Output contract: JSON object with request, summary, repository, files, and execution. File entries expose path identifiers, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts. The tool fails when no configured source file is compressed.",
1192
+ },
1193
+ );
1194
+
1195
+ pi.registerTool({
1196
+ name: "compress",
1197
+ label: "compress",
1198
+ description: "Scope: configured project source directories. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose canonical paths, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts.",
1199
+ promptSnippet: "Return the structured project compression payload from the configured source directories.",
1200
+ promptGuidelines: [
1201
+ "Scope: resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.",
1202
+ "Output contract: request + summary + repository + files + execution. Repository exposes source_directory_paths and file_canonical_paths; file entries expose line_number_mode, source line counts, source line ranges, compressed line counts, removed line counts, compressed_lines, compressed_source_text, symbols, and file_doxygen.",
1203
+ "Configuration contract: output changes with cwd-derived project config, src-dir values, and repository source discovery; the tool does not accept explicit file overrides.",
1204
+ "Behavior contract: configured source files are processed in deterministic order, compression failures become structured error entries, symbol-analysis failures retain compressed output with symbol_analysis_status=error, and the tool fails only when no configured source file is compressed.",
1205
+ ],
1206
+ parameters: compressSchema,
1207
+ async execute(_toolCallId, params) {
1208
+ const projectBase = getProjectBase(process.cwd());
1209
+ const config = loadProjectConfig(process.cwd());
1210
+ const sourceFiles = collectSourceFiles(config["src-dir"], projectBase);
1211
+ const payload = buildCompressToolPayload({
1212
+ toolName: "compress",
1213
+ scope: "configured-source-directories",
1214
+ baseDir: projectBase,
1215
+ requestedPaths: sourceFiles,
1216
+ includeLineNumbers: params.enableLineNumbers ?? false,
1217
+ sourceDirectoryPaths: config["src-dir"],
1218
+ });
1219
+ return buildCompressionToolExecuteResult(payload);
1220
+ },
1221
+ });
1222
+
1223
+ const findSchema = Type.Object(
1224
+ {
1225
+ tag: Type.String({ description: "Pipe-separated construct-tag filter applied case-insensitively; unsupported tags are ignored" }),
1226
+ pattern: Type.String({ description: "JavaScript RegExp applied to construct names only; use ^...$ for exact-name matching" }),
1227
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `code_lines[].display_text` and `stripped_source_text` include original source line-number prefixes" })),
1228
+ },
1229
+ {
1230
+ description: buildFindToolSchemaDescription("configured-source-directories"),
1231
+ },
1232
+ );
1233
+
1234
+ pi.registerTool({
1235
+ name: "find",
1236
+ label: "find",
1237
+ description: "Scope: configured project source directories. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose structured statuses and match records with typed location, symbol, stripped-code, and Doxygen facts.",
1238
+ promptSnippet: "Return the structured construct-search payload from the configured source directories.",
1239
+ promptGuidelines: buildFindToolPromptGuidelines("configured-source-directories"),
1240
+ parameters: findSchema,
1241
+ async execute(_toolCallId, params) {
1242
+ const projectBase = getProjectBase(process.cwd());
1243
+ const config = loadProjectConfig(process.cwd());
1244
+ const sourceFiles = collectSourceFiles(config["src-dir"], projectBase);
1245
+ const payload = buildFindToolPayload({
1246
+ toolName: "find",
1247
+ scope: "configured-source-directories",
1248
+ baseDir: projectBase,
1249
+ tagFilter: params.tag,
1250
+ pattern: params.pattern,
1251
+ requestedPaths: sourceFiles,
1252
+ includeLineNumbers: params.enableLineNumbers ?? false,
1253
+ sourceDirectoryPaths: config["src-dir"],
1254
+ });
1255
+ return buildFindToolExecuteResult(payload);
1256
+ },
1257
+ });
1258
+
1259
+ pi.registerTool({
1260
+ name: "tokens",
1261
+ label: "tokens",
1262
+ description: "Scope: canonical docs from the configured docs-dir. Return the same LLM-oriented JSON contract as files-tokens, plus canonical-doc selection metadata in request for REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md.",
1263
+ promptSnippet: "Return the structured token-analysis payload for canonical documentation files.",
1264
+ promptGuidelines: [
1265
+ "Scope: no params; resolve docs-dir from project config; target canonical docs REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md.",
1266
+ "Output contract: request + summary + files + guidance + execution. Request includes docs_dir_path and canonical_doc_names; file entries expose direct-access path facts, line ranges, sizes, token metrics, and optional metadata.",
1267
+ "Numeric contract: counts, sizes, shares, and line ranges remain in dedicated numeric fields; guidance separates source observations, derived recommendations, and actionable next-step hints.",
1268
+ "Behavior contract: missing canonical docs become skipped entries; read failures become error entries; the tool fails only when no processable canonical docs remain.",
1269
+ ],
1270
+ parameters: tokensSchema,
1271
+ async execute() {
1272
+ const projectBase = getProjectBase(process.cwd());
1273
+ const config = loadProjectConfig(process.cwd());
1274
+ const docsDir = config["docs-dir"].replace(/[/\\]+$/, "");
1275
+ const canonicalDocNames = ["REQUIREMENTS.md", "WORKFLOW.md", "REFERENCES.md"];
1276
+ const payload = buildTokenToolPayload({
1277
+ toolName: "tokens",
1278
+ scope: "canonical-docs",
1279
+ baseDir: projectBase,
1280
+ requestedPaths: canonicalDocNames.map((name) => path.join(docsDir, name)),
1281
+ docsDir,
1282
+ canonicalDocNames,
1283
+ encodingName: TOKEN_COUNTER_ENCODING,
1284
+ });
1285
+ return buildTokenToolExecuteResult(payload);
1286
+ },
1287
+ });
1288
+
1289
+ pi.registerTool({
1290
+ name: "files-static-check",
1291
+ label: "files-static-check",
1292
+ description: "Scope: explicit files. Return a JSON-first payload with request, summary, files, and execution sections. File entries expose canonical paths, detected language, configured checker modules, selection status, and stable error facts.",
1293
+ promptSnippet: "Return the structured explicit-file static-check payload for the current project configuration.",
1294
+ promptGuidelines: [
1295
+ "Input contract: files[]. Scope is explicit caller-selected files resolved from the current working directory.",
1296
+ "Output contract: request + summary + files + execution. File entries expose canonical_path, language_name, configured_checker_modules, status, and error_message.",
1297
+ "Configuration contract: checker selection is derived from the cwd-resolved static-check configuration and file extensions only.",
1298
+ "Failure contract: execution.code mirrors aggregated checker failures; execution.stdout_lines and execution.stderr_lines preserve residual checker diagnostics.",
1299
+ ],
1300
+ parameters: multiFileSchema,
1301
+ async execute(_toolCallId, params) {
1302
+ const projectBase = getProjectBase(process.cwd());
1303
+ const config = loadProjectConfig(process.cwd());
1304
+ const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1305
+ const staticCheckConfig = config["static-check"] ?? {};
1306
+ const result = runFilesStaticCheck(params.files, projectBase, config);
1307
+ const payload = buildStaticCheckToolPayload(
1308
+ "files-static-check",
1309
+ "explicit-files",
1310
+ projectBase,
1311
+ params.files,
1312
+ [],
1313
+ [],
1314
+ staticCheckConfig,
1315
+ runtimePaths,
1316
+ buildToolExecutionSection(result),
1317
+ );
1318
+ return buildStructuredToolExecuteResult(payload);
1319
+ },
1320
+ });
1321
+
1322
+ const staticCheckSchema = Type.Object(
1323
+ {},
1324
+ {
1325
+ description: "Input contract: no params. Scope is the configured src-dir plus tests-dir selection after fixture exclusion. Output contract: JSON object with request, summary, files, and execution.",
1326
+ },
1327
+ );
1328
+ pi.registerTool({
1329
+ name: "static-check",
1330
+ label: "static-check",
1331
+ description: "Scope: configured source and test directories. Return a JSON-first payload with request, summary, files, and execution sections. File entries expose selected-path facts, checker coverage, selection status, and residual diagnostics metadata.",
1332
+ promptSnippet: "Return the structured project static-check payload for the current configuration.",
1333
+ promptGuidelines: [
1334
+ "Input contract: no params. Scope is src-dir plus tests-dir from the cwd-derived project configuration.",
1335
+ "Output contract: request + summary + files + execution. Request exposes selection_directory_paths and excluded_directory_paths; file entries expose configured_checker_modules and status.",
1336
+ "Selection contract: tests/fixtures and <tests-dir>/fixtures are excluded before checker dispatch.",
1337
+ "Failure contract: execution.code mirrors aggregated checker failures or selection failures; execution.stderr_lines preserve residual diagnostics.",
1338
+ ],
1339
+ parameters: staticCheckSchema,
1340
+ async execute() {
1341
+ const projectBase = getProjectBase(process.cwd());
1342
+ const config = loadProjectConfig(process.cwd());
1343
+ const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1344
+ const staticCheckConfig = config["static-check"] ?? {};
1345
+ const selectionDirectoryPaths = [...config["src-dir"], config["tests-dir"]];
1346
+ const testsDirRel = makeRelativeIfContainsProject(config["tests-dir"], projectBase)
1347
+ .split(path.sep)
1348
+ .join("/")
1349
+ .replace(/^\.?\/?/, "")
1350
+ .replace(/\/+$/, "");
1351
+ const excludedDirectoryPaths = [...new Set([
1352
+ "tests/fixtures",
1353
+ testsDirRel ? `${testsDirRel}/fixtures` : "fixtures",
1354
+ ])];
1355
+ let selectedPaths: string[] = [];
1356
+ let execution;
1357
+ try {
1358
+ selectedPaths = collectProjectStaticCheckSelection(projectBase, config).selectedPaths;
1359
+ execution = buildToolExecutionSection(runProjectStaticCheck(projectBase, config));
1360
+ } catch (error) {
1361
+ execution = buildToolExecutionSection(normalizeToolFailure(error));
1362
+ }
1363
+ const payload = buildStaticCheckToolPayload(
1364
+ "static-check",
1365
+ "configured-source-and-test-directories",
1366
+ projectBase,
1367
+ selectedPaths,
1368
+ selectionDirectoryPaths,
1369
+ excludedDirectoryPaths,
1370
+ staticCheckConfig,
1371
+ runtimePaths,
1372
+ execution,
1373
+ );
1374
+ return buildStructuredToolExecuteResult(payload);
1375
+ },
1376
+ });
1377
+
1378
+ const gitCheckSchema = Type.Object(
1379
+ {},
1380
+ {
1381
+ description: "Input contract: no params. Output contract: JSON object with request, result, and execution. Result exposes git-root presence plus clean-versus-error repository status fields.",
1382
+ },
1383
+ );
1384
+ pi.registerTool({
1385
+ name: "git-check",
1386
+ label: "git-check",
1387
+ description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes repository validation status through direct fields instead of empty-success text.",
1388
+ promptSnippet: "Return the structured git-validation payload for the runtime repository.",
1389
+ promptGuidelines: [
1390
+ "Input contract: no params. Scope is the cwd-derived runtime path context.",
1391
+ "Output contract: request + result + execution. Result exposes git_path_present, status, worktree_status, and head_status.",
1392
+ "Behavior contract: the tool checks work-tree membership, porcelain cleanliness, and symbolic-or-detached HEAD validity.",
1393
+ "Failure contract: execution.code and execution.stderr_lines surface git-path or repository-state errors.",
1394
+ ],
1395
+ parameters: gitCheckSchema,
1396
+ async execute() {
1397
+ const projectBase = getProjectBase(process.cwd());
1398
+ const config = loadProjectConfig(process.cwd());
1399
+ const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1400
+ let execution;
1401
+ try {
1402
+ execution = buildToolExecutionSection(runGitCheck(projectBase, config));
1403
+ } catch (error) {
1404
+ execution = buildToolExecutionSection(normalizeToolFailure(error));
1405
+ }
1406
+ const payload = buildGitCheckToolPayload(projectBase, resolveRuntimeGitPath(projectBase), runtimePaths, execution);
1407
+ return buildStructuredToolExecuteResult(payload);
1408
+ },
1409
+ });
1410
+
1411
+ const docsCheckSchema = Type.Object(
1412
+ {},
1413
+ {
1414
+ description: "Input contract: no params. Output contract: JSON object with request, summary, files, and execution. File entries expose canonical paths, prompt_command remediation, and presence status for canonical docs.",
1415
+ },
1416
+ );
1417
+ pi.registerTool({
1418
+ name: "docs-check",
1419
+ label: "docs-check",
1420
+ description: "Scope: canonical docs. Return a JSON-first payload with request, summary, files, and execution sections. File entries expose remediation prompt commands and direct presence facts for REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md.",
1421
+ promptSnippet: "Return the structured canonical-document validation payload.",
1422
+ promptGuidelines: [
1423
+ "Input contract: no params. Scope is docs-dir from the cwd-derived project configuration.",
1424
+ "Output contract: request + summary + files + execution. File entries expose file_name, canonical_path, prompt_command, and status.",
1425
+ "Specialization trigger: remediation differs per missing canonical file through prompt_command.",
1426
+ "Failure contract: execution.code is non-zero when any canonical document is missing; execution.stderr_lines enumerate missing files.",
1427
+ ],
1428
+ parameters: docsCheckSchema,
1429
+ async execute() {
1430
+ const projectBase = getProjectBase(process.cwd());
1431
+ const config = loadProjectConfig(process.cwd());
1432
+ const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1433
+ const payload = buildDocsCheckToolPayload(projectBase, config["docs-dir"], runtimePaths);
1434
+ return buildStructuredToolExecuteResult(payload);
1435
+ },
1436
+ });
1437
+
1438
+ const gitWtNameSchema = Type.Object(
1439
+ {},
1440
+ {
1441
+ description: "Input contract: no params. Output contract: JSON object with request, result, and execution. Result exposes worktree_name and the normative useReq naming format string.",
1442
+ },
1443
+ );
1444
+ pi.registerTool({
1445
+ name: "git-wt-name",
1446
+ label: "git-wt-name",
1447
+ description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the generated worktree name plus its normative format as direct fields.",
1448
+ promptSnippet: "Return the structured worktree-name generation payload.",
1449
+ promptGuidelines: [
1450
+ "Input contract: no params. Scope is the cwd-derived runtime path context.",
1451
+ "Output contract: request + result + execution. Result exposes worktree_name and format_text.",
1452
+ "Behavior contract: generation follows useReq-<project>-<sanitized-branch>-<YYYYMMDDHHMMSS>.",
1453
+ "Failure contract: execution.code and execution.stderr_lines surface git-path or branch-resolution errors.",
1454
+ ],
1455
+ parameters: gitWtNameSchema,
1456
+ async execute() {
1457
+ const projectBase = getProjectBase(process.cwd());
1458
+ const config = loadProjectConfig(process.cwd());
1459
+ const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1460
+ let execution;
1461
+ try {
1462
+ execution = buildToolExecutionSection(runGitWtName(projectBase, config));
1463
+ } catch (error) {
1464
+ execution = buildToolExecutionSection(normalizeToolFailure(error));
1465
+ }
1466
+ const payload = buildWorktreeNameToolPayload(projectBase, resolveRuntimeGitPath(projectBase), runtimePaths, execution);
1467
+ return buildStructuredToolExecuteResult(payload);
1468
+ },
1469
+ });
1470
+
1471
+ const gitWtCreateSchema = Type.Object(
1472
+ {
1473
+ wtName: Type.String({ description: "Exact target worktree name and branch name" }),
1474
+ },
1475
+ {
1476
+ description: "Input contract: wtName. Output contract: JSON object with request, result, and execution. Result exposes operation, worktree_name, branch_name, derived worktree_path, and status.",
1477
+ },
1478
+ );
1479
+ pi.registerTool({
1480
+ name: "git-wt-create",
1481
+ label: "git-wt-create",
1482
+ description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the requested create operation, exact worktree name, derived path, and mutation status.",
1483
+ promptSnippet: "Return the structured worktree-creation payload for the requested name.",
1484
+ promptGuidelines: [
1485
+ "Input contract: wtName is required and must match the exact worktree/branch name to create.",
1486
+ "Output contract: request + result + execution. Result exposes operation=create, worktree_name, branch_name, worktree_path, and status.",
1487
+ "Specialization trigger: worktree_path depends on the runtime git root parent directory.",
1488
+ "Failure contract: execution.code and execution.stderr_lines surface invalid-name, git, or finalization errors.",
1489
+ ],
1490
+ parameters: gitWtCreateSchema,
1491
+ async execute(_toolCallId, params) {
1492
+ const projectBase = getProjectBase(process.cwd());
1493
+ const config = loadProjectConfig(process.cwd());
1494
+ const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1495
+ let execution;
1496
+ try {
1497
+ execution = buildToolExecutionSection(runGitWtCreate(projectBase, params.wtName, config));
1498
+ } catch (error) {
1499
+ execution = buildToolExecutionSection(normalizeToolFailure(error));
1500
+ }
1501
+ const payload = buildWorktreeMutationToolPayload(
1502
+ "git-wt-create",
1503
+ projectBase,
1504
+ resolveRuntimeGitPath(projectBase),
1505
+ params.wtName,
1506
+ runtimePaths,
1507
+ execution,
1508
+ );
1509
+ return buildStructuredToolExecuteResult(payload);
1510
+ },
1511
+ });
1512
+
1513
+ const gitWtDeleteSchema = Type.Object(
1514
+ {
1515
+ wtName: Type.String({ description: "Exact target worktree name and branch name" }),
1516
+ },
1517
+ {
1518
+ description: "Input contract: wtName. Output contract: JSON object with request, result, and execution. Result exposes operation, worktree_name, branch_name, derived worktree_path, and status.",
1519
+ },
1520
+ );
1521
+ pi.registerTool({
1522
+ name: "git-wt-delete",
1523
+ label: "git-wt-delete",
1524
+ description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the requested delete operation, exact worktree name, derived path, and mutation status.",
1525
+ promptSnippet: "Return the structured worktree-deletion payload for the requested name.",
1526
+ promptGuidelines: [
1527
+ "Input contract: wtName is required and must match the exact worktree/branch name to delete.",
1528
+ "Output contract: request + result + execution. Result exposes operation=delete, worktree_name, branch_name, worktree_path, and status.",
1529
+ "Specialization trigger: worktree_path depends on the runtime git root parent directory.",
1530
+ "Failure contract: execution.code and execution.stderr_lines surface missing-target or deletion errors.",
1531
+ ],
1532
+ parameters: gitWtDeleteSchema,
1533
+ async execute(_toolCallId, params) {
1534
+ const projectBase = getProjectBase(process.cwd());
1535
+ const config = loadProjectConfig(process.cwd());
1536
+ const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1537
+ let execution;
1538
+ try {
1539
+ execution = buildToolExecutionSection(runGitWtDelete(projectBase, params.wtName, config));
1540
+ } catch (error) {
1541
+ execution = buildToolExecutionSection(normalizeToolFailure(error));
1542
+ }
1543
+ const payload = buildWorktreeMutationToolPayload(
1544
+ "git-wt-delete",
1545
+ projectBase,
1546
+ resolveRuntimeGitPath(projectBase),
1547
+ params.wtName,
1548
+ runtimePaths,
1549
+ execution,
1550
+ );
1551
+ return buildStructuredToolExecuteResult(payload);
1552
+ },
1553
+ });
1554
+ }
1555
+
1556
+ /**
1557
+ * @brief Builds the shared settings-menu choices for startup-tool management.
1558
+ * @details Serializes startup-tool actions into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(t) in configurable-tool count. No external state is mutated.
1559
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
1560
+ * @param[in] config {UseReqConfig} Effective project configuration.
1561
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered startup-tool menu choices.
1562
+ * @satisfies REQ-007, REQ-151, REQ-152, REQ-153, REQ-154
1563
+ */
1564
+ function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1565
+ const tools = getPiUsereqStartupTools(pi);
1566
+ return [
1567
+ {
1568
+ id: "show-tool-status",
1569
+ label: "Show tool status",
1570
+ value: `${getConfiguredEnabledPiUsereqTools(config).length}/${tools.length} enabled`,
1571
+ description: "Open the full startup-tool reference report in the editor.",
1572
+ },
1573
+ {
1574
+ id: "toggle-tool",
1575
+ label: "Toggle tool",
1576
+ value: `${getConfiguredEnabledPiUsereqTools(config).length} enabled`,
1577
+ description: "Open the per-tool toggle menu for configurable startup tools.",
1578
+ },
1579
+ {
1580
+ id: "enable-all-tools",
1581
+ label: "Enable all configurable tools",
1582
+ value: `${tools.length} targets`,
1583
+ description: "Enable every configurable startup tool for future session starts.",
1584
+ },
1585
+ {
1586
+ id: "disable-all-tools",
1587
+ label: "Disable all configurable tools",
1588
+ value: `${tools.length} targets`,
1589
+ description: "Disable every configurable startup tool for future session starts.",
1590
+ },
1591
+ {
1592
+ id: "reset-tool-defaults",
1593
+ label: "Reset configurable-tool defaults",
1594
+ value: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
1595
+ description: "Restore the documented default startup-tool selection.",
1596
+ },
1597
+ {
1598
+ id: "back",
1599
+ label: "Back",
1600
+ value: "",
1601
+ description: "Return to the parent configuration menu.",
1602
+ },
1603
+ ];
1604
+ }
1605
+
1606
+ /**
1607
+ * @brief Builds the shared settings-menu choices for per-tool startup toggles.
1608
+ * @details Exposes every configurable startup tool as one row whose right-side value reports the current enabled state. Runtime is O(t) in configurable-tool count. No external state is mutated.
1609
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
1610
+ * @param[in] config {UseReqConfig} Effective project configuration.
1611
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered per-tool toggle choices.
1612
+ * @satisfies REQ-007, REQ-151, REQ-152, REQ-153, REQ-154
1613
+ */
1614
+ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1615
+ const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
1616
+ return [
1617
+ ...getPiUsereqStartupTools(pi).map((tool) => ({
1618
+ id: tool.name,
1619
+ label: tool.name,
1620
+ value: enabledTools.has(tool.name) ? "on" : "off",
1621
+ description: tool.description ?? `Toggle startup activation for ${tool.name}.`,
1622
+ })),
1623
+ {
1624
+ id: "back",
1625
+ label: "Back",
1626
+ value: "",
1627
+ description: "Return to the startup-tools menu.",
1628
+ },
1629
+ ];
1630
+ }
1631
+
1632
+ /**
1633
+ * @brief Runs the interactive active-tool configuration menu.
1634
+ * @details Synchronizes runtime active tools with persisted config, renders startup-tool actions through the shared settings-menu UI, and updates configuration state in response to selections until the user exits. Runtime depends on user interaction count. Side effects include UI updates, active-tool changes, and config mutation.
1635
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
1636
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
1637
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
1638
+ * @return {Promise<void>} Promise resolved when the menu closes.
1639
+ * @satisfies REQ-007, REQ-063, REQ-064, REQ-151, REQ-152, REQ-153, REQ-154
1640
+ */
1641
+ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
1642
+ applyConfiguredPiUsereqTools(pi, config);
1643
+ while (true) {
1644
+ const tools = getPiUsereqStartupTools(pi);
1645
+ const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
1646
+ const choice = await showPiUsereqSettingsMenu(ctx, "startup tools", buildPiUsereqToolsMenuChoices(pi, config));
1647
+
1648
+ if (!choice || choice === "back") {
1649
+ return;
1650
+ }
1651
+
1652
+ if (choice === "show-tool-status") {
1653
+ ctx.ui.setEditorText(renderPiUsereqToolsReference(pi, config));
1654
+ continue;
1655
+ }
1656
+
1657
+ if (choice === "enable-all-tools") {
1658
+ setConfiguredPiUsereqTools(pi, config, tools.map((tool) => tool.name));
1659
+ ctx.ui.notify("Enabled all configurable active tools", "info");
1660
+ continue;
1661
+ }
1662
+
1663
+ if (choice === "disable-all-tools") {
1664
+ setConfiguredPiUsereqTools(pi, config, []);
1665
+ ctx.ui.notify("Disabled all configurable active tools", "info");
1666
+ continue;
1667
+ }
1668
+
1669
+ if (choice === "reset-tool-defaults") {
1670
+ setConfiguredPiUsereqTools(pi, config, normalizeEnabledPiUsereqTools(undefined));
1671
+ ctx.ui.notify("Restored default configurable active tools", "info");
1672
+ continue;
1673
+ }
1674
+
1675
+ if (choice === "toggle-tool") {
1676
+ const selectedToolName = await showPiUsereqSettingsMenu(ctx, "toggle startup tool", buildPiUsereqToolToggleChoices(pi, config));
1677
+ if (!selectedToolName || selectedToolName === "back") {
1678
+ continue;
1679
+ }
1680
+ if (enabledTools.has(selectedToolName)) {
1681
+ enabledTools.delete(selectedToolName);
1682
+ } else {
1683
+ enabledTools.add(selectedToolName);
1684
+ }
1685
+ setConfiguredPiUsereqTools(pi, config, tools.map((tool) => tool.name).filter((toolName) => enabledTools.has(toolName)));
1686
+ ctx.ui.notify(
1687
+ `${enabledTools.has(selectedToolName) ? "Enabled" : "Disabled"} ${selectedToolName}`,
1688
+ "info",
1689
+ );
1690
+ }
1691
+ }
1692
+ }
1693
+
1694
+ /**
1695
+ * @brief Formats one static-check configuration entry for UI display.
1696
+ * @details Renders command-backed entries as `Command(cmd args...)` and all other modules as `Module(args...)`. Runtime is O(n) in parameter count. No side effects occur.
1697
+ * @param[in] entry {StaticCheckEntry} Static-check configuration entry.
1698
+ * @return {string} Human-readable entry summary.
1699
+ */
1700
+ function formatStaticCheckEntry(entry: StaticCheckEntry): string {
1701
+ const params = Array.isArray(entry.params) && entry.params.length > 0 ? ` ${entry.params.join(" ")}` : "";
1702
+ if (entry.module === "Command") {
1703
+ return `${entry.module}(${entry.cmd ?? "?"}${params})`;
1704
+ }
1705
+ return `${entry.module}${params ? `(${entry.params!.join(" ")})` : ""}`;
1706
+ }
1707
+
1708
+ /**
1709
+ * @brief Summarizes configured static-check languages.
1710
+ * @details Keeps only languages with at least one configured checker, sorts them, and emits a compact `Language (count)` list. Runtime is O(l log l). No side effects occur.
1711
+ * @param[in] config {UseReqConfig} Effective project configuration.
1712
+ * @return {string} Compact summary string or `(none)`.
1713
+ */
1714
+ function formatStaticCheckLanguagesSummary(config: UseReqConfig): string {
1715
+ const languages = Object.entries(config["static-check"])
1716
+ .filter(([, entries]) => Array.isArray(entries) && entries.length > 0)
1717
+ .sort(([left], [right]) => left.localeCompare(right))
1718
+ .map(([language, entries]) => `${language} (${entries.length})`);
1719
+ return languages.join(", ") || "(none)";
1720
+ }
1721
+
1722
+ /**
1723
+ * @brief Renders the static-check configuration reference view.
1724
+ * @details Produces a markdown-like summary containing configured entries, supported languages, supported modules, and example specifications. Runtime is O(l log l). No side effects occur.
1725
+ * @param[in] config {UseReqConfig} Effective project configuration.
1726
+ * @return {string} Reference text for the editor view.
1727
+ */
1728
+ function renderStaticCheckReference(config: UseReqConfig): string {
1729
+ const lines = ["# Static-check configuration", "", `Configured languages: ${formatStaticCheckLanguagesSummary(config)}`, ""];
1730
+ const configuredLanguages = Object.entries(config["static-check"])
1731
+ .filter(([, entries]) => Array.isArray(entries) && entries.length > 0)
1732
+ .sort(([left], [right]) => left.localeCompare(right));
1733
+
1734
+ if (configuredLanguages.length === 0) {
1735
+ lines.push("Configured entries: (none)");
1736
+ } else {
1737
+ lines.push("Configured entries:");
1738
+ for (const [language, entries] of configuredLanguages) {
1739
+ lines.push(`- ${language}: ${(entries as StaticCheckEntry[]).map(formatStaticCheckEntry).join(", ")}`);
1740
+ }
1741
+ }
1742
+
1743
+ lines.push("", "Supported languages:");
1744
+ for (const { language, extensions } of getSupportedStaticCheckLanguageSupport()) {
1745
+ lines.push(`- ${language}: ${extensions.join(", ")}`);
1746
+ }
1747
+ lines.push("", `Supported modules: ${STATIC_CHECK_MODULES.join(", ")}`, "", "Examples:", "- Python=Ruff", "- Python=Command,mypy,--strict", "- TypeScript=Command,eslint,--max-warnings,0");
1748
+ return `${lines.join("\n")}\n`;
1749
+ }
1750
+
1751
+ /**
1752
+ * @brief Builds the shared settings-menu choices for static-check management.
1753
+ * @details Serializes static-check actions into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(1). No external state is mutated.
1754
+ * @param[in] config {UseReqConfig} Effective project configuration.
1755
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered static-check menu choices.
1756
+ * @satisfies REQ-008, REQ-151, REQ-152, REQ-153, REQ-154
1757
+ */
1758
+ function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1759
+ const supportedLanguageCount = getSupportedStaticCheckLanguageSupport().length;
1760
+ const configuredLanguageCount = Object.values(config["static-check"]).filter((entries) => entries.length > 0).length;
1761
+ return [
1762
+ {
1763
+ id: "add-entry-supported-language",
1764
+ label: "Add entry for supported language",
1765
+ value: `${supportedLanguageCount} languages`,
1766
+ description: "Select a supported language, then select one static-check module to add.",
1767
+ },
1768
+ {
1769
+ id: "add-entry-raw-spec",
1770
+ label: "Add entry from LANG=MODULE[,CMD[,PARAM...]]",
1771
+ value: "raw spec",
1772
+ description: "Enter one raw static-check specification string in canonical CLI format.",
1773
+ },
1774
+ {
1775
+ id: "remove-language-entry",
1776
+ label: "Remove language entry",
1777
+ value: configuredLanguageCount > 0 ? `${configuredLanguageCount} configured` : "(none)",
1778
+ description: "Remove every configured static-check entry for one language.",
1779
+ },
1780
+ {
1781
+ id: "show-supported-languages",
1782
+ label: "Show supported languages",
1783
+ value: `${supportedLanguageCount} languages`,
1784
+ description: "Open the static-check reference report in the editor.",
1785
+ },
1786
+ {
1787
+ id: "back",
1788
+ label: "Back",
1789
+ value: "",
1790
+ description: "Return to the parent configuration menu.",
1791
+ },
1792
+ ];
1793
+ }
1794
+
1795
+ /**
1796
+ * @brief Builds the shared settings-menu choices for supported static-check languages.
1797
+ * @details Exposes every supported language as one row whose right-side value reports extensions plus the current configured checker count. Runtime is O(l log l). No external state is mutated.
1798
+ * @param[in] config {UseReqConfig} Effective project configuration.
1799
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered language-choice vector.
1800
+ */
1801
+ function buildSupportedStaticCheckLanguageChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1802
+ return [
1803
+ ...getSupportedStaticCheckLanguageSupport().map(({ language, extensions }) => {
1804
+ const configuredCount = config["static-check"][language]?.length ?? 0;
1805
+ const suffix = configuredCount === 1 ? "checker" : "checkers";
1806
+ return {
1807
+ id: language,
1808
+ label: language,
1809
+ value: `${extensions.join(", ")} • ${configuredCount} ${suffix}`,
1810
+ description: `Configure static-check modules for ${language}. Supported extensions: ${extensions.join(", ")}.`,
1811
+ };
1812
+ }),
1813
+ {
1814
+ id: "back",
1815
+ label: "Back",
1816
+ value: "",
1817
+ description: "Return to the static-check menu.",
1818
+ },
1819
+ ];
1820
+ }
1821
+
1822
+ /**
1823
+ * @brief Builds the shared settings-menu choices for static-check modules.
1824
+ * @details Exposes every supported static-check module as one selectable row with a concise execution description. Runtime is O(m) in module count. No external state is mutated.
1825
+ * @param[in] language {string} Canonical selected language.
1826
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered module-choice vector.
1827
+ */
1828
+ function buildStaticCheckModuleChoices(language: string): PiUsereqSettingsMenuChoice[] {
1829
+ return [
1830
+ ...STATIC_CHECK_MODULES.map((moduleName) => ({
1831
+ id: moduleName,
1832
+ label: moduleName,
1833
+ value: language,
1834
+ description: moduleName === "Command"
1835
+ ? `Run one explicit external command against ${language} files.`
1836
+ : `Run the built-in ${moduleName} checker for ${language} files.`,
1837
+ })),
1838
+ {
1839
+ id: "back",
1840
+ label: "Back",
1841
+ value: "",
1842
+ description: "Return to language selection.",
1843
+ },
1844
+ ];
1845
+ }
1846
+
1847
+ /**
1848
+ * @brief Builds the shared settings-menu choices for configured static-check languages.
1849
+ * @details Exposes only languages that currently have at least one configured checker so removal remains deterministic. Runtime is O(l log l). No external state is mutated.
1850
+ * @param[in] config {UseReqConfig} Effective project configuration.
1851
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered configured-language vector.
1852
+ */
1853
+ function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1854
+ return [
1855
+ ...getSupportedStaticCheckLanguageSupport()
1856
+ .filter(({ language }) => (config["static-check"][language] ?? []).length > 0)
1857
+ .map(({ language, extensions }) => ({
1858
+ id: language,
1859
+ label: language,
1860
+ value: `${extensions.join(", ")} • ${config["static-check"][language]!.length} configured`,
1861
+ description: `Remove every configured static-check entry for ${language}.`,
1862
+ })),
1863
+ {
1864
+ id: "back",
1865
+ label: "Back",
1866
+ value: "",
1867
+ description: "Return to the static-check menu.",
1868
+ },
1869
+ ];
1870
+ }
1871
+
1872
+ /**
1873
+ * @brief Runs the interactive static-check configuration menu.
1874
+ * @details Lets the user inspect support, add entries by guided prompts or raw spec strings, and remove configured language entries through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
1875
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
1876
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
1877
+ * @return {Promise<void>} Promise resolved when the menu closes.
1878
+ * @satisfies REQ-008, REQ-151, REQ-152, REQ-153, REQ-154
1879
+ */
1880
+ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
1881
+ while (true) {
1882
+ const staticChoice = await showPiUsereqSettingsMenu(ctx, "static-check", buildStaticCheckMenuChoices(config));
1883
+
1884
+ if (!staticChoice || staticChoice === "back") {
1885
+ return;
1886
+ }
1887
+
1888
+ if (staticChoice === "show-supported-languages") {
1889
+ ctx.ui.setEditorText(renderStaticCheckReference(config));
1890
+ continue;
1891
+ }
1892
+
1893
+ if (staticChoice === "add-entry-supported-language") {
1894
+ const selectedLanguage = await showPiUsereqSettingsMenu(ctx, "static-check language", buildSupportedStaticCheckLanguageChoices(config));
1895
+ if (!selectedLanguage || selectedLanguage === "back") {
1896
+ continue;
1897
+ }
1898
+ const moduleName = await showPiUsereqSettingsMenu(ctx, `static-check for ${selectedLanguage}`, buildStaticCheckModuleChoices(selectedLanguage));
1899
+ if (!moduleName || moduleName === "back") {
1900
+ continue;
1901
+ }
1902
+
1903
+ const entry: StaticCheckEntry = { module: moduleName };
1904
+ if (moduleName === "Command") {
1905
+ const cmd = await ctx.ui.input(`Command executable for ${selectedLanguage}`, "");
1906
+ if (!cmd?.trim()) {
1907
+ ctx.ui.notify(`Command executable is required for ${selectedLanguage}`, "error");
1908
+ continue;
1909
+ }
1910
+ entry.cmd = cmd.trim();
1911
+ }
1912
+
1913
+ const paramsInput = await ctx.ui.input(
1914
+ `Additional parameters for ${moduleName} on ${selectedLanguage} (optional, shell-style)`,
1915
+ "",
1916
+ );
1917
+ const params = paramsInput?.trim() ? shellSplit(paramsInput.trim()) : [];
1918
+ if (params.length > 0) {
1919
+ entry.params = params;
1920
+ }
1921
+
1922
+ config["static-check"][selectedLanguage] ??= [];
1923
+ config["static-check"][selectedLanguage]!.push(entry);
1924
+ ctx.ui.notify(`Added ${entry.module} checker for ${selectedLanguage}`, "info");
1925
+ continue;
1926
+ }
1927
+
1928
+ if (staticChoice === "add-entry-raw-spec") {
1929
+ const spec = await ctx.ui.input("Static-check spec", "Python=Ruff");
1930
+ if (!spec?.trim()) {
1931
+ continue;
1932
+ }
1933
+ try {
1934
+ const [canonicalLang, entry] = parseEnableStaticCheck(spec.trim());
1935
+ config["static-check"][canonicalLang] ??= [];
1936
+ config["static-check"][canonicalLang]!.push(entry);
1937
+ ctx.ui.notify(`Added ${entry.module} checker for ${canonicalLang}`, "info");
1938
+ } catch (error) {
1939
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1940
+ }
1941
+ continue;
1942
+ }
1943
+
1944
+ if (staticChoice === "remove-language-entry") {
1945
+ const configuredLanguage = await showPiUsereqSettingsMenu(ctx, "remove static-check language", buildConfiguredStaticCheckLanguageChoices(config));
1946
+ if (!configuredLanguage || configuredLanguage === "back") {
1947
+ continue;
1948
+ }
1949
+ delete config["static-check"][configuredLanguage];
1950
+ ctx.ui.notify(`Removed static-check entries for ${configuredLanguage}`, "info");
1951
+ }
1952
+ }
1953
+ }
1954
+
1955
+ /**
1956
+ * @brief Builds the shared settings-menu choices for the top-level pi-usereq configuration UI.
1957
+ * @details Serializes primary configuration actions into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(s) in source-directory count. No external state is mutated.
1958
+ * @param[in] config {UseReqConfig} Effective project configuration.
1959
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
1960
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152
1961
+ */
1962
+ function buildPiUsereqMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1963
+ return [
1964
+ {
1965
+ id: "docs-dir",
1966
+ label: "docs-dir",
1967
+ value: config["docs-dir"],
1968
+ description: "Edit the repository-relative directory that stores REQUIREMENTS, WORKFLOW, and REFERENCES documents.",
1969
+ },
1970
+ {
1971
+ id: "tests-dir",
1972
+ label: "tests-dir",
1973
+ value: config["tests-dir"],
1974
+ description: "Edit the repository-relative directory used for project test assets and static-check selection.",
1975
+ },
1976
+ {
1977
+ id: "src-dir",
1978
+ label: "src-dir",
1979
+ value: config["src-dir"].join(", "),
1980
+ description: "Manage the repository-relative source directories scanned by project-scope tools.",
1981
+ },
1982
+ {
1983
+ id: "static-check",
1984
+ label: "static-check",
1985
+ value: formatStaticCheckLanguagesSummary(config),
1986
+ description: "Manage configured static-check entries and inspect supported languages and modules.",
1987
+ },
1988
+ {
1989
+ id: "startup-tools",
1990
+ label: "startup tools",
1991
+ value: `${getConfiguredEnabledPiUsereqTools(config).length} enabled`,
1992
+ description: "Manage which configurable tools become active during session_start.",
1993
+ },
1994
+ {
1995
+ id: "notifications",
1996
+ label: "notifications",
1997
+ value: `beep:${formatPiNotifyBeepStatus(config)} • sound:${config["notify-sound"]}`,
1998
+ description: "Manage terminal beep flags, selected notify command, hotkey bind, and per-level notify commands.",
1999
+ },
2000
+ {
2001
+ id: "reset-defaults",
2002
+ label: "Reset defaults",
2003
+ value: "",
2004
+ description: "Restore the default pi-usereq configuration for the current project base.",
2005
+ },
2006
+ {
2007
+ id: "show-config",
2008
+ label: "show-config",
2009
+ value: "",
2010
+ description: "Write the current project configuration JSON into the editor without saving additional changes.",
2011
+ },
2012
+ {
2013
+ id: "save-and-close",
2014
+ label: "Save and close",
2015
+ value: "",
2016
+ description: "Persist the current configuration and return to the normal pi session UI.",
2017
+ },
2018
+ ];
2019
+ }
2020
+
2021
+ /**
2022
+ * @brief Builds the shared settings-menu choices for source-directory management.
2023
+ * @details Exposes add and remove actions for `src-dir` entries through right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(s) in source-directory count. No external state is mutated.
2024
+ * @param[in] config {UseReqConfig} Effective project configuration.
2025
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered source-directory management choices.
2026
+ * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154
2027
+ */
2028
+ function buildSrcDirMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2029
+ return [
2030
+ {
2031
+ id: "add-src-dir-entry",
2032
+ label: "Add src-dir entry",
2033
+ value: `${config["src-dir"].length} configured`,
2034
+ description: "Append one repository-relative source directory to the current configuration.",
2035
+ },
2036
+ {
2037
+ id: "remove-src-dir-entry",
2038
+ label: "Remove src-dir entry",
2039
+ value: config["src-dir"].join(", "),
2040
+ description: "Select one configured source directory to remove from the current configuration.",
2041
+ },
2042
+ {
2043
+ id: "back",
2044
+ label: "Back",
2045
+ value: "",
2046
+ description: "Return to the parent configuration menu.",
2047
+ },
2048
+ ];
2049
+ }
2050
+
2051
+ /**
2052
+ * @brief Builds the shared settings-menu choices for removing one source-directory entry.
2053
+ * @details Exposes every configured `src-dir` entry as one removable row and appends a `Back` action for cancellation. Runtime is O(s) in source-directory count. No external state is mutated.
2054
+ * @param[in] config {UseReqConfig} Effective project configuration.
2055
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered removable source-directory choices.
2056
+ * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154
2057
+ */
2058
+ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2059
+ return [
2060
+ ...config["src-dir"].map((entry) => ({
2061
+ id: entry,
2062
+ label: entry,
2063
+ value: "remove",
2064
+ description: `Remove the source-directory entry ${entry} from the current configuration.`,
2065
+ })),
2066
+ {
2067
+ id: "back",
2068
+ label: "Back",
2069
+ value: "",
2070
+ description: "Return to the source-directory menu.",
2071
+ },
2072
+ ];
2073
+ }
2074
+
2075
+ /**
2076
+ * @brief Runs the top-level pi-usereq configuration menu.
2077
+ * @details Loads project config, exposes docs/test/source/static-check/startup-tool/notification actions through the shared settings-menu renderer, persists changes on exit, and refreshes the single-line status bar. Runtime depends on user interaction count. Side effects include UI updates, config writes, and active-tool changes.
2078
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
2079
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
2080
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2081
+ * @return {Promise<void>} Promise resolved when configuration is saved and the menu closes.
2082
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154
2083
+ */
2084
+ async function configurePiUsereq(
2085
+ pi: ExtensionAPI,
2086
+ ctx: ExtensionCommandContext,
2087
+ statusController: PiUsereqStatusController,
2088
+ ): Promise<void> {
2089
+ let config = loadProjectConfig(ctx.cwd);
2090
+ const projectBase = getProjectBase(ctx.cwd);
2091
+ const initialShortcut = config["notify-sound-toggle-shortcut"];
2092
+ const ensureSaved = () => saveProjectConfig(ctx.cwd, config);
2093
+ const refreshStatus = () => {
2094
+ setPiUsereqStatusConfig(statusController, config);
2095
+ renderPiUsereqStatus(statusController, ctx);
2096
+ };
2097
+
2098
+ while (true) {
2099
+ const choice = await showPiUsereqSettingsMenu(ctx, "pi-usereq", buildPiUsereqMenuChoices(config));
2100
+ if (!choice || choice === "save-and-close") {
2101
+ ensureSaved();
2102
+ refreshStatus();
2103
+ if (config["notify-sound-toggle-shortcut"] !== initialShortcut) {
2104
+ ctx.ui.notify("Sound toggle hotkey bind updated; run /reload to apply the new binding", "info");
2105
+ }
2106
+ return;
2107
+ }
2108
+ if (choice === "docs-dir") {
2109
+ const value = await ctx.ui.input("docs-dir", config["docs-dir"]);
2110
+ if (value?.trim()) config["docs-dir"] = value.trim();
2111
+ continue;
2112
+ }
2113
+ if (choice === "tests-dir") {
2114
+ const value = await ctx.ui.input("tests-dir", config["tests-dir"]);
2115
+ if (value?.trim()) config["tests-dir"] = value.trim();
2116
+ continue;
2117
+ }
2118
+ if (choice === "src-dir") {
2119
+ while (true) {
2120
+ const srcAction = await showPiUsereqSettingsMenu(ctx, "src-dir", buildSrcDirMenuChoices(config));
2121
+ if (!srcAction || srcAction === "back") {
2122
+ break;
2123
+ }
2124
+ if (srcAction === "add-src-dir-entry") {
2125
+ const value = await ctx.ui.input("New src-dir entry", "src");
2126
+ if (value?.trim()) {
2127
+ config["src-dir"] = [...config["src-dir"], value.trim()];
2128
+ }
2129
+ continue;
2130
+ }
2131
+ if (srcAction === "remove-src-dir-entry") {
2132
+ const toRemove = await showPiUsereqSettingsMenu(ctx, "remove src-dir entry", buildSrcDirRemovalChoices(config));
2133
+ if (toRemove && toRemove !== "back") {
2134
+ config["src-dir"] = config["src-dir"].filter((entry) => entry !== toRemove);
2135
+ if (config["src-dir"].length === 0) {
2136
+ config["src-dir"] = ["src"];
2137
+ }
2138
+ }
2139
+ }
2140
+ }
2141
+ continue;
2142
+ }
2143
+ if (choice === "static-check") {
2144
+ await configureStaticCheckMenu(ctx, config);
2145
+ continue;
2146
+ }
2147
+ if (choice === "startup-tools") {
2148
+ await configurePiUsereqToolsMenu(pi, ctx, config);
2149
+ continue;
2150
+ }
2151
+ if (choice === "notifications") {
2152
+ await configurePiNotifyMenu(ctx, config);
2153
+ continue;
2154
+ }
2155
+ if (choice === "reset-defaults") {
2156
+ config = getDefaultConfig(projectBase);
2157
+ applyConfiguredPiUsereqTools(pi, config);
2158
+ continue;
2159
+ }
2160
+ if (choice === "show-config") {
2161
+ ctx.ui.setEditorText(`${JSON.stringify(config, null, 2)}\n`);
2162
+ continue;
2163
+ }
2164
+ }
2165
+ }
2166
+
2167
+ /**
2168
+ * @brief Registers configuration-management commands.
2169
+ * @details Adds the interactive `pi-usereq` configuration command only; the config-viewer action is now exposed exclusively inside that menu. Runtime is O(1) for registration. Side effects include command registration.
2170
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
2171
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2172
+ * @return {void} No return value.
2173
+ * @satisfies REQ-006, REQ-031
2174
+ */
2175
+ function registerConfigCommands(
2176
+ pi: ExtensionAPI,
2177
+ statusController: PiUsereqStatusController,
2178
+ ): void {
2179
+ pi.registerCommand("pi-usereq", {
2180
+ description: "Open the pi-usereq configuration menu",
2181
+ handler: async (_args, ctx) => {
2182
+ await configurePiUsereq(pi, ctx, statusController);
2183
+ },
2184
+ });
2185
+ }
2186
+
2187
+ /**
2188
+ * @brief Registers the complete pi-usereq extension.
2189
+ * @details Validates installation-owned bundled resources, registers prompt and
2190
+ * configuration commands plus agent tools, registers the configurable
2191
+ * successful-run sound shortcut when the runtime supports shortcuts, and
2192
+ * installs shared wrappers for all supported pi lifecycle hooks so status
2193
+ * telemetry, context usage, prompt timing, and pi-notify effects remain
2194
+ * synchronized with runtime events. Runtime is O(h) in hook count during
2195
+ * registration. Side effects include filesystem reads, command/tool/shortcut
2196
+ * registration, UI updates, active-tool changes, and timer scheduling.
2197
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
2198
+ * @return {void} No return value.
2199
+ * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-045, REQ-067, REQ-068, REQ-109, REQ-110, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-129, REQ-130, REQ-131, REQ-132, REQ-133, REQ-134, REQ-135, REQ-136, REQ-137
2200
+ */
2201
+ export default function piUsereqExtension(pi: ExtensionAPI): void {
2202
+ const statusController = createPiUsereqStatusController(() => pi.getActiveTools());
2203
+ ensureBundledResourcesAccessible();
2204
+ registerPromptCommands(pi);
2205
+ registerAgentTools(pi);
2206
+ registerConfigCommands(pi, statusController);
2207
+ registerPiNotifyShortcut(pi, statusController);
2208
+ registerExtensionStatusHooks(pi, statusController);
2209
+ }