pi-usereq 0.7.0 → 0.11.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 (773) hide show
  1. package/.gitignore +2 -0
  2. package/CHANGELOG.md +24 -0
  3. package/README.md +1 -1
  4. package/docs/pi.dev/coding-agent-docs/custom-provider.md +42 -1
  5. package/docs/pi.dev/coding-agent-docs/extensions.md +302 -28
  6. package/docs/pi.dev/coding-agent-docs/keybindings.md +23 -1
  7. package/docs/pi.dev/coding-agent-docs/models.md +3 -0
  8. package/docs/pi.dev/coding-agent-docs/packages.md +2 -2
  9. package/docs/pi.dev/coding-agent-docs/prompt-templates.md +21 -0
  10. package/docs/pi.dev/coding-agent-docs/providers.md +1 -0
  11. package/docs/pi.dev/coding-agent-docs/rpc.md +36 -6
  12. package/docs/pi.dev/coding-agent-docs/sdk.md +30 -5
  13. package/docs/pi.dev/coding-agent-docs/session.md +1 -1
  14. package/docs/pi.dev/coding-agent-docs/settings.md +2 -1
  15. package/docs/pi.dev/coding-agent-docs/skills.md +12 -12
  16. package/docs/pi.dev/coding-agent-docs/tree.md +2 -0
  17. package/docs/pi.dev/coding-agent-docs/tui.md +31 -0
  18. package/docs/pi.dev/mom-docs/new.md +4 -4
  19. package/package.json +1 -1
  20. package/pi-usereq/docs/REFERENCES.md +5214 -24470
  21. package/pi-usereq/docs/REQUIREMENTS.md +307 -170
  22. package/pi-usereq/docs/WORKFLOW.md +334 -316
  23. package/pi.dev-src/pi-mono/agent/src/agent-loop.ts +683 -0
  24. package/pi.dev-src/pi-mono/agent/src/agent.ts +543 -0
  25. package/pi.dev-src/pi-mono/agent/src/index.ts +8 -0
  26. package/pi.dev-src/pi-mono/agent/src/proxy.ts +367 -0
  27. package/pi.dev-src/pi-mono/agent/src/types.ts +364 -0
  28. package/pi.dev-src/pi-mono/agent/test/agent-loop.test.ts +1181 -0
  29. package/pi.dev-src/pi-mono/agent/test/agent.test.ts +463 -0
  30. package/pi.dev-src/pi-mono/agent/test/e2e.test.ts +404 -0
  31. package/pi.dev-src/pi-mono/agent/test/utils/calculate.ts +32 -0
  32. package/pi.dev-src/pi-mono/agent/test/utils/get-current-time.ts +46 -0
  33. package/pi.dev-src/pi-mono/agent/vitest.config.ts +9 -0
  34. package/pi.dev-src/pi-mono/ai/bedrock-provider.d.ts +1 -0
  35. package/pi.dev-src/pi-mono/ai/bedrock-provider.js +1 -0
  36. package/pi.dev-src/pi-mono/ai/scripts/generate-models.ts +1635 -0
  37. package/pi.dev-src/pi-mono/ai/scripts/generate-test-image.ts +34 -0
  38. package/pi.dev-src/pi-mono/ai/src/api-registry.ts +98 -0
  39. package/pi.dev-src/pi-mono/ai/src/bedrock-provider.ts +6 -0
  40. package/pi.dev-src/pi-mono/ai/src/cli.ts +133 -0
  41. package/pi.dev-src/pi-mono/ai/src/env-api-keys.ts +134 -0
  42. package/pi.dev-src/pi-mono/ai/src/index.ts +35 -0
  43. package/pi.dev-src/pi-mono/ai/src/models.generated.ts +14928 -0
  44. package/pi.dev-src/pi-mono/ai/src/models.ts +82 -0
  45. package/pi.dev-src/pi-mono/ai/src/oauth.ts +1 -0
  46. package/pi.dev-src/pi-mono/ai/src/providers/amazon-bedrock.ts +929 -0
  47. package/pi.dev-src/pi-mono/ai/src/providers/anthropic.ts +1125 -0
  48. package/pi.dev-src/pi-mono/ai/src/providers/azure-openai-responses.ts +253 -0
  49. package/pi.dev-src/pi-mono/ai/src/providers/faux.ts +499 -0
  50. package/pi.dev-src/pi-mono/ai/src/providers/github-copilot-headers.ts +37 -0
  51. package/pi.dev-src/pi-mono/ai/src/providers/google-gemini-cli.ts +996 -0
  52. package/pi.dev-src/pi-mono/ai/src/providers/google-shared.ts +354 -0
  53. package/pi.dev-src/pi-mono/ai/src/providers/google-vertex.ts +543 -0
  54. package/pi.dev-src/pi-mono/ai/src/providers/google.ts +500 -0
  55. package/pi.dev-src/pi-mono/ai/src/providers/mistral.ts +629 -0
  56. package/pi.dev-src/pi-mono/ai/src/providers/openai-codex-responses.ts +984 -0
  57. package/pi.dev-src/pi-mono/ai/src/providers/openai-completions.ts +1063 -0
  58. package/pi.dev-src/pi-mono/ai/src/providers/openai-responses-shared.ts +539 -0
  59. package/pi.dev-src/pi-mono/ai/src/providers/openai-responses.ts +264 -0
  60. package/pi.dev-src/pi-mono/ai/src/providers/register-builtins.ts +433 -0
  61. package/pi.dev-src/pi-mono/ai/src/providers/simple-options.ts +47 -0
  62. package/pi.dev-src/pi-mono/ai/src/providers/transform-messages.ts +220 -0
  63. package/pi.dev-src/pi-mono/ai/src/stream.ts +59 -0
  64. package/pi.dev-src/pi-mono/ai/src/types.ts +417 -0
  65. package/pi.dev-src/pi-mono/ai/src/utils/event-stream.ts +87 -0
  66. package/pi.dev-src/pi-mono/ai/src/utils/hash.ts +13 -0
  67. package/pi.dev-src/pi-mono/ai/src/utils/headers.ts +7 -0
  68. package/pi.dev-src/pi-mono/ai/src/utils/json-parse.ts +124 -0
  69. package/pi.dev-src/pi-mono/ai/src/utils/overflow.ts +138 -0
  70. package/pi.dev-src/pi-mono/ai/src/utils/sanitize-unicode.ts +25 -0
  71. package/pi.dev-src/pi-mono/ai/src/utils/typebox-helpers.ts +24 -0
  72. package/pi.dev-src/pi-mono/ai/src/utils/validation.ts +324 -0
  73. package/pi.dev-src/pi-mono/ai/test/abort.test.ts +260 -0
  74. package/pi.dev-src/pi-mono/ai/test/anthropic-oauth.test.ts +99 -0
  75. package/pi.dev-src/pi-mono/ai/test/anthropic-opus-4-7-smoke.test.ts +72 -0
  76. package/pi.dev-src/pi-mono/ai/test/anthropic-sse-parsing.test.ts +113 -0
  77. package/pi.dev-src/pi-mono/ai/test/anthropic-thinking-disable.test.ts +152 -0
  78. package/pi.dev-src/pi-mono/ai/test/anthropic-tool-name-normalization.test.ts +205 -0
  79. package/pi.dev-src/pi-mono/ai/test/azure-utils.ts +28 -0
  80. package/pi.dev-src/pi-mono/ai/test/bedrock-endpoint-resolution.test.ts +131 -0
  81. package/pi.dev-src/pi-mono/ai/test/bedrock-models.test.ts +66 -0
  82. package/pi.dev-src/pi-mono/ai/test/bedrock-thinking-payload.test.ts +107 -0
  83. package/pi.dev-src/pi-mono/ai/test/bedrock-utils.ts +18 -0
  84. package/pi.dev-src/pi-mono/ai/test/cache-retention.test.ts +335 -0
  85. package/pi.dev-src/pi-mono/ai/test/context-overflow.test.ts +714 -0
  86. package/pi.dev-src/pi-mono/ai/test/cross-provider-handoff.test.ts +458 -0
  87. package/pi.dev-src/pi-mono/ai/test/data/red-circle.png +0 -0
  88. package/pi.dev-src/pi-mono/ai/test/empty.test.ts +742 -0
  89. package/pi.dev-src/pi-mono/ai/test/faux-provider.test.ts +597 -0
  90. package/pi.dev-src/pi-mono/ai/test/fireworks-models.test.ts +49 -0
  91. package/pi.dev-src/pi-mono/ai/test/github-copilot-anthropic.test.ts +108 -0
  92. package/pi.dev-src/pi-mono/ai/test/github-copilot-oauth.test.ts +196 -0
  93. package/pi.dev-src/pi-mono/ai/test/google-gemini-cli-claude-thinking-header.test.ts +103 -0
  94. package/pi.dev-src/pi-mono/ai/test/google-gemini-cli-empty-stream.test.ts +108 -0
  95. package/pi.dev-src/pi-mono/ai/test/google-gemini-cli-retry-delay.test.ts +53 -0
  96. package/pi.dev-src/pi-mono/ai/test/google-shared-convert-tools.test.ts +187 -0
  97. package/pi.dev-src/pi-mono/ai/test/google-shared-gemini3-unsigned-tool-call.test.ts +172 -0
  98. package/pi.dev-src/pi-mono/ai/test/google-shared-image-tool-result-routing.test.ts +124 -0
  99. package/pi.dev-src/pi-mono/ai/test/google-thinking-disable.test.ts +183 -0
  100. package/pi.dev-src/pi-mono/ai/test/google-thinking-signature.test.ts +38 -0
  101. package/pi.dev-src/pi-mono/ai/test/google-tool-call-missing-args.test.ts +105 -0
  102. package/pi.dev-src/pi-mono/ai/test/google-vertex-api-key-resolution.test.ts +144 -0
  103. package/pi.dev-src/pi-mono/ai/test/image-tool-result.test.ts +481 -0
  104. package/pi.dev-src/pi-mono/ai/test/interleaved-thinking.test.ts +145 -0
  105. package/pi.dev-src/pi-mono/ai/test/lazy-module-load.test.ts +102 -0
  106. package/pi.dev-src/pi-mono/ai/test/mistral-reasoning-mode.test.ts +66 -0
  107. package/pi.dev-src/pi-mono/ai/test/mistral-tool-schema.test.ts +61 -0
  108. package/pi.dev-src/pi-mono/ai/test/oauth.ts +89 -0
  109. package/pi.dev-src/pi-mono/ai/test/openai-codex-cache-affinity-e2e.test.ts +36 -0
  110. package/pi.dev-src/pi-mono/ai/test/openai-codex-stream.test.ts +686 -0
  111. package/pi.dev-src/pi-mono/ai/test/openai-completions-cache-control-format.test.ts +189 -0
  112. package/pi.dev-src/pi-mono/ai/test/openai-completions-prompt-cache.test.ts +198 -0
  113. package/pi.dev-src/pi-mono/ai/test/openai-completions-thinking-as-text.test.ts +214 -0
  114. package/pi.dev-src/pi-mono/ai/test/openai-completions-tool-choice.test.ts +560 -0
  115. package/pi.dev-src/pi-mono/ai/test/openai-completions-tool-result-images.test.ts +101 -0
  116. package/pi.dev-src/pi-mono/ai/test/openai-responses-cache-affinity-e2e.test.ts +32 -0
  117. package/pi.dev-src/pi-mono/ai/test/openai-responses-copilot-provider.test.ts +128 -0
  118. package/pi.dev-src/pi-mono/ai/test/openai-responses-foreign-toolcall-id.test.ts +66 -0
  119. package/pi.dev-src/pi-mono/ai/test/openai-responses-partial-json-cleanup.test.ts +101 -0
  120. package/pi.dev-src/pi-mono/ai/test/openai-responses-reasoning-replay-e2e.test.ts +292 -0
  121. package/pi.dev-src/pi-mono/ai/test/openai-responses-tool-result-images.test.ts +200 -0
  122. package/pi.dev-src/pi-mono/ai/test/openrouter-cache-write-repro.test.ts +78 -0
  123. package/pi.dev-src/pi-mono/ai/test/overflow.test.ts +64 -0
  124. package/pi.dev-src/pi-mono/ai/test/responseid.test.ts +145 -0
  125. package/pi.dev-src/pi-mono/ai/test/stream.test.ts +1405 -0
  126. package/pi.dev-src/pi-mono/ai/test/supports-xhigh.test.ts +34 -0
  127. package/pi.dev-src/pi-mono/ai/test/tokens.test.ts +304 -0
  128. package/pi.dev-src/pi-mono/ai/test/tool-call-id-normalization.test.ts +291 -0
  129. package/pi.dev-src/pi-mono/ai/test/tool-call-without-result.test.ts +313 -0
  130. package/pi.dev-src/pi-mono/ai/test/total-tokens.test.ts +695 -0
  131. package/pi.dev-src/pi-mono/ai/test/transform-messages-copilot-openai-to-anthropic.test.ts +191 -0
  132. package/pi.dev-src/pi-mono/ai/test/unicode-surrogate.test.ts +754 -0
  133. package/pi.dev-src/pi-mono/ai/test/validation.test.ts +116 -0
  134. package/pi.dev-src/pi-mono/ai/test/xhigh.test.ts +71 -0
  135. package/pi.dev-src/pi-mono/ai/test/zen.test.ts +25 -0
  136. package/pi.dev-src/pi-mono/ai/vitest.config.ts +9 -0
  137. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/antigravity-image-gen.ts +418 -0
  138. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/auto-commit-on-exit.ts +49 -0
  139. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/bash-spawn-hook.ts +30 -0
  140. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/bookmark.ts +50 -0
  141. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/built-in-tool-renderer.ts +249 -0
  142. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/claude-rules.ts +86 -0
  143. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/commands.ts +72 -0
  144. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/confirm-destructive.ts +59 -0
  145. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-compaction.ts +127 -0
  146. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-footer.ts +64 -0
  147. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-header.ts +73 -0
  148. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-anthropic/index.ts +604 -0
  149. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +24 -0
  150. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-gitlab-duo/index.ts +349 -0
  151. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-gitlab-duo/test.ts +82 -0
  152. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/custom-provider-qwen-cli/index.ts +345 -0
  153. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dirty-repo-guard.ts +56 -0
  154. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom/build.sh +152 -0
  155. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom/doomgeneric_pi.c +72 -0
  156. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom-component.ts +132 -0
  157. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom-engine.ts +173 -0
  158. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/doom-keys.ts +104 -0
  159. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/index.ts +74 -0
  160. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/doom-overlay/wad-finder.ts +51 -0
  161. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-resources/SKILL.md +8 -0
  162. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-resources/dynamic.json +79 -0
  163. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-resources/dynamic.md +5 -0
  164. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-resources/index.ts +15 -0
  165. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/dynamic-tools.ts +74 -0
  166. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/event-bus.ts +43 -0
  167. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/file-trigger.ts +41 -0
  168. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/git-checkpoint.ts +53 -0
  169. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/github-issue-autocomplete.ts +185 -0
  170. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/handoff.ts +153 -0
  171. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/hello.ts +26 -0
  172. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/hidden-thinking-label.ts +53 -0
  173. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/inline-bash.ts +94 -0
  174. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/input-transform.ts +43 -0
  175. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/interactive-shell.ts +196 -0
  176. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/mac-system-theme.ts +47 -0
  177. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/message-renderer.ts +59 -0
  178. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/minimal-mode.ts +426 -0
  179. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/modal-editor.ts +85 -0
  180. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/model-status.ts +31 -0
  181. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/notify.ts +55 -0
  182. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/overlay-qa-tests.ts +1348 -0
  183. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/overlay-test.ts +150 -0
  184. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/permission-gate.ts +34 -0
  185. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/pirate.ts +47 -0
  186. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/plan-mode/index.ts +340 -0
  187. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/plan-mode/utils.ts +168 -0
  188. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/preset.ts +430 -0
  189. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/prompt-customizer.ts +97 -0
  190. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/protected-paths.ts +30 -0
  191. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/provider-payload.ts +18 -0
  192. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/qna.ts +122 -0
  193. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/question.ts +264 -0
  194. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/questionnaire.ts +427 -0
  195. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/rainbow-editor.ts +88 -0
  196. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/reload-runtime.ts +37 -0
  197. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/rpc-demo.ts +118 -0
  198. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/sandbox/index.ts +321 -0
  199. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/sandbox/package-lock.json +92 -0
  200. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/send-user-message.ts +97 -0
  201. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/session-name.ts +27 -0
  202. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/shutdown-command.ts +63 -0
  203. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/snake.ts +343 -0
  204. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/space-invaders.ts +560 -0
  205. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/ssh.ts +220 -0
  206. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/status-line.ts +32 -0
  207. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/structured-output.ts +65 -0
  208. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents/planner.md +37 -0
  209. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents/reviewer.md +35 -0
  210. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents/scout.md +50 -0
  211. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents/worker.md +24 -0
  212. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/agents.ts +126 -0
  213. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/index.ts +987 -0
  214. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/prompts/implement-and-review.md +10 -0
  215. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/prompts/implement.md +10 -0
  216. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/subagent/prompts/scout-and-plan.md +9 -0
  217. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/summarize.ts +206 -0
  218. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/system-prompt-header.ts +17 -0
  219. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/tic-tac-toe.ts +1008 -0
  220. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/timed-confirm.ts +70 -0
  221. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/titlebar-spinner.ts +58 -0
  222. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/todo.ts +297 -0
  223. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/tool-override.ts +144 -0
  224. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/tools.ts +141 -0
  225. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/trigger-compact.ts +50 -0
  226. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/truncated-tool.ts +195 -0
  227. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/widget-placement.ts +9 -0
  228. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/with-deps/index.ts +32 -0
  229. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/with-deps/package-lock.json +31 -0
  230. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/working-indicator.ts +123 -0
  231. package/pi.dev-src/pi-mono/coding-agent/examples/extensions/working-message-test.ts +25 -0
  232. package/pi.dev-src/pi-mono/coding-agent/examples/rpc-extension-ui.ts +632 -0
  233. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/01-minimal.ts +22 -0
  234. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/02-custom-model.ts +49 -0
  235. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/03-custom-prompt.ts +62 -0
  236. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/04-skills.ts +55 -0
  237. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/05-tools.ts +44 -0
  238. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/06-extensions.ts +90 -0
  239. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/07-context-files.ts +42 -0
  240. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/08-prompt-templates.ts +51 -0
  241. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/09-api-keys-and-oauth.ts +48 -0
  242. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/10-settings.ts +53 -0
  243. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/11-sessions.ts +48 -0
  244. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/12-full-control.ts +73 -0
  245. package/pi.dev-src/pi-mono/coding-agent/examples/sdk/13-session-runtime.ts +67 -0
  246. package/pi.dev-src/pi-mono/coding-agent/scripts/migrate-sessions.sh +93 -0
  247. package/pi.dev-src/pi-mono/coding-agent/src/bun/cli.ts +6 -0
  248. package/pi.dev-src/pi-mono/coding-agent/src/bun/register-bedrock.ts +4 -0
  249. package/pi.dev-src/pi-mono/coding-agent/src/cli/args.ts +335 -0
  250. package/pi.dev-src/pi-mono/coding-agent/src/cli/config-selector.ts +52 -0
  251. package/pi.dev-src/pi-mono/coding-agent/src/cli/file-processor.ts +100 -0
  252. package/pi.dev-src/pi-mono/coding-agent/src/cli/initial-message.ts +43 -0
  253. package/pi.dev-src/pi-mono/coding-agent/src/cli/list-models.ts +110 -0
  254. package/pi.dev-src/pi-mono/coding-agent/src/cli/session-picker.ts +52 -0
  255. package/pi.dev-src/pi-mono/coding-agent/src/cli.ts +17 -0
  256. package/pi.dev-src/pi-mono/coding-agent/src/config.ts +261 -0
  257. package/pi.dev-src/pi-mono/coding-agent/src/core/agent-session-runtime.ts +394 -0
  258. package/pi.dev-src/pi-mono/coding-agent/src/core/agent-session-services.ts +196 -0
  259. package/pi.dev-src/pi-mono/coding-agent/src/core/agent-session.ts +3096 -0
  260. package/pi.dev-src/pi-mono/coding-agent/src/core/auth-storage.ts +493 -0
  261. package/pi.dev-src/pi-mono/coding-agent/src/core/bash-executor.ts +154 -0
  262. package/pi.dev-src/pi-mono/coding-agent/src/core/compaction/branch-summarization.ts +355 -0
  263. package/pi.dev-src/pi-mono/coding-agent/src/core/compaction/compaction.ts +839 -0
  264. package/pi.dev-src/pi-mono/coding-agent/src/core/compaction/index.ts +7 -0
  265. package/pi.dev-src/pi-mono/coding-agent/src/core/compaction/utils.ts +170 -0
  266. package/pi.dev-src/pi-mono/coding-agent/src/core/defaults.ts +3 -0
  267. package/pi.dev-src/pi-mono/coding-agent/src/core/diagnostics.ts +15 -0
  268. package/pi.dev-src/pi-mono/coding-agent/src/core/event-bus.ts +33 -0
  269. package/pi.dev-src/pi-mono/coding-agent/src/core/exec.ts +107 -0
  270. package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/ansi-to-html.ts +258 -0
  271. package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/index.ts +314 -0
  272. package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/template.css +1017 -0
  273. package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/template.html +55 -0
  274. package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/template.js +1770 -0
  275. package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/tool-renderer.ts +158 -0
  276. package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/vendor/highlight.min.js +1213 -0
  277. package/pi.dev-src/pi-mono/coding-agent/src/core/export-html/vendor/marked.min.js +6 -0
  278. package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/index.ts +171 -0
  279. package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/loader.ts +606 -0
  280. package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/runner.ts +1019 -0
  281. package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/types.ts +1542 -0
  282. package/pi.dev-src/pi-mono/coding-agent/src/core/extensions/wrapper.ts +30 -0
  283. package/pi.dev-src/pi-mono/coding-agent/src/core/footer-data-provider.ts +354 -0
  284. package/pi.dev-src/pi-mono/coding-agent/src/core/index.ts +77 -0
  285. package/pi.dev-src/pi-mono/coding-agent/src/core/keybindings.ts +370 -0
  286. package/pi.dev-src/pi-mono/coding-agent/src/core/messages.ts +195 -0
  287. package/pi.dev-src/pi-mono/coding-agent/src/core/model-registry.ts +858 -0
  288. package/pi.dev-src/pi-mono/coding-agent/src/core/model-resolver.ts +629 -0
  289. package/pi.dev-src/pi-mono/coding-agent/src/core/output-guard.ts +74 -0
  290. package/pi.dev-src/pi-mono/coding-agent/src/core/package-manager.ts +2408 -0
  291. package/pi.dev-src/pi-mono/coding-agent/src/core/prompt-templates.ts +296 -0
  292. package/pi.dev-src/pi-mono/coding-agent/src/core/resolve-config-value.ts +142 -0
  293. package/pi.dev-src/pi-mono/coding-agent/src/core/resource-loader.ts +917 -0
  294. package/pi.dev-src/pi-mono/coding-agent/src/core/sdk.ts +380 -0
  295. package/pi.dev-src/pi-mono/coding-agent/src/core/session-cwd.ts +59 -0
  296. package/pi.dev-src/pi-mono/coding-agent/src/core/session-manager.ts +1425 -0
  297. package/pi.dev-src/pi-mono/coding-agent/src/core/settings-manager.ts +999 -0
  298. package/pi.dev-src/pi-mono/coding-agent/src/core/skills.ts +508 -0
  299. package/pi.dev-src/pi-mono/coding-agent/src/core/slash-commands.ts +39 -0
  300. package/pi.dev-src/pi-mono/coding-agent/src/core/source-info.ts +40 -0
  301. package/pi.dev-src/pi-mono/coding-agent/src/core/system-prompt.ts +172 -0
  302. package/pi.dev-src/pi-mono/coding-agent/src/core/telemetry.ts +13 -0
  303. package/pi.dev-src/pi-mono/coding-agent/src/core/timings.ts +31 -0
  304. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/bash.ts +448 -0
  305. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/edit-diff.ts +445 -0
  306. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/edit.ts +487 -0
  307. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/file-mutation-queue.ts +39 -0
  308. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/find.ts +370 -0
  309. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/grep.ts +384 -0
  310. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/index.ts +196 -0
  311. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/ls.ts +229 -0
  312. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/path-utils.ts +94 -0
  313. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/read.ts +273 -0
  314. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/render-utils.ts +64 -0
  315. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/tool-definition-wrapper.ts +45 -0
  316. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/truncate.ts +265 -0
  317. package/pi.dev-src/pi-mono/coding-agent/src/core/tools/write.ts +281 -0
  318. package/pi.dev-src/pi-mono/coding-agent/src/index.ts +352 -0
  319. package/pi.dev-src/pi-mono/coding-agent/src/main.ts +731 -0
  320. package/pi.dev-src/pi-mono/coding-agent/src/migrations.ts +314 -0
  321. package/pi.dev-src/pi-mono/coding-agent/src/modes/index.ts +9 -0
  322. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/assets/clankolas.png +0 -0
  323. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/armin.ts +382 -0
  324. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/assistant-message.ts +147 -0
  325. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/bash-execution.ts +218 -0
  326. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/bordered-loader.ts +68 -0
  327. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/branch-summary-message.ts +58 -0
  328. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/compaction-summary-message.ts +59 -0
  329. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/config-selector.ts +592 -0
  330. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/countdown-timer.ts +38 -0
  331. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/custom-editor.ts +80 -0
  332. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/custom-message.ts +99 -0
  333. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/daxnuts.ts +164 -0
  334. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/diff.ts +147 -0
  335. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/dynamic-border.ts +25 -0
  336. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/earendil-announcement.ts +53 -0
  337. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/extension-editor.ts +147 -0
  338. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/extension-input.ts +87 -0
  339. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/extension-selector.ts +107 -0
  340. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/footer.ts +220 -0
  341. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/index.ts +32 -0
  342. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/keybinding-hints.ts +24 -0
  343. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/login-dialog.ts +178 -0
  344. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/model-selector.ts +338 -0
  345. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/oauth-selector.ts +121 -0
  346. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/scoped-models-selector.ts +355 -0
  347. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/session-selector-search.ts +194 -0
  348. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/session-selector.ts +1026 -0
  349. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/settings-selector.ts +456 -0
  350. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/show-images-selector.ts +50 -0
  351. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/skill-invocation-message.ts +55 -0
  352. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/theme-selector.ts +67 -0
  353. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/thinking-selector.ts +74 -0
  354. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/tool-execution.ts +352 -0
  355. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/tree-selector.ts +1246 -0
  356. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/user-message-selector.ts +155 -0
  357. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/user-message.ts +35 -0
  358. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/components/visual-truncate.ts +50 -0
  359. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/interactive-mode.ts +5155 -0
  360. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/theme/dark.json +85 -0
  361. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/theme/light.json +84 -0
  362. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/theme/theme-schema.json +335 -0
  363. package/pi.dev-src/pi-mono/coding-agent/src/modes/interactive/theme/theme.ts +1141 -0
  364. package/pi.dev-src/pi-mono/coding-agent/src/modes/print-mode.ts +158 -0
  365. package/pi.dev-src/pi-mono/coding-agent/src/modes/rpc/jsonl.ts +58 -0
  366. package/pi.dev-src/pi-mono/coding-agent/src/modes/rpc/rpc-client.ts +515 -0
  367. package/pi.dev-src/pi-mono/coding-agent/src/modes/rpc/rpc-mode.ts +745 -0
  368. package/pi.dev-src/pi-mono/coding-agent/src/modes/rpc/rpc-types.ts +264 -0
  369. package/pi.dev-src/pi-mono/coding-agent/src/package-manager-cli.ts +270 -0
  370. package/pi.dev-src/pi-mono/coding-agent/src/utils/changelog.ts +99 -0
  371. package/pi.dev-src/pi-mono/coding-agent/src/utils/child-process.ts +86 -0
  372. package/pi.dev-src/pi-mono/coding-agent/src/utils/clipboard-image.ts +300 -0
  373. package/pi.dev-src/pi-mono/coding-agent/src/utils/clipboard-native.ts +22 -0
  374. package/pi.dev-src/pi-mono/coding-agent/src/utils/clipboard.ts +81 -0
  375. package/pi.dev-src/pi-mono/coding-agent/src/utils/exif-orientation.ts +183 -0
  376. package/pi.dev-src/pi-mono/coding-agent/src/utils/frontmatter.ts +39 -0
  377. package/pi.dev-src/pi-mono/coding-agent/src/utils/fs-watch.ts +30 -0
  378. package/pi.dev-src/pi-mono/coding-agent/src/utils/git.ts +192 -0
  379. package/pi.dev-src/pi-mono/coding-agent/src/utils/image-convert.ts +41 -0
  380. package/pi.dev-src/pi-mono/coding-agent/src/utils/image-resize.ts +176 -0
  381. package/pi.dev-src/pi-mono/coding-agent/src/utils/mime.ts +30 -0
  382. package/pi.dev-src/pi-mono/coding-agent/src/utils/paths.ts +20 -0
  383. package/pi.dev-src/pi-mono/coding-agent/src/utils/photon.ts +139 -0
  384. package/pi.dev-src/pi-mono/coding-agent/src/utils/shell.ts +210 -0
  385. package/pi.dev-src/pi-mono/coding-agent/src/utils/sleep.ts +18 -0
  386. package/pi.dev-src/pi-mono/coding-agent/src/utils/tools-manager.ts +287 -0
  387. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-auto-compaction-queue.test.ts +437 -0
  388. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-branching.test.ts +156 -0
  389. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-compaction.test.ts +208 -0
  390. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-concurrent.test.ts +618 -0
  391. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-dynamic-provider.test.ts +117 -0
  392. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-dynamic-tools.test.ts +182 -0
  393. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-retry.test.ts +318 -0
  394. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-runtime-events.test.ts +208 -0
  395. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-stats.test.ts +143 -0
  396. package/pi.dev-src/pi-mono/coding-agent/test/agent-session-tree-navigation.test.ts +323 -0
  397. package/pi.dev-src/pi-mono/coding-agent/test/args.test.ts +328 -0
  398. package/pi.dev-src/pi-mono/coding-agent/test/assistant-message.test.ts +57 -0
  399. package/pi.dev-src/pi-mono/coding-agent/test/auth-storage.test.ts +462 -0
  400. package/pi.dev-src/pi-mono/coding-agent/test/bash-close-hang-windows.test.ts +126 -0
  401. package/pi.dev-src/pi-mono/coding-agent/test/bash-execution-width.test.ts +80 -0
  402. package/pi.dev-src/pi-mono/coding-agent/test/block-images.test.ts +120 -0
  403. package/pi.dev-src/pi-mono/coding-agent/test/clipboard-image-bmp-conversion.test.ts +88 -0
  404. package/pi.dev-src/pi-mono/coding-agent/test/clipboard-image.test.ts +136 -0
  405. package/pi.dev-src/pi-mono/coding-agent/test/compaction-extensions-example.test.ts +66 -0
  406. package/pi.dev-src/pi-mono/coding-agent/test/compaction-extensions.test.ts +415 -0
  407. package/pi.dev-src/pi-mono/coding-agent/test/compaction-serialization.test.ts +79 -0
  408. package/pi.dev-src/pi-mono/coding-agent/test/compaction-summary-reasoning.test.ts +118 -0
  409. package/pi.dev-src/pi-mono/coding-agent/test/compaction-thinking-model.test.ts +223 -0
  410. package/pi.dev-src/pi-mono/coding-agent/test/compaction.test.ts +550 -0
  411. package/pi.dev-src/pi-mono/coding-agent/test/config.test.ts +30 -0
  412. package/pi.dev-src/pi-mono/coding-agent/test/edit-tool-legacy-input.test.ts +116 -0
  413. package/pi.dev-src/pi-mono/coding-agent/test/edit-tool-no-full-redraw.test.ts +235 -0
  414. package/pi.dev-src/pi-mono/coding-agent/test/export-html-whitespace.test.ts +12 -0
  415. package/pi.dev-src/pi-mono/coding-agent/test/export-html-xss.test.ts +28 -0
  416. package/pi.dev-src/pi-mono/coding-agent/test/extensions-discovery.test.ts +463 -0
  417. package/pi.dev-src/pi-mono/coding-agent/test/extensions-input-event.test.ts +112 -0
  418. package/pi.dev-src/pi-mono/coding-agent/test/extensions-runner.test.ts +805 -0
  419. package/pi.dev-src/pi-mono/coding-agent/test/file-mutation-queue.test.ts +163 -0
  420. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/assistant-message-with-thinking-code.json +33 -0
  421. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/before-compaction.jsonl +1003 -0
  422. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/empty-agent/.gitkeep +0 -0
  423. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/empty-cwd/.gitkeep +0 -0
  424. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/large-session.jsonl +1019 -0
  425. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/consecutive-hyphens/SKILL.md +8 -0
  426. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/disable-model-invocation/SKILL.md +9 -0
  427. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/invalid-name-chars/SKILL.md +8 -0
  428. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/invalid-yaml/SKILL.md +8 -0
  429. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/long-name/SKILL.md +8 -0
  430. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/missing-description/SKILL.md +7 -0
  431. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/multiline-description/SKILL.md +11 -0
  432. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/name-mismatch/SKILL.md +8 -0
  433. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/nested/child-skill/SKILL.md +8 -0
  434. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/no-frontmatter/SKILL.md +3 -0
  435. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/root-skill-preferred/SKILL.md +3 -0
  436. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/root-skill-preferred/nested-child/SKILL.md +3 -0
  437. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/unknown-field/SKILL.md +10 -0
  438. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills/valid-skill/SKILL.md +8 -0
  439. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills-collision/first/calendar/SKILL.md +8 -0
  440. package/pi.dev-src/pi-mono/coding-agent/test/fixtures/skills-collision/second/calendar/SKILL.md +8 -0
  441. package/pi.dev-src/pi-mono/coding-agent/test/footer-data-provider.test.ts +265 -0
  442. package/pi.dev-src/pi-mono/coding-agent/test/footer-width.test.ts +115 -0
  443. package/pi.dev-src/pi-mono/coding-agent/test/frontmatter.test.ts +60 -0
  444. package/pi.dev-src/pi-mono/coding-agent/test/git-ssh-url.test.ts +78 -0
  445. package/pi.dev-src/pi-mono/coding-agent/test/git-update.test.ts +411 -0
  446. package/pi.dev-src/pi-mono/coding-agent/test/image-processing.test.ts +145 -0
  447. package/pi.dev-src/pi-mono/coding-agent/test/image-resize-callers.test.ts +53 -0
  448. package/pi.dev-src/pi-mono/coding-agent/test/initial-message.test.ts +48 -0
  449. package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-anthropic-warning.test.ts +73 -0
  450. package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-clone-command.test.ts +77 -0
  451. package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-compaction.test.ts +57 -0
  452. package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-import-command.test.ts +144 -0
  453. package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-status.test.ts +635 -0
  454. package/pi.dev-src/pi-mono/coding-agent/test/interactive-mode-suspend.test.ts +149 -0
  455. package/pi.dev-src/pi-mono/coding-agent/test/keybindings-migration.test.ts +88 -0
  456. package/pi.dev-src/pi-mono/coding-agent/test/model-registry.test.ts +1209 -0
  457. package/pi.dev-src/pi-mono/coding-agent/test/model-resolver.test.ts +436 -0
  458. package/pi.dev-src/pi-mono/coding-agent/test/package-command-paths.test.ts +144 -0
  459. package/pi.dev-src/pi-mono/coding-agent/test/package-manager-ssh.test.ts +97 -0
  460. package/pi.dev-src/pi-mono/coding-agent/test/package-manager.test.ts +1795 -0
  461. package/pi.dev-src/pi-mono/coding-agent/test/path-utils.test.ts +162 -0
  462. package/pi.dev-src/pi-mono/coding-agent/test/plan-mode-utils.test.ts +261 -0
  463. package/pi.dev-src/pi-mono/coding-agent/test/print-mode.test.ts +140 -0
  464. package/pi.dev-src/pi-mono/coding-agent/test/prompt-templates.test.ts +515 -0
  465. package/pi.dev-src/pi-mono/coding-agent/test/resource-loader.test.ts +576 -0
  466. package/pi.dev-src/pi-mono/coding-agent/test/rpc-client-clone.test.ts +29 -0
  467. package/pi.dev-src/pi-mono/coding-agent/test/rpc-example.ts +86 -0
  468. package/pi.dev-src/pi-mono/coding-agent/test/rpc-jsonl.test.ts +65 -0
  469. package/pi.dev-src/pi-mono/coding-agent/test/rpc-prompt-response-semantics.test.ts +285 -0
  470. package/pi.dev-src/pi-mono/coding-agent/test/rpc.test.ts +321 -0
  471. package/pi.dev-src/pi-mono/coding-agent/test/sdk-codex-cache-probe-tool-loop.ts +364 -0
  472. package/pi.dev-src/pi-mono/coding-agent/test/sdk-openrouter-attribution.test.ts +181 -0
  473. package/pi.dev-src/pi-mono/coding-agent/test/sdk-session-manager.test.ts +95 -0
  474. package/pi.dev-src/pi-mono/coding-agent/test/sdk-skills.test.ts +109 -0
  475. package/pi.dev-src/pi-mono/coding-agent/test/session-cwd.test.ts +91 -0
  476. package/pi.dev-src/pi-mono/coding-agent/test/session-info-modified-timestamp.test.ts +83 -0
  477. package/pi.dev-src/pi-mono/coding-agent/test/session-manager/build-context.test.ts +268 -0
  478. package/pi.dev-src/pi-mono/coding-agent/test/session-manager/custom-session-id.test.ts +109 -0
  479. package/pi.dev-src/pi-mono/coding-agent/test/session-manager/file-operations.test.ts +208 -0
  480. package/pi.dev-src/pi-mono/coding-agent/test/session-manager/labels.test.ts +198 -0
  481. package/pi.dev-src/pi-mono/coding-agent/test/session-manager/migration.test.ts +78 -0
  482. package/pi.dev-src/pi-mono/coding-agent/test/session-manager/save-entry.test.ts +55 -0
  483. package/pi.dev-src/pi-mono/coding-agent/test/session-manager/tree-traversal.test.ts +533 -0
  484. package/pi.dev-src/pi-mono/coding-agent/test/session-selector-path-delete.test.ts +315 -0
  485. package/pi.dev-src/pi-mono/coding-agent/test/session-selector-rename.test.ts +111 -0
  486. package/pi.dev-src/pi-mono/coding-agent/test/session-selector-search.test.ts +195 -0
  487. package/pi.dev-src/pi-mono/coding-agent/test/settings-manager-bug.test.ts +147 -0
  488. package/pi.dev-src/pi-mono/coding-agent/test/settings-manager.test.ts +319 -0
  489. package/pi.dev-src/pi-mono/coding-agent/test/skills.test.ts +432 -0
  490. package/pi.dev-src/pi-mono/coding-agent/test/stdout-cleanliness.test.ts +103 -0
  491. package/pi.dev-src/pi-mono/coding-agent/test/streaming-render-debug.ts +97 -0
  492. package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-bash-persistence.test.ts +242 -0
  493. package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-compaction.test.ts +343 -0
  494. package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-model-extension.test.ts +325 -0
  495. package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-prompt.test.ts +323 -0
  496. package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-queue.test.ts +422 -0
  497. package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-retry-events.test.ts +358 -0
  498. package/pi.dev-src/pi-mono/coding-agent/test/suite/agent-session-runtime.test.ts +553 -0
  499. package/pi.dev-src/pi-mono/coding-agent/test/suite/harness.ts +208 -0
  500. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2023-queued-slash-command-followup.test.ts +80 -0
  501. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2753-reload-stale-resource-settings.test.ts +100 -0
  502. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2781-skill-collision-precedence.test.ts +120 -0
  503. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2791-fswatch-error-crash.test.ts +106 -0
  504. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2835-tools-allowlist-filters-extension-tools.test.ts +94 -0
  505. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/2860-replaced-session-context.test.ts +274 -0
  506. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/3217-scoped-model-order.test.ts +104 -0
  507. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts +72 -0
  508. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/3303-find-nested-gitignore.test.ts +83 -0
  509. package/pi.dev-src/pi-mono/coding-agent/test/suite/regressions/3317-network-connection-lost-retry.test.ts +33 -0
  510. package/pi.dev-src/pi-mono/coding-agent/test/system-prompt.test.ts +102 -0
  511. package/pi.dev-src/pi-mono/coding-agent/test/test-harness.test.ts +321 -0
  512. package/pi.dev-src/pi-mono/coding-agent/test/test-harness.ts +446 -0
  513. package/pi.dev-src/pi-mono/coding-agent/test/test-theme-colors.ts +246 -0
  514. package/pi.dev-src/pi-mono/coding-agent/test/theme-export.test.ts +104 -0
  515. package/pi.dev-src/pi-mono/coding-agent/test/tool-execution-component.test.ts +331 -0
  516. package/pi.dev-src/pi-mono/coding-agent/test/tools.test.ts +889 -0
  517. package/pi.dev-src/pi-mono/coding-agent/test/tree-selector.test.ts +655 -0
  518. package/pi.dev-src/pi-mono/coding-agent/test/trigger-compact-extension.test.ts +57 -0
  519. package/pi.dev-src/pi-mono/coding-agent/test/truncate-to-width.test.ts +81 -0
  520. package/pi.dev-src/pi-mono/coding-agent/test/user-message.test.ts +25 -0
  521. package/pi.dev-src/pi-mono/coding-agent/test/utilities.ts +316 -0
  522. package/pi.dev-src/pi-mono/coding-agent/tsconfig.examples.json +16 -0
  523. package/pi.dev-src/pi-mono/coding-agent/vitest.config.ts +26 -0
  524. package/pi.dev-src/pi-mono/mom/dev.sh +30 -0
  525. package/pi.dev-src/pi-mono/mom/docker.sh +95 -0
  526. package/pi.dev-src/pi-mono/mom/scripts/migrate-timestamps.ts +121 -0
  527. package/pi.dev-src/pi-mono/mom/src/agent.ts +882 -0
  528. package/pi.dev-src/pi-mono/mom/src/context.ts +180 -0
  529. package/pi.dev-src/pi-mono/mom/src/download.ts +117 -0
  530. package/pi.dev-src/pi-mono/mom/src/events.ts +442 -0
  531. package/pi.dev-src/pi-mono/mom/src/fs-watch.ts +30 -0
  532. package/pi.dev-src/pi-mono/mom/src/log.ts +271 -0
  533. package/pi.dev-src/pi-mono/mom/src/main.ts +367 -0
  534. package/pi.dev-src/pi-mono/mom/src/sandbox.ts +221 -0
  535. package/pi.dev-src/pi-mono/mom/src/slack.ts +623 -0
  536. package/pi.dev-src/pi-mono/mom/src/store.ts +234 -0
  537. package/pi.dev-src/pi-mono/mom/src/tools/attach.ts +47 -0
  538. package/pi.dev-src/pi-mono/mom/src/tools/bash.ts +97 -0
  539. package/pi.dev-src/pi-mono/mom/src/tools/edit.ts +165 -0
  540. package/pi.dev-src/pi-mono/mom/src/tools/index.ts +19 -0
  541. package/pi.dev-src/pi-mono/mom/src/tools/read.ts +159 -0
  542. package/pi.dev-src/pi-mono/mom/src/tools/truncate.ts +236 -0
  543. package/pi.dev-src/pi-mono/mom/src/tools/write.ts +45 -0
  544. package/pi.dev-src/pi-mono/mom/test/events.test.ts +50 -0
  545. package/pi.dev-src/pi-mono/pods/scripts/model_run.sh +83 -0
  546. package/pi.dev-src/pi-mono/pods/scripts/pod_setup.sh +336 -0
  547. package/pi.dev-src/pi-mono/pods/src/cli.ts +360 -0
  548. package/pi.dev-src/pi-mono/pods/src/commands/models.ts +753 -0
  549. package/pi.dev-src/pi-mono/pods/src/commands/pods.ts +205 -0
  550. package/pi.dev-src/pi-mono/pods/src/commands/prompt.ts +84 -0
  551. package/pi.dev-src/pi-mono/pods/src/config.ts +80 -0
  552. package/pi.dev-src/pi-mono/pods/src/index.ts +2 -0
  553. package/pi.dev-src/pi-mono/pods/src/model-configs.ts +111 -0
  554. package/pi.dev-src/pi-mono/pods/src/models.json +295 -0
  555. package/pi.dev-src/pi-mono/pods/src/ssh.ts +151 -0
  556. package/pi.dev-src/pi-mono/pods/src/types.ts +27 -0
  557. package/pi.dev-src/pi-mono/tui/src/autocomplete.ts +783 -0
  558. package/pi.dev-src/pi-mono/tui/src/components/box.ts +137 -0
  559. package/pi.dev-src/pi-mono/tui/src/components/cancellable-loader.ts +40 -0
  560. package/pi.dev-src/pi-mono/tui/src/components/editor.ts +2280 -0
  561. package/pi.dev-src/pi-mono/tui/src/components/image.ts +104 -0
  562. package/pi.dev-src/pi-mono/tui/src/components/input.ts +503 -0
  563. package/pi.dev-src/pi-mono/tui/src/components/loader.ts +86 -0
  564. package/pi.dev-src/pi-mono/tui/src/components/markdown.ts +852 -0
  565. package/pi.dev-src/pi-mono/tui/src/components/select-list.ts +229 -0
  566. package/pi.dev-src/pi-mono/tui/src/components/settings-list.ts +250 -0
  567. package/pi.dev-src/pi-mono/tui/src/components/spacer.ts +28 -0
  568. package/pi.dev-src/pi-mono/tui/src/components/text.ts +106 -0
  569. package/pi.dev-src/pi-mono/tui/src/components/truncated-text.ts +65 -0
  570. package/pi.dev-src/pi-mono/tui/src/editor-component.ts +74 -0
  571. package/pi.dev-src/pi-mono/tui/src/fuzzy.ts +133 -0
  572. package/pi.dev-src/pi-mono/tui/src/index.ts +106 -0
  573. package/pi.dev-src/pi-mono/tui/src/keybindings.ts +244 -0
  574. package/pi.dev-src/pi-mono/tui/src/keys.ts +1400 -0
  575. package/pi.dev-src/pi-mono/tui/src/kill-ring.ts +46 -0
  576. package/pi.dev-src/pi-mono/tui/src/stdin-buffer.ts +386 -0
  577. package/pi.dev-src/pi-mono/tui/src/terminal-image.ts +414 -0
  578. package/pi.dev-src/pi-mono/tui/src/terminal.ts +373 -0
  579. package/pi.dev-src/pi-mono/tui/src/tui.ts +1243 -0
  580. package/pi.dev-src/pi-mono/tui/src/undo-stack.ts +28 -0
  581. package/pi.dev-src/pi-mono/tui/src/utils.ts +1085 -0
  582. package/pi.dev-src/pi-mono/tui/test/autocomplete.test.ts +542 -0
  583. package/pi.dev-src/pi-mono/tui/test/bug-regression-isimageline-startswith-bug.test.ts +237 -0
  584. package/pi.dev-src/pi-mono/tui/test/chat-simple.ts +129 -0
  585. package/pi.dev-src/pi-mono/tui/test/editor.test.ts +3812 -0
  586. package/pi.dev-src/pi-mono/tui/test/fuzzy.test.ts +98 -0
  587. package/pi.dev-src/pi-mono/tui/test/image-test.ts +56 -0
  588. package/pi.dev-src/pi-mono/tui/test/input.test.ts +580 -0
  589. package/pi.dev-src/pi-mono/tui/test/key-tester.ts +105 -0
  590. package/pi.dev-src/pi-mono/tui/test/keybindings.test.ts +38 -0
  591. package/pi.dev-src/pi-mono/tui/test/keys.test.ts +614 -0
  592. package/pi.dev-src/pi-mono/tui/test/markdown.test.ts +1236 -0
  593. package/pi.dev-src/pi-mono/tui/test/overlay-non-capturing.test.ts +609 -0
  594. package/pi.dev-src/pi-mono/tui/test/overlay-options.test.ts +538 -0
  595. package/pi.dev-src/pi-mono/tui/test/overlay-short-content.test.ts +56 -0
  596. package/pi.dev-src/pi-mono/tui/test/regression-regional-indicator-width.test.ts +52 -0
  597. package/pi.dev-src/pi-mono/tui/test/select-list.test.ts +116 -0
  598. package/pi.dev-src/pi-mono/tui/test/stdin-buffer.test.ts +422 -0
  599. package/pi.dev-src/pi-mono/tui/test/terminal-image.test.ts +279 -0
  600. package/pi.dev-src/pi-mono/tui/test/test-themes.ts +38 -0
  601. package/pi.dev-src/pi-mono/tui/test/truncate-to-width.test.ts +62 -0
  602. package/pi.dev-src/pi-mono/tui/test/truncated-text.test.ts +129 -0
  603. package/pi.dev-src/pi-mono/tui/test/tui-cell-size-input.test.ts +81 -0
  604. package/pi.dev-src/pi-mono/tui/test/tui-overlay-style-leak.test.ts +72 -0
  605. package/pi.dev-src/pi-mono/tui/test/tui-render.test.ts +509 -0
  606. package/pi.dev-src/pi-mono/tui/test/viewport-overwrite-repro.ts +106 -0
  607. package/pi.dev-src/pi-mono/tui/test/virtual-terminal.ts +218 -0
  608. package/pi.dev-src/pi-mono/tui/test/wrap-ansi.test.ts +207 -0
  609. package/pi.dev-src/pi-mono/tui/vitest.config.ts +7 -0
  610. package/pi.dev-src/pi-mono/web-ui/example/index.html +13 -0
  611. package/pi.dev-src/pi-mono/web-ui/example/src/app.css +1 -0
  612. package/pi.dev-src/pi-mono/web-ui/example/src/custom-messages.ts +99 -0
  613. package/pi.dev-src/pi-mono/web-ui/example/src/main.ts +421 -0
  614. package/pi.dev-src/pi-mono/web-ui/example/tsconfig.json +23 -0
  615. package/pi.dev-src/pi-mono/web-ui/example/vite.config.ts +6 -0
  616. package/pi.dev-src/pi-mono/web-ui/scripts/count-prompt-tokens.ts +88 -0
  617. package/pi.dev-src/pi-mono/web-ui/src/ChatPanel.ts +209 -0
  618. package/pi.dev-src/pi-mono/web-ui/src/app.css +68 -0
  619. package/pi.dev-src/pi-mono/web-ui/src/components/AgentInterface.ts +403 -0
  620. package/pi.dev-src/pi-mono/web-ui/src/components/AttachmentTile.ts +107 -0
  621. package/pi.dev-src/pi-mono/web-ui/src/components/ConsoleBlock.ts +72 -0
  622. package/pi.dev-src/pi-mono/web-ui/src/components/CustomProviderCard.ts +100 -0
  623. package/pi.dev-src/pi-mono/web-ui/src/components/ExpandableSection.ts +46 -0
  624. package/pi.dev-src/pi-mono/web-ui/src/components/Input.ts +113 -0
  625. package/pi.dev-src/pi-mono/web-ui/src/components/MessageEditor.ts +405 -0
  626. package/pi.dev-src/pi-mono/web-ui/src/components/MessageList.ts +98 -0
  627. package/pi.dev-src/pi-mono/web-ui/src/components/Messages.ts +383 -0
  628. package/pi.dev-src/pi-mono/web-ui/src/components/ProviderKeyInput.ts +153 -0
  629. package/pi.dev-src/pi-mono/web-ui/src/components/SandboxedIframe.ts +626 -0
  630. package/pi.dev-src/pi-mono/web-ui/src/components/StreamingMessageContainer.ts +103 -0
  631. package/pi.dev-src/pi-mono/web-ui/src/components/ThinkingBlock.ts +43 -0
  632. package/pi.dev-src/pi-mono/web-ui/src/components/message-renderer-registry.ts +28 -0
  633. package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts +220 -0
  634. package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
  635. package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
  636. package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
  637. package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
  638. package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
  639. package/pi.dev-src/pi-mono/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
  640. package/pi.dev-src/pi-mono/web-ui/src/dialogs/ApiKeyPromptDialog.ts +75 -0
  641. package/pi.dev-src/pi-mono/web-ui/src/dialogs/AttachmentOverlay.ts +636 -0
  642. package/pi.dev-src/pi-mono/web-ui/src/dialogs/CustomProviderDialog.ts +274 -0
  643. package/pi.dev-src/pi-mono/web-ui/src/dialogs/ModelSelector.ts +370 -0
  644. package/pi.dev-src/pi-mono/web-ui/src/dialogs/PersistentStorageDialog.ts +144 -0
  645. package/pi.dev-src/pi-mono/web-ui/src/dialogs/ProvidersModelsTab.ts +212 -0
  646. package/pi.dev-src/pi-mono/web-ui/src/dialogs/SessionListDialog.ts +150 -0
  647. package/pi.dev-src/pi-mono/web-ui/src/dialogs/SettingsDialog.ts +218 -0
  648. package/pi.dev-src/pi-mono/web-ui/src/index.ts +120 -0
  649. package/pi.dev-src/pi-mono/web-ui/src/prompts/prompts.ts +282 -0
  650. package/pi.dev-src/pi-mono/web-ui/src/storage/app-storage.ts +60 -0
  651. package/pi.dev-src/pi-mono/web-ui/src/storage/backends/indexeddb-storage-backend.ts +193 -0
  652. package/pi.dev-src/pi-mono/web-ui/src/storage/store.ts +33 -0
  653. package/pi.dev-src/pi-mono/web-ui/src/storage/stores/custom-providers-store.ts +62 -0
  654. package/pi.dev-src/pi-mono/web-ui/src/storage/stores/provider-keys-store.ts +33 -0
  655. package/pi.dev-src/pi-mono/web-ui/src/storage/stores/sessions-store.ts +136 -0
  656. package/pi.dev-src/pi-mono/web-ui/src/storage/stores/settings-store.ts +34 -0
  657. package/pi.dev-src/pi-mono/web-ui/src/storage/types.ts +206 -0
  658. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/ArtifactElement.ts +14 -0
  659. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/ArtifactPill.ts +26 -0
  660. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/Console.ts +93 -0
  661. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/DocxArtifact.ts +213 -0
  662. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/ExcelArtifact.ts +231 -0
  663. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/GenericArtifact.ts +117 -0
  664. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/HtmlArtifact.ts +195 -0
  665. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/ImageArtifact.ts +116 -0
  666. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/MarkdownArtifact.ts +82 -0
  667. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/PdfArtifact.ts +201 -0
  668. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/SvgArtifact.ts +119 -0
  669. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/TextArtifact.ts +148 -0
  670. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/artifacts-tool-renderer.ts +310 -0
  671. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/artifacts.ts +713 -0
  672. package/pi.dev-src/pi-mono/web-ui/src/tools/artifacts/index.ts +7 -0
  673. package/pi.dev-src/pi-mono/web-ui/src/tools/extract-document.ts +275 -0
  674. package/pi.dev-src/pi-mono/web-ui/src/tools/index.ts +46 -0
  675. package/pi.dev-src/pi-mono/web-ui/src/tools/javascript-repl.ts +293 -0
  676. package/pi.dev-src/pi-mono/web-ui/src/tools/renderer-registry.ts +130 -0
  677. package/pi.dev-src/pi-mono/web-ui/src/tools/renderers/BashRenderer.ts +52 -0
  678. package/pi.dev-src/pi-mono/web-ui/src/tools/renderers/CalculateRenderer.ts +58 -0
  679. package/pi.dev-src/pi-mono/web-ui/src/tools/renderers/DefaultRenderer.ts +103 -0
  680. package/pi.dev-src/pi-mono/web-ui/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
  681. package/pi.dev-src/pi-mono/web-ui/src/tools/types.ts +15 -0
  682. package/pi.dev-src/pi-mono/web-ui/src/utils/attachment-utils.ts +472 -0
  683. package/pi.dev-src/pi-mono/web-ui/src/utils/auth-token.ts +22 -0
  684. package/pi.dev-src/pi-mono/web-ui/src/utils/format.ts +42 -0
  685. package/pi.dev-src/pi-mono/web-ui/src/utils/i18n.ts +653 -0
  686. package/pi.dev-src/pi-mono/web-ui/src/utils/model-discovery.ts +277 -0
  687. package/pi.dev-src/pi-mono/web-ui/src/utils/proxy-utils.ts +139 -0
  688. package/pi.dev-src/pi-mono/web-ui/src/utils/test-sessions.ts +2357 -0
  689. package/pi.dev-src/pi-mono/web-ui/tsconfig.json +7 -0
  690. package/scripts/lib/extension-debug-harness.ts +3 -10
  691. package/scripts/lib/recording-extension-api.ts +71 -2
  692. package/scripts/pi-usereq-debug.sh +1 -1
  693. package/scripts/tool-args-to-params.ts +4 -15
  694. package/src/cli.ts +24 -68
  695. package/src/core/agent-tool-json.ts +53 -239
  696. package/src/core/compress-payload.ts +4 -74
  697. package/src/core/compress.ts +26 -15
  698. package/src/core/config.ts +468 -42
  699. package/src/core/debug-runtime.ts +542 -0
  700. package/src/core/extension-status.ts +314 -59
  701. package/src/core/find-constructs.ts +3 -3
  702. package/src/core/find-payload.ts +26 -132
  703. package/src/core/path-context.ts +346 -81
  704. package/src/core/pi-notify.ts +115 -5
  705. package/src/core/pi-usereq-tools.ts +57 -19
  706. package/src/core/prompt-command-catalog.ts +46 -0
  707. package/src/core/prompt-command-runtime.ts +1816 -0
  708. package/src/core/prompt-command-state.ts +211 -0
  709. package/src/core/prompts.ts +173 -41
  710. package/src/core/reference-payload.ts +5 -63
  711. package/src/core/resources.ts +61 -5
  712. package/src/core/runtime-project-paths.ts +21 -1
  713. package/src/core/settings-menu.ts +12 -6
  714. package/src/core/source-analyzer.ts +27 -7
  715. package/src/core/token-counter.ts +6 -76
  716. package/src/core/tool-runner.ts +38 -317
  717. package/src/index.ts +2047 -947
  718. package/src/resources/instructions/git_commit.md +16 -0
  719. package/src/resources/instructions/git_read-only.md +2 -0
  720. package/src/resources/prompts/analyze.md +12 -13
  721. package/src/resources/prompts/change.md +21 -59
  722. package/src/resources/prompts/check.md +14 -16
  723. package/src/resources/prompts/cover.md +21 -59
  724. package/src/resources/prompts/create.md +5 -4
  725. package/src/resources/prompts/fix.md +20 -58
  726. package/src/resources/prompts/flowchart.md +14 -49
  727. package/src/resources/prompts/implement.md +14 -50
  728. package/src/resources/prompts/new.md +20 -58
  729. package/src/resources/prompts/readme.md +14 -50
  730. package/src/resources/prompts/recreate.md +14 -45
  731. package/src/resources/prompts/refactor.md +20 -58
  732. package/src/resources/prompts/references.md +7 -43
  733. package/src/resources/prompts/renumber.md +8 -39
  734. package/src/resources/prompts/workflow.md +14 -50
  735. package/src/resources/prompts/write.md +5 -4
  736. package/tests/attended-results-scenarios.ts +30 -136
  737. package/tests/cli-command-option-parity.test.ts +148 -192
  738. package/tests/debug-extension-harness.test.ts +77 -70
  739. package/tests/extension-registration.test.ts +3205 -658
  740. package/tests/fixtures_attended_results/project/compress-line-numbers.json +1 -1
  741. package/tests/fixtures_attended_results/project/compress.json +1 -1
  742. package/tests/fixtures_attended_results/project/find-line-numbers.json +1 -1
  743. package/tests/fixtures_attended_results/project/find.json +1 -1
  744. package/tests/fixtures_attended_results/project/references.json +2 -2
  745. package/tests/fixtures_attended_results/standalone/files-compress/fixture_go.go.json +1 -1
  746. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_go.go.json +1 -1
  747. package/tests/fixtures_attended_results/standalone/files-find/fixture_go.go.json +1 -1
  748. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_go.go.json +1 -1
  749. package/tests/fixtures_attended_results/standalone/files-references/fixture_c.c.json +1 -1
  750. package/tests/fixtures_attended_results/standalone/files-references/fixture_cpp.cpp.json +1 -1
  751. package/tests/fixtures_attended_results/standalone/files-references/fixture_csharp.cs.json +1 -1
  752. package/tests/fixtures_attended_results/standalone/files-references/fixture_elixir.ex.json +1 -1
  753. package/tests/fixtures_attended_results/standalone/files-references/fixture_go.go.json +1 -1
  754. package/tests/fixtures_attended_results/standalone/files-references/fixture_haskell.hs.json +1 -1
  755. package/tests/fixtures_attended_results/standalone/files-references/fixture_java.java.json +1 -1
  756. package/tests/fixtures_attended_results/standalone/files-references/fixture_javascript.js.json +1 -1
  757. package/tests/fixtures_attended_results/standalone/files-references/fixture_kotlin.kt.json +1 -1
  758. package/tests/fixtures_attended_results/standalone/files-references/fixture_lua.lua.json +1 -1
  759. package/tests/fixtures_attended_results/standalone/files-references/fixture_perl.pl.json +1 -1
  760. package/tests/fixtures_attended_results/standalone/files-references/fixture_php.php.json +1 -1
  761. package/tests/fixtures_attended_results/standalone/files-references/fixture_python.py.json +1 -1
  762. package/tests/fixtures_attended_results/standalone/files-references/fixture_ruby.rb.json +1 -1
  763. package/tests/fixtures_attended_results/standalone/files-references/fixture_rust.rs.json +1 -1
  764. package/tests/fixtures_attended_results/standalone/files-references/fixture_scala.scala.json +1 -1
  765. package/tests/fixtures_attended_results/standalone/files-references/fixture_shell.sh.json +1 -1
  766. package/tests/fixtures_attended_results/standalone/files-references/fixture_swift.swift.json +1 -1
  767. package/tests/fixtures_attended_results/standalone/files-references/fixture_typescript.ts.json +1 -1
  768. package/tests/fixtures_attended_results/standalone/files-references/fixture_zig.zig.json +1 -1
  769. package/tests/helpers.ts +24 -15
  770. package/tests/oracle-project.test.ts +29 -30
  771. package/tests/oracle-standalone.test.ts +27 -1
  772. package/tests/prompt-rendering.test.ts +40 -9
  773. package/tests/python_oracle_stubs/tiktoken.py +76 -0
package/src/index.ts CHANGED
@@ -8,8 +8,9 @@
8
8
  * @brief Declares the extension version string.
9
9
  * @details The value is exported for external inspection and packaging metadata alignment. Access complexity is O(1).
10
10
  */
11
- export const VERSION = "0.7.0"
11
+ export const VERSION = "0.11.0";
12
12
 
13
+ import fs from "node:fs";
13
14
  import path from "node:path";
14
15
  import type {
15
16
  AgentEndEvent,
@@ -18,47 +19,26 @@ import type {
18
19
  ExtensionContext,
19
20
  ToolInfo,
20
21
  } from "@mariozechner/pi-coding-agent";
22
+ import { Text } from "@mariozechner/pi-tui";
21
23
  import { Type } from "@sinclair/typebox";
22
24
  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,
25
+ buildMonolithicToolExecuteResult,
36
26
  normalizeToolFailure,
37
27
  } from "./core/agent-tool-json.js";
28
+ import type { FindToolScope } from "./core/find-payload.js";
38
29
  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 {
30
+ DEFAULT_GIT_WORKTREE_PREFIX,
55
31
  DEFAULT_SRC_DIRS,
32
+ createStaticCheckLanguageConfig,
56
33
  getDefaultConfig,
34
+ getDefaultStaticCheckConfig,
57
35
  getProjectConfigPath,
58
36
  loadConfig,
59
37
  normalizeConfigPaths,
38
+ resolveEffectiveGitWorktreeEnabled,
60
39
  saveConfig,
61
40
  type StaticCheckEntry,
41
+ type StaticCheckLanguageConfig,
62
42
  type UseReqConfig,
63
43
  } from "./core/config.js";
64
44
  import {
@@ -69,47 +49,101 @@ import {
69
49
  DEFAULT_PI_NOTIFY_SOUND_LOW_CMD,
70
50
  DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
71
51
  cyclePiNotifySoundLevel,
52
+ formatPiNotifyControlSequenceText,
72
53
  formatPiNotifyPushoverStatus,
73
54
  formatPiNotifyStatus,
55
+ hasPiNotifyPushoverCredentials,
74
56
  normalizePiNotifyCommand,
75
57
  normalizePiNotifyPushoverCredential,
76
58
  normalizePiNotifyPushoverPriority,
77
59
  normalizePiNotifyTemplateValue,
60
+ parsePiNotifyControlSequenceText,
78
61
  runPiNotifyEffects,
79
62
  type PiNotifyEventRequest,
80
63
  type PiNotifyPushoverPriority,
81
64
  type PiNotifySoundLevel,
82
65
  } from "./core/pi-notify.js";
83
- import { formatRuntimePathForDisplay } from "./core/path-context.js";
84
- import { resolveRuntimeGitPath } from "./core/runtime-project-paths.js";
66
+ import {
67
+ bootstrapRuntimePathState,
68
+ formatRuntimePathForDisplay,
69
+ getRuntimeBasePath,
70
+ getRuntimeContextPath,
71
+ setRuntimeGitPath,
72
+ } from "./core/path-context.js";
85
73
  import { showPiUsereqSettingsMenu, type PiUsereqSettingsMenuChoice } from "./core/settings-menu.js";
86
74
  import {
75
+ comparePiUsereqStartupToolNames,
76
+ PI_USEREQ_CUSTOM_TOOL_NAMES,
77
+ PI_USEREQ_EMBEDDED_TOOL_NAMES,
87
78
  PI_USEREQ_STARTUP_TOOL_SET,
88
79
  isPiUsereqEmbeddedToolName,
89
80
  normalizeEnabledPiUsereqTools,
81
+ type PiUsereqStartupToolName,
90
82
  } from "./core/pi-usereq-tools.js";
91
83
  import { renderPrompt } from "./core/prompts.js";
92
- import { ensureBundledResourcesAccessible } from "./core/resources.js";
84
+ import {
85
+ abortPromptCommandExecution,
86
+ activatePromptCommandExecution,
87
+ classifyPromptCommandOutcome,
88
+ finalizePromptCommandExecution,
89
+ getPromptCommandErrorContext,
90
+ preparePromptCommandExecution,
91
+ restorePromptCommandExecution,
92
+ type PromptCommandExecutionPlan,
93
+ } from "./core/prompt-command-runtime.js";
94
+ import {
95
+ DEBUG_PROMPT_NAMES,
96
+ DEBUG_WORKFLOW_STATES,
97
+ DEFAULT_DEBUG_LOG_FILE,
98
+ DEFAULT_DEBUG_LOG_ON_STATUS,
99
+ DEFAULT_DEBUG_STATUS_CHANGES,
100
+ DEFAULT_DEBUG_WORKFLOW_EVENTS,
101
+ logDebugPromptEvent,
102
+ logDebugPromptWorkflowEvent,
103
+ logDebugToolExecution,
104
+ normalizeDebugEnabledPrompts,
105
+ normalizeDebugEnabledTools,
106
+ normalizeDebugLogFile,
107
+ normalizeDebugLogOnStatus,
108
+ normalizeDebugStatusChanges,
109
+ normalizeDebugWorkflowEvents,
110
+ shouldLogDebugPromptWorkflowState,
111
+ type DebugLogOnStatus,
112
+ type DebugWorkflowState,
113
+ } from "./core/debug-runtime.js";
114
+ import { PROMPT_COMMAND_NAMES } from "./core/prompt-command-catalog.js";
115
+ import { resolveRuntimeGitPath } from "./core/runtime-project-paths.js";
116
+ import {
117
+ readPersistedPromptCommandRuntimeState,
118
+ writePersistedPromptCommandRuntimeState,
119
+ } from "./core/prompt-command-state.js";
120
+ import { ensureBundledResourcesAccessible, readBundledPromptDescription } from "./core/resources.js";
121
+ import { ReqError } from "./core/errors.js";
93
122
  import {
94
123
  PI_USEREQ_STATUS_HOOK_NAMES,
95
124
  createPiUsereqStatusController,
96
125
  disposePiUsereqStatusController,
126
+ isStaleExtensionContextError,
97
127
  renderPiUsereqStatus,
98
128
  setPiUsereqStatusConfig,
129
+ setPiUsereqWorkflowState,
130
+ shouldPreservePromptCommandStateOnShutdown,
99
131
  updateExtensionStatus,
100
132
  type PiUsereqStatusController,
101
133
  type PiUsereqStatusHookName,
102
134
  } from "./core/extension-status.js";
103
135
  import {
104
- collectSourceFiles,
136
+ runCompress,
137
+ runFilesCompress,
138
+ runFilesReferences,
139
+ runFilesSearch,
105
140
  runFilesStaticCheck,
106
- runGetBasePath,
107
- runGitCheck,
108
- runGitPath,
109
- runGitWtCreate,
110
- runGitWtDelete,
111
- runGitWtName,
141
+ runFilesTokens,
112
142
  runProjectStaticCheck,
143
+ runReferences,
144
+ runSearch,
145
+ runTokens,
146
+ type ToolResult,
113
147
  } from "./core/tool-runner.js";
114
148
  import { LANGUAGE_TAGS } from "./core/find-constructs.js";
115
149
  import {
@@ -117,29 +151,6 @@ import {
117
151
  } from "./core/static-check.js";
118
152
  import { makeRelativeIfContainsProject, shellSplit } from "./core/utils.js";
119
153
 
120
- /**
121
- * @brief Lists bundled prompt commands exposed by the extension.
122
- * @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).
123
- */
124
- const PROMPT_NAMES = [
125
- "analyze",
126
- "change",
127
- "check",
128
- "cover",
129
- "create",
130
- "fix",
131
- "flowchart",
132
- "implement",
133
- "new",
134
- "readme",
135
- "recreate",
136
- "refactor",
137
- "references",
138
- "renumber",
139
- "workflow",
140
- "write",
141
- ] as const;
142
-
143
154
  /**
144
155
  * @brief Describes the optional shortcut-registration surface used by pi-usereq.
145
156
  * @details Narrows the runtime API to the documented `registerShortcut(...)`
@@ -164,7 +175,68 @@ interface PiShortcutRegistrar {
164
175
  * @return {string} Absolute project base path.
165
176
  */
166
177
  function getProjectBase(cwd: string): string {
167
- return path.resolve(cwd);
178
+ const runtimeBasePath = getRuntimeBasePath(cwd);
179
+ if (fs.existsSync(runtimeBasePath)) {
180
+ return runtimeBasePath;
181
+ }
182
+ bootstrapRuntimePathState(cwd, {
183
+ gitPath: resolveRuntimeGitPath(cwd),
184
+ });
185
+ return getRuntimeBasePath(cwd);
186
+ }
187
+
188
+ /**
189
+ * @brief Resolves a safe process working directory for extension-load paths.
190
+ * @details Returns `process.cwd()` when available and falls back to absolute `PWD`, `HOME`, or `/` when the current shell directory has been deleted. Runtime is O(1). No external state is mutated.
191
+ * @return {string} Absolute fallback-safe process working directory.
192
+ */
193
+ function getProcessCwdSafe(): string {
194
+ try {
195
+ return process.cwd();
196
+ } catch {
197
+ const fallbackCwd = process.env.PWD ?? process.env.HOME ?? path.sep;
198
+ return path.isAbsolute(fallbackCwd)
199
+ ? fallbackCwd
200
+ : path.resolve(path.sep, fallbackCwd);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * @brief Resolves the live working directory used for bootstrap-sensitive flows.
206
+ * @details Prefers the supplied cwd when it still exists. Otherwise reuses the tracked runtime context path when it remains live, then the tracked runtime base path, and finally a process-safe cwd so deleted worktree paths retained by stale contexts cannot poison later prompt preflight or lifecycle bootstrap. Runtime is O(1) plus bounded filesystem probes. No external state is mutated.
207
+ * @param[in] cwd {string} Candidate context cwd.
208
+ * @return {string} Existing absolute cwd used for bootstrap work.
209
+ */
210
+ function resolveLiveBootstrapCwd(cwd: string): string {
211
+ const normalizedCwd = path.resolve(cwd);
212
+ if (fs.existsSync(normalizedCwd)) {
213
+ return normalizedCwd;
214
+ }
215
+ const processCwd = getProcessCwdSafe();
216
+ const runtimeContextPath = getRuntimeContextPath(processCwd);
217
+ if (fs.existsSync(runtimeContextPath)) {
218
+ return runtimeContextPath;
219
+ }
220
+ const runtimeBasePath = getRuntimeBasePath(processCwd);
221
+ return fs.existsSync(runtimeBasePath) ? runtimeBasePath : processCwd;
222
+ }
223
+
224
+ /**
225
+ * @brief Best-effort synchronizes one context `cwd` mirror with bootstrap reality.
226
+ * @details Applies the resolved live cwd to the supplied context when writable and ignores stale or read-only mirrors so command bootstrap can continue using authoritative filesystem probes. Runtime is O(1). Side effects are limited to optional `ctx.cwd` mutation.
227
+ * @param[in,out] ctx {{ cwd?: string }} Mutable context-like object.
228
+ * @param[in] cwd {string} Resolved live cwd.
229
+ * @return {void} No return value.
230
+ */
231
+ function syncContextCwdMirror(ctx: { cwd?: string }, cwd: string): void {
232
+ if (ctx.cwd === cwd) {
233
+ return;
234
+ }
235
+ try {
236
+ Reflect.set(ctx, "cwd", cwd);
237
+ } catch {
238
+ // Ignore stale or read-only context mirrors.
239
+ }
168
240
  }
169
241
 
170
242
  /**
@@ -194,188 +266,152 @@ function saveProjectConfig(cwd: string, config: UseReqConfig): void {
194
266
 
195
267
  /**
196
268
  * @brief Formats the current project config path for top-level menu display.
197
- * @details Resolves `<base-path>/.pi-usereq/config.json` from the cwd-derived project base, reuses the shared runtime-path formatter, and rewrites a leading POSIX `$HOME` token to `~` for the `Show configuration` row only. Runtime is O(p) in path length. No external state is mutated.
269
+ * @details Resolves `<base-path>/.pi-usereq.json` from the cwd-derived project base, reuses the shared runtime-path formatter, and rewrites a leading POSIX `$HOME` token to `~` for the `Show configuration` row only. Runtime is O(p) in path length. No external state is mutated.
198
270
  * @param[in] cwd {string} Current working directory.
199
271
  * @return {string} `~`-relative or absolute config path display value.
200
272
  * @satisfies REQ-162
201
273
  */
202
274
  function formatProjectConfigPathForMenu(cwd: string): string {
203
- const displayPath = formatRuntimePathForDisplay(getProjectConfigPath(getProjectBase(cwd)));
204
- if (displayPath === "$HOME") {
205
- return "~";
206
- }
207
- return displayPath.startsWith("$HOME/") ? `~/${displayPath.slice("$HOME/".length)}` : displayPath;
275
+ return formatRuntimePathForDisplay(
276
+ getProjectConfigPath(getProjectBase(cwd)),
277
+ );
208
278
  }
209
279
 
210
280
  /**
211
- * @brief Collects the project-scoped static-check selection used by the agent tool.
212
- * @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`.
213
- * @param[in] projectBase {string} Resolved project base path.
214
- * @param[in] config {UseReqConfig} Effective project configuration.
215
- * @return {{ selectionDirectoryPaths: string[]; excludedDirectoryPaths: string[]; selectedPaths: string[] }} Structured static-check selection facts.
281
+ * @brief Builds the standardized terminal rows appended to every configuration menu.
282
+ * @details Returns the canonical value-less `Reset defaults` row so all configuration menus and descendant selector menus share the same terminal ordering contract without rendering `Save and close`. Runtime is O(1). No external state is mutated.
283
+ * @param[in] options {{ resetDefaultsDescription: string }} Menu-specific terminal-row metadata.
284
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered terminal menu rows.
285
+ * @satisfies REQ-193
216
286
  */
217
- function collectProjectStaticCheckSelection(
218
- projectBase: string,
219
- config: UseReqConfig,
220
- ): {
221
- selectionDirectoryPaths: string[];
222
- excludedDirectoryPaths: string[];
223
- selectedPaths: string[];
224
- } {
225
- const selectionDirectoryPaths = [...config["src-dir"], config["tests-dir"]];
226
- const testsDirRel = makeRelativeIfContainsProject(config["tests-dir"], projectBase)
227
- .split(path.sep)
228
- .join("/")
229
- .replace(/^\.?\/?/, "")
230
- .replace(/\/+$/, "");
231
- const excludedDirectoryPaths = [...new Set([
232
- "tests/fixtures",
233
- testsDirRel ? `${testsDirRel}/fixtures` : "fixtures",
234
- ])];
235
- const selectedPaths = collectSourceFiles(selectionDirectoryPaths, projectBase)
236
- .filter((filePath) => {
237
- const relativePath = path.relative(projectBase, filePath).split(path.sep).join("/");
238
- return !excludedDirectoryPaths.some((excludedDirectoryPath) => {
239
- return relativePath === excludedDirectoryPath || relativePath.startsWith(`${excludedDirectoryPath}/`);
240
- });
241
- })
242
- .map((filePath) => path.relative(projectBase, filePath).split(path.sep).join("/"));
243
- return {
244
- selectionDirectoryPaths,
245
- excludedDirectoryPaths,
246
- selectedPaths,
247
- };
287
+ function buildTerminalSettingsMenuChoices(options: {
288
+ resetDefaultsDescription: string;
289
+ }): PiUsereqSettingsMenuChoice[] {
290
+ return [
291
+ {
292
+ id: "reset-defaults",
293
+ label: "Reset defaults",
294
+ value: "",
295
+ description: options.resetDefaultsDescription,
296
+ },
297
+ ];
248
298
  }
249
299
 
250
300
  /**
251
- * @brief Builds execution diagnostics for one token-tool payload.
252
- * @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.
253
- * @param[in] payload {TokenToolPayload} Structured token payload.
254
- * @return {string} Newline-delimited execution diagnostics.
301
+ * @brief Describes one pending reset value change shown in confirmation menus.
302
+ * @details Stores the row label plus its previous and next values so reset-confirmation submenus can expose machine-readable and human-verifiable change previews. The interface is compile-time only and introduces no runtime cost.
255
303
  */
256
- function buildTokenToolExecutionStderr(payload: TokenToolPayload): string {
257
- const skippedLines = payload.files
258
- .filter((entry) => entry.status === "skipped" && entry.error_message)
259
- .map((entry) => `skipped: ${entry.canonical_path}: ${entry.error_message!}`);
260
- const errorLines = payload.files
261
- .filter((entry) => entry.status === "error" && entry.error_message)
262
- .map((entry) => `error: ${entry.canonical_path}: ${entry.error_message!}`);
263
- return [...skippedLines, ...errorLines].join("\n");
304
+ interface ResetConfirmationChange {
305
+ label: string;
306
+ previousValue: string;
307
+ nextValue: string;
264
308
  }
265
309
 
266
310
  /**
267
- * @brief Builds the agent-oriented execute result returned by token-count tools.
268
- * @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.
269
- * @param[in] payload {TokenToolPayload} Structured token payload.
270
- * @return {{ content: Array<{ type: "text"; text: string }>; details: TokenToolPayload & { execution: { code: number; stderr: string } } }} Token-tool execute result.
271
- * @satisfies REQ-069, REQ-070, REQ-071, REQ-073, REQ-074, REQ-075, REQ-099, REQ-102
311
+ * @brief Formats one reset-confirmation value pair for menu display.
312
+ * @details Serializes the previous and next values into a deterministic `previous -> next` preview string used by confirmation submenus. Runtime is O(n) in combined value length. No external state is mutated.
313
+ * @param[in] previousValue {string} Current persisted value.
314
+ * @param[in] nextValue {string} Candidate default value.
315
+ * @return {string} Rendered preview string.
272
316
  */
273
- function buildTokenToolExecuteResult(
274
- payload: TokenToolPayload,
275
- ): {
276
- content: Array<{ type: "text"; text: string }>;
277
- details: TokenToolPayload & { execution: { code: number; stderr: string } };
278
- } {
279
- const details = {
280
- summary: payload.summary,
281
- files: payload.files,
282
- execution: {
283
- code: payload.summary.counted_file_count > 0 ? 0 : 1,
284
- stderr: buildTokenToolExecutionStderr(payload),
285
- },
286
- };
287
- return buildStructuredToolExecuteResult(details);
317
+ function formatResetConfirmationValue(previousValue: string, nextValue: string): string {
318
+ return `${previousValue} -> ${nextValue}`;
288
319
  }
289
320
 
290
321
  /**
291
- * @brief Builds the fallback execute result returned when token counting fails before payload construction.
292
- * @details Normalizes one thrown `ReqError` into the stable token-tool response shape so missing runtime dependencies or other pre-count failures do not abort extension tool execution. Runtime is O(1). No external state is mutated.
293
- * @param[in] error {unknown} Thrown token-tool failure.
294
- * @return {{ content: Array<{ type: "text"; text: string }>; details: TokenToolPayload & { execution: { code: number; stderr: string } } }} Structured token-tool failure result.
322
+ * @brief Builds the shared settings-menu choices for one reset-confirmation submenu.
323
+ * @details Renders each pending changed value as a disabled preview row, appends explicit approve and abort actions, and falls back to one disabled no-op row when no values would change. Runtime is O(n) in changed-value count. No external state is mutated.
324
+ * @param[in] changes {ResetConfirmationChange[]} Changed-value preview rows.
325
+ * @param[in] approveDescription {string} Description for the approve action.
326
+ * @param[in] abortDescription {string} Description for the abort action.
327
+ * @return {PiUsereqSettingsMenuChoice[]} Reset-confirmation submenu choices.
295
328
  */
296
- function buildFailedTokenToolExecuteResult(
297
- error: unknown,
298
- ): {
299
- content: Array<{ type: "text"; text: string }>;
300
- details: TokenToolPayload & { execution: { code: number; stderr: string } };
301
- } {
302
- const result = normalizeToolFailure(error);
303
- const details = {
304
- summary: {
305
- processable_file_count: 0,
306
- counted_file_count: 0,
307
- error_file_count: 0,
308
- skipped_file_count: 0,
309
- total_token_count: 0,
310
- total_character_count: 0,
311
- total_byte_count: 0,
312
- total_line_count: 0,
313
- average_token_count_per_counted_file: 0,
314
- average_character_count_per_counted_file: 0,
315
- average_byte_count_per_counted_file: 0,
316
- average_line_count_per_counted_file: 0,
317
- },
318
- files: [],
319
- execution: {
320
- code: result.code,
321
- stderr: result.stderr,
322
- },
323
- };
324
- return buildStructuredToolExecuteResult(details);
325
- }
326
-
327
- /**
328
- * @brief Builds the agent-oriented execute result returned by references tools.
329
- * @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.
330
- * @param[in] payload {ReferenceToolPayload} Structured references payload.
331
- * @return {{ content: Array<{ type: "text"; text: string }>; details: ReferenceToolPayload & { execution: { code: number; stderr: string } } }} References-tool execute result.
332
- * @satisfies REQ-076, REQ-077, REQ-078, REQ-079, REQ-099, REQ-102
333
- */
334
- function buildReferenceToolExecuteResult(
335
- payload: ReferenceToolPayload,
336
- ): {
337
- content: Array<{ type: "text"; text: string }>;
338
- details: ReferenceToolPayload & { execution: { code: number; stderr: string } };
339
- } {
340
- const details = {
341
- summary: payload.summary,
342
- repository: payload.repository,
343
- files: payload.files,
344
- execution: {
345
- code: payload.summary.analyzed_file_count > 0 ? 0 : 1,
346
- stderr: buildReferenceToolExecutionStderr(payload),
329
+ function buildResetConfirmationChoices(
330
+ changes: ResetConfirmationChange[],
331
+ approveDescription: string,
332
+ abortDescription: string,
333
+ ): PiUsereqSettingsMenuChoice[] {
334
+ const previewRows = changes.length > 0
335
+ ? changes.map((change, index) => ({
336
+ id: `reset-preview:${index}`,
337
+ label: change.label,
338
+ value: formatResetConfirmationValue(change.previousValue, change.nextValue),
339
+ description: `Reset ${change.label} from ${change.previousValue} to ${change.nextValue}.`,
340
+ disabled: true,
341
+ labelTone: "dim" as const,
342
+ valueTone: "dim" as const,
343
+ }))
344
+ : [{
345
+ id: "reset-preview:none",
346
+ label: "No value changes",
347
+ value: "unchanged",
348
+ description: "Approving the reset leaves the current values unchanged.",
349
+ disabled: true,
350
+ labelTone: "dim" as const,
351
+ valueTone: "dim" as const,
352
+ }];
353
+ return [
354
+ ...previewRows,
355
+ {
356
+ id: "reset-approve",
357
+ label: "Approve reset",
358
+ value: `${changes.length} changes`,
359
+ description: approveDescription,
347
360
  },
348
- };
349
- return buildStructuredToolExecuteResult(details);
350
- }
351
-
352
- /**
353
- * @brief Builds the agent-oriented execute result returned by compression tools.
354
- * @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.
355
- * @param[in] payload {CompressToolPayload} Structured compression payload.
356
- * @return {{ content: Array<{ type: "text"; text: string }>; details: CompressToolPayload & { execution: { code: number; stderr: string } } }} Compression-tool execute result.
357
- * @satisfies REQ-081, REQ-082, REQ-083, REQ-084, REQ-085, REQ-087, REQ-088, REQ-099, REQ-102
358
- */
359
- function buildCompressionToolExecuteResult(
360
- payload: CompressToolPayload,
361
- ): {
362
- content: Array<{ type: "text"; text: string }>;
363
- details: CompressToolPayload & { execution: { code: number; stderr: string } };
364
- } {
365
- const details = {
366
- summary: payload.summary,
367
- repository: payload.repository,
368
- files: payload.files,
369
- execution: {
370
- code: payload.summary.compressed_file_count > 0 ? 0 : 1,
371
- stderr: buildCompressToolExecutionStderr(payload),
361
+ {
362
+ id: "reset-abort",
363
+ label: "Abort reset",
364
+ value: "keep current",
365
+ description: abortDescription,
372
366
  },
373
- };
374
- return buildStructuredToolExecuteResult(details);
367
+ ];
368
+ }
369
+
370
+ /**
371
+ * @brief Opens one explicit reset-confirmation submenu.
372
+ * @details Uses the shared settings-menu renderer to show every changed value before reset application and returns `true` only when the user selects the explicit approval action. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
373
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
374
+ * @param[in] title {string} Confirmation submenu title.
375
+ * @param[in] changes {ResetConfirmationChange[]} Changed-value preview rows.
376
+ * @param[in] approveDescription {string} Description for the approve action.
377
+ * @param[in] abortDescription {string} Description for the abort action.
378
+ * @return {Promise<boolean>} `true` when the reset is explicitly approved.
379
+ */
380
+ async function confirmResetChanges(
381
+ ctx: ExtensionCommandContext,
382
+ title: string,
383
+ changes: ResetConfirmationChange[],
384
+ approveDescription: string,
385
+ abortDescription: string,
386
+ ): Promise<boolean> {
387
+ const choice = await showPiUsereqSettingsMenu(
388
+ ctx,
389
+ title,
390
+ buildResetConfirmationChoices(changes, approveDescription, abortDescription),
391
+ );
392
+ return choice === "reset-approve";
393
+ }
394
+
395
+ /**
396
+ * @brief Writes the already-persisted project configuration file text into the editor.
397
+ * @details Reads the current `.pi-usereq.json` file content from disk after the caller has saved any pending configuration changes and forwards that exact persisted text into the editor. Runtime is O(n) in serialized config size. Side effects include filesystem reads and editor-text mutation.
398
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
399
+ * @param[in] cwd {string} Current working directory.
400
+ * @param[in] _config {UseReqConfig} Unused effective project configuration retained for stable call-site shape.
401
+ * @return {void} No return value.
402
+ * @satisfies REQ-031
403
+ */
404
+ function writePersistedProjectConfigToEditor(
405
+ ctx: ExtensionCommandContext,
406
+ cwd: string,
407
+ _config: UseReqConfig,
408
+ ): void {
409
+ const projectBase = getProjectBase(cwd);
410
+ ctx.ui.setEditorText(fs.readFileSync(getProjectConfigPath(projectBase), "utf8"));
375
411
  }
376
412
 
377
413
  /**
378
- * @brief Maps find-payload language identifiers to stable registration labels.
414
+ * @brief Maps search-tool language identifiers to stable registration labels.
379
415
  * @details Preserves the canonical capitalization used by tool descriptions so supported-tag guidance remains deterministic across inspection snapshots. Access complexity is O(1).
380
416
  */
381
417
  const FIND_TOOL_LANGUAGE_LABELS: Record<string, string> = {
@@ -402,7 +438,7 @@ const FIND_TOOL_LANGUAGE_LABELS: Record<string, string> = {
402
438
  };
403
439
 
404
440
  /**
405
- * @brief Defines the stable language order used by find-tool supported-tag guidance.
441
+ * @brief Defines the stable language order used by search-tool supported-tag guidance.
406
442
  * @details Keeps registration descriptions aligned with the repository-supported language matrix and preserves deterministic inspection snapshots. Access complexity is O(1).
407
443
  */
408
444
  const FIND_TOOL_LANGUAGE_ORDER = [
@@ -429,103 +465,445 @@ const FIND_TOOL_LANGUAGE_ORDER = [
429
465
  ] as const;
430
466
 
431
467
  /**
432
- * @brief Builds the supported-tag guidance lines embedded in find-tool registrations.
468
+ * @brief Builds the supported-tag guidance lines embedded in search-tool registrations.
433
469
  * @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.
434
470
  * @return {string[]} Supported-tag guidance lines.
435
471
  */
436
- function buildFindToolSupportedTagGuidelines(): string[] {
472
+ function buildSearchToolSupportedTagGuidelines(): string[] {
437
473
  return FIND_TOOL_LANGUAGE_ORDER
438
474
  .filter((language) => language in LANGUAGE_TAGS)
439
475
  .map((language) => `Supported tags [${FIND_TOOL_LANGUAGE_LABELS[language]}]: ${[...LANGUAGE_TAGS[language]!].sort().join(", ")}`);
440
476
  }
441
477
 
442
478
  /**
443
- * @brief Builds the schema description for one find-tool registration.
444
- * @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.
445
- * @param[in] scope {FindToolScope} Find-tool scope.
479
+ * @brief Builds the schema description for one search-tool registration.
480
+ * @details Specializes the explicit-file and configured-directory input contracts while documenting the monolithic markdown output channel and minimal execution details shape. Runtime is O(1). No side effects occur.
481
+ * @param[in] scope {FindToolScope} Search-tool scope.
446
482
  * @return {string} Parameter-schema description.
447
483
  */
448
- function buildFindToolSchemaDescription(scope: FindToolScope): string {
484
+ function buildSearchToolSchemaDescription(scope: FindToolScope): string {
449
485
  const inputContract = scope === "explicit-files"
450
486
  ? "Input contract: tag + pattern + files[] + optional enableLineNumbers."
451
487
  : "Input contract: tag + pattern + optional enableLineNumbers. Scope is the configured src-dir list resolved from the current project configuration.";
452
- return `${inputContract} Output contract: JSON object with summary, repository, files, and execution. Static supported-tag matrices are documented in tool registration metadata instead of runtime responses. File entries expose structured statuses, file_doxygen, and match records with typed line ranges, stripped code lines, and structured Doxygen fields. Regex matches construct names only.`;
488
+ return `${inputContract} Output contract: monolithic markdown in content[0].text plus details.execution diagnostics. Regex matches construct names only.`;
453
489
  }
454
490
 
455
491
  /**
456
- * @brief Builds the prompt-guideline set for one find-tool registration.
457
- * @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.
458
- * @param[in] scope {FindToolScope} Find-tool scope.
492
+ * @brief Builds the prompt-guideline set for one search-tool registration.
493
+ * @details Encodes scope selection, monolithic markdown output semantics, 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.
494
+ * @param[in] scope {FindToolScope} Search-tool scope.
459
495
  * @return {string[]} Prompt-guideline strings.
460
496
  */
461
- function buildFindToolPromptGuidelines(scope: FindToolScope): string[] {
497
+ function buildSearchToolPromptGuidelines(scope: FindToolScope): string[] {
462
498
  const scopeLine = scope === "explicit-files"
463
499
  ? "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute."
464
500
  : "Scope: resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.";
465
- const outputLine = scope === "explicit-files"
466
- ? "Output contract: summary + repository + files + execution. Repository exposes requested file scope only when it adds dynamic search context; supported tags remain documented in registration metadata. File entries expose status, line ranges, file_doxygen, and matches; match entries expose symbol_kind, signature_text, line ranges, code_lines, stripped_source_text, and structured Doxygen fields."
467
- : "Output contract: summary + repository + files + execution. Repository exposes source_directory_paths and file_canonical_paths when project-scope search context varies; supported tags remain documented in registration metadata. File entries expose status, line ranges, file_doxygen, and matches; match entries expose symbol_kind, signature_text, line ranges, code_lines, stripped_source_text, and structured Doxygen fields.";
468
501
  return [
469
502
  scopeLine,
470
- outputLine,
503
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
471
504
  "Regex rule: pattern is applied to construct names only with JavaScript RegExp search semantics; it never matches construct bodies; use ^...$ for exact-name matching.",
472
- "Tag rule: tag is pipe-separated and case-insensitive; unsupported tags are ignored; if no valid tag remains, the response search_status becomes invalid_tag_filter.",
473
- "Line-number behavior: enableLineNumbers changes only display_text and stripped_source_text rendering; numeric source_line_number and line_range facts remain dedicated fields.",
474
- "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.",
475
- ...buildFindToolSupportedTagGuidelines(),
505
+ "Tag rule: tag is pipe-separated and case-insensitive; unsupported tags are ignored.",
506
+ "Line-number behavior: enableLineNumbers toggles original source line prefixes inside fenced code blocks.",
507
+ "Failure contract: invalid tag filters, invalid regex patterns, unsupported extensions, unsupported tag-language combinations, no-match files, and analysis failures surface through details.execution diagnostics.",
508
+ ...buildSearchToolSupportedTagGuidelines(),
476
509
  ];
477
510
  }
478
511
 
479
512
  /**
480
- * @brief Builds the agent-oriented execute result returned by find tools.
481
- * @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.
482
- * @param[in] payload {FindToolPayload} Structured find payload.
483
- * @return {{ content: Array<{ type: "text"; text: string }>; details: FindToolPayload & { execution: { code: number; stderr: string } } }} Find-tool execute result.
484
- * @satisfies REQ-089, REQ-090, REQ-091, REQ-092, REQ-093, REQ-094, REQ-097, REQ-098, REQ-099, REQ-102
485
- */
486
- function buildFindToolExecuteResult(
487
- payload: FindToolPayload,
488
- ): {
489
- content: Array<{ type: "text"; text: string }>;
490
- details: FindToolPayload & { execution: { code: number; stderr: string } };
491
- } {
492
- const stderr = buildFindToolExecutionStderr(payload);
493
- const details = {
494
- summary: payload.summary,
495
- repository: payload.repository,
496
- files: payload.files,
497
- execution: {
498
- code: payload.summary.search_status === "matched" ? 0 : 1,
499
- stderr,
500
- },
513
+ * @brief Describes the monolithic tool-result surface consumed by tool-row renderers.
514
+ * @details Narrows execute-result data to the primary text content block plus the minimal `details.execution` metadata returned by monolithic tool wrappers. The alias is compile-time only and introduces no runtime cost.
515
+ */
516
+ type MonolithicToolRenderResult = {
517
+ content?: Array<{ type?: string; text?: string }>;
518
+ details?: {
519
+ execution?: {
520
+ code?: number;
521
+ stderr_lines?: string[];
522
+ stderr?: string;
523
+ };
501
524
  };
502
- return buildStructuredToolExecuteResult(details);
525
+ };
526
+
527
+ /**
528
+ * @brief Extracts the primary monolithic text block from one tool result.
529
+ * @details Returns the first text content block when present and falls back to an empty string when the tool emitted no LLM-facing content. Runtime is O(1). No external state is mutated.
530
+ * @param[in] result {MonolithicToolRenderResult} Tool result wrapper.
531
+ * @return {string} Primary monolithic content text.
532
+ */
533
+ function getMonolithicToolText(result: MonolithicToolRenderResult): string {
534
+ const firstBlock = result.content?.find((entry) => entry?.type === "text" && typeof entry.text === "string");
535
+ return typeof firstBlock?.text === "string" ? firstBlock.text : "";
503
536
  }
504
537
 
505
538
  /**
506
- * @brief Delivers one rendered prompt into the active session.
507
- * @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.
508
- * @param[in] pi {ExtensionAPI} Active extension API instance.
539
+ * @brief Reads the first residual execution error string from one monolithic tool result.
540
+ * @details Prefers the first `stderr_lines` entry when present and otherwise falls back to the first line of `stderr`. Runtime is O(1) plus first-line split cost. No external state is mutated.
541
+ * @param[in] result {MonolithicToolRenderResult} Tool result wrapper.
542
+ * @return {string | undefined} First residual execution error string.
543
+ */
544
+ function getMonolithicToolErrorText(result: MonolithicToolRenderResult): string | undefined {
545
+ const stderrLines = result.details?.execution?.stderr_lines;
546
+ if (Array.isArray(stderrLines) && stderrLines.length > 0 && typeof stderrLines[0] === "string") {
547
+ return stderrLines[0];
548
+ }
549
+ const stderrText = result.details?.execution?.stderr;
550
+ if (typeof stderrText !== "string" || stderrText === "") {
551
+ return undefined;
552
+ }
553
+ return stderrText.split(/\r?\n/)[0] || undefined;
554
+ }
555
+
556
+ /**
557
+ * @brief Formats one scalar or structural tool argument for compact render summaries.
558
+ * @details Truncates long strings, compresses arrays into short previews, and renders plain object arguments as key indexes so collapsed tool rows stay compact while still exposing the essential invocation shape. Runtime is O(n) in preview size. No external state is mutated.
559
+ * @param[in] value {unknown} Candidate tool argument value.
560
+ * @return {string | undefined} Compact preview string or `undefined` when the value carries no useful summary.
561
+ */
562
+ function formatCompactToolArgumentValue(value: unknown): string | undefined {
563
+ if (value === undefined || value === null) {
564
+ return undefined;
565
+ }
566
+ if (typeof value === "string") {
567
+ const compactText = value.length > 28 ? `${value.slice(0, 25)}...` : value;
568
+ return JSON.stringify(compactText);
569
+ }
570
+ if (typeof value === "number" || typeof value === "bigint") {
571
+ return String(value);
572
+ }
573
+ if (typeof value === "boolean") {
574
+ return value ? "true" : "false";
575
+ }
576
+ if (Array.isArray(value)) {
577
+ if (value.length === 0) {
578
+ return undefined;
579
+ }
580
+ const renderedItems = value
581
+ .slice(0, 2)
582
+ .map((item) => formatCompactToolArgumentValue(item))
583
+ .filter((item): item is string => typeof item === "string");
584
+ if (renderedItems.length === 0) {
585
+ return `[${value.length}]`;
586
+ }
587
+ return value.length <= 2
588
+ ? `[${renderedItems.join(", ")}]`
589
+ : `[${renderedItems.join(", ")}, ...+${value.length - 2}]`;
590
+ }
591
+ if (typeof value === "object") {
592
+ const keys = Object.keys(value as Record<string, unknown>);
593
+ if (keys.length === 0) {
594
+ return undefined;
595
+ }
596
+ return keys.length <= 2
597
+ ? `{${keys.join(",")}}`
598
+ : `{${keys.slice(0, 2).join(",")},...+${keys.length - 2}}`;
599
+ }
600
+ return undefined;
601
+ }
602
+
603
+ /**
604
+ * @brief Builds the compact invocation summary appended to collapsed tool rows.
605
+ * @details Renders only caller-supplied parameters that have stable, non-empty compact previews and joins them in insertion order so agents can infer how the tool was used without expanding the full result. Runtime is O(n) in argument count and preview size. No external state is mutated.
606
+ * @param[in] args {Record<string, unknown> | undefined} Current tool call arguments.
607
+ * @return {string} Compact invocation summary prefixed with one separating space, or the empty string when no useful preview exists.
608
+ */
609
+ function buildCompactToolInvocationText(args: Record<string, unknown> | undefined): string {
610
+ if (!args) {
611
+ return "";
612
+ }
613
+ const entries = Object.entries(args)
614
+ .map(([key, value]) => {
615
+ const renderedValue = formatCompactToolArgumentValue(value);
616
+ return renderedValue === undefined ? undefined : `${key}=${renderedValue}`;
617
+ })
618
+ .filter((entry): entry is string => typeof entry === "string");
619
+ return entries.length === 0 ? "" : ` ${entries.join(" · ")}`;
620
+ }
621
+
622
+ /**
623
+ * @brief Builds the compact default text for one monolithic tool result row.
624
+ * @details Prefers the tool name, compact invocation preview, and success marker for collapsed rows, and falls back to residual execution diagnostics when the tool failed before completing successfully. Runtime is O(n) in compact argument-preview size. No external state is mutated.
625
+ * @param[in] toolName {string} Registered tool name.
626
+ * @param[in] result {MonolithicToolRenderResult} Tool result wrapper.
627
+ * @param[in] args {Record<string, unknown> | undefined} Current tool call arguments.
628
+ * @return {string} Compact single-line summary.
629
+ */
630
+ function summarizeStructuredToolResult(
631
+ toolName: string,
632
+ result: MonolithicToolRenderResult,
633
+ args?: Record<string, unknown>,
634
+ ): string {
635
+ const success = result.details?.execution?.code === undefined
636
+ ? true
637
+ : result.details.execution.code === 0;
638
+ const prefix = success ? "✓" : "✗";
639
+ const invocationText = buildCompactToolInvocationText(args);
640
+ const errorText = getMonolithicToolErrorText(result);
641
+ if (!success && errorText) {
642
+ return `${prefix} ${toolName}${invocationText}: ${errorText}`;
643
+ }
644
+ return `${prefix} ${toolName}${invocationText}`;
645
+ }
646
+
647
+ /**
648
+ * @brief Builds a custom `renderResult` implementation for one monolithic tool.
649
+ * @details Reuses a mutable `Text` component when possible, keeps the default collapsed row compact with essential invocation parameters plus result status, and reveals the full monolithic content only when the tool row is expanded. Runtime is O(n) in expanded content length and compact argument-preview size. No external state is mutated.
650
+ * @param[in] toolName {string} Registered tool name.
651
+ * @return {(result: MonolithicToolRenderResult, options: { expanded?: boolean; isPartial?: boolean }, _theme: unknown, context: { args?: Record<string, unknown>; lastComponent?: unknown }) => Text} Custom result renderer.
652
+ * @satisfies REQ-210
653
+ */
654
+ function buildStructuredToolRenderResult(toolName: string) {
655
+ return (
656
+ result: MonolithicToolRenderResult,
657
+ options: { expanded?: boolean; isPartial?: boolean },
658
+ _theme: unknown,
659
+ context: { args?: Record<string, unknown>; lastComponent?: unknown },
660
+ ): Text => {
661
+ const textComponent = context.lastComponent instanceof Text
662
+ ? context.lastComponent
663
+ : new Text("", 0, 0);
664
+ if (options.isPartial) {
665
+ textComponent.setText(`… ${toolName}`);
666
+ return textComponent;
667
+ }
668
+ const compactText = summarizeStructuredToolResult(toolName, result, context.args);
669
+ const fullText = getMonolithicToolText(result);
670
+ textComponent.setText(options.expanded && fullText !== "" ? fullText : compactText);
671
+ return textComponent;
672
+ };
673
+ }
674
+
675
+ /**
676
+ * @brief Executes one CLI-style runner for a monolithic agent tool.
677
+ * @details Reuses the standalone tool-runner contract, normalizes thrown failures into `ToolResult`, and wraps the selected stdout or stderr text into the monolithic content channel. Runtime is dominated by the delegated runner. Side effects depend on the selected tool.
678
+ * @param[in] operation {() => ToolResult} Runner callback.
679
+ * @return {ReturnType<typeof buildMonolithicToolExecuteResult>} Monolithic tool execute result.
680
+ */
681
+ function executeMonolithicTool(operation: () => ToolResult): ReturnType<typeof buildMonolithicToolExecuteResult> {
682
+ try {
683
+ return buildMonolithicToolExecuteResult(operation());
684
+ } catch (error) {
685
+ return buildMonolithicToolExecuteResult(normalizeToolFailure(error));
686
+ }
687
+ }
688
+
689
+ /**
690
+ * @brief Starts delivery of one rendered prompt into the current active session.
691
+ * @details Prefers the replacement-session `sendUserMessage(...)` helper exposed by `withSession(...)` callbacks after session replacement so post-switch prompt delivery never reuses stale pre-switch session-bound extension objects. Returns the underlying delivery promise without awaiting it so callers can record the `running` workflow transition as soon as prompt handoff is accepted instead of waiting for the full agent turn to complete on runtimes whose async replacement-session helpers resolve only after `agent_end`. When pi later invalidates that replacement-session context during successful prompt-end restoration, the helper suppresses the documented stale-extension-context rejection because the prompt was already accepted and late rethrow would surface a false orchestration failure. Falls back to `pi.sendUserMessage(...)` only for non-replacement flows or runtimes that do not expose replacement-session helpers. Runtime is O(n) in prompt length. Side effects are limited to user-message delivery.
692
+ * @param[in] pi {ExtensionAPI} Handler-scoped extension API instance retained as the fallback dispatcher.
509
693
  * @param[in] content {string} Rendered prompt markdown.
510
- * @return {Promise<void>} Promise resolved after the prompt is queued for delivery.
511
- * @satisfies REQ-004, REQ-067, REQ-068
694
+ * @param[in] context {unknown} Optional replacement-session helper context.
695
+ * @return {Promise<void>} Promise representing eventual prompt-delivery completion.
696
+ * @satisfies REQ-004, REQ-067, REQ-068, REQ-227, REQ-281
512
697
  */
513
- async function deliverPromptCommand(pi: ExtensionAPI, content: string): Promise<void> {
698
+ function deliverPromptCommand(
699
+ pi: ExtensionAPI,
700
+ content: string,
701
+ context?: unknown,
702
+ ): Promise<void> {
703
+ const replacementContext = context as {
704
+ sendUserMessage?: (message: string) => Promise<void> | void;
705
+ } | undefined;
706
+ if (typeof replacementContext?.sendUserMessage === "function") {
707
+ return Promise.resolve(replacementContext.sendUserMessage(content)).catch((error) => {
708
+ if (isStaleExtensionContextError(error)) {
709
+ return;
710
+ }
711
+ throw error;
712
+ });
713
+ }
514
714
  pi.sendUserMessage(content);
715
+ return Promise.resolve();
716
+ }
717
+
718
+ /**
719
+ * @brief Detects prompt-delivery failures that can be ignored after prompt ownership has moved past the command handler.
720
+ * @details Matches the documented stale-extension-context runtime error once prompt ownership has already moved beyond command-side preflight. The helper treats the failure as ignorable when the persisted prompt runtime state shows the same execution session as the active prompt run or when the persisted workflow state has already advanced beyond `checking|running`, because rethrowing at that point would incorrectly re-enter command-side abort logic after the prompt was already accepted. Runtime is O(n) in error-message length plus path length. No external state is mutated.
721
+ * @param[in] error {unknown} Candidate prompt-delivery failure.
722
+ * @param[in] workflowState {import("./core/extension-status.js").PiUsereqWorkflowState} Current shared prompt workflow state.
723
+ * @param[in] executionPlan {PromptCommandExecutionPlan | undefined} Prepared execution plan for the current prompt command.
724
+ * @return {boolean} `true` when the failure is a late stale-context delivery rejection that MUST be ignored.
725
+ * @satisfies REQ-208, REQ-280, REQ-281, REQ-282
726
+ */
727
+ function shouldIgnoreLatePromptDeliveryFailure(
728
+ error: unknown,
729
+ workflowState: import("./core/extension-status.js").PiUsereqWorkflowState,
730
+ executionPlan?: PromptCommandExecutionPlan,
731
+ ): boolean {
732
+ const persistedRuntimeState = readPersistedPromptCommandRuntimeState();
733
+ const effectiveWorkflowState = persistedRuntimeState.workflowState || workflowState;
734
+ const activeExecutionSessionFile = persistedRuntimeState.activePromptRequest?.executionSessionFile;
735
+ const promptRunAlreadyActive = executionPlan !== undefined
736
+ && typeof activeExecutionSessionFile === "string"
737
+ && path.resolve(activeExecutionSessionFile) === path.resolve(executionPlan.executionSessionFile);
738
+ return isStaleExtensionContextError(error)
739
+ && (
740
+ promptRunAlreadyActive
741
+ || (effectiveWorkflowState !== "checking" && effectiveWorkflowState !== "running")
742
+ );
743
+ }
744
+
745
+ /**
746
+ * @brief Appends one workflow-state debug entry for a bundled prompt when selected.
747
+ * @details Reuses the shared debug logger so `req-*` command handlers and prompt-end orchestration can record deterministic workflow transitions without duplicating JSON payload shaping. Runtime is O(n) in serialized payload size only when logging is enabled and O(1) otherwise. Side effects include debug-log file writes for matching enabled prompts.
748
+ * @param[in] projectBase {string} Absolute original project base path.
749
+ * @param[in] config {UseReqConfig} Effective project configuration.
750
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
751
+ * @param[in] previousState {string} Prior workflow state.
752
+ * @param[in] nextState {import("./core/debug-runtime.js").DebugWorkflowState} Next workflow state.
753
+ * @return {void} No return value.
754
+ * @satisfies REQ-245, REQ-246, REQ-247
755
+ */
756
+ function logPromptWorkflowStateChange(
757
+ projectBase: string,
758
+ config: UseReqConfig,
759
+ promptName: import("./core/prompt-command-catalog.js").PromptCommandName,
760
+ previousState: string,
761
+ nextState: import("./core/debug-runtime.js").DebugWorkflowState,
762
+ ): void {
763
+ if (!shouldLogDebugPromptWorkflowState(config, nextState, promptName)) {
764
+ return;
765
+ }
766
+ logDebugPromptEvent(
767
+ projectBase,
768
+ config,
769
+ nextState,
770
+ promptName,
771
+ "workflow_state",
772
+ { from_state: previousState, to_state: nextState },
773
+ { success: true, workflow_state: nextState },
774
+ );
775
+ }
776
+
777
+ /**
778
+ * @brief Appends one dedicated prompt workflow debug entry when selected.
779
+ * @details Reuses the shared workflow-event logger so prompt activation, restoration, closure, and session-shutdown paths can emit higher-granularity orchestration diagnostics without duplicating JSON payload shaping. Runtime is O(n) in serialized payload size only when logging is enabled and O(1) otherwise. Side effects include debug-log file writes for matching enabled prompts.
780
+ * @param[in] projectBase {string} Absolute original project base path.
781
+ * @param[in] config {UseReqConfig} Effective project configuration.
782
+ * @param[in] workflowState {DebugWorkflowState} Current workflow state.
783
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
784
+ * @param[in] action {string} Workflow debug action identifier.
785
+ * @param[in] input {unknown} Optional workflow debug input payload.
786
+ * @param[in] result {unknown} Optional workflow debug result payload.
787
+ * @param[in] isError {boolean} Workflow debug error flag.
788
+ * @return {void} No return value.
789
+ * @satisfies REQ-245, REQ-246, REQ-247, REQ-277
790
+ */
791
+ function logPromptWorkflowEvent(
792
+ projectBase: string,
793
+ config: UseReqConfig,
794
+ workflowState: DebugWorkflowState,
795
+ promptName: import("./core/prompt-command-catalog.js").PromptCommandName,
796
+ action: string,
797
+ input?: unknown,
798
+ result?: unknown,
799
+ isError = false,
800
+ ): void {
801
+ logDebugPromptWorkflowEvent(
802
+ projectBase,
803
+ config,
804
+ workflowState,
805
+ promptName,
806
+ action,
807
+ input,
808
+ result,
809
+ isError,
810
+ );
811
+ }
812
+
813
+ /**
814
+ * @brief Transitions one prompt workflow state and logs the transition immediately after the state update.
815
+ * @details Captures the previous workflow state, applies the new state through the shared status helper, and appends the gated `workflow_state` debug entry only after the transition has completed. Runtime is O(1). Side effects include status mutation, status-bar rendering, and optional debug-log writes.
816
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
817
+ * @param[in] ctx {ExtensionContext | ExtensionCommandContext} Active extension context.
818
+ * @param[in] projectBase {string} Absolute original project base path.
819
+ * @param[in] config {UseReqConfig} Effective project configuration.
820
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
821
+ * @param[in] nextState {DebugWorkflowState} Next workflow state.
822
+ * @return {void} No return value.
823
+ */
824
+ function transitionPromptWorkflowState(
825
+ statusController: PiUsereqStatusController,
826
+ ctx: ExtensionContext | ExtensionCommandContext,
827
+ projectBase: string,
828
+ config: UseReqConfig,
829
+ promptName: import("./core/prompt-command-catalog.js").PromptCommandName,
830
+ nextState: DebugWorkflowState,
831
+ ): void {
832
+ const previousState = statusController.state.workflowState;
833
+ setPiUsereqWorkflowState(statusController, nextState, ctx);
834
+ if (previousState !== nextState) {
835
+ logPromptWorkflowStateChange(projectBase, config, promptName, previousState, nextState);
836
+ }
837
+ }
838
+
839
+ /**
840
+ * @brief Resolves the runtime slash-command description for one bundled prompt.
841
+ * @details Reads the bundled prompt front matter, extracts its normalized `description` field, and falls back to the historical generated label when the prompt metadata omits a description. Runtime is O(n) in prompt length. Side effects are limited to filesystem reads.
842
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
843
+ * @return {string} Runtime command description.
844
+ */
845
+ function resolvePromptCommandDescription(
846
+ promptName: import("./core/prompt-command-catalog.js").PromptCommandName,
847
+ ): string {
848
+ return readBundledPromptDescription(promptName) || `Run pi-usereq prompt ${promptName}`;
849
+ }
850
+
851
+ /**
852
+ * @brief Resolves the original project base used for debug-log file writes.
853
+ * @details Prefers the active or pending prompt execution plan so tool-result logging during worktree-backed prompt runs persists into the original repository path instead of transient worktree directories. Runtime is O(1). No external state is mutated.
854
+ * @param[in] cwd {string} Current extension working directory.
855
+ * @param[in] statusController {PiUsereqStatusController} Mutable status controller.
856
+ * @return {string} Absolute original project base path for debug logging.
857
+ */
858
+ function resolveDebugProjectBase(cwd: string, statusController: PiUsereqStatusController): string {
859
+ return statusController.state.activePromptRequest?.basePath
860
+ ?? statusController.state.pendingPromptRequest?.basePath
861
+ ?? getProjectBase(resolveLiveBootstrapCwd(cwd));
862
+ }
863
+
864
+ /**
865
+ * @brief Delivers one best-effort UI notification without failing on stale replacement contexts.
866
+ * @details Attempts to use the supplied extension context for UI notification delivery and suppresses the documented stale-extension-context runtime error raised after session replacement, because prompt-orchestration closure can outlive the context that initiated the switch. Runtime is O(n) in message length. Side effects are limited to user notification delivery when the context is still active.
867
+ * @param[in] ctx {ExtensionContext | ExtensionCommandContext | undefined} Candidate UI context.
868
+ * @param[in] message {string} Notification message.
869
+ * @param[in] level {"info" | "error"} Notification severity.
870
+ * @return {boolean} `true` when the notification was delivered and `false` when the context was already stale.
871
+ * @satisfies REQ-280
872
+ */
873
+ function notifyContextSafely(
874
+ ctx: (ExtensionContext | ExtensionCommandContext) | undefined,
875
+ message: string,
876
+ level: "info" | "error",
877
+ ): boolean {
878
+ if (ctx === undefined) {
879
+ return false;
880
+ }
881
+ try {
882
+ ctx.ui.notify(message, level);
883
+ return true;
884
+ } catch (error) {
885
+ if (isStaleExtensionContextError(error)) {
886
+ return false;
887
+ }
888
+ throw error;
889
+ }
515
890
  }
516
891
 
517
892
  /**
518
893
  * @brief Returns the configurable active-tool inventory visible to the extension.
519
- * @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.
894
+ * @details Filters runtime tools against the canonical configurable-tool set, keeps only builtin-backed embedded tools, and orders the result by the documented custom/files/embedded/default-disabled grouping. Runtime is O(t log t). No external state is mutated.
520
895
  * @param[in] pi {ExtensionAPI} Active extension API instance.
521
896
  * @return {ToolInfo[]} Sorted configurable tool descriptors.
522
- * @satisfies REQ-007, REQ-063
897
+ * @satisfies REQ-007, REQ-063, REQ-231, REQ-232
523
898
  */
524
899
  function getPiUsereqStartupTools(pi: ExtensionAPI): ToolInfo[] {
525
900
  return pi.getAllTools()
526
901
  .filter((tool) => PI_USEREQ_STARTUP_TOOL_SET.has(tool.name))
527
902
  .filter((tool) => !isPiUsereqEmbeddedToolName(tool.name) || tool.sourceInfo?.source === "builtin")
528
- .sort((left, right) => left.name.localeCompare(right.name));
903
+ .sort((left, right) => comparePiUsereqStartupToolNames(
904
+ left.name as PiUsereqStartupToolName,
905
+ right.name as PiUsereqStartupToolName,
906
+ ));
529
907
  }
530
908
 
531
909
  /**
@@ -569,22 +947,14 @@ function applyConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig): v
569
947
 
570
948
  /**
571
949
  * @brief Handles one intercepted pi lifecycle hook for pi-usereq status updates.
572
- * @details Applies session-start-specific resource validation, project-config
573
- * refresh, and startup-tool enablement before forwarding the originating hook
574
- * name and payload into the shared `updateExtensionStatus(...)` pipeline.
575
- * On `agent_end`, also dispatches configured command-notify, sound, and
576
- * prompt-specific Pushover effects when the current run originates from a
577
- * bundled prompt command. Runtime is dominated by configuration loading during
578
- * `session_start`; all other hooks are O(1). Side effects include resource
579
- * checks, active-tool mutation, status updates, live-ticker disposal on
580
- * shutdown, optional child-process spawning, and outbound HTTPS requests.
950
+ * @details Applies session-start-specific resource validation, project-config refresh, startup-tool enablement, and selected debug-tool logging before forwarding the originating hook name and payload into the shared `updateExtensionStatus(...)` pipeline. Before `agent_start`, re-verifies any prepared prompt execution session switch. On `agent_end`, dispatches configured command-notify, sound, and prompt-specific Pushover effects, logs dedicated workflow-closure diagnostics, restores the original session-backed `base-path` for every matched worktree-backed completion by reusing persisted replacement-session command contexts when event contexts omit `switchSession()`, merges and deletes the worktree only for matched successful completions, tolerates stale replacement-session notification contexts after session replacement, retains the worktree plus notifies closure failure for interrupted or failed outcomes, logs selected prompt workflow transitions, and transitions workflow state through `merging`, `error`, and `idle` as required. On `session_shutdown`, captures pre-update prompt snapshots so workflow-shutdown diagnostics and same-runtime command continuation preserve the active prompt workflow state across switch-triggered rebinding, then disposes the shared controller. Runtime is dominated by configuration loading during `session_start` and git finalization during matched successful `agent_end` handling; all other hooks are O(1). Side effects include resource checks, active-tool mutation, active-session replacement, status updates, live-ticker disposal on shutdown, optional child-process spawning, outbound HTTPS requests, branch merges, worktree deletion, and optional debug-log writes.
581
951
  * @param[in] pi {ExtensionAPI} Active extension API instance.
582
952
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
583
953
  * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
584
954
  * @param[in] event {unknown} Hook payload forwarded by pi.
585
955
  * @param[in] ctx {ExtensionContext} Active extension context.
586
956
  * @return {Promise<void>} Promise resolved when hook processing completes.
587
- * @satisfies REQ-117, REQ-118, REQ-119, REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-176, REQ-178, REQ-184, REQ-185, REQ-186, REQ-187
957
+ * @satisfies REQ-117, REQ-118, REQ-119, REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-176, REQ-178, REQ-184, REQ-185, REQ-186, REQ-187, REQ-208, REQ-209, REQ-221, REQ-228, REQ-229, REQ-230, REQ-244, REQ-245, REQ-246, REQ-247, REQ-276, REQ-277, REQ-278, REQ-279, REQ-280
588
958
  */
589
959
  async function handleExtensionStatusEvent(
590
960
  pi: ExtensionAPI,
@@ -593,23 +963,65 @@ async function handleExtensionStatusEvent(
593
963
  event: unknown,
594
964
  ctx: ExtensionContext,
595
965
  ): Promise<void> {
596
- const notifyRequest: PiNotifyEventRequest | undefined = hookName === "agent_end"
597
- && statusController.state.activePromptRequest !== undefined
598
- && statusController.state.runStartTimeMs !== undefined
599
- ? {
600
- promptName: statusController.state.activePromptRequest.promptName,
601
- promptArgs: statusController.state.activePromptRequest.promptArgs,
602
- basePath: path.resolve(ctx.cwd),
603
- completionTimeMs: Math.max(0, Date.now() - statusController.state.runStartTimeMs),
604
- }
966
+ const pendingPromptRequest = statusController.state.pendingPromptRequest;
967
+ const activePromptRequest = statusController.state.activePromptRequest;
968
+ const shutdownPromptRequest = hookName === "session_shutdown"
969
+ ? activePromptRequest ?? pendingPromptRequest
605
970
  : undefined;
971
+ const preservePromptCommandStateOnShutdown = hookName === "session_shutdown"
972
+ ? shouldPreservePromptCommandStateOnShutdown(statusController.state)
973
+ : false;
606
974
  if (hookName === "session_start") {
607
975
  ensureBundledResourcesAccessible();
608
- const config = loadProjectConfig(ctx.cwd);
976
+ const startupCwd = resolveLiveBootstrapCwd(ctx.cwd);
977
+ syncContextCwdMirror(ctx, startupCwd);
978
+ bootstrapRuntimePathState(startupCwd, {
979
+ gitPath: resolveRuntimeGitPath(startupCwd),
980
+ });
981
+ const config = loadProjectConfig(startupCwd);
609
982
  applyConfiguredPiUsereqTools(pi, config);
610
983
  setPiUsereqStatusConfig(statusController, config);
611
984
  }
985
+ if (hookName === "before_agent_start") {
986
+ const requestForActivation = activePromptRequest ?? pendingPromptRequest;
987
+ if (requestForActivation !== undefined) {
988
+ await activatePromptCommandExecution(requestForActivation, ctx);
989
+ }
990
+ }
991
+ const notifyRequest: PiNotifyEventRequest | undefined = hookName === "agent_end"
992
+ && activePromptRequest !== undefined
993
+ && statusController.state.runStartTimeMs !== undefined
994
+ ? {
995
+ promptName: activePromptRequest.promptName,
996
+ promptArgs: activePromptRequest.promptArgs,
997
+ basePath: activePromptRequest.basePath,
998
+ completionTimeMs: Math.max(0, Date.now() - statusController.state.runStartTimeMs),
999
+ }
1000
+ : undefined;
612
1001
  updateExtensionStatus(statusController, hookName, event, ctx);
1002
+ if (hookName === "tool_result" && statusController.config) {
1003
+ const toolEvent = event as {
1004
+ toolName?: string;
1005
+ input?: unknown;
1006
+ content?: unknown;
1007
+ details?: unknown;
1008
+ isError?: boolean;
1009
+ };
1010
+ if (typeof toolEvent.toolName === "string") {
1011
+ logDebugToolExecution(
1012
+ resolveDebugProjectBase(ctx.cwd, statusController),
1013
+ statusController.config,
1014
+ statusController.state.workflowState,
1015
+ toolEvent.toolName,
1016
+ toolEvent.input,
1017
+ {
1018
+ content: toolEvent.content,
1019
+ details: toolEvent.details,
1020
+ },
1021
+ toolEvent.isError === true,
1022
+ );
1023
+ }
1024
+ }
613
1025
  if (hookName === "agent_end") {
614
1026
  if (statusController.config) {
615
1027
  runPiNotifyEffects(
@@ -618,9 +1030,190 @@ async function handleExtensionStatusEvent(
618
1030
  notifyRequest,
619
1031
  );
620
1032
  }
621
- statusController.state.activePromptRequest = undefined;
1033
+ if (activePromptRequest !== undefined) {
1034
+ const debugConfig = statusController.config;
1035
+ let promptContext = ctx;
1036
+ const outcome = classifyPromptCommandOutcome(
1037
+ event as { messages: AgentEndEvent["messages"] },
1038
+ );
1039
+ const closureFailureMessage = outcome !== "completed"
1040
+ && activePromptRequest.worktreeDir !== undefined
1041
+ ? `ERROR: Prompt closure retained worktree ${activePromptRequest.worktreeDir} after ${outcome} outcome.`
1042
+ : undefined;
1043
+ const shouldFinalizeMatchedSuccess = outcome === "completed"
1044
+ && statusController.state.workflowState === "running"
1045
+ && activePromptRequest.worktreeDir !== undefined;
1046
+ if (debugConfig) {
1047
+ logPromptWorkflowEvent(
1048
+ activePromptRequest.basePath,
1049
+ debugConfig,
1050
+ statusController.state.workflowState,
1051
+ activePromptRequest.promptName,
1052
+ "workflow_closure",
1053
+ {
1054
+ hook: "agent_end",
1055
+ outcome,
1056
+ },
1057
+ {
1058
+ should_finalize_matched_success: shouldFinalizeMatchedSuccess,
1059
+ worktree_dir: activePromptRequest.worktreeDir,
1060
+ event_context_has_switch_session: typeof (ctx as ExtensionCommandContext).switchSession === "function",
1061
+ },
1062
+ );
1063
+ }
1064
+ if (shouldFinalizeMatchedSuccess) {
1065
+ if (debugConfig) {
1066
+ transitionPromptWorkflowState(
1067
+ statusController,
1068
+ promptContext,
1069
+ activePromptRequest.basePath,
1070
+ debugConfig,
1071
+ activePromptRequest.promptName,
1072
+ "merging",
1073
+ );
1074
+ } else {
1075
+ setPiUsereqWorkflowState(statusController, "merging", promptContext);
1076
+ }
1077
+ let finalization:
1078
+ | {
1079
+ mergeAttempted: boolean;
1080
+ mergeSucceeded: boolean;
1081
+ cleanupSucceeded: boolean;
1082
+ errorMessage?: string;
1083
+ activeContext?: unknown;
1084
+ }
1085
+ | undefined;
1086
+ try {
1087
+ finalization = await finalizePromptCommandExecution(
1088
+ activePromptRequest,
1089
+ promptContext,
1090
+ debugConfig
1091
+ ? { config: debugConfig, workflowState: statusController.state.workflowState }
1092
+ : undefined,
1093
+ );
1094
+ promptContext = (finalization.activeContext ?? promptContext) as typeof ctx;
1095
+ } catch (error) {
1096
+ promptContext = (getPromptCommandErrorContext(error) ?? promptContext) as typeof ctx;
1097
+ let errorMessage = error instanceof Error ? error.message : String(error);
1098
+ let cleanupSucceeded = false;
1099
+ try {
1100
+ promptContext = (await restorePromptCommandExecution(
1101
+ activePromptRequest,
1102
+ promptContext,
1103
+ debugConfig
1104
+ ? { config: debugConfig, workflowState: statusController.state.workflowState }
1105
+ : undefined,
1106
+ ) ?? promptContext) as typeof ctx;
1107
+ cleanupSucceeded = true;
1108
+ } catch (restoreError) {
1109
+ promptContext = (getPromptCommandErrorContext(restoreError) ?? promptContext) as typeof ctx;
1110
+ errorMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
1111
+ }
1112
+ finalization = {
1113
+ mergeAttempted: false,
1114
+ mergeSucceeded: false,
1115
+ cleanupSucceeded,
1116
+ errorMessage,
1117
+ };
1118
+ }
1119
+ if (
1120
+ finalization.errorMessage
1121
+ && (!finalization.cleanupSucceeded || !finalization.mergeSucceeded)
1122
+ ) {
1123
+ if (debugConfig) {
1124
+ transitionPromptWorkflowState(
1125
+ statusController,
1126
+ promptContext,
1127
+ activePromptRequest.basePath,
1128
+ debugConfig,
1129
+ activePromptRequest.promptName,
1130
+ "error",
1131
+ );
1132
+ } else {
1133
+ setPiUsereqWorkflowState(statusController, "error", promptContext);
1134
+ }
1135
+ notifyContextSafely(promptContext, finalization.errorMessage, "error");
1136
+ }
1137
+ } else {
1138
+ try {
1139
+ promptContext = (await restorePromptCommandExecution(
1140
+ activePromptRequest,
1141
+ promptContext,
1142
+ debugConfig
1143
+ ? { config: debugConfig, workflowState: statusController.state.workflowState }
1144
+ : undefined,
1145
+ ) ?? promptContext) as typeof ctx;
1146
+ if (closureFailureMessage !== undefined) {
1147
+ notifyContextSafely(promptContext, closureFailureMessage, "error");
1148
+ }
1149
+ } catch (error) {
1150
+ promptContext = (getPromptCommandErrorContext(error) ?? promptContext) as typeof ctx;
1151
+ if (debugConfig) {
1152
+ transitionPromptWorkflowState(
1153
+ statusController,
1154
+ promptContext,
1155
+ activePromptRequest.basePath,
1156
+ debugConfig,
1157
+ activePromptRequest.promptName,
1158
+ "error",
1159
+ );
1160
+ } else {
1161
+ setPiUsereqWorkflowState(statusController, "error", promptContext);
1162
+ }
1163
+ notifyContextSafely(promptContext, error instanceof Error ? error.message : String(error), "error");
1164
+ }
1165
+ }
1166
+ statusController.state.pendingPromptRequest = undefined;
1167
+ statusController.state.activePromptRequest = undefined;
1168
+ if (debugConfig) {
1169
+ transitionPromptWorkflowState(
1170
+ statusController,
1171
+ promptContext,
1172
+ activePromptRequest.basePath,
1173
+ debugConfig,
1174
+ activePromptRequest.promptName,
1175
+ "idle",
1176
+ );
1177
+ } else {
1178
+ setPiUsereqWorkflowState(statusController, "idle", promptContext);
1179
+ }
1180
+ }
622
1181
  }
623
1182
  if (hookName === "session_shutdown") {
1183
+ if (shutdownPromptRequest !== undefined && statusController.config) {
1184
+ logPromptWorkflowEvent(
1185
+ shutdownPromptRequest.basePath,
1186
+ statusController.config,
1187
+ preservePromptCommandStateOnShutdown
1188
+ ? statusController.state.workflowState
1189
+ : "idle",
1190
+ shutdownPromptRequest.promptName,
1191
+ "workflow_session_shutdown",
1192
+ {
1193
+ reason: (event as { reason?: unknown }).reason,
1194
+ target_session_file: (event as { targetSessionFile?: unknown }).targetSessionFile,
1195
+ },
1196
+ {
1197
+ preserve_prompt_command_state: preservePromptCommandStateOnShutdown,
1198
+ pending_prompt_request: pendingPromptRequest !== undefined,
1199
+ active_prompt_request: activePromptRequest !== undefined,
1200
+ },
1201
+ );
1202
+ }
1203
+ if (!preservePromptCommandStateOnShutdown) {
1204
+ if (shutdownPromptRequest !== undefined && statusController.config) {
1205
+ transitionPromptWorkflowState(
1206
+ statusController,
1207
+ ctx,
1208
+ shutdownPromptRequest.basePath,
1209
+ statusController.config,
1210
+ shutdownPromptRequest.promptName,
1211
+ "idle",
1212
+ );
1213
+ } else {
1214
+ setPiUsereqWorkflowState(statusController, "idle", ctx);
1215
+ }
1216
+ }
624
1217
  disposePiUsereqStatusController(statusController);
625
1218
  }
626
1219
  }
@@ -629,8 +1222,11 @@ async function handleExtensionStatusEvent(
629
1222
  * @brief Registers shared wrappers for every supported pi lifecycle hook.
630
1223
  * @details Installs one generic wrapper per intercepted hook so every resource,
631
1224
  * session, agent, model, tool, bash, and input event is routed through the
632
- * same extension-status update pipeline. Runtime is O(h) in registered hook
633
- * count. Side effects include hook registration.
1225
+ * same extension-status update pipeline. The wrapper suppresses the documented
1226
+ * stale-extension-context error because pi can continue delivering late
1227
+ * lifecycle callbacks against contexts invalidated by session replacement after
1228
+ * prompt orchestration has already completed successfully. Runtime is O(h) in
1229
+ * registered hook count. Side effects include hook registration.
634
1230
  * @param[in] pi {ExtensionAPI} Active extension API instance.
635
1231
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
636
1232
  * @return {void} No return value.
@@ -646,7 +1242,13 @@ function registerExtensionStatusHooks(
646
1242
  ) => void;
647
1243
  for (const hookName of PI_USEREQ_STATUS_HOOK_NAMES) {
648
1244
  registerHook(hookName, async (event, ctx) => {
649
- await handleExtensionStatusEvent(pi, statusController, hookName, event, ctx);
1245
+ try {
1246
+ await handleExtensionStatusEvent(pi, statusController, hookName, event, ctx);
1247
+ } catch (error) {
1248
+ if (!isStaleExtensionContextError(error)) {
1249
+ throw error;
1250
+ }
1251
+ }
650
1252
  });
651
1253
  }
652
1254
  }
@@ -664,6 +1266,314 @@ function setConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig, enab
664
1266
  applyConfiguredPiUsereqTools(pi, config);
665
1267
  }
666
1268
 
1269
+ /**
1270
+ * @brief Returns the canonical debug-tool toggle order.
1271
+ * @details Reuses the documented configurable-tool ordering so debug toggles list extension-owned tools before embedded tools and remain deterministic across sessions. Runtime is O(t log t). No external state is mutated.
1272
+ * @return {PiUsereqStartupToolName[]} Ordered debug-tool toggle names.
1273
+ * @satisfies REQ-242
1274
+ */
1275
+ function getDebugToolToggleNames(): PiUsereqStartupToolName[] {
1276
+ return [...PI_USEREQ_CUSTOM_TOOL_NAMES, ...PI_USEREQ_EMBEDDED_TOOL_NAMES].sort(comparePiUsereqStartupToolNames);
1277
+ }
1278
+
1279
+ /**
1280
+ * @brief Restores the debug configuration subtree to its documented defaults.
1281
+ * @details Resets global debug enablement, log path, workflow-state filter, dedicated workflow-event logging, and selected tool plus prompt debug toggles without mutating unrelated settings. Runtime is O(1). Side effect: mutates `config`.
1282
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
1283
+ * @return {void} No return value.
1284
+ * @satisfies REQ-236, REQ-237, REQ-238, REQ-239, REQ-195, REQ-277
1285
+ */
1286
+ function resetDebugConfigToDefaults(config: UseReqConfig): void {
1287
+ config.DEBUG_ENABLED = "disable";
1288
+ config.DEBUG_LOG_FILE = DEFAULT_DEBUG_LOG_FILE;
1289
+ config.DEBUG_STATUS_CHANGES = DEFAULT_DEBUG_STATUS_CHANGES;
1290
+ config.DEBUG_WORKFLOW_EVENTS = DEFAULT_DEBUG_WORKFLOW_EVENTS;
1291
+ config.DEBUG_LOG_ON_STATUS = DEFAULT_DEBUG_LOG_ON_STATUS;
1292
+ config.DEBUG_ENABLED_TOOLS = [];
1293
+ config.DEBUG_ENABLED_PROMPTS = [];
1294
+ }
1295
+
1296
+ /**
1297
+ * @brief Formats the top-level Debug summary value.
1298
+ * @details Emits the current global debug mode plus compact selected-tool and selected-prompt counts for right-aligned menu display. Runtime is O(n) in configured selector count. No external state is mutated.
1299
+ * @param[in] config {UseReqConfig} Effective project configuration.
1300
+ * @return {string} Compact debug summary string.
1301
+ */
1302
+ function formatDebugMenuSummary(config: UseReqConfig): string {
1303
+ const toolCount = normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS).length;
1304
+ const promptCount = normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS).length;
1305
+ return config.DEBUG_ENABLED === "enable"
1306
+ ? `enable • ${toolCount} tools • ${promptCount} prompts`
1307
+ : "disable";
1308
+ }
1309
+
1310
+ /**
1311
+ * @brief Builds one debug-menu row with optional disabled styling.
1312
+ * @details Applies dim styling and disables selection whenever global debug is off for all rows except the global `Debug` toggle row. Runtime is O(1). No external state is mutated.
1313
+ * @param[in] choice {PiUsereqSettingsMenuChoice} Base debug-menu row.
1314
+ * @param[in] debugEnabled {boolean} Whether global debug is enabled.
1315
+ * @return {PiUsereqSettingsMenuChoice} Styled debug-menu row.
1316
+ * @satisfies REQ-241
1317
+ */
1318
+ function buildDebugMenuChoice(
1319
+ choice: PiUsereqSettingsMenuChoice,
1320
+ debugEnabled: boolean,
1321
+ ): PiUsereqSettingsMenuChoice {
1322
+ if (debugEnabled || choice.id === "debug-enabled") {
1323
+ return choice;
1324
+ }
1325
+ return {
1326
+ ...choice,
1327
+ disabled: true,
1328
+ labelTone: "dim",
1329
+ valueTone: "dim",
1330
+ };
1331
+ }
1332
+
1333
+ /**
1334
+ * @brief Opens the workflow-state filter selector used by the Debug submenu.
1335
+ * @details Exposes `any` plus each canonical workflow state through the shared settings-menu renderer and returns the selected normalized filter or `undefined` when the user cancels the submenu. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1336
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
1337
+ * @param[in] currentValue {DebugLogOnStatus} Current persisted workflow-state filter.
1338
+ * @return {Promise<DebugLogOnStatus | undefined>} Selected workflow-state filter or `undefined` when cancelled.
1339
+ */
1340
+ async function selectDebugLogOnStatus(
1341
+ ctx: ExtensionCommandContext,
1342
+ currentValue: DebugLogOnStatus,
1343
+ ): Promise<DebugLogOnStatus | undefined> {
1344
+ const choice = await showPiUsereqSettingsMenu(ctx, "Log on status", [
1345
+ {
1346
+ id: "any",
1347
+ label: "any",
1348
+ value: currentValue === "any" ? "selected" : "",
1349
+ description: "Write matching debug entries regardless of the current workflow state.",
1350
+ },
1351
+ ...DEBUG_WORKFLOW_STATES.map((workflowState) => ({
1352
+ id: workflowState,
1353
+ label: workflowState,
1354
+ value: currentValue === workflowState ? "selected" : "",
1355
+ description: `Write matching debug entries only while workflow state is ${workflowState}.`,
1356
+ })),
1357
+ ...buildTerminalSettingsMenuChoices({
1358
+ resetDefaultsDescription: "Restore the documented default workflow-state filter.",
1359
+ }),
1360
+ ], { initialSelectedId: currentValue });
1361
+ if (!choice) {
1362
+ return undefined;
1363
+ }
1364
+ if (choice === "reset-defaults") {
1365
+ return DEFAULT_DEBUG_LOG_ON_STATUS;
1366
+ }
1367
+ return normalizeDebugLogOnStatus(choice);
1368
+ }
1369
+
1370
+ /**
1371
+ * @brief Builds the shared settings-menu choices for debug logging configuration.
1372
+ * @details Serializes global debug controls plus workflow-state, dedicated workflow-event, per-tool, and per-prompt toggles into one submenu, deriving inventories from the canonical tool and prompt lists and dimming locked rows while debug is disabled. Runtime is O(t + p). No external state is mutated.
1373
+ * @param[in] config {UseReqConfig} Effective project configuration.
1374
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered debug-menu choices.
1375
+ * @satisfies REQ-240, REQ-241, REQ-242, REQ-243, REQ-193, REQ-277
1376
+ */
1377
+ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1378
+ const debugEnabled = config.DEBUG_ENABLED === "enable";
1379
+ const enabledTools = new Set(normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS));
1380
+ const enabledPrompts = new Set(normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS));
1381
+ return [
1382
+ {
1383
+ id: "debug-enabled",
1384
+ label: "Debug",
1385
+ value: config.DEBUG_ENABLED,
1386
+ description: "Enable or disable all debug logging behavior and unlock the remaining Debug rows.",
1387
+ },
1388
+ buildDebugMenuChoice(
1389
+ {
1390
+ id: "debug-log-file",
1391
+ label: "Log file",
1392
+ value: config.DEBUG_LOG_FILE,
1393
+ description: "Edit the JSON debug log file path. Relative paths resolve against the original project base.",
1394
+ },
1395
+ debugEnabled,
1396
+ ),
1397
+ buildDebugMenuChoice(
1398
+ {
1399
+ id: "debug-log-on-status",
1400
+ label: "Log on status",
1401
+ value: config.DEBUG_LOG_ON_STATUS,
1402
+ description: "Select whether matching debug entries are written for one explicit workflow state or for any workflow state.",
1403
+ },
1404
+ debugEnabled,
1405
+ ),
1406
+ buildDebugMenuChoice(
1407
+ {
1408
+ id: "debug-status-changes",
1409
+ label: "Status changes",
1410
+ value: normalizeDebugStatusChanges(config.DEBUG_STATUS_CHANGES),
1411
+ description: "Enable or disable `workflow_state` debug entries for prompt-orchestration transitions.",
1412
+ },
1413
+ debugEnabled,
1414
+ ),
1415
+ buildDebugMenuChoice(
1416
+ {
1417
+ id: "debug-workflow-events",
1418
+ label: "Workflow events",
1419
+ value: normalizeDebugWorkflowEvents(config.DEBUG_WORKFLOW_EVENTS),
1420
+ description: "Enable or disable dedicated workflow debug entries for activation, restoration, closure, and session-shutdown diagnostics.",
1421
+ },
1422
+ debugEnabled,
1423
+ ),
1424
+ ...getDebugToolToggleNames().map((toolName) => buildDebugMenuChoice(
1425
+ {
1426
+ id: `debug-tool:${toolName}`,
1427
+ label: toolName,
1428
+ value: enabledTools.has(toolName) ? "enable" : "disable",
1429
+ description: PI_USEREQ_CUSTOM_TOOL_NAMES.includes(toolName as never)
1430
+ ? `Toggle debug logging for custom tool ${toolName}.`
1431
+ : `Toggle debug logging for embedded tool ${toolName}.`,
1432
+ },
1433
+ debugEnabled,
1434
+ )),
1435
+ ...DEBUG_PROMPT_NAMES.map((promptName) => buildDebugMenuChoice(
1436
+ {
1437
+ id: `debug-prompt:${promptName}`,
1438
+ label: promptName,
1439
+ value: enabledPrompts.has(promptName) ? "enable" : "disable",
1440
+ description: `Toggle prompt-orchestration debug logging for /${promptName}.`,
1441
+ },
1442
+ debugEnabled,
1443
+ )),
1444
+ ...buildTerminalSettingsMenuChoices({
1445
+ resetDefaultsDescription: "Restore the documented default Debug configuration.",
1446
+ }),
1447
+ ];
1448
+ }
1449
+
1450
+ /**
1451
+ * @brief Runs the interactive Debug submenu.
1452
+ * @details Lets the user toggle global debug enablement, edit debug file and workflow filters, toggle dedicated workflow-event logging, mutate per-tool and per-prompt debug selectors, and restore subtree defaults while preserving row focus across re-renders. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
1453
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
1454
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
1455
+ * @return {Promise<void>} Promise resolved when the submenu closes.
1456
+ * @satisfies REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243, REQ-192, REQ-193, REQ-195, REQ-277
1457
+ */
1458
+ async function configureDebugMenu(
1459
+ ctx: ExtensionCommandContext,
1460
+ config: UseReqConfig,
1461
+ onConfigChange: () => void,
1462
+ ): Promise<void> {
1463
+ let focusedChoiceId: string | undefined;
1464
+ while (true) {
1465
+ const choice = await showPiUsereqSettingsMenu(ctx, "Debug", buildDebugMenuChoices(config), {
1466
+ initialSelectedId: focusedChoiceId,
1467
+ });
1468
+ if (!choice) {
1469
+ return;
1470
+ }
1471
+ focusedChoiceId = choice;
1472
+
1473
+ if (choice === "debug-enabled") {
1474
+ config.DEBUG_ENABLED = config.DEBUG_ENABLED === "enable" ? "disable" : "enable";
1475
+ onConfigChange();
1476
+ ctx.ui.notify(`Debug ${config.DEBUG_ENABLED}`, "info");
1477
+ continue;
1478
+ }
1479
+ if (choice === "reset-defaults") {
1480
+ const resetPreview: ResetConfirmationChange[] = [
1481
+ { label: "Debug", previousValue: config.DEBUG_ENABLED, nextValue: "disable" },
1482
+ { label: "Log file", previousValue: config.DEBUG_LOG_FILE, nextValue: DEFAULT_DEBUG_LOG_FILE },
1483
+ { label: "Status changes", previousValue: normalizeDebugStatusChanges(config.DEBUG_STATUS_CHANGES), nextValue: DEFAULT_DEBUG_STATUS_CHANGES },
1484
+ { label: "Workflow events", previousValue: normalizeDebugWorkflowEvents(config.DEBUG_WORKFLOW_EVENTS), nextValue: DEFAULT_DEBUG_WORKFLOW_EVENTS },
1485
+ { label: "Log on status", previousValue: config.DEBUG_LOG_ON_STATUS, nextValue: DEFAULT_DEBUG_LOG_ON_STATUS },
1486
+ { label: "Enabled debug tools", previousValue: String(normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS).length), nextValue: "0" },
1487
+ { label: "Enabled debug prompts", previousValue: String(normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS).length), nextValue: "0" },
1488
+ ].filter((change) => change.previousValue !== change.nextValue);
1489
+ const approved = await confirmResetChanges(
1490
+ ctx,
1491
+ "Confirm Debug reset",
1492
+ resetPreview,
1493
+ "Approve restoring the documented default Debug configuration.",
1494
+ "Abort the Debug reset and keep the current values.",
1495
+ );
1496
+ if (!approved) {
1497
+ ctx.ui.notify("Aborted Debug reset", "info");
1498
+ continue;
1499
+ }
1500
+ resetDebugConfigToDefaults(config);
1501
+ onConfigChange();
1502
+ ctx.ui.notify("Restored default Debug configuration", "info");
1503
+ continue;
1504
+ }
1505
+ if (config.DEBUG_ENABLED !== "enable") {
1506
+ ctx.ui.notify("Debug rows are locked while Debug is disabled", "info");
1507
+ continue;
1508
+ }
1509
+ if (choice === "debug-log-file") {
1510
+ const value = await ctx.ui.input("Log file", config.DEBUG_LOG_FILE);
1511
+ if (value !== undefined) {
1512
+ config.DEBUG_LOG_FILE = normalizeDebugLogFile(value);
1513
+ onConfigChange();
1514
+ ctx.ui.notify(`Debug log file set to ${config.DEBUG_LOG_FILE}`, "info");
1515
+ }
1516
+ continue;
1517
+ }
1518
+ if (choice === "debug-status-changes") {
1519
+ config.DEBUG_STATUS_CHANGES = normalizeDebugStatusChanges(
1520
+ config.DEBUG_STATUS_CHANGES === "enable" ? "disable" : "enable",
1521
+ );
1522
+ onConfigChange();
1523
+ ctx.ui.notify(`Debug status-change logging ${config.DEBUG_STATUS_CHANGES}`, "info");
1524
+ continue;
1525
+ }
1526
+ if (choice === "debug-workflow-events") {
1527
+ config.DEBUG_WORKFLOW_EVENTS = normalizeDebugWorkflowEvents(
1528
+ config.DEBUG_WORKFLOW_EVENTS === "enable" ? "disable" : "enable",
1529
+ );
1530
+ onConfigChange();
1531
+ ctx.ui.notify(`Debug workflow-event logging ${config.DEBUG_WORKFLOW_EVENTS}`, "info");
1532
+ continue;
1533
+ }
1534
+ if (choice === "debug-log-on-status") {
1535
+ const selectedStatus = await selectDebugLogOnStatus(ctx, config.DEBUG_LOG_ON_STATUS);
1536
+ if (selectedStatus !== undefined) {
1537
+ config.DEBUG_LOG_ON_STATUS = selectedStatus;
1538
+ onConfigChange();
1539
+ ctx.ui.notify(`Debug workflow filter set to ${config.DEBUG_LOG_ON_STATUS}`, "info");
1540
+ }
1541
+ continue;
1542
+ }
1543
+ if (choice.startsWith("debug-tool:")) {
1544
+ const toolName = choice.slice("debug-tool:".length);
1545
+ const enabledTools = new Set(normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS));
1546
+ if (enabledTools.has(toolName as PiUsereqStartupToolName)) {
1547
+ enabledTools.delete(toolName as PiUsereqStartupToolName);
1548
+ } else {
1549
+ enabledTools.add(toolName as PiUsereqStartupToolName);
1550
+ }
1551
+ config.DEBUG_ENABLED_TOOLS = getDebugToolToggleNames().filter((name) => enabledTools.has(name));
1552
+ onConfigChange();
1553
+ ctx.ui.notify(
1554
+ `${enabledTools.has(toolName as PiUsereqStartupToolName) ? "Enabled" : "Disabled"} debug logging for ${toolName}`,
1555
+ "info",
1556
+ );
1557
+ continue;
1558
+ }
1559
+ if (choice.startsWith("debug-prompt:")) {
1560
+ const promptName = choice.slice("debug-prompt:".length);
1561
+ const enabledPrompts = new Set(normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS));
1562
+ if (enabledPrompts.has(promptName as (typeof DEBUG_PROMPT_NAMES)[number])) {
1563
+ enabledPrompts.delete(promptName as (typeof DEBUG_PROMPT_NAMES)[number]);
1564
+ } else {
1565
+ enabledPrompts.add(promptName as (typeof DEBUG_PROMPT_NAMES)[number]);
1566
+ }
1567
+ config.DEBUG_ENABLED_PROMPTS = DEBUG_PROMPT_NAMES.filter((name) => enabledPrompts.has(name));
1568
+ onConfigChange();
1569
+ ctx.ui.notify(
1570
+ `${enabledPrompts.has(promptName as (typeof DEBUG_PROMPT_NAMES)[number]) ? "Enabled" : "Disabled"} debug logging for ${promptName}`,
1571
+ "info",
1572
+ );
1573
+ }
1574
+ }
1575
+ }
1576
+
667
1577
  /**
668
1578
  * @brief Represents one persisted boolean notification-setting key.
669
1579
  * @details Restricts menu toggles to the global enable flags and completed/interrupted/failed event toggles used by command-notify, sound, and Pushover configuration. Compile-time only and introduces no runtime cost.
@@ -881,7 +1791,7 @@ function buildPiNotifyEventLauncherChoice(
881
1791
 
882
1792
  /**
883
1793
  * @brief Builds the shared settings-menu choices for one notification event submenu.
884
- * @details Serializes completed/interrupted/failed rows with right-aligned `on|off` values, then appends `Reset defaults` and `Save and close` for submenu-scoped mutation control. Runtime is O(1). No external state is mutated.
1794
+ * @details Serializes completed/interrupted/failed rows with right-aligned `on|off` values, then appends a value-less `Reset defaults` row for submenu-scoped mutation control. Runtime is O(1). No external state is mutated.
885
1795
  * @param[in] config {UseReqConfig} Effective project configuration.
886
1796
  * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
887
1797
  * @return {PiUsereqSettingsMenuChoice[]} Ordered event-submenu choice vector.
@@ -898,18 +1808,9 @@ function buildPiNotifyEventMenuChoices(
898
1808
  value: config[eventMenu.keys[row.eventId]] ? "on" : "off",
899
1809
  description: `${eventMenu.systemLabel}: ${row.description}`,
900
1810
  })),
901
- {
902
- id: "reset-defaults",
903
- label: "Reset defaults",
904
- value: "",
905
- description: `Restore the documented default ${eventMenu.systemLabel.toLowerCase()} event toggles.`,
906
- },
907
- {
908
- id: "save-and-close",
909
- label: "Save and close",
910
- value: "",
911
- description: "Return to the notifications menu.",
912
- },
1811
+ ...buildTerminalSettingsMenuChoices({
1812
+ resetDefaultsDescription: `Restore the documented default ${eventMenu.systemLabel.toLowerCase()} event toggles.`,
1813
+ }),
913
1814
  ];
914
1815
  }
915
1816
 
@@ -961,6 +1862,7 @@ async function configurePiNotifyEventMenu(
961
1862
  ctx: ExtensionCommandContext,
962
1863
  config: UseReqConfig,
963
1864
  eventMenu: PiNotifyEventMenuDefinition,
1865
+ onConfigChange: () => void,
964
1866
  ): Promise<void> {
965
1867
  let focusedChoiceId: string | undefined;
966
1868
  while (true) {
@@ -970,12 +1872,32 @@ async function configurePiNotifyEventMenu(
970
1872
  buildPiNotifyEventMenuChoices(config, eventMenu),
971
1873
  { initialSelectedId: focusedChoiceId },
972
1874
  );
973
- if (!choice || choice === "save-and-close") {
1875
+ if (!choice) {
974
1876
  return;
975
1877
  }
976
1878
  focusedChoiceId = choice;
977
1879
  if (choice === "reset-defaults") {
1880
+ const defaults = getDefaultConfig("");
1881
+ const resetPreview = PI_NOTIFY_EVENT_ROW_DEFINITIONS
1882
+ .map((row) => ({
1883
+ label: row.label,
1884
+ previousValue: config[eventMenu.keys[row.eventId]] ? "on" : "off",
1885
+ nextValue: defaults[eventMenu.keys[row.eventId]] ? "on" : "off",
1886
+ }))
1887
+ .filter((change) => change.previousValue !== change.nextValue);
1888
+ const approved = await confirmResetChanges(
1889
+ ctx,
1890
+ `Confirm ${eventMenu.systemLabel} event reset`,
1891
+ resetPreview,
1892
+ `Approve restoring default ${eventMenu.systemLabel.toLowerCase()} event toggles.`,
1893
+ `Abort the ${eventMenu.systemLabel.toLowerCase()} event reset and keep the current values.`,
1894
+ );
1895
+ if (!approved) {
1896
+ ctx.ui.notify(`Aborted ${eventMenu.systemLabel.toLowerCase()} event reset`, "info");
1897
+ continue;
1898
+ }
978
1899
  resetPiNotifyEventMenuToDefaults(config, eventMenu);
1900
+ onConfigChange();
979
1901
  ctx.ui.notify(
980
1902
  `Restored default ${eventMenu.systemLabel.toLowerCase()} events`,
981
1903
  "info",
@@ -986,6 +1908,7 @@ async function configurePiNotifyEventMenu(
986
1908
  config,
987
1909
  choice as PiNotifyEventBooleanConfigKey,
988
1910
  );
1911
+ onConfigChange();
989
1912
  const eventLabel = resolvePiNotifyEventLabel(
990
1913
  choice as PiNotifyEventBooleanConfigKey,
991
1914
  eventMenu,
@@ -999,18 +1922,24 @@ async function configurePiNotifyEventMenu(
999
1922
 
1000
1923
  /**
1001
1924
  * @brief Builds the direct Pushover rows rendered inside `Notifications`.
1002
- * @details Serializes the global enable flag, shared-event submenu launcher, priority, title, text, and credential rows into right-valued menu items appended after the sound-command rows. Runtime is O(1). No external state is mutated.
1925
+ * @details Serializes the global enable flag, shared-event submenu launcher, priority, title, text, and credential rows into right-valued menu items appended after the sound-command rows, dims and disables the enable row until both credentials are populated, and escapes control characters for the single-line `Pushover text` value. Runtime is O(n) in the rendered text-template length. No external state is mutated.
1003
1926
  * @param[in] config {UseReqConfig} Effective project configuration.
1004
1927
  * @return {PiUsereqSettingsMenuChoice[]} Ordered direct Pushover rows.
1005
- * @satisfies REQ-163, REQ-165, REQ-172, REQ-184, REQ-185, REQ-198
1928
+ * @satisfies REQ-163, REQ-165, REQ-172, REQ-184, REQ-185, REQ-198, REQ-234, REQ-235
1006
1929
  */
1007
1930
  function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1931
+ const pushoverCredentialsReady = hasPiNotifyPushoverCredentials(config);
1008
1932
  return [
1009
1933
  {
1010
1934
  id: "notify-pushover-enabled",
1011
1935
  label: "Enable pushover",
1012
- value: formatPiNotifyPushoverStatus(config),
1013
- description: "Enable or disable all Pushover delivery globally.",
1936
+ labelTone: pushoverCredentialsReady ? undefined : "dim",
1937
+ value: pushoverCredentialsReady ? formatPiNotifyPushoverStatus(config) : "off",
1938
+ valueTone: pushoverCredentialsReady ? undefined : "dim",
1939
+ disabled: !pushoverCredentialsReady,
1940
+ description: pushoverCredentialsReady
1941
+ ? "Enable or disable all Pushover delivery globally."
1942
+ : "Populate both Pushover credential fields to unlock global Pushover enablement.",
1014
1943
  },
1015
1944
  buildPiNotifyEventLauncherChoice(
1016
1945
  config,
@@ -1031,7 +1960,7 @@ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuCh
1031
1960
  {
1032
1961
  id: "notify-pushover-text",
1033
1962
  label: "Pushover text",
1034
- value: config["notify-pushover-text"],
1963
+ value: formatPiNotifyControlSequenceText(config["notify-pushover-text"]),
1035
1964
  description: "Edit the text template used for outbound Pushover messages.",
1036
1965
  },
1037
1966
  {
@@ -1051,16 +1980,16 @@ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuCh
1051
1980
 
1052
1981
  /**
1053
1982
  * @brief Opens the shared settings-menu selector for Pushover priority.
1054
- * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the notification menus and returns the chosen priority or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1983
+ * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the notification menus and appends a value-less subtree-local `Reset defaults` row. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1055
1984
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1056
1985
  * @param[in] currentPriority {PiNotifyPushoverPriority} Persisted priority value.
1057
- * @return {Promise<PiNotifyPushoverPriority | undefined>} Selected priority or `undefined` when cancelled.
1986
+ * @return {Promise<PiNotifyPushoverPriority | "reset-defaults" | undefined>} Selected priority, reset action, or `undefined` when cancelled.
1058
1987
  * @satisfies REQ-172, REQ-192
1059
1988
  */
1060
1989
  async function selectPiNotifyPushoverPriority(
1061
1990
  ctx: ExtensionCommandContext,
1062
1991
  currentPriority: PiNotifyPushoverPriority,
1063
- ): Promise<PiNotifyPushoverPriority | undefined> {
1992
+ ): Promise<PiNotifyPushoverPriority | "reset-defaults" | undefined> {
1064
1993
  const choice = await showPiUsereqSettingsMenu(ctx, "Pushover priority", [
1065
1994
  {
1066
1995
  id: "0",
@@ -1074,19 +2003,25 @@ async function selectPiNotifyPushoverPriority(
1074
2003
  value: currentPriority === 1 ? "selected" : "",
1075
2004
  description: "Send outbound Pushover messages with high priority `1`.",
1076
2005
  },
2006
+ ...buildTerminalSettingsMenuChoices({
2007
+ resetDefaultsDescription: "Restore the documented default Pushover priority.",
2008
+ }),
1077
2009
  ], { initialSelectedId: String(currentPriority) });
1078
2010
  if (!choice) {
1079
2011
  return undefined;
1080
2012
  }
2013
+ if (choice === "reset-defaults") {
2014
+ return "reset-defaults";
2015
+ }
1081
2016
  return normalizePiNotifyPushoverPriority(choice);
1082
2017
  }
1083
2018
 
1084
2019
  /**
1085
2020
  * @brief Builds the shared settings-menu choices for notification configuration.
1086
- * @details Serializes command-notify, sound, and Pushover blocks with dedicated shared-event submenu launchers so the settings-menu renderer can expose one unified but modular configuration surface. Runtime is O(1) plus command-length formatting. No external state is mutated.
2021
+ * @details Serializes command-notify, sound, and Pushover blocks with dedicated shared-event submenu launchers so the settings-menu renderer can expose one unified but modular configuration surface, including locked Pushover enablement and escaped single-line rendering for `Pushover text`. Runtime is O(n) in the longest rendered command or text field. No external state is mutated.
1087
2022
  * @param[in] config {UseReqConfig} Effective project configuration.
1088
2023
  * @return {PiUsereqSettingsMenuChoice[]} Ordered notification-menu choice vector.
1089
- * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-188, REQ-193, REQ-198
2024
+ * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-188, REQ-193, REQ-198, REQ-234, REQ-235
1090
2025
  */
1091
2026
  function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1092
2027
  return [
@@ -1141,33 +2076,24 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
1141
2076
  description: "Edit the shell command used when the selected sound command is `high`.",
1142
2077
  },
1143
2078
  ...buildPiNotifyPushoverRows(config),
1144
- {
1145
- id: "reset-defaults",
1146
- label: "Reset defaults",
1147
- value: "",
1148
- description: "Restore the documented notification defaults for command-notify, sound, and Pushover settings.",
1149
- },
1150
- {
1151
- id: "save-and-close",
1152
- label: "Save and close",
1153
- value: "",
1154
- description: "Return to the parent configuration menu.",
1155
- },
2079
+ ...buildTerminalSettingsMenuChoices({
2080
+ resetDefaultsDescription: "Restore the documented notification defaults for command-notify, sound, and Pushover settings.",
2081
+ }),
1156
2082
  ];
1157
2083
  }
1158
2084
 
1159
2085
  /**
1160
2086
  * @brief Opens the shared settings-menu selector for the active sound level.
1161
- * @details Reuses the pi-usereq settings-menu renderer so sound-level selection remains stylistically aligned with the notification menu 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.
2087
+ * @details Reuses the pi-usereq settings-menu renderer so sound-level selection remains stylistically aligned with the notification menu and appends a value-less subtree-local `Reset defaults` row. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1162
2088
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1163
2089
  * @param[in] currentLevel {PiNotifySoundLevel} Currently selected sound level.
1164
- * @return {Promise<PiNotifySoundLevel | undefined>} Selected sound level or `undefined` when cancelled.
2090
+ * @return {Promise<PiNotifySoundLevel | "reset-defaults" | undefined>} Selected sound level, reset action, or `undefined` when cancelled.
1165
2091
  * @satisfies REQ-131, REQ-179, REQ-192
1166
2092
  */
1167
2093
  async function selectPiNotifySoundLevel(
1168
2094
  ctx: ExtensionCommandContext,
1169
2095
  currentLevel: PiNotifySoundLevel,
1170
- ): Promise<PiNotifySoundLevel | undefined> {
2096
+ ): Promise<PiNotifySoundLevel | "reset-defaults" | undefined> {
1171
2097
  const choice = await showPiUsereqSettingsMenu(ctx, "Enable sound", [
1172
2098
  {
1173
2099
  id: "none",
@@ -1193,21 +2119,31 @@ async function selectPiNotifySoundLevel(
1193
2119
  value: currentLevel === "high" ? "selected" : "",
1194
2120
  description: "Use the high-volume sound command when sound delivery is enabled for the current event.",
1195
2121
  },
2122
+ ...buildTerminalSettingsMenuChoices({
2123
+ resetDefaultsDescription: "Restore the documented default sound level.",
2124
+ }),
1196
2125
  ], { initialSelectedId: currentLevel });
1197
- return choice ? choice as PiNotifySoundLevel : undefined;
2126
+ if (!choice) {
2127
+ return undefined;
2128
+ }
2129
+ if (choice === "reset-defaults") {
2130
+ return "reset-defaults";
2131
+ }
2132
+ return choice as PiNotifySoundLevel;
1198
2133
  }
1199
2134
 
1200
2135
  /**
1201
2136
  * @brief Runs the interactive notification-configuration menu.
1202
- * @details Exposes command-notify, sound, and Pushover controls through the shared settings-menu renderer, delegates completed/interrupted/failed toggles to dedicated event submenus, and preserves row focus across menu re-renders. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
2137
+ * @details Exposes command-notify, sound, and Pushover controls through the shared settings-menu renderer, delegates completed/interrupted/failed toggles to dedicated event submenus, keeps `Enable pushover` locked until both credentials are populated, decodes escaped control-sequence input for `Pushover text`, and preserves row focus across menu re-renders. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
1203
2138
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1204
2139
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
1205
2140
  * @return {Promise<boolean>} `true` when the sound-toggle shortcut changed.
1206
- * @satisfies REQ-131, REQ-133, REQ-134, REQ-137, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-184, REQ-188, REQ-192, REQ-193, REQ-195, REQ-196, REQ-198
2141
+ * @satisfies REQ-131, REQ-133, REQ-134, REQ-137, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-184, REQ-188, REQ-192, REQ-193, REQ-195, REQ-196, REQ-198, REQ-234, REQ-235
1207
2142
  */
1208
2143
  async function configurePiNotifyMenu(
1209
2144
  ctx: ExtensionCommandContext,
1210
2145
  config: UseReqConfig,
2146
+ onConfigChange: () => void,
1211
2147
  ): Promise<boolean> {
1212
2148
  const originalShortcut = config["notify-sound-toggle-shortcut"];
1213
2149
  let focusedChoiceId: string | undefined;
@@ -1218,12 +2154,18 @@ async function configurePiNotifyMenu(
1218
2154
  buildPiNotifyMenuChoices(config),
1219
2155
  { initialSelectedId: focusedChoiceId },
1220
2156
  );
1221
- if (!choice || choice === "save-and-close") {
2157
+ if (!choice) {
1222
2158
  return config["notify-sound-toggle-shortcut"] !== originalShortcut;
1223
2159
  }
1224
2160
  focusedChoiceId = choice;
1225
2161
  if (choice === "notify-enabled" || choice === "notify-pushover-enabled") {
2162
+ if (choice === "notify-pushover-enabled" && !hasPiNotifyPushoverCredentials(config)) {
2163
+ config["notify-pushover-enabled"] = false;
2164
+ ctx.ui.notify("Populate both Pushover credential fields before enabling Pushover", "info");
2165
+ continue;
2166
+ }
1226
2167
  const enabled = togglePiNotifyFlag(config, choice as PiNotifyBooleanConfigKey);
2168
+ onConfigChange();
1227
2169
  const labelMap: Record<string, string> = {
1228
2170
  "notify-enabled": "Notification",
1229
2171
  "notify-pushover-enabled": "Pushover",
@@ -1236,6 +2178,7 @@ async function configurePiNotifyMenu(
1236
2178
  ctx,
1237
2179
  config,
1238
2180
  PI_NOTIFY_EVENT_MENU_DEFINITIONS.notification,
2181
+ onConfigChange,
1239
2182
  );
1240
2183
  continue;
1241
2184
  }
@@ -1244,6 +2187,7 @@ async function configurePiNotifyMenu(
1244
2187
  ctx,
1245
2188
  config,
1246
2189
  PI_NOTIFY_EVENT_MENU_DEFINITIONS.sound,
2190
+ onConfigChange,
1247
2191
  );
1248
2192
  continue;
1249
2193
  }
@@ -1252,6 +2196,7 @@ async function configurePiNotifyMenu(
1252
2196
  ctx,
1253
2197
  config,
1254
2198
  PI_NOTIFY_EVENT_MENU_DEFINITIONS.pushover,
2199
+ onConfigChange,
1255
2200
  );
1256
2201
  continue;
1257
2202
  }
@@ -1259,14 +2204,33 @@ async function configurePiNotifyMenu(
1259
2204
  const value = await ctx.ui.input("Notify command", config.PI_NOTIFY_CMD);
1260
2205
  if (value !== undefined) {
1261
2206
  config.PI_NOTIFY_CMD = normalizePiNotifyCommand(value, DEFAULT_PI_NOTIFY_CMD);
2207
+ onConfigChange();
1262
2208
  ctx.ui.notify("Updated notify command", "info");
1263
2209
  }
1264
2210
  continue;
1265
2211
  }
1266
2212
  if (choice === "selected-sound-command") {
1267
2213
  const nextLevel = await selectPiNotifySoundLevel(ctx, config["notify-sound"]);
1268
- if (nextLevel !== undefined) {
2214
+ if (nextLevel === "reset-defaults") {
2215
+ const defaultSoundLevel = getDefaultConfig("")["notify-sound"];
2216
+ const approved = await confirmResetChanges(
2217
+ ctx,
2218
+ "Confirm sound reset",
2219
+ [{ label: "Enable sound", previousValue: config["notify-sound"], nextValue: defaultSoundLevel }]
2220
+ .filter((change) => change.previousValue !== change.nextValue),
2221
+ "Approve restoring the documented default sound level.",
2222
+ "Abort the sound reset and keep the current value.",
2223
+ );
2224
+ if (!approved) {
2225
+ ctx.ui.notify("Aborted sound reset", "info");
2226
+ } else {
2227
+ config["notify-sound"] = defaultSoundLevel;
2228
+ onConfigChange();
2229
+ ctx.ui.notify("Restored default sound level", "info");
2230
+ }
2231
+ } else if (nextLevel !== undefined) {
1269
2232
  config["notify-sound"] = nextLevel;
2233
+ onConfigChange();
1270
2234
  ctx.ui.notify(`Enable sound set to ${nextLevel}`, "info");
1271
2235
  }
1272
2236
  continue;
@@ -1278,6 +2242,7 @@ async function configurePiNotifyMenu(
1278
2242
  );
1279
2243
  if (value?.trim()) {
1280
2244
  config["notify-sound-toggle-shortcut"] = value.trim();
2245
+ onConfigChange();
1281
2246
  ctx.ui.notify(
1282
2247
  `Sound toggle hotkey bind set to ${config["notify-sound-toggle-shortcut"]}`,
1283
2248
  "info",
@@ -1292,6 +2257,7 @@ async function configurePiNotifyMenu(
1292
2257
  value,
1293
2258
  DEFAULT_PI_NOTIFY_SOUND_LOW_CMD,
1294
2259
  );
2260
+ onConfigChange();
1295
2261
  ctx.ui.notify("Updated sound command (low vol.)", "info");
1296
2262
  }
1297
2263
  continue;
@@ -1303,6 +2269,7 @@ async function configurePiNotifyMenu(
1303
2269
  value,
1304
2270
  DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
1305
2271
  );
2272
+ onConfigChange();
1306
2273
  ctx.ui.notify("Updated sound command (mid vol.)", "info");
1307
2274
  }
1308
2275
  continue;
@@ -1314,14 +2281,36 @@ async function configurePiNotifyMenu(
1314
2281
  value,
1315
2282
  DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD,
1316
2283
  );
2284
+ onConfigChange();
1317
2285
  ctx.ui.notify("Updated sound command (high vol.)", "info");
1318
2286
  }
1319
2287
  continue;
1320
2288
  }
1321
2289
  if (choice === "notify-pushover-priority") {
1322
2290
  const nextPriority = await selectPiNotifyPushoverPriority(ctx, config["notify-pushover-priority"]);
1323
- if (nextPriority !== undefined) {
2291
+ if (nextPriority === "reset-defaults") {
2292
+ const defaultPriority = getDefaultConfig("")["notify-pushover-priority"];
2293
+ const approved = await confirmResetChanges(
2294
+ ctx,
2295
+ "Confirm Pushover priority reset",
2296
+ [{
2297
+ label: "Pushover priority",
2298
+ previousValue: formatPiNotifyPushoverPriority(config["notify-pushover-priority"]),
2299
+ nextValue: formatPiNotifyPushoverPriority(defaultPriority),
2300
+ }].filter((change) => change.previousValue !== change.nextValue),
2301
+ "Approve restoring the documented default Pushover priority.",
2302
+ "Abort the Pushover priority reset and keep the current value.",
2303
+ );
2304
+ if (!approved) {
2305
+ ctx.ui.notify("Aborted Pushover priority reset", "info");
2306
+ } else {
2307
+ config["notify-pushover-priority"] = defaultPriority;
2308
+ onConfigChange();
2309
+ ctx.ui.notify("Restored default Pushover priority", "info");
2310
+ }
2311
+ } else if (nextPriority !== undefined) {
1324
2312
  config["notify-pushover-priority"] = nextPriority;
2313
+ onConfigChange();
1325
2314
  ctx.ui.notify(`Pushover priority set to ${formatPiNotifyPushoverPriority(nextPriority)}`, "info");
1326
2315
  }
1327
2316
  continue;
@@ -1333,17 +2322,22 @@ async function configurePiNotifyMenu(
1333
2322
  value,
1334
2323
  DEFAULT_PI_NOTIFY_PUSHOVER_TITLE,
1335
2324
  );
2325
+ onConfigChange();
1336
2326
  ctx.ui.notify("Updated Pushover title", "info");
1337
2327
  }
1338
2328
  continue;
1339
2329
  }
1340
2330
  if (choice === "notify-pushover-text") {
1341
- const value = await ctx.ui.input("Pushover text", config["notify-pushover-text"]);
2331
+ const value = await ctx.ui.input(
2332
+ "Pushover text",
2333
+ formatPiNotifyControlSequenceText(config["notify-pushover-text"]),
2334
+ );
1342
2335
  if (value !== undefined) {
1343
2336
  config["notify-pushover-text"] = normalizePiNotifyTemplateValue(
1344
- value,
2337
+ parsePiNotifyControlSequenceText(value),
1345
2338
  DEFAULT_PI_NOTIFY_PUSHOVER_TEXT,
1346
2339
  );
2340
+ onConfigChange();
1347
2341
  ctx.ui.notify("Updated Pushover text", "info");
1348
2342
  }
1349
2343
  continue;
@@ -1355,6 +2349,10 @@ async function configurePiNotifyMenu(
1355
2349
  );
1356
2350
  if (value !== undefined) {
1357
2351
  config["notify-pushover-user-key"] = normalizePiNotifyPushoverCredential(value);
2352
+ if (!hasPiNotifyPushoverCredentials(config)) {
2353
+ config["notify-pushover-enabled"] = false;
2354
+ }
2355
+ onConfigChange();
1358
2356
  ctx.ui.notify("Updated Pushover user key", "info");
1359
2357
  }
1360
2358
  continue;
@@ -1366,12 +2364,44 @@ async function configurePiNotifyMenu(
1366
2364
  );
1367
2365
  if (value !== undefined) {
1368
2366
  config["notify-pushover-api-token"] = normalizePiNotifyPushoverCredential(value);
2367
+ if (!hasPiNotifyPushoverCredentials(config)) {
2368
+ config["notify-pushover-enabled"] = false;
2369
+ }
2370
+ onConfigChange();
1369
2371
  ctx.ui.notify("Updated Pushover API token", "info");
1370
2372
  }
1371
2373
  continue;
1372
2374
  }
1373
2375
  if (choice === "reset-defaults") {
2376
+ const defaults = getDefaultConfig("");
2377
+ const resetPreview: ResetConfirmationChange[] = [
2378
+ { label: "Enable notification", previousValue: formatPiNotifyStatus(config), nextValue: formatPiNotifyStatus(defaults) },
2379
+ { label: "Enable sound", previousValue: config["notify-sound"], nextValue: defaults["notify-sound"] },
2380
+ { label: "Sound toggle hotkey bind", previousValue: config["notify-sound-toggle-shortcut"], nextValue: defaults["notify-sound-toggle-shortcut"] },
2381
+ { label: "Notify command", previousValue: config.PI_NOTIFY_CMD, nextValue: defaults.PI_NOTIFY_CMD },
2382
+ { label: "Sound command (low vol.)", previousValue: config.PI_NOTIFY_SOUND_LOW_CMD, nextValue: defaults.PI_NOTIFY_SOUND_LOW_CMD },
2383
+ { label: "Sound command (mid vol.)", previousValue: config.PI_NOTIFY_SOUND_MID_CMD, nextValue: defaults.PI_NOTIFY_SOUND_MID_CMD },
2384
+ { label: "Sound command (high vol.)", previousValue: config.PI_NOTIFY_SOUND_HIGH_CMD, nextValue: defaults.PI_NOTIFY_SOUND_HIGH_CMD },
2385
+ { label: "Enable pushover", previousValue: formatPiNotifyPushoverStatus(config), nextValue: formatPiNotifyPushoverStatus(defaults) },
2386
+ { label: "Pushover priority", previousValue: formatPiNotifyPushoverPriority(config["notify-pushover-priority"]), nextValue: formatPiNotifyPushoverPriority(defaults["notify-pushover-priority"]) },
2387
+ { label: "Pushover title", previousValue: config["notify-pushover-title"], nextValue: defaults["notify-pushover-title"] },
2388
+ { label: "Pushover text", previousValue: formatPiNotifyControlSequenceText(config["notify-pushover-text"]), nextValue: formatPiNotifyControlSequenceText(defaults["notify-pushover-text"]) },
2389
+ { label: "Pushover User Key/Delivery Group Key", previousValue: config["notify-pushover-user-key"] || "(empty)", nextValue: defaults["notify-pushover-user-key"] || "(empty)" },
2390
+ { label: "Pushover Token/API Token Key", previousValue: config["notify-pushover-api-token"] || "(empty)", nextValue: defaults["notify-pushover-api-token"] || "(empty)" },
2391
+ ].filter((change) => change.previousValue !== change.nextValue);
2392
+ const approved = await confirmResetChanges(
2393
+ ctx,
2394
+ "Confirm Notifications reset",
2395
+ resetPreview,
2396
+ "Approve restoring the documented notification defaults.",
2397
+ "Abort the notification reset and keep the current values.",
2398
+ );
2399
+ if (!approved) {
2400
+ ctx.ui.notify("Aborted notification reset", "info");
2401
+ continue;
2402
+ }
1374
2403
  resetPiNotifyConfigToDefaults(config);
2404
+ onConfigChange();
1375
2405
  ctx.ui.notify("Restored notification defaults", "info");
1376
2406
  continue;
1377
2407
  }
@@ -1399,7 +2429,7 @@ function registerPiNotifyShortcut(
1399
2429
  if (typeof shortcutRegistrar.registerShortcut !== "function") {
1400
2430
  return;
1401
2431
  }
1402
- const config = loadProjectConfig(process.cwd());
2432
+ const config = loadProjectConfig(getProcessCwdSafe());
1403
2433
  shortcutRegistrar.registerShortcut(config["notify-sound-toggle-shortcut"], {
1404
2434
  description: "Cycle pi-usereq notification sound level",
1405
2435
  handler: async (ctx) => {
@@ -1415,110 +2445,141 @@ function registerPiNotifyShortcut(
1415
2445
 
1416
2446
  /**
1417
2447
  * @brief Registers bundled prompt commands with the extension.
1418
- * @details Creates one `req-<prompt>` command per bundled prompt name. Each handler ensures resources exist, records the prompt metadata needed for successful completion notifications, 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, status-controller mutation, and user-message delivery during execution.
2448
+ * @details Creates one `req-<prompt>` command per bundled prompt name. Each handler rejects non-`idle` workflow state, transitions the shared workflow state through `checking`, `error`, and `running`, runs dedicated prompt-command git and required-doc preflight checks, optionally prepares a dedicated worktree execution plan using the active session directory, persists the prompt metadata needed for switch-triggered rebinding, switches the active session to the verified execution cwd before prompt handoff, logs dedicated workflow-activation diagnostics, renders the prompt, starts prompt delivery into the forked active session, records `running` immediately after delivery handoff begins, and then awaits the wrapped prompt-delivery promise whose stale post-restore rejections are suppressed. Runtime is O(p) for registration; handler cost depends on prompt preflight, worktree preparation, session switching, prompt rendering, prompt dispatch, and optional debug logging. Side effects include command registration, status-controller mutation, worktree creation, active-session replacement, optional worktree rollback, user-message delivery during execution, and optional debug-log writes.
1419
2449
  * @param[in] pi {ExtensionAPI} Active extension API instance.
1420
2450
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
1421
2451
  * @return {void} No return value.
1422
- * @satisfies REQ-004, REQ-067, REQ-068, REQ-169
2452
+ * @satisfies REQ-004, REQ-067, REQ-068, REQ-169, REQ-200, REQ-201, REQ-202, REQ-203, REQ-206, REQ-207, REQ-219, REQ-220, REQ-221, REQ-224, REQ-225, REQ-226, REQ-227, REQ-245, REQ-246, REQ-247, REQ-277, REQ-281
1423
2453
  */
1424
2454
  function registerPromptCommands(
1425
2455
  pi: ExtensionAPI,
1426
2456
  statusController: PiUsereqStatusController,
1427
2457
  ): void {
1428
- PROMPT_NAMES.forEach((promptName) => {
2458
+ PROMPT_COMMAND_NAMES.forEach((promptName) => {
1429
2459
  pi.registerCommand(`req-${promptName}`, {
1430
- description: `Run pi-usereq prompt ${promptName}`,
2460
+ description: resolvePromptCommandDescription(promptName),
1431
2461
  handler: async (args, ctx) => {
1432
- ensureBundledResourcesAccessible();
1433
- const projectBase = getProjectBase(ctx.cwd);
1434
- const config = loadProjectConfig(ctx.cwd);
1435
- statusController.state.pendingPromptRequest = {
2462
+ if (statusController.state.workflowState !== "idle") {
2463
+ const message = `ERROR: Prompt workflow state is ${statusController.state.workflowState}, expected idle.`;
2464
+ ctx.ui.notify(message, "error");
2465
+ throw new ReqError(message, 1);
2466
+ }
2467
+ const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
2468
+ syncContextCwdMirror(ctx, commandCwd);
2469
+ bootstrapRuntimePathState(commandCwd, {
2470
+ gitPath: resolveRuntimeGitPath(commandCwd),
2471
+ });
2472
+ const projectBase = getProjectBase(commandCwd);
2473
+ const config = loadProjectConfig(commandCwd);
2474
+ transitionPromptWorkflowState(
2475
+ statusController,
2476
+ ctx,
2477
+ projectBase,
2478
+ config,
1436
2479
  promptName,
1437
- promptArgs: args,
1438
- };
1439
- const content = renderPrompt(promptName, args, projectBase, config);
1440
- await deliverPromptCommand(pi, content);
2480
+ "checking",
2481
+ );
2482
+ ensureBundledResourcesAccessible();
2483
+ let executionPlan: PromptCommandExecutionPlan | undefined;
2484
+ let promptContext = ctx;
2485
+ try {
2486
+ executionPlan = preparePromptCommandExecution(
2487
+ promptName,
2488
+ args,
2489
+ projectBase,
2490
+ config,
2491
+ ctx.sessionManager.getSessionFile(),
2492
+ ctx.sessionManager.getSessionDir?.(),
2493
+ ctx.sessionManager.getBranch?.(),
2494
+ {
2495
+ config,
2496
+ workflowState: statusController.state.workflowState,
2497
+ },
2498
+ );
2499
+ const content = renderPrompt(
2500
+ promptName,
2501
+ args,
2502
+ projectBase,
2503
+ config,
2504
+ executionPlan,
2505
+ );
2506
+ statusController.state.pendingPromptRequest = executionPlan;
2507
+ writePersistedPromptCommandRuntimeState({
2508
+ workflowState: statusController.state.workflowState,
2509
+ pendingPromptRequest: statusController.state.pendingPromptRequest,
2510
+ activePromptRequest: statusController.state.activePromptRequest,
2511
+ });
2512
+ promptContext = (await activatePromptCommandExecution(executionPlan, ctx) ?? ctx) as typeof ctx;
2513
+ logPromptWorkflowEvent(
2514
+ projectBase,
2515
+ config,
2516
+ statusController.state.workflowState,
2517
+ promptName,
2518
+ "workflow_activation",
2519
+ {
2520
+ execution_session_file: executionPlan.executionSessionFile,
2521
+ context_path: executionPlan.contextPath,
2522
+ },
2523
+ {
2524
+ success: true,
2525
+ context_path: promptContext.cwd ?? executionPlan.contextPath,
2526
+ prompt_context_has_switch_session: typeof promptContext.switchSession === "function",
2527
+ },
2528
+ );
2529
+ renderPiUsereqStatus(statusController, promptContext);
2530
+ const promptDelivery = deliverPromptCommand(pi, content, promptContext);
2531
+ transitionPromptWorkflowState(
2532
+ statusController,
2533
+ promptContext,
2534
+ projectBase,
2535
+ config,
2536
+ promptName,
2537
+ "running",
2538
+ );
2539
+ await promptDelivery;
2540
+ } catch (error) {
2541
+ if (shouldIgnoreLatePromptDeliveryFailure(error, statusController.state.workflowState, executionPlan)) {
2542
+ return;
2543
+ }
2544
+ promptContext = (getPromptCommandErrorContext(error) ?? promptContext) as typeof ctx;
2545
+ statusController.state.pendingPromptRequest = undefined;
2546
+ statusController.state.activePromptRequest = undefined;
2547
+ transitionPromptWorkflowState(
2548
+ statusController,
2549
+ promptContext,
2550
+ projectBase,
2551
+ config,
2552
+ promptName,
2553
+ "error",
2554
+ );
2555
+ if (executionPlan !== undefined) {
2556
+ const abortResult = await abortPromptCommandExecution(executionPlan, promptContext, {
2557
+ config,
2558
+ workflowState: statusController.state.workflowState,
2559
+ });
2560
+ promptContext = (abortResult.activeContext ?? promptContext) as typeof ctx;
2561
+ if (!abortResult.cleanupSucceeded && abortResult.errorMessage) {
2562
+ notifyContextSafely(promptContext, abortResult.errorMessage, "error");
2563
+ }
2564
+ }
2565
+ const message = error instanceof Error ? error.message : String(error);
2566
+ notifyContextSafely(promptContext, message, "error");
2567
+ throw error;
2568
+ }
1441
2569
  },
1442
2570
  });
1443
2571
  });
1444
2572
  }
1445
2573
 
1446
2574
 
1447
- /**
1448
- * @brief Registers pi-usereq agent tools exposed to the model.
1449
- * @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.
1450
- * @param[in] pi {ExtensionAPI} Active extension API instance.
1451
- * @return {void} No return value.
1452
- * @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
1453
- */
1454
- function registerAgentTools(pi: ExtensionAPI): void {
1455
- const gitPathSchema = Type.Object(
1456
- {},
1457
- {
1458
- description: "Input contract: no params. Output contract: JSON object with result and execution. Result exposes path_value and path_present for the cwd-derived runtime git root.",
1459
- },
1460
- );
1461
- pi.registerTool({
1462
- name: "git-path",
1463
- label: "git-path",
1464
- description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the resolved `git-path` value through direct-access fields without request echoes.",
1465
- promptSnippet: "Return the structured runtime git-root payload for the current project.",
1466
- promptGuidelines: [
1467
- "Input contract: no params. Scope is the cwd-derived runtime path context.",
1468
- "Output contract: result + execution. Result exposes path_value and path_present.",
1469
- "Behavior contract: git-path is derived at runtime from the current working directory and repository ancestry rules.",
1470
- "Failure contract: configuration-loading failures surface through execution.code and execution.stderr_lines.",
1471
- ],
1472
- parameters: gitPathSchema,
1473
- async execute() {
1474
- ensureBundledResourcesAccessible();
1475
- const projectBase = getProjectBase(process.cwd());
1476
- const config = loadProjectConfig(process.cwd());
1477
- const result = runGitPath(projectBase, config);
1478
- const payload = buildPathQueryToolPayload(
1479
- "git-path",
1480
- process.cwd(),
1481
- projectBase,
1482
- result.stdout.trimEnd(),
1483
- buildToolExecutionSection(result),
1484
- );
1485
- return buildStructuredToolExecuteResult(payload);
1486
- },
1487
- });
1488
-
1489
- const basePathSchema = Type.Object(
1490
- {},
1491
- {
1492
- description: "Input contract: no params. Output contract: JSON object with result and execution. Result exposes path_value and path_present for the cwd-derived runtime base path.",
1493
- },
1494
- );
1495
- pi.registerTool({
1496
- name: "get-base-path",
1497
- label: "get-base-path",
1498
- description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the resolved `base-path` value through direct-access fields without request echoes.",
1499
- promptSnippet: "Return the structured runtime project-base payload.",
1500
- promptGuidelines: [
1501
- "Input contract: no params. Scope is the cwd-derived runtime path context.",
1502
- "Output contract: result + execution. Result exposes path_value and path_present.",
1503
- "Behavior contract: base-path equals the current working directory used by the extension command or tool.",
1504
- "Failure contract: configuration-loading failures surface through execution.code and execution.stderr_lines.",
1505
- ],
1506
- parameters: basePathSchema,
1507
- async execute() {
1508
- const projectBase = getProjectBase(process.cwd());
1509
- const config = loadProjectConfig(process.cwd());
1510
- const result = runGetBasePath(projectBase, config);
1511
- const payload = buildPathQueryToolPayload(
1512
- "get-base-path",
1513
- process.cwd(),
1514
- projectBase,
1515
- result.stdout.trimEnd(),
1516
- buildToolExecutionSection(result),
1517
- );
1518
- return buildStructuredToolExecuteResult(payload);
1519
- },
1520
- });
1521
-
2575
+ /**
2576
+ * @brief Registers pi-usereq agent tools exposed to the model.
2577
+ * @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.
2578
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
2579
+ * @return {void} No return value.
2580
+ * @satisfies REQ-005, REQ-010, REQ-011, REQ-014, REQ-017, REQ-044, 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
2581
+ */
2582
+ function registerAgentTools(pi: ExtensionAPI): void {
1522
2583
  const filesReferencesSchema = Type.Object(
1523
2584
  {
1524
2585
  files: Type.Array(
@@ -1527,7 +2588,7 @@ function registerAgentTools(pi: ExtensionAPI): void {
1527
2588
  ),
1528
2589
  },
1529
2590
  {
1530
- description: "Input contract: files[]. Output contract: JSON object with 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.",
2591
+ description: "Input contract: files[]. Output contract: monolithic markdown in content[0].text plus details.execution diagnostics. Missing or unsupported inputs surface through execution diagnostics. The tool fails when no source file can be analyzed.",
1531
2592
  },
1532
2593
  );
1533
2594
  const multiFileSchema = Type.Object(
@@ -1538,7 +2599,7 @@ function registerAgentTools(pi: ExtensionAPI): void {
1538
2599
  ),
1539
2600
  },
1540
2601
  {
1541
- description: "Input contract: files[]. Output contract: JSON object with summary, files, and execution. File entries expose canonical paths, detected language, configured checker modules, selection status, and error facts.",
2602
+ description: "Input contract: files[]. Output contract: monolithic text in content[0].text plus details.execution diagnostics.",
1542
2603
  },
1543
2604
  );
1544
2605
  const filesTokensSchema = Type.Object(
@@ -1549,58 +2610,43 @@ function registerAgentTools(pi: ExtensionAPI): void {
1549
2610
  ),
1550
2611
  },
1551
2612
  {
1552
- description: "Input contract: files[]. Output contract: JSON object with summary, files, and execution. File entries expose direct-access facts, token metrics, and optional heading or Doxygen metadata. Missing or non-file inputs become skipped entries. The tool fails when no processable files remain.",
2613
+ description: "Input contract: files[]. Output contract: monolithic pack-summary text in content[0].text plus details.execution diagnostics. The tool fails when no processable files remain.",
1553
2614
  },
1554
2615
  );
1555
2616
 
1556
2617
  pi.registerTool({
1557
2618
  name: "files-tokens",
1558
2619
  label: "files-tokens",
1559
- description: "Scope: explicit files. Return a token-optimized JSON payload with summary, files, and execution sections. File entries expose direct-access path facts, status, size metrics, and optional heading or Doxygen metadata.",
1560
- promptSnippet: "Return the structured token-analysis payload for caller-selected files.",
2620
+ description: "Scope: explicit files. Return the monolithic token pack summary in content[0].text and keep only execution metadata in details.execution.",
2621
+ promptSnippet: "Return the monolithic token summary for caller-selected files.",
1561
2622
  promptGuidelines: [
1562
2623
  "Scope: explicit files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
1563
- "Output contract: summary + files + 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.",
1564
- "Numeric contract: counts, sizes, shares, and line ranges remain in dedicated numeric fields; static request metadata and derived guidance are omitted from runtime responses.",
1565
- "Behavior contract: missing or non-file inputs become skipped entries; read failures become error entries; the tool fails only when no processable files remain.",
2624
+ "Output contract: monolithic pack-summary text in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2625
+ "Behavior contract: missing or non-file inputs surface through details.execution diagnostics; the tool fails when no processable files remain.",
1566
2626
  ],
2627
+ renderResult: buildStructuredToolRenderResult("files-tokens"),
1567
2628
  parameters: filesTokensSchema,
1568
2629
  async execute(_toolCallId, params) {
1569
- try {
1570
- const payload = buildTokenToolPayload({
1571
- toolName: "files-tokens",
1572
- scope: "explicit-files",
1573
- baseDir: process.cwd(),
1574
- requestedPaths: params.files,
1575
- encodingName: TOKEN_COUNTER_ENCODING,
1576
- });
1577
- return buildTokenToolExecuteResult(payload);
1578
- } catch (error) {
1579
- return buildFailedTokenToolExecuteResult(error);
1580
- }
2630
+ return executeMonolithicTool(() => runFilesTokens(params.files));
1581
2631
  },
1582
2632
  });
1583
2633
 
1584
2634
  pi.registerTool({
1585
2635
  name: "files-references",
1586
2636
  label: "files-references",
1587
- description: "Scope: explicit source files. Return a token-optimized JSON payload with 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.",
1588
- promptSnippet: "Return the structured references payload for caller-selected source files.",
2637
+ description: "Scope: explicit source files. Return the monolithic references markdown report in content[0].text and keep only execution metadata in details.execution.",
2638
+ promptSnippet: "Return the monolithic references markdown report for caller-selected source files.",
1589
2639
  promptGuidelines: [
1590
2640
  "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
1591
- "Output contract: 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.",
1592
- "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.",
1593
- "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.",
2641
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2642
+ "Formatting contract: content matches the Python reference renderer used by `generate_markdown.py`.",
2643
+ "Behavior contract: missing inputs, non-file inputs, unsupported extensions, and analysis failures surface through details.execution diagnostics.",
1594
2644
  ],
2645
+ renderResult: buildStructuredToolRenderResult("files-references"),
1595
2646
  parameters: filesReferencesSchema,
1596
2647
  async execute(_toolCallId, params) {
1597
- const payload = buildReferenceToolPayload({
1598
- toolName: "files-references",
1599
- scope: "explicit-files",
1600
- baseDir: process.cwd(),
1601
- requestedPaths: params.files,
1602
- });
1603
- return buildReferenceToolExecuteResult(payload);
2648
+ const contextPath = getRuntimeContextPath(process.cwd());
2649
+ return executeMonolithicTool(() => runFilesReferences(params.files, contextPath));
1604
2650
  },
1605
2651
  });
1606
2652
 
@@ -1610,38 +2656,37 @@ function registerAgentTools(pi: ExtensionAPI): void {
1610
2656
  Type.String({ description: "Project-relative or absolute source file path resolved from the current working directory when not already absolute" }),
1611
2657
  { description: "Explicit source-file list preserved in caller order" },
1612
2658
  ),
1613
- enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `compressed_source_text` and `compressed_lines[].display_text` include original source line-number prefixes" })),
2659
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, fenced code block lines include original source line-number prefixes" })),
1614
2660
  },
1615
2661
  {
1616
- description: "Input contract: files[] plus optional enableLineNumbers. Output contract: JSON object with 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.",
2662
+ description: "Input contract: files[] plus optional enableLineNumbers. Output contract: monolithic markdown in content[0].text plus details.execution diagnostics. The tool fails when no file is compressed.",
1617
2663
  },
1618
2664
  );
1619
2665
 
1620
2666
  pi.registerTool({
1621
2667
  name: "files-compress",
1622
2668
  label: "files-compress",
1623
- description: "Scope: explicit files. Return a token-optimized JSON payload with 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.",
1624
- promptSnippet: "Return the structured compression payload for caller-selected source files.",
2669
+ description: "Scope: explicit files. Return the monolithic compression markdown report in content[0].text and keep only execution metadata in details.execution.",
2670
+ promptSnippet: "Return the monolithic compression markdown report for caller-selected source files.",
1625
2671
  promptGuidelines: [
1626
2672
  "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
1627
- "Output contract: 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.",
1628
- "Line-number behavior: enableLineNumbers changes only rendered display strings; numeric source_line_number facts remain dedicated fields on compressed_lines for direct access.",
1629
- "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.",
2673
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2674
+ "Formatting contract: output uses `@@@ <path> | <language>` headers, `> Lines:` metadata, and fenced code blocks matching `compress_files.py`.",
2675
+ "Line-number behavior: enableLineNumbers toggles original source line prefixes inside fenced code blocks.",
1630
2676
  ],
2677
+ renderResult: buildStructuredToolRenderResult("files-compress"),
1631
2678
  parameters: filesCompressSchema,
1632
2679
  async execute(_toolCallId, params) {
1633
- const payload = buildCompressToolPayload({
1634
- toolName: "files-compress",
1635
- scope: "explicit-files",
1636
- baseDir: process.cwd(),
1637
- requestedPaths: params.files,
1638
- includeLineNumbers: params.enableLineNumbers ?? false,
1639
- });
1640
- return buildCompressionToolExecuteResult(payload);
2680
+ const contextPath = getRuntimeContextPath(process.cwd());
2681
+ return executeMonolithicTool(() => runFilesCompress(
2682
+ params.files,
2683
+ contextPath,
2684
+ params.enableLineNumbers ?? false,
2685
+ ));
1641
2686
  },
1642
2687
  });
1643
2688
 
1644
- const filesFindSchema = Type.Object(
2689
+ const filesSearchSchema = Type.Object(
1645
2690
  {
1646
2691
  tag: Type.String({ description: "Pipe-separated construct-tag filter applied case-insensitively; unsupported tags are ignored" }),
1647
2692
  pattern: Type.String({ description: "JavaScript RegExp applied to construct names only; use ^...$ for exact-name matching" }),
@@ -1649,434 +2694,198 @@ function registerAgentTools(pi: ExtensionAPI): void {
1649
2694
  Type.String({ description: "Project-relative or absolute source file path resolved from the current working directory when not already absolute" }),
1650
2695
  { description: "Explicit source-file list preserved in caller order" },
1651
2696
  ),
1652
- enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `code_lines[].display_text` and `stripped_source_text` include original source line-number prefixes" })),
2697
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, fenced code block lines include original source line-number prefixes" })),
1653
2698
  },
1654
2699
  {
1655
- description: buildFindToolSchemaDescription("explicit-files"),
2700
+ description: buildSearchToolSchemaDescription("explicit-files"),
1656
2701
  },
1657
2702
  );
1658
2703
 
1659
2704
  pi.registerTool({
1660
- name: "files-find",
1661
- label: "files-find",
1662
- description: "Scope: explicit source files. Return a token-optimized JSON payload with summary, repository, files, and execution sections. File entries expose structured statuses and match records with typed location, symbol, stripped-code, and Doxygen facts.",
1663
- promptSnippet: "Return the structured construct-search payload for caller-selected source files.",
1664
- promptGuidelines: buildFindToolPromptGuidelines("explicit-files"),
1665
- parameters: filesFindSchema,
2705
+ name: "files-search",
2706
+ label: "files-search",
2707
+ description: "Scope: explicit source files. Return the monolithic construct-search markdown report in content[0].text and keep only execution metadata in details.execution.",
2708
+ promptSnippet: "Return the monolithic construct-search markdown report for caller-selected source files.",
2709
+ promptGuidelines: buildSearchToolPromptGuidelines("explicit-files"),
2710
+ renderResult: buildStructuredToolRenderResult("files-search"),
2711
+ parameters: filesSearchSchema,
1666
2712
  async execute(_toolCallId, params) {
1667
- const payload = buildFindToolPayload({
1668
- toolName: "files-find",
1669
- scope: "explicit-files",
1670
- baseDir: process.cwd(),
1671
- tagFilter: params.tag,
1672
- pattern: params.pattern,
1673
- requestedPaths: params.files,
1674
- includeLineNumbers: params.enableLineNumbers ?? false,
1675
- });
1676
- return buildFindToolExecuteResult(payload);
2713
+ return executeMonolithicTool(() => runFilesSearch(
2714
+ [params.tag, params.pattern, ...params.files],
2715
+ params.enableLineNumbers ?? false,
2716
+ ));
1677
2717
  },
1678
2718
  });
1679
2719
 
1680
2720
  const referencesSchema = Type.Object(
1681
2721
  {},
1682
2722
  {
1683
- description: "Input contract: no params. Scope is the configured src-dir list resolved from the current project configuration. Output contract: JSON object with 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.",
2723
+ description: "Input contract: no params. Scope is the configured src-dir list resolved from the current project configuration. Output contract: monolithic markdown in content[0].text plus details.execution diagnostics.",
1684
2724
  },
1685
2725
  );
1686
2726
  const tokensSchema = Type.Object(
1687
2727
  {},
1688
2728
  {
1689
- description: "Input contract: no params. Scope is the configured docs-dir plus canonical docs REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md. Output contract: same token-optimized JSON shape as files-tokens. Missing canonical docs become skipped entries. The tool fails when no processable canonical docs remain.",
2729
+ description: "Input contract: no params. Scope is the configured docs-dir plus canonical docs REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md. Output contract: monolithic pack-summary text in content[0].text plus details.execution diagnostics.",
1690
2730
  },
1691
2731
  );
1692
2732
 
1693
2733
  pi.registerTool({
1694
2734
  name: "references",
1695
2735
  label: "references",
1696
- description: "Scope: configured project source directories. Return a token-optimized JSON payload with 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.",
1697
- promptSnippet: "Return the structured project references payload from the configured source directories.",
2736
+ description: "Scope: configured project source directories. Return the monolithic references markdown report in content[0].text and keep only execution metadata in details.execution.",
2737
+ promptSnippet: "Return the monolithic project references markdown report from the configured source directories.",
1698
2738
  promptGuidelines: [
1699
2739
  "Scope: no params; resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.",
1700
- "Output contract: 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.",
1701
- "Configuration contract: output changes with cwd-derived project config, src-dir values, and repository source discovery; the tool does not accept explicit file overrides.",
1702
- "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.",
2740
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2741
+ "Formatting contract: content prepends the file-structure markdown block before the per-file markdown produced by `generate_markdown.py`.",
2742
+ "Configuration contract: output changes with cwd-derived project config and src-dir values; the tool does not accept explicit file overrides.",
1703
2743
  ],
2744
+ renderResult: buildStructuredToolRenderResult("references"),
1704
2745
  parameters: referencesSchema,
1705
2746
  async execute() {
1706
- const projectBase = getProjectBase(process.cwd());
1707
- const config = loadProjectConfig(process.cwd());
1708
- const payload = buildReferenceToolPayload({
1709
- toolName: "references",
1710
- scope: "configured-source-directories",
1711
- baseDir: projectBase,
1712
- requestedPaths: collectSourceFiles(config["src-dir"], projectBase),
1713
- sourceDirectoryPaths: config["src-dir"],
1714
- });
1715
- return buildReferenceToolExecuteResult(payload);
2747
+ const contextPath = getRuntimeContextPath(process.cwd());
2748
+ const projectBase = getProjectBase(contextPath);
2749
+ const config = loadProjectConfig(projectBase);
2750
+ return executeMonolithicTool(() => runReferences(contextPath, config));
1716
2751
  },
1717
2752
  });
1718
2753
 
1719
2754
  const compressSchema = Type.Object(
1720
2755
  {
1721
- enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `compressed_source_text` and `compressed_lines[].display_text` include original source line-number prefixes" })),
2756
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, fenced code block lines include original source line-number prefixes" })),
1722
2757
  },
1723
2758
  {
1724
- description: "Input contract: optional enableLineNumbers boolean. Scope is the configured src-dir list resolved from the current project configuration. Output contract: JSON object with 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.",
2759
+ description: "Input contract: optional enableLineNumbers boolean. Scope is the configured src-dir list resolved from the current project configuration. Output contract: monolithic markdown in content[0].text plus details.execution diagnostics.",
1725
2760
  },
1726
2761
  );
1727
2762
 
1728
2763
  pi.registerTool({
1729
2764
  name: "compress",
1730
2765
  label: "compress",
1731
- description: "Scope: configured project source directories. Return a token-optimized JSON payload with 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.",
1732
- promptSnippet: "Return the structured project compression payload from the configured source directories.",
2766
+ description: "Scope: configured project source directories. Return the monolithic compression markdown report in content[0].text and keep only execution metadata in details.execution.",
2767
+ promptSnippet: "Return the monolithic project compression markdown report from the configured source directories.",
1733
2768
  promptGuidelines: [
1734
2769
  "Scope: resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.",
1735
- "Output contract: 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.",
1736
- "Configuration contract: output changes with cwd-derived project config, src-dir values, and repository source discovery; the tool does not accept explicit file overrides.",
1737
- "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.",
2770
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2771
+ "Formatting contract: output uses `@@@ <path> | <language>` headers, `> Lines:` metadata, and fenced code blocks matching `compress_files.py`.",
2772
+ "Line-number behavior: enableLineNumbers toggles original source line prefixes inside fenced code blocks.",
1738
2773
  ],
2774
+ renderResult: buildStructuredToolRenderResult("compress"),
1739
2775
  parameters: compressSchema,
1740
2776
  async execute(_toolCallId, params) {
1741
- const projectBase = getProjectBase(process.cwd());
1742
- const config = loadProjectConfig(process.cwd());
1743
- const sourceFiles = collectSourceFiles(config["src-dir"], projectBase);
1744
- const payload = buildCompressToolPayload({
1745
- toolName: "compress",
1746
- scope: "configured-source-directories",
1747
- baseDir: projectBase,
1748
- requestedPaths: sourceFiles,
1749
- includeLineNumbers: params.enableLineNumbers ?? false,
1750
- sourceDirectoryPaths: config["src-dir"],
1751
- });
1752
- return buildCompressionToolExecuteResult(payload);
2777
+ const contextPath = getRuntimeContextPath(process.cwd());
2778
+ const projectBase = getProjectBase(contextPath);
2779
+ const config = loadProjectConfig(projectBase);
2780
+ return executeMonolithicTool(() => runCompress(
2781
+ contextPath,
2782
+ config,
2783
+ params.enableLineNumbers ?? false,
2784
+ ));
1753
2785
  },
1754
2786
  });
1755
2787
 
1756
- const findSchema = Type.Object(
2788
+ const searchSchema = Type.Object(
1757
2789
  {
1758
2790
  tag: Type.String({ description: "Pipe-separated construct-tag filter applied case-insensitively; unsupported tags are ignored" }),
1759
2791
  pattern: Type.String({ description: "JavaScript RegExp applied to construct names only; use ^...$ for exact-name matching" }),
1760
- enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `code_lines[].display_text` and `stripped_source_text` include original source line-number prefixes" })),
2792
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, fenced code block lines include original source line-number prefixes" })),
1761
2793
  },
1762
2794
  {
1763
- description: buildFindToolSchemaDescription("configured-source-directories"),
2795
+ description: buildSearchToolSchemaDescription("configured-source-directories"),
1764
2796
  },
1765
2797
  );
1766
2798
 
1767
2799
  pi.registerTool({
1768
- name: "find",
1769
- label: "find",
1770
- description: "Scope: configured project source directories. Return a token-optimized JSON payload with summary, repository, files, and execution sections. File entries expose structured statuses and match records with typed location, symbol, stripped-code, and Doxygen facts.",
1771
- promptSnippet: "Return the structured construct-search payload from the configured source directories.",
1772
- promptGuidelines: buildFindToolPromptGuidelines("configured-source-directories"),
1773
- parameters: findSchema,
2800
+ name: "search",
2801
+ label: "search",
2802
+ description: "Scope: configured project source directories. Return the monolithic construct-search markdown report in content[0].text and keep only execution metadata in details.execution.",
2803
+ promptSnippet: "Return the monolithic construct-search markdown report from the configured source directories.",
2804
+ promptGuidelines: buildSearchToolPromptGuidelines("configured-source-directories"),
2805
+ renderResult: buildStructuredToolRenderResult("search"),
2806
+ parameters: searchSchema,
1774
2807
  async execute(_toolCallId, params) {
1775
- const projectBase = getProjectBase(process.cwd());
1776
- const config = loadProjectConfig(process.cwd());
1777
- const sourceFiles = collectSourceFiles(config["src-dir"], projectBase);
1778
- const payload = buildFindToolPayload({
1779
- toolName: "find",
1780
- scope: "configured-source-directories",
1781
- baseDir: projectBase,
1782
- tagFilter: params.tag,
1783
- pattern: params.pattern,
1784
- requestedPaths: sourceFiles,
1785
- includeLineNumbers: params.enableLineNumbers ?? false,
1786
- sourceDirectoryPaths: config["src-dir"],
1787
- });
1788
- return buildFindToolExecuteResult(payload);
2808
+ const contextPath = getRuntimeContextPath(process.cwd());
2809
+ const projectBase = getProjectBase(contextPath);
2810
+ const config = loadProjectConfig(projectBase);
2811
+ return executeMonolithicTool(() => runSearch(
2812
+ contextPath,
2813
+ params.tag,
2814
+ params.pattern,
2815
+ config,
2816
+ params.enableLineNumbers ?? false,
2817
+ ));
1789
2818
  },
1790
2819
  });
1791
2820
 
1792
2821
  pi.registerTool({
1793
2822
  name: "tokens",
1794
2823
  label: "tokens",
1795
- description: "Scope: canonical docs from the configured docs-dir. Return the same token-optimized JSON contract as files-tokens, omitting canonical-doc request echoes from runtime responses.",
1796
- promptSnippet: "Return the structured token-analysis payload for canonical documentation files.",
2824
+ description: "Scope: canonical docs from the configured docs-dir. Return the monolithic token pack summary in content[0].text and keep only execution metadata in details.execution.",
2825
+ promptSnippet: "Return the monolithic token summary for canonical documentation files.",
1797
2826
  promptGuidelines: [
1798
2827
  "Scope: no params; resolve docs-dir from project config; target canonical docs REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md.",
1799
- "Output contract: summary + files + execution. Static docs-dir and canonical-doc selection facts remain documented in registration metadata; file entries expose direct-access path facts, line ranges, sizes, token metrics, and optional metadata.",
1800
- "Numeric contract: counts, sizes, shares, and line ranges remain in dedicated numeric fields; derived guidance is omitted from runtime responses to reduce token cost.",
1801
- "Behavior contract: missing canonical docs become skipped entries; read failures become error entries; the tool fails only when no processable canonical docs remain.",
2828
+ "Output contract: monolithic pack-summary text in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2829
+ "Behavior contract: missing canonical docs surface through details.execution diagnostics; the tool fails when no processable canonical docs remain.",
1802
2830
  ],
2831
+ renderResult: buildStructuredToolRenderResult("tokens"),
1803
2832
  parameters: tokensSchema,
1804
2833
  async execute() {
1805
- try {
1806
- const projectBase = getProjectBase(process.cwd());
1807
- const config = loadProjectConfig(process.cwd());
1808
- const docsDir = config["docs-dir"].replace(/[/\\]+$/, "");
1809
- const canonicalDocNames = ["REQUIREMENTS.md", "WORKFLOW.md", "REFERENCES.md"];
1810
- const payload = buildTokenToolPayload({
1811
- toolName: "tokens",
1812
- scope: "canonical-docs",
1813
- baseDir: projectBase,
1814
- requestedPaths: canonicalDocNames.map((name) => path.join(docsDir, name)),
1815
- docsDir,
1816
- canonicalDocNames,
1817
- encodingName: TOKEN_COUNTER_ENCODING,
1818
- });
1819
- return buildTokenToolExecuteResult(payload);
1820
- } catch (error) {
1821
- return buildFailedTokenToolExecuteResult(error);
1822
- }
2834
+ const contextPath = getRuntimeContextPath(process.cwd());
2835
+ const projectBase = getProjectBase(contextPath);
2836
+ const config = loadProjectConfig(projectBase);
2837
+ return executeMonolithicTool(() => runTokens(contextPath, config));
1823
2838
  },
1824
2839
  });
1825
2840
 
1826
2841
  pi.registerTool({
1827
2842
  name: "files-static-check",
1828
2843
  label: "files-static-check",
1829
- description: "Scope: explicit files. Return a token-optimized JSON payload with summary, files, and execution sections. File entries expose canonical paths, detected language, configured checker modules, selection status, and stable error facts.",
1830
- promptSnippet: "Return the structured explicit-file static-check payload for the current project configuration.",
2844
+ description: "Scope: explicit files. Return the monolithic static-check report in content[0].text and keep only execution metadata in details.execution.",
2845
+ promptSnippet: "Return the monolithic explicit-file static-check report for the current project configuration.",
1831
2846
  promptGuidelines: [
1832
2847
  "Input contract: files[]. Scope is explicit caller-selected files resolved from the current working directory.",
1833
- "Output contract: summary + files + execution. File entries expose canonical_path, language_name, configured_checker_modules, status, and error_message.",
2848
+ "Output contract: monolithic text in content[0].text; details.execution preserves only exit code and residual diagnostics.",
1834
2849
  "Configuration contract: checker selection is derived from the cwd-resolved static-check configuration and file extensions only.",
1835
- "Failure contract: execution.code mirrors aggregated checker failures; execution.stdout_lines and execution.stderr_lines preserve residual checker diagnostics.",
2850
+ "Failure contract: execution diagnostics preserve failing checker output and skipped-input warnings.",
1836
2851
  ],
2852
+ renderResult: buildStructuredToolRenderResult("files-static-check"),
1837
2853
  parameters: multiFileSchema,
1838
2854
  async execute(_toolCallId, params) {
1839
- const projectBase = getProjectBase(process.cwd());
1840
- const config = loadProjectConfig(process.cwd());
1841
- const staticCheckConfig = config["static-check"] ?? {};
1842
- const result = runFilesStaticCheck(params.files, projectBase, config);
1843
- const payload = buildStaticCheckToolPayload(
1844
- "files-static-check",
1845
- "explicit-files",
1846
- projectBase,
1847
- params.files,
1848
- [],
1849
- [],
1850
- staticCheckConfig,
1851
- buildToolExecutionSection(result),
1852
- );
1853
- return buildStructuredToolExecuteResult(payload);
2855
+ const contextPath = getRuntimeContextPath(process.cwd());
2856
+ const projectBase = getProjectBase(contextPath);
2857
+ const config = loadProjectConfig(projectBase);
2858
+ return executeMonolithicTool(() => runFilesStaticCheck(params.files, projectBase, config));
1854
2859
  },
1855
2860
  });
1856
2861
 
1857
2862
  const staticCheckSchema = Type.Object(
1858
2863
  {},
1859
2864
  {
1860
- description: "Input contract: no params. Scope is the configured src-dir plus tests-dir selection after fixture exclusion. Output contract: JSON object with summary, files, and execution.",
2865
+ description: "Input contract: no params. Scope is the configured src-dir plus tests-dir selection after fixture exclusion. Output contract: monolithic text in content[0].text plus details.execution diagnostics.",
1861
2866
  },
1862
2867
  );
1863
2868
  pi.registerTool({
1864
2869
  name: "static-check",
1865
2870
  label: "static-check",
1866
- description: "Scope: configured source and test directories. Return a token-optimized JSON payload with summary, files, and execution sections. File entries expose selected-path facts, checker coverage, selection status, and residual diagnostics metadata.",
1867
- promptSnippet: "Return the structured project static-check payload for the current configuration.",
2871
+ description: "Scope: configured source and test directories. Return the monolithic static-check report in content[0].text and keep only execution metadata in details.execution.",
2872
+ promptSnippet: "Return the monolithic project static-check report for the current configuration.",
1868
2873
  promptGuidelines: [
1869
2874
  "Input contract: no params. Scope is src-dir plus tests-dir from the cwd-derived project configuration.",
1870
- "Output contract: summary + files + execution. Selection-directory rules remain documented in registration metadata; file entries expose configured_checker_modules and status.",
2875
+ "Output contract: monolithic text in content[0].text; details.execution preserves only exit code and residual diagnostics.",
1871
2876
  "Selection contract: tests/fixtures and <tests-dir>/fixtures are excluded before checker dispatch.",
1872
- "Failure contract: execution.code mirrors aggregated checker failures or selection failures; execution.stderr_lines preserve residual diagnostics.",
2877
+ "Failure contract: execution diagnostics preserve failing checker output and skipped-selection warnings.",
1873
2878
  ],
2879
+ renderResult: buildStructuredToolRenderResult("static-check"),
1874
2880
  parameters: staticCheckSchema,
1875
2881
  async execute() {
1876
- const projectBase = getProjectBase(process.cwd());
1877
- const config = loadProjectConfig(process.cwd());
1878
- const staticCheckConfig = config["static-check"] ?? {};
1879
- const selectionDirectoryPaths = [...config["src-dir"], config["tests-dir"]];
1880
- const testsDirRel = makeRelativeIfContainsProject(config["tests-dir"], projectBase)
1881
- .split(path.sep)
1882
- .join("/")
1883
- .replace(/^\.?\/?/, "")
1884
- .replace(/\/+$/, "");
1885
- const excludedDirectoryPaths = [...new Set([
1886
- "tests/fixtures",
1887
- testsDirRel ? `${testsDirRel}/fixtures` : "fixtures",
1888
- ])];
1889
- let selectedPaths: string[] = [];
1890
- let execution;
1891
- try {
1892
- selectedPaths = collectProjectStaticCheckSelection(projectBase, config).selectedPaths;
1893
- execution = buildToolExecutionSection(runProjectStaticCheck(projectBase, config));
1894
- } catch (error) {
1895
- execution = buildToolExecutionSection(normalizeToolFailure(error));
1896
- }
1897
- const payload = buildStaticCheckToolPayload(
1898
- "static-check",
1899
- "configured-source-and-test-directories",
1900
- projectBase,
1901
- selectedPaths,
1902
- selectionDirectoryPaths,
1903
- excludedDirectoryPaths,
1904
- staticCheckConfig,
1905
- execution,
1906
- );
1907
- return buildStructuredToolExecuteResult(payload);
1908
- },
1909
- });
1910
-
1911
- const gitCheckSchema = Type.Object(
1912
- {},
1913
- {
1914
- description: "Input contract: no params. Output contract: JSON object with result and execution. Result exposes git-path presence plus aggregate repository status fields.",
1915
- },
1916
- );
1917
- pi.registerTool({
1918
- name: "git-check",
1919
- label: "git-check",
1920
- description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes repository validation status through direct fields without request echoes.",
1921
- promptSnippet: "Return the structured git-validation payload for the runtime repository.",
1922
- promptGuidelines: [
1923
- "Input contract: no params. Scope is the cwd-derived runtime path context.",
1924
- "Output contract: result + execution. Result exposes git_path_present and aggregate status.",
1925
- "Behavior contract: the tool checks work-tree membership, porcelain cleanliness, and symbolic-or-detached HEAD validity.",
1926
- "Failure contract: execution.code and execution.stderr_lines surface git-path or repository-state errors.",
1927
- ],
1928
- parameters: gitCheckSchema,
1929
- async execute() {
1930
- const projectBase = getProjectBase(process.cwd());
1931
- const config = loadProjectConfig(process.cwd());
1932
- let execution;
1933
- try {
1934
- execution = buildToolExecutionSection(runGitCheck(projectBase, config));
1935
- } catch (error) {
1936
- execution = buildToolExecutionSection(normalizeToolFailure(error));
1937
- }
1938
- const payload = buildGitCheckToolPayload(projectBase, resolveRuntimeGitPath(projectBase), execution);
1939
- return buildStructuredToolExecuteResult(payload);
1940
- },
1941
- });
1942
-
1943
- const docsCheckSchema = Type.Object(
1944
- {},
1945
- {
1946
- description: "Input contract: no params. Output contract: JSON object with summary, files, and execution. File entries expose canonical paths, prompt_command remediation, and presence status for canonical docs.",
1947
- },
1948
- );
1949
- pi.registerTool({
1950
- name: "docs-check",
1951
- label: "docs-check",
1952
- description: "Scope: canonical docs. Return a token-optimized JSON payload with summary, files, and execution sections. File entries expose remediation prompt commands and direct presence facts for REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md.",
1953
- promptSnippet: "Return the structured canonical-document validation payload.",
1954
- promptGuidelines: [
1955
- "Input contract: no params. Scope is docs-dir from the cwd-derived project configuration.",
1956
- "Output contract: summary + files + execution. File entries expose file_name, canonical_path, prompt_command, and status.",
1957
- "Specialization trigger: remediation differs per missing canonical file through prompt_command.",
1958
- "Failure contract: execution.code is non-zero when any canonical document is missing; execution.stderr_lines enumerate missing files.",
1959
- ],
1960
- parameters: docsCheckSchema,
1961
- async execute() {
1962
- const projectBase = getProjectBase(process.cwd());
1963
- const config = loadProjectConfig(process.cwd());
1964
- const payload = buildDocsCheckToolPayload(projectBase, config["docs-dir"]);
1965
- return buildStructuredToolExecuteResult(payload);
1966
- },
1967
- });
1968
-
1969
- const gitWtNameSchema = Type.Object(
1970
- {},
1971
- {
1972
- description: "Input contract: no params. Output contract: JSON object with result and execution. Result exposes worktree_name when generation succeeds.",
1973
- },
1974
- );
1975
- pi.registerTool({
1976
- name: "git-wt-name",
1977
- label: "git-wt-name",
1978
- description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the generated worktree name while static naming rules remain in registration metadata.",
1979
- promptSnippet: "Return the structured worktree-name generation payload.",
1980
- promptGuidelines: [
1981
- "Input contract: no params. Scope is the cwd-derived runtime path context.",
1982
- "Output contract: result + execution. Result exposes worktree_name.",
1983
- "Behavior contract: generation follows useReq-<project>-<sanitized-branch>-<YYYYMMDDHHMMSS>.",
1984
- "Failure contract: execution.code and execution.stderr_lines surface git-path or branch-resolution errors.",
1985
- ],
1986
- parameters: gitWtNameSchema,
1987
- async execute() {
1988
- const projectBase = getProjectBase(process.cwd());
1989
- const config = loadProjectConfig(process.cwd());
1990
- let execution;
1991
- try {
1992
- execution = buildToolExecutionSection(runGitWtName(projectBase, config));
1993
- } catch (error) {
1994
- execution = buildToolExecutionSection(normalizeToolFailure(error));
1995
- }
1996
- const payload = buildWorktreeNameToolPayload(projectBase, resolveRuntimeGitPath(projectBase), execution);
1997
- return buildStructuredToolExecuteResult(payload);
1998
- },
1999
- });
2000
-
2001
- const gitWtCreateSchema = Type.Object(
2002
- {
2003
- wtName: Type.String({ description: "Exact target worktree name and branch name" }),
2004
- },
2005
- {
2006
- description: "Input contract: wtName. Output contract: JSON object with result and execution. Result exposes worktree_name and derived worktree_path.",
2007
- },
2008
- );
2009
- pi.registerTool({
2010
- name: "git-wt-create",
2011
- label: "git-wt-create",
2012
- description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the exact worktree name and derived path without static operation echoes.",
2013
- promptSnippet: "Return the structured worktree-creation payload for the requested name.",
2014
- promptGuidelines: [
2015
- "Input contract: wtName is required and must match the exact worktree/branch name to create.",
2016
- "Output contract: result + execution. Result exposes worktree_name and worktree_path.",
2017
- "Specialization trigger: worktree_path depends on the runtime git root parent directory.",
2018
- "Failure contract: execution.code and execution.stderr_lines surface invalid-name, git, or finalization errors.",
2019
- ],
2020
- parameters: gitWtCreateSchema,
2021
- async execute(_toolCallId, params) {
2022
- const projectBase = getProjectBase(process.cwd());
2023
- const config = loadProjectConfig(process.cwd());
2024
- let execution;
2025
- try {
2026
- execution = buildToolExecutionSection(runGitWtCreate(projectBase, params.wtName, config));
2027
- } catch (error) {
2028
- execution = buildToolExecutionSection(normalizeToolFailure(error));
2029
- }
2030
- const payload = buildWorktreeMutationToolPayload(
2031
- "git-wt-create",
2032
- projectBase,
2033
- resolveRuntimeGitPath(projectBase),
2034
- params.wtName,
2035
- execution,
2036
- );
2037
- return buildStructuredToolExecuteResult(payload);
2882
+ const contextPath = getRuntimeContextPath(process.cwd());
2883
+ const projectBase = getProjectBase(contextPath);
2884
+ const config = loadProjectConfig(projectBase);
2885
+ return executeMonolithicTool(() => runProjectStaticCheck(contextPath, config));
2038
2886
  },
2039
2887
  });
2040
2888
 
2041
- const gitWtDeleteSchema = Type.Object(
2042
- {
2043
- wtName: Type.String({ description: "Exact target worktree name and branch name" }),
2044
- },
2045
- {
2046
- description: "Input contract: wtName. Output contract: JSON object with result and execution. Result exposes worktree_name and derived worktree_path.",
2047
- },
2048
- );
2049
- pi.registerTool({
2050
- name: "git-wt-delete",
2051
- label: "git-wt-delete",
2052
- description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the exact worktree name and derived path without static operation echoes.",
2053
- promptSnippet: "Return the structured worktree-deletion payload for the requested name.",
2054
- promptGuidelines: [
2055
- "Input contract: wtName is required and must match the exact worktree/branch name to delete.",
2056
- "Output contract: result + execution. Result exposes worktree_name and worktree_path.",
2057
- "Specialization trigger: worktree_path depends on the runtime git root parent directory.",
2058
- "Failure contract: execution.code and execution.stderr_lines surface missing-target or deletion errors.",
2059
- ],
2060
- parameters: gitWtDeleteSchema,
2061
- async execute(_toolCallId, params) {
2062
- const projectBase = getProjectBase(process.cwd());
2063
- const config = loadProjectConfig(process.cwd());
2064
- let execution;
2065
- try {
2066
- execution = buildToolExecutionSection(runGitWtDelete(projectBase, params.wtName, config));
2067
- } catch (error) {
2068
- execution = buildToolExecutionSection(normalizeToolFailure(error));
2069
- }
2070
- const payload = buildWorktreeMutationToolPayload(
2071
- "git-wt-delete",
2072
- projectBase,
2073
- resolveRuntimeGitPath(projectBase),
2074
- params.wtName,
2075
- execution,
2076
- );
2077
- return buildStructuredToolExecuteResult(payload);
2078
- },
2079
- });
2080
2889
  }
2081
2890
 
2082
2891
  /**
@@ -2108,28 +2917,19 @@ function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig):
2108
2917
  value: `${tools.length} targets`,
2109
2918
  description: "Disable every configurable startup tool for future session starts.",
2110
2919
  },
2111
- {
2112
- id: "reset-defaults",
2113
- label: "Reset defaults",
2114
- value: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
2115
- description: "Restore the documented default startup-tool selection.",
2116
- },
2117
- {
2118
- id: "save-and-close",
2119
- label: "Save and close",
2120
- value: "",
2121
- description: "Return to the parent configuration menu.",
2122
- },
2920
+ ...buildTerminalSettingsMenuChoices({
2921
+ resetDefaultsDescription: "Restore the documented default startup-tool selection.",
2922
+ }),
2123
2923
  ];
2124
2924
  }
2125
2925
 
2126
2926
  /**
2127
2927
  * @brief Builds the shared settings-menu choices for per-tool startup toggles.
2128
- * @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.
2928
+ * @details Exposes every configurable startup tool as one row whose right-side value reports the current enabled state, preserves the documented custom/files/embedded/default-disabled ordering, and appends a value-less subtree-local `Reset defaults` row. Runtime is O(t) in configurable-tool count. No external state is mutated.
2129
2929
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2130
2930
  * @param[in] config {UseReqConfig} Effective project configuration.
2131
2931
  * @return {PiUsereqSettingsMenuChoice[]} Ordered per-tool toggle choices.
2132
- * @satisfies REQ-007, REQ-151, REQ-152, REQ-153, REQ-154
2932
+ * @satisfies REQ-007, REQ-151, REQ-152, REQ-153, REQ-154, REQ-231, REQ-232
2133
2933
  */
2134
2934
  function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2135
2935
  const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
@@ -2140,25 +2940,27 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
2140
2940
  value: enabledTools.has(tool.name) ? "on" : "off",
2141
2941
  description: tool.description ?? `Toggle startup activation for ${tool.name}.`,
2142
2942
  })),
2143
- {
2144
- id: "back",
2145
- label: "Back",
2146
- value: "",
2147
- description: "Return to the Enable tools menu.",
2148
- },
2943
+ ...buildTerminalSettingsMenuChoices({
2944
+ resetDefaultsDescription: "Restore the documented default startup-tool selection.",
2945
+ }),
2149
2946
  ];
2150
2947
  }
2151
2948
 
2152
2949
  /**
2153
2950
  * @brief Runs the interactive active-tool configuration menu.
2154
- * @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.
2951
+ * @details Synchronizes runtime active tools with persisted config, renders startup-tool actions through the shared settings-menu UI, preserves the documented per-tool ordering, 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.
2155
2952
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2156
2953
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2157
2954
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
2158
2955
  * @return {Promise<void>} Promise resolved when the menu closes.
2159
- * @satisfies REQ-007, REQ-063, REQ-064, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
2956
+ * @satisfies REQ-007, REQ-063, REQ-064, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193, REQ-231, REQ-232
2160
2957
  */
2161
- async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
2958
+ async function configurePiUsereqToolsMenu(
2959
+ pi: ExtensionAPI,
2960
+ ctx: ExtensionCommandContext,
2961
+ config: UseReqConfig,
2962
+ onConfigChange: () => void,
2963
+ ): Promise<void> {
2162
2964
  applyConfiguredPiUsereqTools(pi, config);
2163
2965
  let focusedChoiceId: string | undefined;
2164
2966
  while (true) {
@@ -2168,32 +2970,71 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
2168
2970
  initialSelectedId: focusedChoiceId,
2169
2971
  });
2170
2972
 
2171
- if (!choice || choice === "save-and-close") {
2973
+ if (!choice) {
2172
2974
  return;
2173
2975
  }
2174
2976
  focusedChoiceId = choice;
2175
2977
 
2176
2978
  if (choice === "enable-all-tools") {
2177
2979
  setConfiguredPiUsereqTools(pi, config, tools.map((tool) => tool.name));
2980
+ onConfigChange();
2178
2981
  ctx.ui.notify("Enabled all configurable active tools", "info");
2179
2982
  continue;
2180
2983
  }
2181
2984
 
2182
2985
  if (choice === "disable-all-tools") {
2183
2986
  setConfiguredPiUsereqTools(pi, config, []);
2987
+ onConfigChange();
2184
2988
  ctx.ui.notify("Disabled all configurable active tools", "info");
2185
2989
  continue;
2186
2990
  }
2187
2991
 
2188
2992
  if (choice === "reset-defaults") {
2993
+ const approved = await confirmResetChanges(
2994
+ ctx,
2995
+ "Confirm Enable tools reset",
2996
+ [{
2997
+ label: "Enable tools",
2998
+ previousValue: String(getConfiguredEnabledPiUsereqTools(config).length),
2999
+ nextValue: String(normalizeEnabledPiUsereqTools(undefined).length),
3000
+ }].filter((change) => change.previousValue !== change.nextValue),
3001
+ "Approve restoring the documented default startup-tool selection.",
3002
+ "Abort the startup-tool reset and keep the current values.",
3003
+ );
3004
+ if (!approved) {
3005
+ ctx.ui.notify("Aborted startup-tool reset", "info");
3006
+ continue;
3007
+ }
2189
3008
  setConfiguredPiUsereqTools(pi, config, normalizeEnabledPiUsereqTools(undefined));
3009
+ onConfigChange();
2190
3010
  ctx.ui.notify("Restored default configurable active tools", "info");
2191
3011
  continue;
2192
3012
  }
2193
3013
 
2194
3014
  if (choice === "enable-tools") {
2195
3015
  const selectedToolName = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolToggleChoices(pi, config));
2196
- if (!selectedToolName || selectedToolName === "back") {
3016
+ if (!selectedToolName) {
3017
+ continue;
3018
+ }
3019
+ if (selectedToolName === "reset-defaults") {
3020
+ const approved = await confirmResetChanges(
3021
+ ctx,
3022
+ "Confirm Enable tools reset",
3023
+ [{
3024
+ label: "Enable tools",
3025
+ previousValue: String(getConfiguredEnabledPiUsereqTools(config).length),
3026
+ nextValue: String(normalizeEnabledPiUsereqTools(undefined).length),
3027
+ }].filter((change) => change.previousValue !== change.nextValue),
3028
+ "Approve restoring the documented default startup-tool selection.",
3029
+ "Abort the startup-tool reset and keep the current values.",
3030
+ );
3031
+ if (!approved) {
3032
+ ctx.ui.notify("Aborted startup-tool reset", "info");
3033
+ continue;
3034
+ }
3035
+ setConfiguredPiUsereqTools(pi, config, normalizeEnabledPiUsereqTools(undefined));
3036
+ onConfigChange();
3037
+ ctx.ui.notify("Restored default configurable active tools", "info");
2197
3038
  continue;
2198
3039
  }
2199
3040
  if (enabledTools.has(selectedToolName)) {
@@ -2202,6 +3043,7 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
2202
3043
  enabledTools.add(selectedToolName);
2203
3044
  }
2204
3045
  setConfiguredPiUsereqTools(pi, config, tools.map((tool) => tool.name).filter((toolName) => enabledTools.has(toolName)));
3046
+ onConfigChange();
2205
3047
  ctx.ui.notify(
2206
3048
  `${enabledTools.has(selectedToolName) ? "Enabled" : "Disabled"} ${selectedToolName}`,
2207
3049
  "info",
@@ -2211,34 +3053,75 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
2211
3053
  }
2212
3054
 
2213
3055
  /**
2214
- * @brief Summarizes configured static-check languages.
2215
- * @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.
3056
+ * @brief Resolves one static-check language config for menu rendering.
3057
+ * @details Returns the configured per-language static-check object when present and otherwise synthesizes a disabled empty-language object so menu code can render all supported languages deterministically. Runtime is O(1). No external state is mutated.
3058
+ * @param[in] config {UseReqConfig} Effective project configuration.
3059
+ * @param[in] language {string} Canonical language name.
3060
+ * @return {StaticCheckLanguageConfig} Resolved per-language config object.
3061
+ */
3062
+ function getStaticCheckLanguageConfigForMenu(
3063
+ config: UseReqConfig,
3064
+ language: string,
3065
+ ): StaticCheckLanguageConfig {
3066
+ return config["static-check"][language] ?? createStaticCheckLanguageConfig([]);
3067
+ }
3068
+
3069
+ /**
3070
+ * @brief Counts languages that currently expose at least one configured checker.
3071
+ * @details Treats configured-but-disabled languages as configured when their checker list is non-empty so removal actions remain deterministic. Runtime is O(l). No external state is mutated.
3072
+ * @param[in] config {UseReqConfig} Effective project configuration.
3073
+ * @return {number} Number of languages with at least one configured checker.
3074
+ */
3075
+ function countConfiguredStaticCheckLanguages(config: UseReqConfig): number {
3076
+ return Object.values(config["static-check"]).filter((languageConfig) => languageConfig.checkers.length > 0).length;
3077
+ }
3078
+
3079
+ /**
3080
+ * @brief Counts languages whose static-check enable flag is on.
3081
+ * @details Counts only languages whose persisted per-language config explicitly sets `enabled=enable`, regardless of checker count. Runtime is O(l). No external state is mutated.
3082
+ * @param[in] config {UseReqConfig} Effective project configuration.
3083
+ * @return {number} Number of enabled languages.
3084
+ */
3085
+ function countEnabledStaticCheckLanguages(config: UseReqConfig): number {
3086
+ return Object.values(config["static-check"]).filter((languageConfig) => languageConfig.enabled === "enable").length;
3087
+ }
3088
+
3089
+ /**
3090
+ * @brief Restores the documented static-check default configuration.
3091
+ * @details Replaces the mutable config subtree with a fresh clone of the documented per-language defaults so menu reset actions restore both enable flags and checker lists in one step. Runtime is O(l + c). Side effect: mutates `config`.
3092
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
3093
+ * @return {void} No return value.
3094
+ * @satisfies REQ-250, REQ-251, REQ-252
3095
+ */
3096
+ function resetStaticCheckConfig(config: UseReqConfig): void {
3097
+ config["static-check"] = getDefaultStaticCheckConfig();
3098
+ }
3099
+
3100
+ /**
3101
+ * @brief Summarizes enabled and configured static-check languages.
3102
+ * @details Counts enabled languages and languages with at least one checker, then emits one compact summary string suitable for the top-level configuration menu. Runtime is O(l). No external state is mutated.
2216
3103
  * @param[in] config {UseReqConfig} Effective project configuration.
2217
- * @return {string} Compact summary string or `(none)`.
3104
+ * @return {string} Compact summary string.
2218
3105
  */
2219
3106
  function formatStaticCheckLanguagesSummary(config: UseReqConfig): string {
2220
- const languages = Object.entries(config["static-check"])
2221
- .filter(([, entries]) => Array.isArray(entries) && entries.length > 0)
2222
- .sort(([left], [right]) => left.localeCompare(right))
2223
- .map(([language, entries]) => `${language} (${entries.length})`);
2224
- return languages.join(", ") || "(none)";
3107
+ return `${countEnabledStaticCheckLanguages(config)} enabled • ${countConfiguredStaticCheckLanguages(config)} configured`;
2225
3108
  }
2226
3109
 
2227
3110
  /**
2228
3111
  * @brief Builds the shared settings-menu choices for static-check management.
2229
- * @details Serializes guided Command-oriented static-check actions into right-valued menu rows consumed by the shared settings-menu renderer while omitting raw-spec and reference-only actions. Runtime is O(1). No external state is mutated.
3112
+ * @details Serializes guided Command-oriented add and remove actions, renders one direct on/off toggle row for every supported language, and appends canonical terminal rows while omitting raw-spec and reference-only actions. Runtime is O(l). No external state is mutated.
2230
3113
  * @param[in] config {UseReqConfig} Effective project configuration.
2231
3114
  * @return {PiUsereqSettingsMenuChoice[]} Ordered static-check menu choices.
2232
- * @satisfies REQ-008, REQ-150, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
3115
+ * @satisfies REQ-008, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-160, REQ-161, REQ-193, REQ-248
2233
3116
  */
2234
3117
  function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2235
- const supportedLanguageCount = getSupportedStaticCheckLanguageSupport().length;
2236
- const configuredLanguageCount = Object.values(config["static-check"]).filter((entries) => entries.length > 0).length;
3118
+ const supportedLanguages = getSupportedStaticCheckLanguageSupport();
3119
+ const configuredLanguageCount = countConfiguredStaticCheckLanguages(config);
2237
3120
  return [
2238
3121
  {
2239
3122
  id: "add-static-check-entry",
2240
3123
  label: "Add static code checker",
2241
- value: `${supportedLanguageCount} languages`,
3124
+ value: `${supportedLanguages.length} languages`,
2242
3125
  description: "Select a supported language, then configure one Command static-check executable.",
2243
3126
  },
2244
3127
  {
@@ -2247,96 +3130,134 @@ function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenu
2247
3130
  value: configuredLanguageCount > 0 ? `${configuredLanguageCount} configured` : "(none)",
2248
3131
  description: "Remove every configured static-check entry for one language.",
2249
3132
  },
2250
- {
2251
- id: "reset-defaults",
2252
- label: "Reset defaults",
2253
- value: configuredLanguageCount > 0 ? `${configuredLanguageCount} configured` : "(none)",
2254
- description: "Remove every configured static-check entry and restore the default empty static-check configuration.",
2255
- },
2256
- {
2257
- id: "save-and-close",
2258
- label: "Save and close",
2259
- value: "",
2260
- description: "Return to the parent configuration menu.",
2261
- },
3133
+ ...supportedLanguages.map(({ language, extensions }) => {
3134
+ const languageConfig = getStaticCheckLanguageConfigForMenu(config, language);
3135
+ const configuredCount = languageConfig.checkers.length;
3136
+ const suffix = configuredCount === 1 ? "checker" : "checkers";
3137
+ return {
3138
+ id: `toggle-static-check-language:${language}`,
3139
+ label: language,
3140
+ value: languageConfig.enabled === "enable" ? "on" : "off",
3141
+ description: `Toggle static-check execution for ${language}. Configured ${configuredCount} ${suffix}. Supported extensions: ${extensions.join(", ")}.`,
3142
+ };
3143
+ }),
3144
+ ...buildTerminalSettingsMenuChoices({
3145
+ resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3146
+ }),
2262
3147
  ];
2263
3148
  }
2264
3149
 
2265
3150
  /**
2266
3151
  * @brief Builds the shared settings-menu choices for supported static-check languages.
2267
- * @details Exposes every supported language as one row whose right-side value reports extensions plus the current configured checker count for Command-oriented configuration flows. Runtime is O(l log l). No external state is mutated.
3152
+ * @details Exposes every supported language as one row whose right-side value reports extensions, enablement, and configured checker count for guided Command configuration flows, then appends subtree-local terminal rows. Runtime is O(l). No external state is mutated.
2268
3153
  * @param[in] config {UseReqConfig} Effective project configuration.
2269
3154
  * @return {PiUsereqSettingsMenuChoice[]} Ordered language-choice vector.
2270
3155
  */
2271
3156
  function buildSupportedStaticCheckLanguageChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2272
3157
  return [
2273
3158
  ...getSupportedStaticCheckLanguageSupport().map(({ language, extensions }) => {
2274
- const configuredCount = config["static-check"][language]?.length ?? 0;
3159
+ const languageConfig = getStaticCheckLanguageConfigForMenu(config, language);
3160
+ const configuredCount = languageConfig.checkers.length;
2275
3161
  const suffix = configuredCount === 1 ? "checker" : "checkers";
2276
3162
  return {
2277
3163
  id: language,
2278
3164
  label: language,
2279
- value: `${extensions.join(", ")} • ${configuredCount} ${suffix}`,
2280
- description: `Configure the Command static-check entry for ${language}. Supported extensions: ${extensions.join(", ")}.`,
3165
+ value: `${extensions.join(", ")} • ${languageConfig.enabled === "enable" ? "on" : "off"} • ${configuredCount} ${suffix}`,
3166
+ description: `Configure the Command static-check entries for ${language}. Supported extensions: ${extensions.join(", ")}.`,
2281
3167
  };
2282
3168
  }),
2283
- {
2284
- id: "back",
2285
- label: "Back",
2286
- value: "",
2287
- description: "Return to the Language static code checkers menu.",
2288
- },
3169
+ ...buildTerminalSettingsMenuChoices({
3170
+ resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3171
+ }),
2289
3172
  ];
2290
3173
  }
2291
3174
 
2292
3175
  /**
2293
3176
  * @brief Builds the shared settings-menu choices for configured static-check languages.
2294
- * @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.
3177
+ * @details Exposes only languages whose checker lists are non-empty so removal remains deterministic, then appends subtree-local terminal rows. Runtime is O(l). No external state is mutated.
2295
3178
  * @param[in] config {UseReqConfig} Effective project configuration.
2296
3179
  * @return {PiUsereqSettingsMenuChoice[]} Ordered configured-language vector.
2297
3180
  */
2298
3181
  function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2299
3182
  return [
2300
3183
  ...getSupportedStaticCheckLanguageSupport()
2301
- .filter(({ language }) => (config["static-check"][language] ?? []).length > 0)
2302
- .map(({ language, extensions }) => ({
2303
- id: language,
2304
- label: language,
2305
- value: `${extensions.join(", ")} • ${config["static-check"][language]!.length} configured`,
2306
- description: `Remove every configured static-check entry for ${language}.`,
2307
- })),
2308
- {
2309
- id: "back",
2310
- label: "Back",
2311
- value: "",
2312
- description: "Return to the Language static code checkers menu.",
2313
- },
3184
+ .filter(({ language }) => getStaticCheckLanguageConfigForMenu(config, language).checkers.length > 0)
3185
+ .map(({ language, extensions }) => {
3186
+ const languageConfig = getStaticCheckLanguageConfigForMenu(config, language);
3187
+ return {
3188
+ id: language,
3189
+ label: language,
3190
+ value: `${extensions.join(", ")} • ${languageConfig.checkers.length} configured`,
3191
+ description: `Remove every configured static-check entry for ${language}.`,
3192
+ };
3193
+ }),
3194
+ ...buildTerminalSettingsMenuChoices({
3195
+ resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3196
+ }),
2314
3197
  ];
2315
3198
  }
2316
3199
 
2317
3200
  /**
2318
3201
  * @brief Runs the interactive static-check configuration menu.
2319
- * @details Lets the user add Command entries by guided prompts, remove configured language entries, and reset the static-check configuration through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
3202
+ * @details Lets the user add Command entries by guided prompts, remove configured language entries, toggle direct per-language enable flags, and reset the subtree to documented defaults through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
2320
3203
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2321
3204
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
2322
3205
  * @return {Promise<void>} Promise resolved when the menu closes.
2323
- * @satisfies REQ-008, REQ-150, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193, REQ-195
3206
+ * @satisfies REQ-008, REQ-151, REQ-152, REQ-153, REQ-154, REQ-160, REQ-161, REQ-193, REQ-195, REQ-248, REQ-253
2324
3207
  */
2325
- async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
3208
+ async function configureStaticCheckMenu(
3209
+ ctx: ExtensionCommandContext,
3210
+ config: UseReqConfig,
3211
+ onConfigChange: () => void,
3212
+ ): Promise<void> {
2326
3213
  let focusedChoiceId: string | undefined;
2327
3214
  while (true) {
2328
3215
  const staticChoice = await showPiUsereqSettingsMenu(ctx, "Language static code checkers", buildStaticCheckMenuChoices(config), {
2329
3216
  initialSelectedId: focusedChoiceId,
2330
3217
  });
2331
3218
 
2332
- if (!staticChoice || staticChoice === "save-and-close") {
3219
+ if (!staticChoice) {
2333
3220
  return;
2334
3221
  }
2335
3222
  focusedChoiceId = staticChoice;
2336
3223
 
3224
+ if (staticChoice.startsWith("toggle-static-check-language:")) {
3225
+ const language = staticChoice.slice("toggle-static-check-language:".length);
3226
+ config["static-check"][language] ??= createStaticCheckLanguageConfig([]);
3227
+ const languageConfig = config["static-check"][language]!;
3228
+ languageConfig.enabled = languageConfig.enabled === "enable" ? "disable" : "enable";
3229
+ onConfigChange();
3230
+ ctx.ui.notify(
3231
+ `${languageConfig.enabled === "enable" ? "Enabled" : "Disabled"} static-check for ${language}`,
3232
+ "info",
3233
+ );
3234
+ continue;
3235
+ }
3236
+
2337
3237
  if (staticChoice === "add-static-check-entry") {
2338
3238
  const selectedLanguage = await showPiUsereqSettingsMenu(ctx, "static-check language", buildSupportedStaticCheckLanguageChoices(config));
2339
- if (!selectedLanguage || selectedLanguage === "back") {
3239
+ if (!selectedLanguage) {
3240
+ continue;
3241
+ }
3242
+ if (selectedLanguage === "reset-defaults") {
3243
+ const approved = await confirmResetChanges(
3244
+ ctx,
3245
+ "Confirm static-check reset",
3246
+ [{
3247
+ label: "Language static code checkers",
3248
+ previousValue: formatStaticCheckLanguagesSummary(config),
3249
+ nextValue: formatStaticCheckLanguagesSummary({ ...config, "static-check": getDefaultStaticCheckConfig() }),
3250
+ }].filter((change) => change.previousValue !== change.nextValue),
3251
+ "Approve restoring the documented per-language static-check defaults.",
3252
+ "Abort the static-check reset and keep the current values.",
3253
+ );
3254
+ if (!approved) {
3255
+ ctx.ui.notify("Aborted static-check reset", "info");
3256
+ continue;
3257
+ }
3258
+ resetStaticCheckConfig(config);
3259
+ onConfigChange();
3260
+ ctx.ui.notify("Restored default static code checker configuration", "info");
2340
3261
  continue;
2341
3262
  }
2342
3263
 
@@ -2356,23 +3277,64 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
2356
3277
  entry.params = params;
2357
3278
  }
2358
3279
 
2359
- config["static-check"][selectedLanguage] ??= [];
2360
- config["static-check"][selectedLanguage]!.push(entry);
3280
+ config["static-check"][selectedLanguage] ??= createStaticCheckLanguageConfig([]);
3281
+ config["static-check"][selectedLanguage]!.enabled = "enable";
3282
+ config["static-check"][selectedLanguage]!.checkers.push(entry);
3283
+ onConfigChange();
2361
3284
  ctx.ui.notify(`Added ${entry.module} checker for ${selectedLanguage}`, "info");
2362
3285
  continue;
2363
3286
  }
2364
3287
 
2365
3288
  if (staticChoice === "remove-static-check-entry") {
2366
3289
  const configuredLanguage = await showPiUsereqSettingsMenu(ctx, "Remove static code checker", buildConfiguredStaticCheckLanguageChoices(config));
2367
- if (!configuredLanguage || configuredLanguage === "back") {
3290
+ if (!configuredLanguage) {
3291
+ continue;
3292
+ }
3293
+ if (configuredLanguage === "reset-defaults") {
3294
+ const approved = await confirmResetChanges(
3295
+ ctx,
3296
+ "Confirm static-check reset",
3297
+ [{
3298
+ label: "Language static code checkers",
3299
+ previousValue: formatStaticCheckLanguagesSummary(config),
3300
+ nextValue: formatStaticCheckLanguagesSummary({ ...config, "static-check": getDefaultStaticCheckConfig() }),
3301
+ }].filter((change) => change.previousValue !== change.nextValue),
3302
+ "Approve restoring the documented per-language static-check defaults.",
3303
+ "Abort the static-check reset and keep the current values.",
3304
+ );
3305
+ if (!approved) {
3306
+ ctx.ui.notify("Aborted static-check reset", "info");
3307
+ continue;
3308
+ }
3309
+ resetStaticCheckConfig(config);
3310
+ onConfigChange();
3311
+ ctx.ui.notify("Restored default static code checker configuration", "info");
2368
3312
  continue;
2369
3313
  }
2370
- delete config["static-check"][configuredLanguage];
3314
+ config["static-check"][configuredLanguage] = createStaticCheckLanguageConfig([]);
3315
+ onConfigChange();
2371
3316
  ctx.ui.notify(`Removed static-check entries for ${configuredLanguage}`, "info");
2372
3317
  continue;
2373
3318
  }
3319
+
2374
3320
  if (staticChoice === "reset-defaults") {
2375
- config["static-check"] = {};
3321
+ const approved = await confirmResetChanges(
3322
+ ctx,
3323
+ "Confirm static-check reset",
3324
+ [{
3325
+ label: "Language static code checkers",
3326
+ previousValue: formatStaticCheckLanguagesSummary(config),
3327
+ nextValue: formatStaticCheckLanguagesSummary({ ...config, "static-check": getDefaultStaticCheckConfig() }),
3328
+ }].filter((change) => change.previousValue !== change.nextValue),
3329
+ "Approve restoring the documented per-language static-check defaults.",
3330
+ "Abort the static-check reset and keep the current values.",
3331
+ );
3332
+ if (!approved) {
3333
+ ctx.ui.notify("Aborted static-check reset", "info");
3334
+ continue;
3335
+ }
3336
+ resetStaticCheckConfig(config);
3337
+ onConfigChange();
2376
3338
  ctx.ui.notify("Restored default static code checker configuration", "info");
2377
3339
  }
2378
3340
  }
@@ -2380,16 +3342,21 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
2380
3342
 
2381
3343
  /**
2382
3344
  * @brief Builds the shared settings-menu choices for the top-level pi-usereq configuration UI.
2383
- * @details Serializes primary configuration actions into right-valued menu rows consumed by the shared settings-menu renderer, including the display-only config path beside `show-config`. Runtime is O(s) in source-directory count. No external state is mutated.
3345
+ * @details Serializes primary configuration actions into right-valued menu rows consumed by the shared settings-menu renderer, including automatic git-commit mode, effective prompt-command worktree state, notification summary, debug summary, locked worktree rows when automatic git commit is disabled, and the display-only config path beside `show-config`. Runtime is O(s) in source-directory count. No external state is mutated.
2384
3346
  * @param[in] cwd {string} Current working directory.
2385
3347
  * @param[in] config {UseReqConfig} Effective project configuration.
2386
3348
  * @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
2387
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197
3349
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240
2388
3350
  */
2389
3351
  function buildPiUsereqMenuChoices(
2390
3352
  cwd: string,
2391
3353
  config: UseReqConfig,
2392
3354
  ): PiUsereqSettingsMenuChoice[] {
3355
+ const autoGitCommitDisabled = config.AUTO_GIT_COMMIT === "disable";
3356
+ const effectiveGitWorktreeEnabled = resolveEffectiveGitWorktreeEnabled(
3357
+ config.AUTO_GIT_COMMIT,
3358
+ config.GIT_WORKTREE_ENABLED,
3359
+ );
2393
3360
  return [
2394
3361
  {
2395
3362
  id: "docs-dir",
@@ -2409,11 +3376,37 @@ function buildPiUsereqMenuChoices(
2409
3376
  value: config["tests-dir"],
2410
3377
  description: "Edit the repository-relative directory used for project test assets and static-check selection.",
2411
3378
  },
3379
+ {
3380
+ id: "auto-git-commit",
3381
+ label: "Auto git commit",
3382
+ value: config.AUTO_GIT_COMMIT,
3383
+ description: "Select bundled `git_commit.md` or `git_read-only.md` for `%%COMMIT%%`; disabling also forces prompt-command worktrees off.",
3384
+ },
3385
+ {
3386
+ id: "git-worktree-enabled",
3387
+ label: "Git worktree",
3388
+ labelTone: autoGitCommitDisabled ? "dim" : undefined,
3389
+ value: effectiveGitWorktreeEnabled,
3390
+ valueTone: autoGitCommitDisabled ? "dim" : undefined,
3391
+ description: autoGitCommitDisabled
3392
+ ? "Forced to `disable` while `Auto git commit` is disabled."
3393
+ : "Enable or disable prompt-command worktree orchestration.",
3394
+ },
3395
+ {
3396
+ id: "git-worktree-prefix",
3397
+ label: "Worktree prefix",
3398
+ labelTone: autoGitCommitDisabled ? "dim" : undefined,
3399
+ value: config.GIT_WORKTREE_PREFIX,
3400
+ valueTone: autoGitCommitDisabled ? "dim" : undefined,
3401
+ description: autoGitCommitDisabled
3402
+ ? "Stored prefix is locked while `Auto git commit` is disabled."
3403
+ : "Edit the static prefix used by generated worktree names.",
3404
+ },
2412
3405
  {
2413
3406
  id: "static-check",
2414
3407
  label: "Language static code checkers",
2415
3408
  value: formatStaticCheckLanguagesSummary(config),
2416
- description: "Manage guided Command static-check entries by language.",
3409
+ description: "Manage guided Command static-check entries and per-language enable flags.",
2417
3410
  },
2418
3411
  {
2419
3412
  id: "startup-tools",
@@ -2427,25 +3420,22 @@ function buildPiUsereqMenuChoices(
2427
3420
  value: `notification:${formatPiNotifyStatus(config)} • sound:${config["notify-sound"]} • pushover:${formatPiNotifyPushoverStatus(config)}`,
2428
3421
  description: "Manage command-notify, sound, and Pushover settings with dedicated event submenus.",
2429
3422
  },
3423
+ {
3424
+ id: "debug",
3425
+ label: "Debug",
3426
+ value: formatDebugMenuSummary(config),
3427
+ description: "Manage debug logging for tools and `req-*` prompt orchestration.",
3428
+ },
2430
3429
  {
2431
3430
  id: "show-config",
2432
3431
  label: "Show configuration",
2433
3432
  value: formatProjectConfigPathForMenu(cwd),
2434
3433
  valueTone: "dim",
2435
- description: "Write the current project configuration JSON into the editor without saving additional changes.",
2436
- },
2437
- {
2438
- id: "reset-defaults",
2439
- label: "Reset defaults",
2440
- value: "",
2441
- description: "Restore the default pi-usereq configuration for the current project base.",
2442
- },
2443
- {
2444
- id: "save-and-close",
2445
- label: "Save and close",
2446
- value: "",
2447
- description: "Persist the current configuration and return to the normal pi session UI.",
3434
+ description: "Persist the current project configuration file and write its exact text into the editor.",
2448
3435
  },
3436
+ ...buildTerminalSettingsMenuChoices({
3437
+ resetDefaultsDescription: "Restore the default pi-usereq configuration for the current project base.",
3438
+ }),
2449
3439
  ];
2450
3440
  }
2451
3441
 
@@ -2470,24 +3460,15 @@ function buildSrcDirMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoic
2470
3460
  value: config["src-dir"].join(", "),
2471
3461
  description: "Select one configured source directory to remove from the current configuration.",
2472
3462
  },
2473
- {
2474
- id: "reset-defaults",
2475
- label: "Reset defaults",
2476
- value: `${DEFAULT_SRC_DIRS.join(", ")}`,
2477
- description: "Restore the documented default source-directory configuration.",
2478
- },
2479
- {
2480
- id: "save-and-close",
2481
- label: "Save and close",
2482
- value: "",
2483
- description: "Return to the parent configuration menu.",
2484
- },
3463
+ ...buildTerminalSettingsMenuChoices({
3464
+ resetDefaultsDescription: "Restore the documented default source-directory configuration.",
3465
+ }),
2485
3466
  ];
2486
3467
  }
2487
3468
 
2488
3469
  /**
2489
3470
  * @brief Builds the shared settings-menu choices for removing one source-directory entry.
2490
- * @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.
3471
+ * @details Exposes every configured `src-dir` entry as one removable row and appends a value-less subtree-local `Reset defaults` row. Runtime is O(s) in source-directory count. No external state is mutated.
2491
3472
  * @param[in] config {UseReqConfig} Effective project configuration.
2492
3473
  * @return {PiUsereqSettingsMenuChoice[]} Ordered removable source-directory choices.
2493
3474
  * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154
@@ -2500,34 +3481,35 @@ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuCh
2500
3481
  value: "remove",
2501
3482
  description: `Remove the source-directory entry ${entry} from the current configuration.`,
2502
3483
  })),
2503
- {
2504
- id: "back",
2505
- label: "Back",
2506
- value: "",
2507
- description: "Return to the source-directory menu.",
2508
- },
3484
+ ...buildTerminalSettingsMenuChoices({
3485
+ resetDefaultsDescription: "Restore the documented default source-directory configuration.",
3486
+ }),
2509
3487
  ];
2510
3488
  }
2511
3489
 
2512
3490
  /**
2513
3491
  * @brief Runs the top-level pi-usereq configuration menu.
2514
- * @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, active-tool changes, and editor text updates.
3492
+ * @details Loads project config, exposes docs/test/source/automatic-commit/worktree/static-check/startup-tool/notification/debug actions through the shared settings-menu renderer, forces worktree disablement when automatic git commit is disabled, prevents locked row edits, persists changes on exit, closes immediately after `Show configuration`, and refreshes the single-line status bar. Runtime depends on user interaction count. Side effects include UI updates, config writes, active-tool changes, and editor text updates.
2515
3493
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2516
3494
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2517
3495
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2518
3496
  * @return {Promise<void>} Promise resolved when configuration is saved and the menu closes.
2519
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162, REQ-190, REQ-191, REQ-192, REQ-194, REQ-195
3497
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162, REQ-190, REQ-191, REQ-192, REQ-194, REQ-195, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243
2520
3498
  */
2521
3499
  async function configurePiUsereq(
2522
3500
  pi: ExtensionAPI,
2523
3501
  ctx: ExtensionCommandContext,
2524
3502
  statusController: PiUsereqStatusController,
2525
3503
  ): Promise<void> {
3504
+ bootstrapRuntimePathState(ctx.cwd, {
3505
+ gitPath: resolveRuntimeGitPath(ctx.cwd),
3506
+ });
2526
3507
  let config = loadProjectConfig(ctx.cwd);
2527
3508
  const projectBase = getProjectBase(ctx.cwd);
2528
3509
  const initialShortcut = config["notify-sound-toggle-shortcut"];
2529
- const ensureSaved = () => saveProjectConfig(ctx.cwd, config);
2530
- const refreshStatus = () => {
3510
+ const persistConfigChange = () => {
3511
+ Object.assign(config, normalizeConfigPaths(projectBase, config));
3512
+ saveProjectConfig(ctx.cwd, config);
2531
3513
  setPiUsereqStatusConfig(statusController, config);
2532
3514
  renderPiUsereqStatus(statusController, ctx);
2533
3515
  };
@@ -2540,9 +3522,7 @@ async function configurePiUsereq(
2540
3522
  buildPiUsereqMenuChoices(ctx.cwd, config),
2541
3523
  { initialSelectedId: focusedChoiceId },
2542
3524
  );
2543
- if (!choice || choice === "save-and-close") {
2544
- ensureSaved();
2545
- refreshStatus();
3525
+ if (!choice) {
2546
3526
  if (config["notify-sound-toggle-shortcut"] !== initialShortcut) {
2547
3527
  ctx.ui.notify("Sound toggle hotkey bind updated; run /reload to apply the new binding", "info");
2548
3528
  }
@@ -2551,12 +3531,61 @@ async function configurePiUsereq(
2551
3531
  focusedChoiceId = choice;
2552
3532
  if (choice === "docs-dir") {
2553
3533
  const value = await ctx.ui.input("Document directory", config["docs-dir"]);
2554
- if (value?.trim()) config["docs-dir"] = value.trim();
3534
+ if (value?.trim()) {
3535
+ config["docs-dir"] = value.trim();
3536
+ persistConfigChange();
3537
+ }
2555
3538
  continue;
2556
3539
  }
2557
3540
  if (choice === "tests-dir") {
2558
3541
  const value = await ctx.ui.input("Unit tests directory", config["tests-dir"]);
2559
- if (value?.trim()) config["tests-dir"] = value.trim();
3542
+ if (value?.trim()) {
3543
+ config["tests-dir"] = value.trim();
3544
+ persistConfigChange();
3545
+ }
3546
+ continue;
3547
+ }
3548
+ if (choice === "auto-git-commit") {
3549
+ const nextAutoGitCommit = config.AUTO_GIT_COMMIT === "enable"
3550
+ ? "disable"
3551
+ : "enable";
3552
+ config.AUTO_GIT_COMMIT = nextAutoGitCommit;
3553
+ if (nextAutoGitCommit === "disable") {
3554
+ config.GIT_WORKTREE_ENABLED = "disable";
3555
+ persistConfigChange();
3556
+ ctx.ui.notify("Auto git commit disabled; Git worktree forced off", "info");
3557
+ } else {
3558
+ persistConfigChange();
3559
+ ctx.ui.notify("Auto git commit enabled", "info");
3560
+ }
3561
+ continue;
3562
+ }
3563
+ if (choice === "git-worktree-enabled") {
3564
+ if (config.AUTO_GIT_COMMIT === "disable") {
3565
+ ctx.ui.notify("Git worktree is locked while Auto git commit is disabled", "info");
3566
+ continue;
3567
+ }
3568
+ config.GIT_WORKTREE_ENABLED = config.GIT_WORKTREE_ENABLED === "enable"
3569
+ ? "disable"
3570
+ : "enable";
3571
+ persistConfigChange();
3572
+ ctx.ui.notify(
3573
+ `Git worktree ${resolveEffectiveGitWorktreeEnabled(config.AUTO_GIT_COMMIT, config.GIT_WORKTREE_ENABLED) === "enable" ? "enabled" : "disabled"}`,
3574
+ "info",
3575
+ );
3576
+ continue;
3577
+ }
3578
+ if (choice === "git-worktree-prefix") {
3579
+ if (config.AUTO_GIT_COMMIT === "disable") {
3580
+ ctx.ui.notify("Worktree prefix is locked while Auto git commit is disabled", "info");
3581
+ continue;
3582
+ }
3583
+ const value = await ctx.ui.input("Worktree prefix", config.GIT_WORKTREE_PREFIX);
3584
+ if (value !== undefined) {
3585
+ config.GIT_WORKTREE_PREFIX = value.trim() || DEFAULT_GIT_WORKTREE_PREFIX;
3586
+ persistConfigChange();
3587
+ ctx.ui.notify(`Worktree prefix set to ${config.GIT_WORKTREE_PREFIX}`, "info");
3588
+ }
2560
3589
  continue;
2561
3590
  }
2562
3591
  if (choice === "src-dir") {
@@ -2565,7 +3594,7 @@ async function configurePiUsereq(
2565
3594
  const srcAction = await showPiUsereqSettingsMenu(ctx, "Source-code directories", buildSrcDirMenuChoices(config), {
2566
3595
  initialSelectedId: srcFocusedChoiceId,
2567
3596
  });
2568
- if (!srcAction || srcAction === "save-and-close") {
3597
+ if (!srcAction) {
2569
3598
  break;
2570
3599
  }
2571
3600
  srcFocusedChoiceId = srcAction;
@@ -2573,47 +3602,118 @@ async function configurePiUsereq(
2573
3602
  const value = await ctx.ui.input("New source-code directory", "src");
2574
3603
  if (value?.trim()) {
2575
3604
  config["src-dir"] = [...config["src-dir"], value.trim()];
3605
+ persistConfigChange();
2576
3606
  }
2577
3607
  continue;
2578
3608
  }
2579
3609
  if (srcAction === "remove-src-dir-entry") {
2580
3610
  const toRemove = await showPiUsereqSettingsMenu(ctx, "Remove source-code directory", buildSrcDirRemovalChoices(config));
2581
- if (toRemove && toRemove !== "back") {
2582
- config["src-dir"] = config["src-dir"].filter((entry) => entry !== toRemove);
2583
- if (config["src-dir"].length === 0) {
2584
- config["src-dir"] = ["src"];
3611
+ if (!toRemove) {
3612
+ continue;
3613
+ }
3614
+ if (toRemove === "reset-defaults") {
3615
+ const approved = await confirmResetChanges(
3616
+ ctx,
3617
+ "Confirm source-directory reset",
3618
+ [{
3619
+ label: "Source-code directories",
3620
+ previousValue: config["src-dir"].join(", "),
3621
+ nextValue: DEFAULT_SRC_DIRS.join(", "),
3622
+ }].filter((change) => change.previousValue !== change.nextValue),
3623
+ "Approve restoring the documented default source-directory configuration.",
3624
+ "Abort the source-directory reset and keep the current values.",
3625
+ );
3626
+ if (!approved) {
3627
+ ctx.ui.notify("Aborted source-directory reset", "info");
3628
+ continue;
2585
3629
  }
3630
+ config["src-dir"] = [...DEFAULT_SRC_DIRS];
3631
+ persistConfigChange();
3632
+ ctx.ui.notify("Restored default source-code directories", "info");
3633
+ continue;
3634
+ }
3635
+ config["src-dir"] = config["src-dir"].filter((entry) => entry !== toRemove);
3636
+ if (config["src-dir"].length === 0) {
3637
+ config["src-dir"] = ["src"];
2586
3638
  }
3639
+ persistConfigChange();
2587
3640
  continue;
2588
3641
  }
2589
3642
  if (srcAction === "reset-defaults") {
3643
+ const approved = await confirmResetChanges(
3644
+ ctx,
3645
+ "Confirm source-directory reset",
3646
+ [{
3647
+ label: "Source-code directories",
3648
+ previousValue: config["src-dir"].join(", "),
3649
+ nextValue: DEFAULT_SRC_DIRS.join(", "),
3650
+ }].filter((change) => change.previousValue !== change.nextValue),
3651
+ "Approve restoring the documented default source-directory configuration.",
3652
+ "Abort the source-directory reset and keep the current values.",
3653
+ );
3654
+ if (!approved) {
3655
+ ctx.ui.notify("Aborted source-directory reset", "info");
3656
+ continue;
3657
+ }
2590
3658
  config["src-dir"] = [...DEFAULT_SRC_DIRS];
3659
+ persistConfigChange();
2591
3660
  ctx.ui.notify("Restored default source-code directories", "info");
2592
3661
  }
2593
3662
  }
2594
3663
  continue;
2595
3664
  }
2596
3665
  if (choice === "static-check") {
2597
- await configureStaticCheckMenu(ctx, config);
3666
+ await configureStaticCheckMenu(ctx, config, persistConfigChange);
2598
3667
  continue;
2599
3668
  }
2600
3669
  if (choice === "startup-tools") {
2601
- await configurePiUsereqToolsMenu(pi, ctx, config);
3670
+ await configurePiUsereqToolsMenu(pi, ctx, config, persistConfigChange);
2602
3671
  continue;
2603
3672
  }
2604
3673
  if (choice === "notifications") {
2605
- await configurePiNotifyMenu(ctx, config);
3674
+ await configurePiNotifyMenu(ctx, config, persistConfigChange);
3675
+ continue;
3676
+ }
3677
+ if (choice === "debug") {
3678
+ await configureDebugMenu(ctx, config, persistConfigChange);
2606
3679
  continue;
2607
3680
  }
2608
3681
  if (choice === "reset-defaults") {
2609
- config = getDefaultConfig(projectBase);
3682
+ const defaultConfig = getDefaultConfig(projectBase);
3683
+ const approved = await confirmResetChanges(
3684
+ ctx,
3685
+ "Confirm pi-usereq reset",
3686
+ [
3687
+ { label: "Document directory", previousValue: config["docs-dir"], nextValue: defaultConfig["docs-dir"] },
3688
+ { label: "Source-code directories", previousValue: config["src-dir"].join(", "), nextValue: defaultConfig["src-dir"].join(", ") },
3689
+ { label: "Unit tests directory", previousValue: config["tests-dir"], nextValue: defaultConfig["tests-dir"] },
3690
+ { label: "Auto git commit", previousValue: config.AUTO_GIT_COMMIT, nextValue: defaultConfig.AUTO_GIT_COMMIT },
3691
+ { label: "Git worktree", previousValue: config.GIT_WORKTREE_ENABLED, nextValue: defaultConfig.GIT_WORKTREE_ENABLED },
3692
+ { label: "Worktree prefix", previousValue: config.GIT_WORKTREE_PREFIX, nextValue: defaultConfig.GIT_WORKTREE_PREFIX },
3693
+ { label: "Enable tools", previousValue: String(getConfiguredEnabledPiUsereqTools(config).length), nextValue: String(getConfiguredEnabledPiUsereqTools(defaultConfig).length) },
3694
+ { label: "Notifications", previousValue: buildPiNotifyMenuChoices(config).length.toString(), nextValue: buildPiNotifyMenuChoices(defaultConfig).length.toString() },
3695
+ { label: "Debug", previousValue: formatDebugMenuSummary(config), nextValue: formatDebugMenuSummary(defaultConfig) },
3696
+ ].filter((change) => change.previousValue !== change.nextValue),
3697
+ "Approve restoring the default pi-usereq configuration.",
3698
+ "Abort the pi-usereq reset and keep the current values.",
3699
+ );
3700
+ if (!approved) {
3701
+ ctx.ui.notify("Aborted pi-usereq reset", "info");
3702
+ continue;
3703
+ }
3704
+ config = defaultConfig;
2610
3705
  applyConfiguredPiUsereqTools(pi, config);
3706
+ persistConfigChange();
2611
3707
  ctx.ui.notify("Restored all default configuration values", "info");
2612
3708
  continue;
2613
3709
  }
2614
3710
  if (choice === "show-config") {
2615
- ctx.ui.setEditorText(`${JSON.stringify(config, null, 2)}\n`);
2616
- continue;
3711
+ persistConfigChange();
3712
+ if (config["notify-sound-toggle-shortcut"] !== initialShortcut) {
3713
+ ctx.ui.notify("Sound toggle hotkey bind updated; run /reload to apply the new binding", "info");
3714
+ }
3715
+ writePersistedProjectConfigToEditor(ctx, ctx.cwd, config);
3716
+ return;
2617
3717
  }
2618
3718
  }
2619
3719
  }
@@ -2645,14 +3745,14 @@ function registerConfigCommands(
2645
3745
  * notification-sound shortcut when the runtime supports shortcuts, and
2646
3746
  * installs shared wrappers for all supported pi lifecycle hooks so status
2647
3747
  * telemetry, context usage, prompt timing, cumulative runtime, prompt-specific
2648
- * Pushover metadata, and pi-notify effects remain synchronized with runtime
2649
- * events. Runtime is O(h) in hook
3748
+ * Pushover metadata, tool-result debug logging, and prompt-orchestration debug
3749
+ * effects remain synchronized with runtime events. Runtime is O(h) in hook
2650
3750
  * count during registration. Side effects include filesystem reads,
2651
- * command/tool/shortcut registration, UI updates, active-tool changes, and
2652
- * timer scheduling.
3751
+ * command/tool/shortcut registration, UI updates, active-tool changes,
3752
+ * optional debug-log writes, and timer scheduling.
2653
3753
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2654
3754
  * @return {void} No return value.
2655
- * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-045, REQ-067, REQ-068, REQ-109, 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-127, REQ-128, REQ-131, REQ-132, REQ-133, REQ-134, REQ-137, REQ-148, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-174, REQ-179, REQ-180, REQ-184, REQ-188, REQ-190, REQ-191, REQ-192, REQ-193, REQ-194, REQ-195, REQ-196, REQ-197
3755
+ * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-067, REQ-068, REQ-109, 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-127, REQ-128, REQ-131, REQ-132, REQ-133, REQ-134, REQ-137, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-174, REQ-179, REQ-180, REQ-184, REQ-188, REQ-190, REQ-191, REQ-192, REQ-193, REQ-194, REQ-195, REQ-196, REQ-197, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243, REQ-244, REQ-245, REQ-246, REQ-247
2656
3756
  */
2657
3757
  export default function piUsereqExtension(pi: ExtensionAPI): void {
2658
3758
  const statusController = createPiUsereqStatusController();