pi-usereq 0.7.0 → 0.10.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 (772) hide show
  1. package/.gitignore +2 -0
  2. package/CHANGELOG.md +6 -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 +5184 -24470
  21. package/pi-usereq/docs/REQUIREMENTS.md +300 -166
  22. package/pi-usereq/docs/WORKFLOW.md +303 -314
  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 +296 -46
  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 +1650 -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/settings-menu.ts +12 -6
  713. package/src/core/source-analyzer.ts +27 -7
  714. package/src/core/token-counter.ts +6 -76
  715. package/src/core/tool-runner.ts +38 -317
  716. package/src/index.ts +2062 -950
  717. package/src/resources/instructions/git_commit.md +16 -0
  718. package/src/resources/instructions/git_read-only.md +2 -0
  719. package/src/resources/prompts/analyze.md +12 -13
  720. package/src/resources/prompts/change.md +21 -59
  721. package/src/resources/prompts/check.md +14 -16
  722. package/src/resources/prompts/cover.md +21 -59
  723. package/src/resources/prompts/create.md +5 -4
  724. package/src/resources/prompts/fix.md +20 -58
  725. package/src/resources/prompts/flowchart.md +14 -49
  726. package/src/resources/prompts/implement.md +14 -50
  727. package/src/resources/prompts/new.md +20 -58
  728. package/src/resources/prompts/readme.md +14 -50
  729. package/src/resources/prompts/recreate.md +14 -45
  730. package/src/resources/prompts/refactor.md +20 -58
  731. package/src/resources/prompts/references.md +7 -43
  732. package/src/resources/prompts/renumber.md +8 -39
  733. package/src/resources/prompts/workflow.md +14 -50
  734. package/src/resources/prompts/write.md +5 -4
  735. package/tests/attended-results-scenarios.ts +30 -136
  736. package/tests/cli-command-option-parity.test.ts +148 -192
  737. package/tests/debug-extension-harness.test.ts +69 -68
  738. package/tests/extension-registration.test.ts +3118 -657
  739. package/tests/fixtures_attended_results/project/compress-line-numbers.json +1 -1
  740. package/tests/fixtures_attended_results/project/compress.json +1 -1
  741. package/tests/fixtures_attended_results/project/find-line-numbers.json +1 -1
  742. package/tests/fixtures_attended_results/project/find.json +1 -1
  743. package/tests/fixtures_attended_results/project/references.json +2 -2
  744. package/tests/fixtures_attended_results/standalone/files-compress/fixture_go.go.json +1 -1
  745. package/tests/fixtures_attended_results/standalone/files-compress-line-numbers/fixture_go.go.json +1 -1
  746. package/tests/fixtures_attended_results/standalone/files-find/fixture_go.go.json +1 -1
  747. package/tests/fixtures_attended_results/standalone/files-find-line-numbers/fixture_go.go.json +1 -1
  748. package/tests/fixtures_attended_results/standalone/files-references/fixture_c.c.json +1 -1
  749. package/tests/fixtures_attended_results/standalone/files-references/fixture_cpp.cpp.json +1 -1
  750. package/tests/fixtures_attended_results/standalone/files-references/fixture_csharp.cs.json +1 -1
  751. package/tests/fixtures_attended_results/standalone/files-references/fixture_elixir.ex.json +1 -1
  752. package/tests/fixtures_attended_results/standalone/files-references/fixture_go.go.json +1 -1
  753. package/tests/fixtures_attended_results/standalone/files-references/fixture_haskell.hs.json +1 -1
  754. package/tests/fixtures_attended_results/standalone/files-references/fixture_java.java.json +1 -1
  755. package/tests/fixtures_attended_results/standalone/files-references/fixture_javascript.js.json +1 -1
  756. package/tests/fixtures_attended_results/standalone/files-references/fixture_kotlin.kt.json +1 -1
  757. package/tests/fixtures_attended_results/standalone/files-references/fixture_lua.lua.json +1 -1
  758. package/tests/fixtures_attended_results/standalone/files-references/fixture_perl.pl.json +1 -1
  759. package/tests/fixtures_attended_results/standalone/files-references/fixture_php.php.json +1 -1
  760. package/tests/fixtures_attended_results/standalone/files-references/fixture_python.py.json +1 -1
  761. package/tests/fixtures_attended_results/standalone/files-references/fixture_ruby.rb.json +1 -1
  762. package/tests/fixtures_attended_results/standalone/files-references/fixture_rust.rs.json +1 -1
  763. package/tests/fixtures_attended_results/standalone/files-references/fixture_scala.scala.json +1 -1
  764. package/tests/fixtures_attended_results/standalone/files-references/fixture_shell.sh.json +1 -1
  765. package/tests/fixtures_attended_results/standalone/files-references/fixture_swift.swift.json +1 -1
  766. package/tests/fixtures_attended_results/standalone/files-references/fixture_typescript.ts.json +1 -1
  767. package/tests/fixtures_attended_results/standalone/files-references/fixture_zig.zig.json +1 -1
  768. package/tests/helpers.ts +24 -15
  769. package/tests/oracle-project.test.ts +29 -30
  770. package/tests/oracle-standalone.test.ts +27 -1
  771. package/tests/prompt-rendering.test.ts +40 -9
  772. 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.10.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,153 @@ 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 `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; resetDefaultsValue?: string | undefined }} 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
+ resetDefaultsValue?: string;
290
+ }): PiUsereqSettingsMenuChoice[] {
291
+ return [
292
+ {
293
+ id: "reset-defaults",
294
+ label: "Reset defaults",
295
+ value: options.resetDefaultsValue ?? "",
296
+ description: options.resetDefaultsDescription,
297
+ },
298
+ ];
248
299
  }
249
300
 
250
301
  /**
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.
302
+ * @brief Describes one pending reset value change shown in confirmation menus.
303
+ * @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
304
  */
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");
305
+ interface ResetConfirmationChange {
306
+ label: string;
307
+ previousValue: string;
308
+ nextValue: string;
264
309
  }
265
310
 
266
311
  /**
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
312
+ * @brief Formats one reset-confirmation value pair for menu display.
313
+ * @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.
314
+ * @param[in] previousValue {string} Current persisted value.
315
+ * @param[in] nextValue {string} Candidate default value.
316
+ * @return {string} Rendered preview string.
272
317
  */
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);
318
+ function formatResetConfirmationValue(previousValue: string, nextValue: string): string {
319
+ return `${previousValue} -> ${nextValue}`;
288
320
  }
289
321
 
290
322
  /**
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.
323
+ * @brief Builds the shared settings-menu choices for one reset-confirmation submenu.
324
+ * @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.
325
+ * @param[in] changes {ResetConfirmationChange[]} Changed-value preview rows.
326
+ * @param[in] approveDescription {string} Description for the approve action.
327
+ * @param[in] abortDescription {string} Description for the abort action.
328
+ * @return {PiUsereqSettingsMenuChoice[]} Reset-confirmation submenu choices.
295
329
  */
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),
330
+ function buildResetConfirmationChoices(
331
+ changes: ResetConfirmationChange[],
332
+ approveDescription: string,
333
+ abortDescription: string,
334
+ ): PiUsereqSettingsMenuChoice[] {
335
+ const previewRows = changes.length > 0
336
+ ? changes.map((change, index) => ({
337
+ id: `reset-preview:${index}`,
338
+ label: change.label,
339
+ value: formatResetConfirmationValue(change.previousValue, change.nextValue),
340
+ description: `Reset ${change.label} from ${change.previousValue} to ${change.nextValue}.`,
341
+ disabled: true,
342
+ labelTone: "dim" as const,
343
+ valueTone: "dim" as const,
344
+ }))
345
+ : [{
346
+ id: "reset-preview:none",
347
+ label: "No value changes",
348
+ value: "unchanged",
349
+ description: "Approving the reset leaves the current values unchanged.",
350
+ disabled: true,
351
+ labelTone: "dim" as const,
352
+ valueTone: "dim" as const,
353
+ }];
354
+ return [
355
+ ...previewRows,
356
+ {
357
+ id: "reset-approve",
358
+ label: "Approve reset",
359
+ value: `${changes.length} changes`,
360
+ description: approveDescription,
347
361
  },
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),
362
+ {
363
+ id: "reset-abort",
364
+ label: "Abort reset",
365
+ value: "keep current",
366
+ description: abortDescription,
372
367
  },
373
- };
374
- return buildStructuredToolExecuteResult(details);
368
+ ];
375
369
  }
376
370
 
377
371
  /**
378
- * @brief Maps find-payload language identifiers to stable registration labels.
372
+ * @brief Opens one explicit reset-confirmation submenu.
373
+ * @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.
374
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
375
+ * @param[in] title {string} Confirmation submenu title.
376
+ * @param[in] changes {ResetConfirmationChange[]} Changed-value preview rows.
377
+ * @param[in] approveDescription {string} Description for the approve action.
378
+ * @param[in] abortDescription {string} Description for the abort action.
379
+ * @return {Promise<boolean>} `true` when the reset is explicitly approved.
380
+ */
381
+ async function confirmResetChanges(
382
+ ctx: ExtensionCommandContext,
383
+ title: string,
384
+ changes: ResetConfirmationChange[],
385
+ approveDescription: string,
386
+ abortDescription: string,
387
+ ): Promise<boolean> {
388
+ const choice = await showPiUsereqSettingsMenu(
389
+ ctx,
390
+ title,
391
+ buildResetConfirmationChoices(changes, approveDescription, abortDescription),
392
+ );
393
+ return choice === "reset-approve";
394
+ }
395
+
396
+ /**
397
+ * @brief Writes the already-persisted project configuration file text into the editor.
398
+ * @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.
399
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
400
+ * @param[in] cwd {string} Current working directory.
401
+ * @param[in] _config {UseReqConfig} Unused effective project configuration retained for stable call-site shape.
402
+ * @return {void} No return value.
403
+ * @satisfies REQ-031
404
+ */
405
+ function writePersistedProjectConfigToEditor(
406
+ ctx: ExtensionCommandContext,
407
+ cwd: string,
408
+ _config: UseReqConfig,
409
+ ): void {
410
+ const projectBase = getProjectBase(cwd);
411
+ ctx.ui.setEditorText(fs.readFileSync(getProjectConfigPath(projectBase), "utf8"));
412
+ }
413
+
414
+ /**
415
+ * @brief Maps search-tool language identifiers to stable registration labels.
379
416
  * @details Preserves the canonical capitalization used by tool descriptions so supported-tag guidance remains deterministic across inspection snapshots. Access complexity is O(1).
380
417
  */
381
418
  const FIND_TOOL_LANGUAGE_LABELS: Record<string, string> = {
@@ -402,7 +439,7 @@ const FIND_TOOL_LANGUAGE_LABELS: Record<string, string> = {
402
439
  };
403
440
 
404
441
  /**
405
- * @brief Defines the stable language order used by find-tool supported-tag guidance.
442
+ * @brief Defines the stable language order used by search-tool supported-tag guidance.
406
443
  * @details Keeps registration descriptions aligned with the repository-supported language matrix and preserves deterministic inspection snapshots. Access complexity is O(1).
407
444
  */
408
445
  const FIND_TOOL_LANGUAGE_ORDER = [
@@ -429,103 +466,445 @@ const FIND_TOOL_LANGUAGE_ORDER = [
429
466
  ] as const;
430
467
 
431
468
  /**
432
- * @brief Builds the supported-tag guidance lines embedded in find-tool registrations.
469
+ * @brief Builds the supported-tag guidance lines embedded in search-tool registrations.
433
470
  * @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
471
  * @return {string[]} Supported-tag guidance lines.
435
472
  */
436
- function buildFindToolSupportedTagGuidelines(): string[] {
473
+ function buildSearchToolSupportedTagGuidelines(): string[] {
437
474
  return FIND_TOOL_LANGUAGE_ORDER
438
475
  .filter((language) => language in LANGUAGE_TAGS)
439
476
  .map((language) => `Supported tags [${FIND_TOOL_LANGUAGE_LABELS[language]}]: ${[...LANGUAGE_TAGS[language]!].sort().join(", ")}`);
440
477
  }
441
478
 
442
479
  /**
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.
480
+ * @brief Builds the schema description for one search-tool registration.
481
+ * @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.
482
+ * @param[in] scope {FindToolScope} Search-tool scope.
446
483
  * @return {string} Parameter-schema description.
447
484
  */
448
- function buildFindToolSchemaDescription(scope: FindToolScope): string {
485
+ function buildSearchToolSchemaDescription(scope: FindToolScope): string {
449
486
  const inputContract = scope === "explicit-files"
450
487
  ? "Input contract: tag + pattern + files[] + optional enableLineNumbers."
451
488
  : "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.`;
489
+ return `${inputContract} Output contract: monolithic markdown in content[0].text plus details.execution diagnostics. Regex matches construct names only.`;
453
490
  }
454
491
 
455
492
  /**
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.
493
+ * @brief Builds the prompt-guideline set for one search-tool registration.
494
+ * @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.
495
+ * @param[in] scope {FindToolScope} Search-tool scope.
459
496
  * @return {string[]} Prompt-guideline strings.
460
497
  */
461
- function buildFindToolPromptGuidelines(scope: FindToolScope): string[] {
498
+ function buildSearchToolPromptGuidelines(scope: FindToolScope): string[] {
462
499
  const scopeLine = scope === "explicit-files"
463
500
  ? "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute."
464
501
  : "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
502
  return [
469
503
  scopeLine,
470
- outputLine,
504
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
471
505
  "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(),
506
+ "Tag rule: tag is pipe-separated and case-insensitive; unsupported tags are ignored.",
507
+ "Line-number behavior: enableLineNumbers toggles original source line prefixes inside fenced code blocks.",
508
+ "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.",
509
+ ...buildSearchToolSupportedTagGuidelines(),
476
510
  ];
477
511
  }
478
512
 
479
513
  /**
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
- },
514
+ * @brief Describes the monolithic tool-result surface consumed by tool-row renderers.
515
+ * @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.
516
+ */
517
+ type MonolithicToolRenderResult = {
518
+ content?: Array<{ type?: string; text?: string }>;
519
+ details?: {
520
+ execution?: {
521
+ code?: number;
522
+ stderr_lines?: string[];
523
+ stderr?: string;
524
+ };
501
525
  };
502
- return buildStructuredToolExecuteResult(details);
526
+ };
527
+
528
+ /**
529
+ * @brief Extracts the primary monolithic text block from one tool result.
530
+ * @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.
531
+ * @param[in] result {MonolithicToolRenderResult} Tool result wrapper.
532
+ * @return {string} Primary monolithic content text.
533
+ */
534
+ function getMonolithicToolText(result: MonolithicToolRenderResult): string {
535
+ const firstBlock = result.content?.find((entry) => entry?.type === "text" && typeof entry.text === "string");
536
+ return typeof firstBlock?.text === "string" ? firstBlock.text : "";
503
537
  }
504
538
 
505
539
  /**
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.
540
+ * @brief Reads the first residual execution error string from one monolithic tool result.
541
+ * @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.
542
+ * @param[in] result {MonolithicToolRenderResult} Tool result wrapper.
543
+ * @return {string | undefined} First residual execution error string.
544
+ */
545
+ function getMonolithicToolErrorText(result: MonolithicToolRenderResult): string | undefined {
546
+ const stderrLines = result.details?.execution?.stderr_lines;
547
+ if (Array.isArray(stderrLines) && stderrLines.length > 0 && typeof stderrLines[0] === "string") {
548
+ return stderrLines[0];
549
+ }
550
+ const stderrText = result.details?.execution?.stderr;
551
+ if (typeof stderrText !== "string" || stderrText === "") {
552
+ return undefined;
553
+ }
554
+ return stderrText.split(/\r?\n/)[0] || undefined;
555
+ }
556
+
557
+ /**
558
+ * @brief Formats one scalar or structural tool argument for compact render summaries.
559
+ * @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.
560
+ * @param[in] value {unknown} Candidate tool argument value.
561
+ * @return {string | undefined} Compact preview string or `undefined` when the value carries no useful summary.
562
+ */
563
+ function formatCompactToolArgumentValue(value: unknown): string | undefined {
564
+ if (value === undefined || value === null) {
565
+ return undefined;
566
+ }
567
+ if (typeof value === "string") {
568
+ const compactText = value.length > 28 ? `${value.slice(0, 25)}...` : value;
569
+ return JSON.stringify(compactText);
570
+ }
571
+ if (typeof value === "number" || typeof value === "bigint") {
572
+ return String(value);
573
+ }
574
+ if (typeof value === "boolean") {
575
+ return value ? "true" : "false";
576
+ }
577
+ if (Array.isArray(value)) {
578
+ if (value.length === 0) {
579
+ return undefined;
580
+ }
581
+ const renderedItems = value
582
+ .slice(0, 2)
583
+ .map((item) => formatCompactToolArgumentValue(item))
584
+ .filter((item): item is string => typeof item === "string");
585
+ if (renderedItems.length === 0) {
586
+ return `[${value.length}]`;
587
+ }
588
+ return value.length <= 2
589
+ ? `[${renderedItems.join(", ")}]`
590
+ : `[${renderedItems.join(", ")}, ...+${value.length - 2}]`;
591
+ }
592
+ if (typeof value === "object") {
593
+ const keys = Object.keys(value as Record<string, unknown>);
594
+ if (keys.length === 0) {
595
+ return undefined;
596
+ }
597
+ return keys.length <= 2
598
+ ? `{${keys.join(",")}}`
599
+ : `{${keys.slice(0, 2).join(",")},...+${keys.length - 2}}`;
600
+ }
601
+ return undefined;
602
+ }
603
+
604
+ /**
605
+ * @brief Builds the compact invocation summary appended to collapsed tool rows.
606
+ * @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.
607
+ * @param[in] args {Record<string, unknown> | undefined} Current tool call arguments.
608
+ * @return {string} Compact invocation summary prefixed with one separating space, or the empty string when no useful preview exists.
609
+ */
610
+ function buildCompactToolInvocationText(args: Record<string, unknown> | undefined): string {
611
+ if (!args) {
612
+ return "";
613
+ }
614
+ const entries = Object.entries(args)
615
+ .map(([key, value]) => {
616
+ const renderedValue = formatCompactToolArgumentValue(value);
617
+ return renderedValue === undefined ? undefined : `${key}=${renderedValue}`;
618
+ })
619
+ .filter((entry): entry is string => typeof entry === "string");
620
+ return entries.length === 0 ? "" : ` ${entries.join(" · ")}`;
621
+ }
622
+
623
+ /**
624
+ * @brief Builds the compact default text for one monolithic tool result row.
625
+ * @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.
626
+ * @param[in] toolName {string} Registered tool name.
627
+ * @param[in] result {MonolithicToolRenderResult} Tool result wrapper.
628
+ * @param[in] args {Record<string, unknown> | undefined} Current tool call arguments.
629
+ * @return {string} Compact single-line summary.
630
+ */
631
+ function summarizeStructuredToolResult(
632
+ toolName: string,
633
+ result: MonolithicToolRenderResult,
634
+ args?: Record<string, unknown>,
635
+ ): string {
636
+ const success = result.details?.execution?.code === undefined
637
+ ? true
638
+ : result.details.execution.code === 0;
639
+ const prefix = success ? "✓" : "✗";
640
+ const invocationText = buildCompactToolInvocationText(args);
641
+ const errorText = getMonolithicToolErrorText(result);
642
+ if (!success && errorText) {
643
+ return `${prefix} ${toolName}${invocationText}: ${errorText}`;
644
+ }
645
+ return `${prefix} ${toolName}${invocationText}`;
646
+ }
647
+
648
+ /**
649
+ * @brief Builds a custom `renderResult` implementation for one monolithic tool.
650
+ * @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.
651
+ * @param[in] toolName {string} Registered tool name.
652
+ * @return {(result: MonolithicToolRenderResult, options: { expanded?: boolean; isPartial?: boolean }, _theme: unknown, context: { args?: Record<string, unknown>; lastComponent?: unknown }) => Text} Custom result renderer.
653
+ * @satisfies REQ-210
654
+ */
655
+ function buildStructuredToolRenderResult(toolName: string) {
656
+ return (
657
+ result: MonolithicToolRenderResult,
658
+ options: { expanded?: boolean; isPartial?: boolean },
659
+ _theme: unknown,
660
+ context: { args?: Record<string, unknown>; lastComponent?: unknown },
661
+ ): Text => {
662
+ const textComponent = context.lastComponent instanceof Text
663
+ ? context.lastComponent
664
+ : new Text("", 0, 0);
665
+ if (options.isPartial) {
666
+ textComponent.setText(`… ${toolName}`);
667
+ return textComponent;
668
+ }
669
+ const compactText = summarizeStructuredToolResult(toolName, result, context.args);
670
+ const fullText = getMonolithicToolText(result);
671
+ textComponent.setText(options.expanded && fullText !== "" ? fullText : compactText);
672
+ return textComponent;
673
+ };
674
+ }
675
+
676
+ /**
677
+ * @brief Executes one CLI-style runner for a monolithic agent tool.
678
+ * @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.
679
+ * @param[in] operation {() => ToolResult} Runner callback.
680
+ * @return {ReturnType<typeof buildMonolithicToolExecuteResult>} Monolithic tool execute result.
681
+ */
682
+ function executeMonolithicTool(operation: () => ToolResult): ReturnType<typeof buildMonolithicToolExecuteResult> {
683
+ try {
684
+ return buildMonolithicToolExecuteResult(operation());
685
+ } catch (error) {
686
+ return buildMonolithicToolExecuteResult(normalizeToolFailure(error));
687
+ }
688
+ }
689
+
690
+ /**
691
+ * @brief Starts delivery of one rendered prompt into the current active session.
692
+ * @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.
693
+ * @param[in] pi {ExtensionAPI} Handler-scoped extension API instance retained as the fallback dispatcher.
509
694
  * @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
695
+ * @param[in] context {unknown} Optional replacement-session helper context.
696
+ * @return {Promise<void>} Promise representing eventual prompt-delivery completion.
697
+ * @satisfies REQ-004, REQ-067, REQ-068, REQ-227, REQ-281
512
698
  */
513
- async function deliverPromptCommand(pi: ExtensionAPI, content: string): Promise<void> {
699
+ function deliverPromptCommand(
700
+ pi: ExtensionAPI,
701
+ content: string,
702
+ context?: unknown,
703
+ ): Promise<void> {
704
+ const replacementContext = context as {
705
+ sendUserMessage?: (message: string) => Promise<void> | void;
706
+ } | undefined;
707
+ if (typeof replacementContext?.sendUserMessage === "function") {
708
+ return Promise.resolve(replacementContext.sendUserMessage(content)).catch((error) => {
709
+ if (isStaleExtensionContextError(error)) {
710
+ return;
711
+ }
712
+ throw error;
713
+ });
714
+ }
514
715
  pi.sendUserMessage(content);
716
+ return Promise.resolve();
717
+ }
718
+
719
+ /**
720
+ * @brief Detects prompt-delivery failures that can be ignored after prompt ownership has moved past the command handler.
721
+ * @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.
722
+ * @param[in] error {unknown} Candidate prompt-delivery failure.
723
+ * @param[in] workflowState {import("./core/extension-status.js").PiUsereqWorkflowState} Current shared prompt workflow state.
724
+ * @param[in] executionPlan {PromptCommandExecutionPlan | undefined} Prepared execution plan for the current prompt command.
725
+ * @return {boolean} `true` when the failure is a late stale-context delivery rejection that MUST be ignored.
726
+ * @satisfies REQ-208, REQ-280, REQ-281, REQ-282
727
+ */
728
+ function shouldIgnoreLatePromptDeliveryFailure(
729
+ error: unknown,
730
+ workflowState: import("./core/extension-status.js").PiUsereqWorkflowState,
731
+ executionPlan?: PromptCommandExecutionPlan,
732
+ ): boolean {
733
+ const persistedRuntimeState = readPersistedPromptCommandRuntimeState();
734
+ const effectiveWorkflowState = persistedRuntimeState.workflowState || workflowState;
735
+ const activeExecutionSessionFile = persistedRuntimeState.activePromptRequest?.executionSessionFile;
736
+ const promptRunAlreadyActive = executionPlan !== undefined
737
+ && typeof activeExecutionSessionFile === "string"
738
+ && path.resolve(activeExecutionSessionFile) === path.resolve(executionPlan.executionSessionFile);
739
+ return isStaleExtensionContextError(error)
740
+ && (
741
+ promptRunAlreadyActive
742
+ || (effectiveWorkflowState !== "checking" && effectiveWorkflowState !== "running")
743
+ );
744
+ }
745
+
746
+ /**
747
+ * @brief Appends one workflow-state debug entry for a bundled prompt when selected.
748
+ * @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.
749
+ * @param[in] projectBase {string} Absolute original project base path.
750
+ * @param[in] config {UseReqConfig} Effective project configuration.
751
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
752
+ * @param[in] previousState {string} Prior workflow state.
753
+ * @param[in] nextState {import("./core/debug-runtime.js").DebugWorkflowState} Next workflow state.
754
+ * @return {void} No return value.
755
+ * @satisfies REQ-245, REQ-246, REQ-247
756
+ */
757
+ function logPromptWorkflowStateChange(
758
+ projectBase: string,
759
+ config: UseReqConfig,
760
+ promptName: import("./core/prompt-command-catalog.js").PromptCommandName,
761
+ previousState: string,
762
+ nextState: import("./core/debug-runtime.js").DebugWorkflowState,
763
+ ): void {
764
+ if (!shouldLogDebugPromptWorkflowState(config, nextState, promptName)) {
765
+ return;
766
+ }
767
+ logDebugPromptEvent(
768
+ projectBase,
769
+ config,
770
+ nextState,
771
+ promptName,
772
+ "workflow_state",
773
+ { from_state: previousState, to_state: nextState },
774
+ { success: true, workflow_state: nextState },
775
+ );
776
+ }
777
+
778
+ /**
779
+ * @brief Appends one dedicated prompt workflow debug entry when selected.
780
+ * @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.
781
+ * @param[in] projectBase {string} Absolute original project base path.
782
+ * @param[in] config {UseReqConfig} Effective project configuration.
783
+ * @param[in] workflowState {DebugWorkflowState} Current workflow state.
784
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
785
+ * @param[in] action {string} Workflow debug action identifier.
786
+ * @param[in] input {unknown} Optional workflow debug input payload.
787
+ * @param[in] result {unknown} Optional workflow debug result payload.
788
+ * @param[in] isError {boolean} Workflow debug error flag.
789
+ * @return {void} No return value.
790
+ * @satisfies REQ-245, REQ-246, REQ-247, REQ-277
791
+ */
792
+ function logPromptWorkflowEvent(
793
+ projectBase: string,
794
+ config: UseReqConfig,
795
+ workflowState: DebugWorkflowState,
796
+ promptName: import("./core/prompt-command-catalog.js").PromptCommandName,
797
+ action: string,
798
+ input?: unknown,
799
+ result?: unknown,
800
+ isError = false,
801
+ ): void {
802
+ logDebugPromptWorkflowEvent(
803
+ projectBase,
804
+ config,
805
+ workflowState,
806
+ promptName,
807
+ action,
808
+ input,
809
+ result,
810
+ isError,
811
+ );
812
+ }
813
+
814
+ /**
815
+ * @brief Transitions one prompt workflow state and logs the transition immediately after the state update.
816
+ * @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.
817
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
818
+ * @param[in] ctx {ExtensionContext | ExtensionCommandContext} Active extension context.
819
+ * @param[in] projectBase {string} Absolute original project base path.
820
+ * @param[in] config {UseReqConfig} Effective project configuration.
821
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
822
+ * @param[in] nextState {DebugWorkflowState} Next workflow state.
823
+ * @return {void} No return value.
824
+ */
825
+ function transitionPromptWorkflowState(
826
+ statusController: PiUsereqStatusController,
827
+ ctx: ExtensionContext | ExtensionCommandContext,
828
+ projectBase: string,
829
+ config: UseReqConfig,
830
+ promptName: import("./core/prompt-command-catalog.js").PromptCommandName,
831
+ nextState: DebugWorkflowState,
832
+ ): void {
833
+ const previousState = statusController.state.workflowState;
834
+ setPiUsereqWorkflowState(statusController, nextState, ctx);
835
+ if (previousState !== nextState) {
836
+ logPromptWorkflowStateChange(projectBase, config, promptName, previousState, nextState);
837
+ }
838
+ }
839
+
840
+ /**
841
+ * @brief Resolves the runtime slash-command description for one bundled prompt.
842
+ * @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.
843
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
844
+ * @return {string} Runtime command description.
845
+ */
846
+ function resolvePromptCommandDescription(
847
+ promptName: import("./core/prompt-command-catalog.js").PromptCommandName,
848
+ ): string {
849
+ return readBundledPromptDescription(promptName) || `Run pi-usereq prompt ${promptName}`;
850
+ }
851
+
852
+ /**
853
+ * @brief Resolves the original project base used for debug-log file writes.
854
+ * @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.
855
+ * @param[in] cwd {string} Current extension working directory.
856
+ * @param[in] statusController {PiUsereqStatusController} Mutable status controller.
857
+ * @return {string} Absolute original project base path for debug logging.
858
+ */
859
+ function resolveDebugProjectBase(cwd: string, statusController: PiUsereqStatusController): string {
860
+ return statusController.state.activePromptRequest?.basePath
861
+ ?? statusController.state.pendingPromptRequest?.basePath
862
+ ?? getProjectBase(resolveLiveBootstrapCwd(cwd));
863
+ }
864
+
865
+ /**
866
+ * @brief Delivers one best-effort UI notification without failing on stale replacement contexts.
867
+ * @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.
868
+ * @param[in] ctx {ExtensionContext | ExtensionCommandContext | undefined} Candidate UI context.
869
+ * @param[in] message {string} Notification message.
870
+ * @param[in] level {"info" | "error"} Notification severity.
871
+ * @return {boolean} `true` when the notification was delivered and `false` when the context was already stale.
872
+ * @satisfies REQ-280
873
+ */
874
+ function notifyContextSafely(
875
+ ctx: (ExtensionContext | ExtensionCommandContext) | undefined,
876
+ message: string,
877
+ level: "info" | "error",
878
+ ): boolean {
879
+ if (ctx === undefined) {
880
+ return false;
881
+ }
882
+ try {
883
+ ctx.ui.notify(message, level);
884
+ return true;
885
+ } catch (error) {
886
+ if (isStaleExtensionContextError(error)) {
887
+ return false;
888
+ }
889
+ throw error;
890
+ }
515
891
  }
516
892
 
517
893
  /**
518
894
  * @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.
895
+ * @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
896
  * @param[in] pi {ExtensionAPI} Active extension API instance.
521
897
  * @return {ToolInfo[]} Sorted configurable tool descriptors.
522
- * @satisfies REQ-007, REQ-063
898
+ * @satisfies REQ-007, REQ-063, REQ-231, REQ-232
523
899
  */
524
900
  function getPiUsereqStartupTools(pi: ExtensionAPI): ToolInfo[] {
525
901
  return pi.getAllTools()
526
902
  .filter((tool) => PI_USEREQ_STARTUP_TOOL_SET.has(tool.name))
527
903
  .filter((tool) => !isPiUsereqEmbeddedToolName(tool.name) || tool.sourceInfo?.source === "builtin")
528
- .sort((left, right) => left.name.localeCompare(right.name));
904
+ .sort((left, right) => comparePiUsereqStartupToolNames(
905
+ left.name as PiUsereqStartupToolName,
906
+ right.name as PiUsereqStartupToolName,
907
+ ));
529
908
  }
530
909
 
531
910
  /**
@@ -569,22 +948,14 @@ function applyConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig): v
569
948
 
570
949
  /**
571
950
  * @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.
951
+ * @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
952
  * @param[in] pi {ExtensionAPI} Active extension API instance.
582
953
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
583
954
  * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
584
955
  * @param[in] event {unknown} Hook payload forwarded by pi.
585
956
  * @param[in] ctx {ExtensionContext} Active extension context.
586
957
  * @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
958
+ * @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
959
  */
589
960
  async function handleExtensionStatusEvent(
590
961
  pi: ExtensionAPI,
@@ -593,23 +964,65 @@ async function handleExtensionStatusEvent(
593
964
  event: unknown,
594
965
  ctx: ExtensionContext,
595
966
  ): 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
- }
967
+ const pendingPromptRequest = statusController.state.pendingPromptRequest;
968
+ const activePromptRequest = statusController.state.activePromptRequest;
969
+ const shutdownPromptRequest = hookName === "session_shutdown"
970
+ ? activePromptRequest ?? pendingPromptRequest
605
971
  : undefined;
972
+ const preservePromptCommandStateOnShutdown = hookName === "session_shutdown"
973
+ ? shouldPreservePromptCommandStateOnShutdown(statusController.state)
974
+ : false;
606
975
  if (hookName === "session_start") {
607
976
  ensureBundledResourcesAccessible();
608
- const config = loadProjectConfig(ctx.cwd);
977
+ const startupCwd = resolveLiveBootstrapCwd(ctx.cwd);
978
+ syncContextCwdMirror(ctx, startupCwd);
979
+ bootstrapRuntimePathState(startupCwd, {
980
+ gitPath: resolveRuntimeGitPath(startupCwd),
981
+ });
982
+ const config = loadProjectConfig(startupCwd);
609
983
  applyConfiguredPiUsereqTools(pi, config);
610
984
  setPiUsereqStatusConfig(statusController, config);
611
985
  }
986
+ if (hookName === "before_agent_start") {
987
+ const requestForActivation = activePromptRequest ?? pendingPromptRequest;
988
+ if (requestForActivation !== undefined) {
989
+ await activatePromptCommandExecution(requestForActivation, ctx);
990
+ }
991
+ }
992
+ const notifyRequest: PiNotifyEventRequest | undefined = hookName === "agent_end"
993
+ && activePromptRequest !== undefined
994
+ && statusController.state.runStartTimeMs !== undefined
995
+ ? {
996
+ promptName: activePromptRequest.promptName,
997
+ promptArgs: activePromptRequest.promptArgs,
998
+ basePath: activePromptRequest.basePath,
999
+ completionTimeMs: Math.max(0, Date.now() - statusController.state.runStartTimeMs),
1000
+ }
1001
+ : undefined;
612
1002
  updateExtensionStatus(statusController, hookName, event, ctx);
1003
+ if (hookName === "tool_result" && statusController.config) {
1004
+ const toolEvent = event as {
1005
+ toolName?: string;
1006
+ input?: unknown;
1007
+ content?: unknown;
1008
+ details?: unknown;
1009
+ isError?: boolean;
1010
+ };
1011
+ if (typeof toolEvent.toolName === "string") {
1012
+ logDebugToolExecution(
1013
+ resolveDebugProjectBase(ctx.cwd, statusController),
1014
+ statusController.config,
1015
+ statusController.state.workflowState,
1016
+ toolEvent.toolName,
1017
+ toolEvent.input,
1018
+ {
1019
+ content: toolEvent.content,
1020
+ details: toolEvent.details,
1021
+ },
1022
+ toolEvent.isError === true,
1023
+ );
1024
+ }
1025
+ }
613
1026
  if (hookName === "agent_end") {
614
1027
  if (statusController.config) {
615
1028
  runPiNotifyEffects(
@@ -618,9 +1031,190 @@ async function handleExtensionStatusEvent(
618
1031
  notifyRequest,
619
1032
  );
620
1033
  }
621
- statusController.state.activePromptRequest = undefined;
1034
+ if (activePromptRequest !== undefined) {
1035
+ const debugConfig = statusController.config;
1036
+ let promptContext = ctx;
1037
+ const outcome = classifyPromptCommandOutcome(
1038
+ event as { messages: AgentEndEvent["messages"] },
1039
+ );
1040
+ const closureFailureMessage = outcome !== "completed"
1041
+ && activePromptRequest.worktreeDir !== undefined
1042
+ ? `ERROR: Prompt closure retained worktree ${activePromptRequest.worktreeDir} after ${outcome} outcome.`
1043
+ : undefined;
1044
+ const shouldFinalizeMatchedSuccess = outcome === "completed"
1045
+ && statusController.state.workflowState === "running"
1046
+ && activePromptRequest.worktreeDir !== undefined;
1047
+ if (debugConfig) {
1048
+ logPromptWorkflowEvent(
1049
+ activePromptRequest.basePath,
1050
+ debugConfig,
1051
+ statusController.state.workflowState,
1052
+ activePromptRequest.promptName,
1053
+ "workflow_closure",
1054
+ {
1055
+ hook: "agent_end",
1056
+ outcome,
1057
+ },
1058
+ {
1059
+ should_finalize_matched_success: shouldFinalizeMatchedSuccess,
1060
+ worktree_dir: activePromptRequest.worktreeDir,
1061
+ event_context_has_switch_session: typeof (ctx as ExtensionCommandContext).switchSession === "function",
1062
+ },
1063
+ );
1064
+ }
1065
+ if (shouldFinalizeMatchedSuccess) {
1066
+ if (debugConfig) {
1067
+ transitionPromptWorkflowState(
1068
+ statusController,
1069
+ promptContext,
1070
+ activePromptRequest.basePath,
1071
+ debugConfig,
1072
+ activePromptRequest.promptName,
1073
+ "merging",
1074
+ );
1075
+ } else {
1076
+ setPiUsereqWorkflowState(statusController, "merging", promptContext);
1077
+ }
1078
+ let finalization:
1079
+ | {
1080
+ mergeAttempted: boolean;
1081
+ mergeSucceeded: boolean;
1082
+ cleanupSucceeded: boolean;
1083
+ errorMessage?: string;
1084
+ activeContext?: unknown;
1085
+ }
1086
+ | undefined;
1087
+ try {
1088
+ finalization = await finalizePromptCommandExecution(
1089
+ activePromptRequest,
1090
+ promptContext,
1091
+ debugConfig
1092
+ ? { config: debugConfig, workflowState: statusController.state.workflowState }
1093
+ : undefined,
1094
+ );
1095
+ promptContext = (finalization.activeContext ?? promptContext) as typeof ctx;
1096
+ } catch (error) {
1097
+ promptContext = (getPromptCommandErrorContext(error) ?? promptContext) as typeof ctx;
1098
+ let errorMessage = error instanceof Error ? error.message : String(error);
1099
+ let cleanupSucceeded = false;
1100
+ try {
1101
+ promptContext = (await restorePromptCommandExecution(
1102
+ activePromptRequest,
1103
+ promptContext,
1104
+ debugConfig
1105
+ ? { config: debugConfig, workflowState: statusController.state.workflowState }
1106
+ : undefined,
1107
+ ) ?? promptContext) as typeof ctx;
1108
+ cleanupSucceeded = true;
1109
+ } catch (restoreError) {
1110
+ promptContext = (getPromptCommandErrorContext(restoreError) ?? promptContext) as typeof ctx;
1111
+ errorMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
1112
+ }
1113
+ finalization = {
1114
+ mergeAttempted: false,
1115
+ mergeSucceeded: false,
1116
+ cleanupSucceeded,
1117
+ errorMessage,
1118
+ };
1119
+ }
1120
+ if (
1121
+ finalization.errorMessage
1122
+ && (!finalization.cleanupSucceeded || !finalization.mergeSucceeded)
1123
+ ) {
1124
+ if (debugConfig) {
1125
+ transitionPromptWorkflowState(
1126
+ statusController,
1127
+ promptContext,
1128
+ activePromptRequest.basePath,
1129
+ debugConfig,
1130
+ activePromptRequest.promptName,
1131
+ "error",
1132
+ );
1133
+ } else {
1134
+ setPiUsereqWorkflowState(statusController, "error", promptContext);
1135
+ }
1136
+ notifyContextSafely(promptContext, finalization.errorMessage, "error");
1137
+ }
1138
+ } else {
1139
+ try {
1140
+ promptContext = (await restorePromptCommandExecution(
1141
+ activePromptRequest,
1142
+ promptContext,
1143
+ debugConfig
1144
+ ? { config: debugConfig, workflowState: statusController.state.workflowState }
1145
+ : undefined,
1146
+ ) ?? promptContext) as typeof ctx;
1147
+ if (closureFailureMessage !== undefined) {
1148
+ notifyContextSafely(promptContext, closureFailureMessage, "error");
1149
+ }
1150
+ } catch (error) {
1151
+ promptContext = (getPromptCommandErrorContext(error) ?? promptContext) as typeof ctx;
1152
+ if (debugConfig) {
1153
+ transitionPromptWorkflowState(
1154
+ statusController,
1155
+ promptContext,
1156
+ activePromptRequest.basePath,
1157
+ debugConfig,
1158
+ activePromptRequest.promptName,
1159
+ "error",
1160
+ );
1161
+ } else {
1162
+ setPiUsereqWorkflowState(statusController, "error", promptContext);
1163
+ }
1164
+ notifyContextSafely(promptContext, error instanceof Error ? error.message : String(error), "error");
1165
+ }
1166
+ }
1167
+ statusController.state.pendingPromptRequest = undefined;
1168
+ statusController.state.activePromptRequest = undefined;
1169
+ if (debugConfig) {
1170
+ transitionPromptWorkflowState(
1171
+ statusController,
1172
+ promptContext,
1173
+ activePromptRequest.basePath,
1174
+ debugConfig,
1175
+ activePromptRequest.promptName,
1176
+ "idle",
1177
+ );
1178
+ } else {
1179
+ setPiUsereqWorkflowState(statusController, "idle", promptContext);
1180
+ }
1181
+ }
622
1182
  }
623
1183
  if (hookName === "session_shutdown") {
1184
+ if (shutdownPromptRequest !== undefined && statusController.config) {
1185
+ logPromptWorkflowEvent(
1186
+ shutdownPromptRequest.basePath,
1187
+ statusController.config,
1188
+ preservePromptCommandStateOnShutdown
1189
+ ? statusController.state.workflowState
1190
+ : "idle",
1191
+ shutdownPromptRequest.promptName,
1192
+ "workflow_session_shutdown",
1193
+ {
1194
+ reason: (event as { reason?: unknown }).reason,
1195
+ target_session_file: (event as { targetSessionFile?: unknown }).targetSessionFile,
1196
+ },
1197
+ {
1198
+ preserve_prompt_command_state: preservePromptCommandStateOnShutdown,
1199
+ pending_prompt_request: pendingPromptRequest !== undefined,
1200
+ active_prompt_request: activePromptRequest !== undefined,
1201
+ },
1202
+ );
1203
+ }
1204
+ if (!preservePromptCommandStateOnShutdown) {
1205
+ if (shutdownPromptRequest !== undefined && statusController.config) {
1206
+ transitionPromptWorkflowState(
1207
+ statusController,
1208
+ ctx,
1209
+ shutdownPromptRequest.basePath,
1210
+ statusController.config,
1211
+ shutdownPromptRequest.promptName,
1212
+ "idle",
1213
+ );
1214
+ } else {
1215
+ setPiUsereqWorkflowState(statusController, "idle", ctx);
1216
+ }
1217
+ }
624
1218
  disposePiUsereqStatusController(statusController);
625
1219
  }
626
1220
  }
@@ -629,8 +1223,11 @@ async function handleExtensionStatusEvent(
629
1223
  * @brief Registers shared wrappers for every supported pi lifecycle hook.
630
1224
  * @details Installs one generic wrapper per intercepted hook so every resource,
631
1225
  * 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.
1226
+ * same extension-status update pipeline. The wrapper suppresses the documented
1227
+ * stale-extension-context error because pi can continue delivering late
1228
+ * lifecycle callbacks against contexts invalidated by session replacement after
1229
+ * prompt orchestration has already completed successfully. Runtime is O(h) in
1230
+ * registered hook count. Side effects include hook registration.
634
1231
  * @param[in] pi {ExtensionAPI} Active extension API instance.
635
1232
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
636
1233
  * @return {void} No return value.
@@ -646,7 +1243,13 @@ function registerExtensionStatusHooks(
646
1243
  ) => void;
647
1244
  for (const hookName of PI_USEREQ_STATUS_HOOK_NAMES) {
648
1245
  registerHook(hookName, async (event, ctx) => {
649
- await handleExtensionStatusEvent(pi, statusController, hookName, event, ctx);
1246
+ try {
1247
+ await handleExtensionStatusEvent(pi, statusController, hookName, event, ctx);
1248
+ } catch (error) {
1249
+ if (!isStaleExtensionContextError(error)) {
1250
+ throw error;
1251
+ }
1252
+ }
650
1253
  });
651
1254
  }
652
1255
  }
@@ -664,6 +1267,316 @@ function setConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig, enab
664
1267
  applyConfiguredPiUsereqTools(pi, config);
665
1268
  }
666
1269
 
1270
+ /**
1271
+ * @brief Returns the canonical debug-tool toggle order.
1272
+ * @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.
1273
+ * @return {PiUsereqStartupToolName[]} Ordered debug-tool toggle names.
1274
+ * @satisfies REQ-242
1275
+ */
1276
+ function getDebugToolToggleNames(): PiUsereqStartupToolName[] {
1277
+ return [...PI_USEREQ_CUSTOM_TOOL_NAMES, ...PI_USEREQ_EMBEDDED_TOOL_NAMES].sort(comparePiUsereqStartupToolNames);
1278
+ }
1279
+
1280
+ /**
1281
+ * @brief Restores the debug configuration subtree to its documented defaults.
1282
+ * @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`.
1283
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
1284
+ * @return {void} No return value.
1285
+ * @satisfies REQ-236, REQ-237, REQ-238, REQ-239, REQ-195, REQ-277
1286
+ */
1287
+ function resetDebugConfigToDefaults(config: UseReqConfig): void {
1288
+ config.DEBUG_ENABLED = "disable";
1289
+ config.DEBUG_LOG_FILE = DEFAULT_DEBUG_LOG_FILE;
1290
+ config.DEBUG_STATUS_CHANGES = DEFAULT_DEBUG_STATUS_CHANGES;
1291
+ config.DEBUG_WORKFLOW_EVENTS = DEFAULT_DEBUG_WORKFLOW_EVENTS;
1292
+ config.DEBUG_LOG_ON_STATUS = DEFAULT_DEBUG_LOG_ON_STATUS;
1293
+ config.DEBUG_ENABLED_TOOLS = [];
1294
+ config.DEBUG_ENABLED_PROMPTS = [];
1295
+ }
1296
+
1297
+ /**
1298
+ * @brief Formats the top-level Debug summary value.
1299
+ * @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.
1300
+ * @param[in] config {UseReqConfig} Effective project configuration.
1301
+ * @return {string} Compact debug summary string.
1302
+ */
1303
+ function formatDebugMenuSummary(config: UseReqConfig): string {
1304
+ const toolCount = normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS).length;
1305
+ const promptCount = normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS).length;
1306
+ return config.DEBUG_ENABLED === "enable"
1307
+ ? `enable • ${toolCount} tools • ${promptCount} prompts`
1308
+ : "disable";
1309
+ }
1310
+
1311
+ /**
1312
+ * @brief Builds one debug-menu row with optional disabled styling.
1313
+ * @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.
1314
+ * @param[in] choice {PiUsereqSettingsMenuChoice} Base debug-menu row.
1315
+ * @param[in] debugEnabled {boolean} Whether global debug is enabled.
1316
+ * @return {PiUsereqSettingsMenuChoice} Styled debug-menu row.
1317
+ * @satisfies REQ-241
1318
+ */
1319
+ function buildDebugMenuChoice(
1320
+ choice: PiUsereqSettingsMenuChoice,
1321
+ debugEnabled: boolean,
1322
+ ): PiUsereqSettingsMenuChoice {
1323
+ if (debugEnabled || choice.id === "debug-enabled") {
1324
+ return choice;
1325
+ }
1326
+ return {
1327
+ ...choice,
1328
+ disabled: true,
1329
+ labelTone: "dim",
1330
+ valueTone: "dim",
1331
+ };
1332
+ }
1333
+
1334
+ /**
1335
+ * @brief Opens the workflow-state filter selector used by the Debug submenu.
1336
+ * @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.
1337
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
1338
+ * @param[in] currentValue {DebugLogOnStatus} Current persisted workflow-state filter.
1339
+ * @return {Promise<DebugLogOnStatus | undefined>} Selected workflow-state filter or `undefined` when cancelled.
1340
+ */
1341
+ async function selectDebugLogOnStatus(
1342
+ ctx: ExtensionCommandContext,
1343
+ currentValue: DebugLogOnStatus,
1344
+ ): Promise<DebugLogOnStatus | undefined> {
1345
+ const choice = await showPiUsereqSettingsMenu(ctx, "Log on status", [
1346
+ {
1347
+ id: "any",
1348
+ label: "any",
1349
+ value: currentValue === "any" ? "selected" : "",
1350
+ description: "Write matching debug entries regardless of the current workflow state.",
1351
+ },
1352
+ ...DEBUG_WORKFLOW_STATES.map((workflowState) => ({
1353
+ id: workflowState,
1354
+ label: workflowState,
1355
+ value: currentValue === workflowState ? "selected" : "",
1356
+ description: `Write matching debug entries only while workflow state is ${workflowState}.`,
1357
+ })),
1358
+ ...buildTerminalSettingsMenuChoices({
1359
+ resetDefaultsValue: DEFAULT_DEBUG_LOG_ON_STATUS,
1360
+ resetDefaultsDescription: "Restore the documented default workflow-state filter.",
1361
+ }),
1362
+ ], { initialSelectedId: currentValue });
1363
+ if (!choice) {
1364
+ return undefined;
1365
+ }
1366
+ if (choice === "reset-defaults") {
1367
+ return DEFAULT_DEBUG_LOG_ON_STATUS;
1368
+ }
1369
+ return normalizeDebugLogOnStatus(choice);
1370
+ }
1371
+
1372
+ /**
1373
+ * @brief Builds the shared settings-menu choices for debug logging configuration.
1374
+ * @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.
1375
+ * @param[in] config {UseReqConfig} Effective project configuration.
1376
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered debug-menu choices.
1377
+ * @satisfies REQ-240, REQ-241, REQ-242, REQ-243, REQ-193, REQ-277
1378
+ */
1379
+ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1380
+ const debugEnabled = config.DEBUG_ENABLED === "enable";
1381
+ const enabledTools = new Set(normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS));
1382
+ const enabledPrompts = new Set(normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS));
1383
+ return [
1384
+ {
1385
+ id: "debug-enabled",
1386
+ label: "Debug",
1387
+ value: config.DEBUG_ENABLED,
1388
+ description: "Enable or disable all debug logging behavior and unlock the remaining Debug rows.",
1389
+ },
1390
+ buildDebugMenuChoice(
1391
+ {
1392
+ id: "debug-log-file",
1393
+ label: "Log file",
1394
+ value: config.DEBUG_LOG_FILE,
1395
+ description: "Edit the JSON debug log file path. Relative paths resolve against the original project base.",
1396
+ },
1397
+ debugEnabled,
1398
+ ),
1399
+ buildDebugMenuChoice(
1400
+ {
1401
+ id: "debug-log-on-status",
1402
+ label: "Log on status",
1403
+ value: config.DEBUG_LOG_ON_STATUS,
1404
+ description: "Select whether matching debug entries are written for one explicit workflow state or for any workflow state.",
1405
+ },
1406
+ debugEnabled,
1407
+ ),
1408
+ buildDebugMenuChoice(
1409
+ {
1410
+ id: "debug-status-changes",
1411
+ label: "Status changes",
1412
+ value: normalizeDebugStatusChanges(config.DEBUG_STATUS_CHANGES),
1413
+ description: "Enable or disable `workflow_state` debug entries for prompt-orchestration transitions.",
1414
+ },
1415
+ debugEnabled,
1416
+ ),
1417
+ buildDebugMenuChoice(
1418
+ {
1419
+ id: "debug-workflow-events",
1420
+ label: "Workflow events",
1421
+ value: normalizeDebugWorkflowEvents(config.DEBUG_WORKFLOW_EVENTS),
1422
+ description: "Enable or disable dedicated workflow debug entries for activation, restoration, closure, and session-shutdown diagnostics.",
1423
+ },
1424
+ debugEnabled,
1425
+ ),
1426
+ ...getDebugToolToggleNames().map((toolName) => buildDebugMenuChoice(
1427
+ {
1428
+ id: `debug-tool:${toolName}`,
1429
+ label: toolName,
1430
+ value: enabledTools.has(toolName) ? "enable" : "disable",
1431
+ description: PI_USEREQ_CUSTOM_TOOL_NAMES.includes(toolName as never)
1432
+ ? `Toggle debug logging for custom tool ${toolName}.`
1433
+ : `Toggle debug logging for embedded tool ${toolName}.`,
1434
+ },
1435
+ debugEnabled,
1436
+ )),
1437
+ ...DEBUG_PROMPT_NAMES.map((promptName) => buildDebugMenuChoice(
1438
+ {
1439
+ id: `debug-prompt:${promptName}`,
1440
+ label: promptName,
1441
+ value: enabledPrompts.has(promptName) ? "enable" : "disable",
1442
+ description: `Toggle prompt-orchestration debug logging for /${promptName}.`,
1443
+ },
1444
+ debugEnabled,
1445
+ )),
1446
+ ...buildTerminalSettingsMenuChoices({
1447
+ resetDefaultsValue: DEFAULT_DEBUG_LOG_ON_STATUS,
1448
+ resetDefaultsDescription: "Restore the documented default Debug configuration.",
1449
+ }),
1450
+ ];
1451
+ }
1452
+
1453
+ /**
1454
+ * @brief Runs the interactive Debug submenu.
1455
+ * @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.
1456
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
1457
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
1458
+ * @return {Promise<void>} Promise resolved when the submenu closes.
1459
+ * @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
1460
+ */
1461
+ async function configureDebugMenu(
1462
+ ctx: ExtensionCommandContext,
1463
+ config: UseReqConfig,
1464
+ onConfigChange: () => void,
1465
+ ): Promise<void> {
1466
+ let focusedChoiceId: string | undefined;
1467
+ while (true) {
1468
+ const choice = await showPiUsereqSettingsMenu(ctx, "Debug", buildDebugMenuChoices(config), {
1469
+ initialSelectedId: focusedChoiceId,
1470
+ });
1471
+ if (!choice) {
1472
+ return;
1473
+ }
1474
+ focusedChoiceId = choice;
1475
+
1476
+ if (choice === "debug-enabled") {
1477
+ config.DEBUG_ENABLED = config.DEBUG_ENABLED === "enable" ? "disable" : "enable";
1478
+ onConfigChange();
1479
+ ctx.ui.notify(`Debug ${config.DEBUG_ENABLED}`, "info");
1480
+ continue;
1481
+ }
1482
+ if (choice === "reset-defaults") {
1483
+ const resetPreview: ResetConfirmationChange[] = [
1484
+ { label: "Debug", previousValue: config.DEBUG_ENABLED, nextValue: "disable" },
1485
+ { label: "Log file", previousValue: config.DEBUG_LOG_FILE, nextValue: DEFAULT_DEBUG_LOG_FILE },
1486
+ { label: "Status changes", previousValue: normalizeDebugStatusChanges(config.DEBUG_STATUS_CHANGES), nextValue: DEFAULT_DEBUG_STATUS_CHANGES },
1487
+ { label: "Workflow events", previousValue: normalizeDebugWorkflowEvents(config.DEBUG_WORKFLOW_EVENTS), nextValue: DEFAULT_DEBUG_WORKFLOW_EVENTS },
1488
+ { label: "Log on status", previousValue: config.DEBUG_LOG_ON_STATUS, nextValue: DEFAULT_DEBUG_LOG_ON_STATUS },
1489
+ { label: "Enabled debug tools", previousValue: String(normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS).length), nextValue: "0" },
1490
+ { label: "Enabled debug prompts", previousValue: String(normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS).length), nextValue: "0" },
1491
+ ].filter((change) => change.previousValue !== change.nextValue);
1492
+ const approved = await confirmResetChanges(
1493
+ ctx,
1494
+ "Confirm Debug reset",
1495
+ resetPreview,
1496
+ "Approve restoring the documented default Debug configuration.",
1497
+ "Abort the Debug reset and keep the current values.",
1498
+ );
1499
+ if (!approved) {
1500
+ ctx.ui.notify("Aborted Debug reset", "info");
1501
+ continue;
1502
+ }
1503
+ resetDebugConfigToDefaults(config);
1504
+ onConfigChange();
1505
+ ctx.ui.notify("Restored default Debug configuration", "info");
1506
+ continue;
1507
+ }
1508
+ if (config.DEBUG_ENABLED !== "enable") {
1509
+ ctx.ui.notify("Debug rows are locked while Debug is disabled", "info");
1510
+ continue;
1511
+ }
1512
+ if (choice === "debug-log-file") {
1513
+ const value = await ctx.ui.input("Log file", config.DEBUG_LOG_FILE);
1514
+ if (value !== undefined) {
1515
+ config.DEBUG_LOG_FILE = normalizeDebugLogFile(value);
1516
+ onConfigChange();
1517
+ ctx.ui.notify(`Debug log file set to ${config.DEBUG_LOG_FILE}`, "info");
1518
+ }
1519
+ continue;
1520
+ }
1521
+ if (choice === "debug-status-changes") {
1522
+ config.DEBUG_STATUS_CHANGES = normalizeDebugStatusChanges(
1523
+ config.DEBUG_STATUS_CHANGES === "enable" ? "disable" : "enable",
1524
+ );
1525
+ onConfigChange();
1526
+ ctx.ui.notify(`Debug status-change logging ${config.DEBUG_STATUS_CHANGES}`, "info");
1527
+ continue;
1528
+ }
1529
+ if (choice === "debug-workflow-events") {
1530
+ config.DEBUG_WORKFLOW_EVENTS = normalizeDebugWorkflowEvents(
1531
+ config.DEBUG_WORKFLOW_EVENTS === "enable" ? "disable" : "enable",
1532
+ );
1533
+ onConfigChange();
1534
+ ctx.ui.notify(`Debug workflow-event logging ${config.DEBUG_WORKFLOW_EVENTS}`, "info");
1535
+ continue;
1536
+ }
1537
+ if (choice === "debug-log-on-status") {
1538
+ const selectedStatus = await selectDebugLogOnStatus(ctx, config.DEBUG_LOG_ON_STATUS);
1539
+ if (selectedStatus !== undefined) {
1540
+ config.DEBUG_LOG_ON_STATUS = selectedStatus;
1541
+ onConfigChange();
1542
+ ctx.ui.notify(`Debug workflow filter set to ${config.DEBUG_LOG_ON_STATUS}`, "info");
1543
+ }
1544
+ continue;
1545
+ }
1546
+ if (choice.startsWith("debug-tool:")) {
1547
+ const toolName = choice.slice("debug-tool:".length);
1548
+ const enabledTools = new Set(normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS));
1549
+ if (enabledTools.has(toolName as PiUsereqStartupToolName)) {
1550
+ enabledTools.delete(toolName as PiUsereqStartupToolName);
1551
+ } else {
1552
+ enabledTools.add(toolName as PiUsereqStartupToolName);
1553
+ }
1554
+ config.DEBUG_ENABLED_TOOLS = getDebugToolToggleNames().filter((name) => enabledTools.has(name));
1555
+ onConfigChange();
1556
+ ctx.ui.notify(
1557
+ `${enabledTools.has(toolName as PiUsereqStartupToolName) ? "Enabled" : "Disabled"} debug logging for ${toolName}`,
1558
+ "info",
1559
+ );
1560
+ continue;
1561
+ }
1562
+ if (choice.startsWith("debug-prompt:")) {
1563
+ const promptName = choice.slice("debug-prompt:".length);
1564
+ const enabledPrompts = new Set(normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS));
1565
+ if (enabledPrompts.has(promptName as (typeof DEBUG_PROMPT_NAMES)[number])) {
1566
+ enabledPrompts.delete(promptName as (typeof DEBUG_PROMPT_NAMES)[number]);
1567
+ } else {
1568
+ enabledPrompts.add(promptName as (typeof DEBUG_PROMPT_NAMES)[number]);
1569
+ }
1570
+ config.DEBUG_ENABLED_PROMPTS = DEBUG_PROMPT_NAMES.filter((name) => enabledPrompts.has(name));
1571
+ onConfigChange();
1572
+ ctx.ui.notify(
1573
+ `${enabledPrompts.has(promptName as (typeof DEBUG_PROMPT_NAMES)[number]) ? "Enabled" : "Disabled"} debug logging for ${promptName}`,
1574
+ "info",
1575
+ );
1576
+ }
1577
+ }
1578
+ }
1579
+
667
1580
  /**
668
1581
  * @brief Represents one persisted boolean notification-setting key.
669
1582
  * @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.
@@ -898,18 +1811,9 @@ function buildPiNotifyEventMenuChoices(
898
1811
  value: config[eventMenu.keys[row.eventId]] ? "on" : "off",
899
1812
  description: `${eventMenu.systemLabel}: ${row.description}`,
900
1813
  })),
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
- },
1814
+ ...buildTerminalSettingsMenuChoices({
1815
+ resetDefaultsDescription: `Restore the documented default ${eventMenu.systemLabel.toLowerCase()} event toggles.`,
1816
+ }),
913
1817
  ];
914
1818
  }
915
1819
 
@@ -961,6 +1865,7 @@ async function configurePiNotifyEventMenu(
961
1865
  ctx: ExtensionCommandContext,
962
1866
  config: UseReqConfig,
963
1867
  eventMenu: PiNotifyEventMenuDefinition,
1868
+ onConfigChange: () => void,
964
1869
  ): Promise<void> {
965
1870
  let focusedChoiceId: string | undefined;
966
1871
  while (true) {
@@ -970,12 +1875,32 @@ async function configurePiNotifyEventMenu(
970
1875
  buildPiNotifyEventMenuChoices(config, eventMenu),
971
1876
  { initialSelectedId: focusedChoiceId },
972
1877
  );
973
- if (!choice || choice === "save-and-close") {
1878
+ if (!choice) {
974
1879
  return;
975
1880
  }
976
1881
  focusedChoiceId = choice;
977
1882
  if (choice === "reset-defaults") {
1883
+ const defaults = getDefaultConfig("");
1884
+ const resetPreview = PI_NOTIFY_EVENT_ROW_DEFINITIONS
1885
+ .map((row) => ({
1886
+ label: row.label,
1887
+ previousValue: config[eventMenu.keys[row.eventId]] ? "on" : "off",
1888
+ nextValue: defaults[eventMenu.keys[row.eventId]] ? "on" : "off",
1889
+ }))
1890
+ .filter((change) => change.previousValue !== change.nextValue);
1891
+ const approved = await confirmResetChanges(
1892
+ ctx,
1893
+ `Confirm ${eventMenu.systemLabel} event reset`,
1894
+ resetPreview,
1895
+ `Approve restoring default ${eventMenu.systemLabel.toLowerCase()} event toggles.`,
1896
+ `Abort the ${eventMenu.systemLabel.toLowerCase()} event reset and keep the current values.`,
1897
+ );
1898
+ if (!approved) {
1899
+ ctx.ui.notify(`Aborted ${eventMenu.systemLabel.toLowerCase()} event reset`, "info");
1900
+ continue;
1901
+ }
978
1902
  resetPiNotifyEventMenuToDefaults(config, eventMenu);
1903
+ onConfigChange();
979
1904
  ctx.ui.notify(
980
1905
  `Restored default ${eventMenu.systemLabel.toLowerCase()} events`,
981
1906
  "info",
@@ -986,6 +1911,7 @@ async function configurePiNotifyEventMenu(
986
1911
  config,
987
1912
  choice as PiNotifyEventBooleanConfigKey,
988
1913
  );
1914
+ onConfigChange();
989
1915
  const eventLabel = resolvePiNotifyEventLabel(
990
1916
  choice as PiNotifyEventBooleanConfigKey,
991
1917
  eventMenu,
@@ -999,18 +1925,24 @@ async function configurePiNotifyEventMenu(
999
1925
 
1000
1926
  /**
1001
1927
  * @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.
1928
+ * @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
1929
  * @param[in] config {UseReqConfig} Effective project configuration.
1004
1930
  * @return {PiUsereqSettingsMenuChoice[]} Ordered direct Pushover rows.
1005
- * @satisfies REQ-163, REQ-165, REQ-172, REQ-184, REQ-185, REQ-198
1931
+ * @satisfies REQ-163, REQ-165, REQ-172, REQ-184, REQ-185, REQ-198, REQ-234, REQ-235
1006
1932
  */
1007
1933
  function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1934
+ const pushoverCredentialsReady = hasPiNotifyPushoverCredentials(config);
1008
1935
  return [
1009
1936
  {
1010
1937
  id: "notify-pushover-enabled",
1011
1938
  label: "Enable pushover",
1012
- value: formatPiNotifyPushoverStatus(config),
1013
- description: "Enable or disable all Pushover delivery globally.",
1939
+ labelTone: pushoverCredentialsReady ? undefined : "dim",
1940
+ value: pushoverCredentialsReady ? formatPiNotifyPushoverStatus(config) : "off",
1941
+ valueTone: pushoverCredentialsReady ? undefined : "dim",
1942
+ disabled: !pushoverCredentialsReady,
1943
+ description: pushoverCredentialsReady
1944
+ ? "Enable or disable all Pushover delivery globally."
1945
+ : "Populate both Pushover credential fields to unlock global Pushover enablement.",
1014
1946
  },
1015
1947
  buildPiNotifyEventLauncherChoice(
1016
1948
  config,
@@ -1031,7 +1963,7 @@ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuCh
1031
1963
  {
1032
1964
  id: "notify-pushover-text",
1033
1965
  label: "Pushover text",
1034
- value: config["notify-pushover-text"],
1966
+ value: formatPiNotifyControlSequenceText(config["notify-pushover-text"]),
1035
1967
  description: "Edit the text template used for outbound Pushover messages.",
1036
1968
  },
1037
1969
  {
@@ -1051,16 +1983,16 @@ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuCh
1051
1983
 
1052
1984
  /**
1053
1985
  * @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.
1986
+ * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the notification menus and appends subtree-local `Reset defaults` plus `Save and close` rows. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1055
1987
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1056
1988
  * @param[in] currentPriority {PiNotifyPushoverPriority} Persisted priority value.
1057
- * @return {Promise<PiNotifyPushoverPriority | undefined>} Selected priority or `undefined` when cancelled.
1989
+ * @return {Promise<PiNotifyPushoverPriority | "reset-defaults" | undefined>} Selected priority, reset action, or `undefined` when cancelled.
1058
1990
  * @satisfies REQ-172, REQ-192
1059
1991
  */
1060
1992
  async function selectPiNotifyPushoverPriority(
1061
1993
  ctx: ExtensionCommandContext,
1062
1994
  currentPriority: PiNotifyPushoverPriority,
1063
- ): Promise<PiNotifyPushoverPriority | undefined> {
1995
+ ): Promise<PiNotifyPushoverPriority | "reset-defaults" | undefined> {
1064
1996
  const choice = await showPiUsereqSettingsMenu(ctx, "Pushover priority", [
1065
1997
  {
1066
1998
  id: "0",
@@ -1074,19 +2006,26 @@ async function selectPiNotifyPushoverPriority(
1074
2006
  value: currentPriority === 1 ? "selected" : "",
1075
2007
  description: "Send outbound Pushover messages with high priority `1`.",
1076
2008
  },
2009
+ ...buildTerminalSettingsMenuChoices({
2010
+ resetDefaultsValue: formatPiNotifyPushoverPriority(getDefaultConfig("")["notify-pushover-priority"]),
2011
+ resetDefaultsDescription: "Restore the documented default Pushover priority.",
2012
+ }),
1077
2013
  ], { initialSelectedId: String(currentPriority) });
1078
2014
  if (!choice) {
1079
2015
  return undefined;
1080
2016
  }
2017
+ if (choice === "reset-defaults") {
2018
+ return "reset-defaults";
2019
+ }
1081
2020
  return normalizePiNotifyPushoverPriority(choice);
1082
2021
  }
1083
2022
 
1084
2023
  /**
1085
2024
  * @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.
2025
+ * @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
2026
  * @param[in] config {UseReqConfig} Effective project configuration.
1088
2027
  * @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
2028
+ * @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
2029
  */
1091
2030
  function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1092
2031
  return [
@@ -1141,33 +2080,24 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
1141
2080
  description: "Edit the shell command used when the selected sound command is `high`.",
1142
2081
  },
1143
2082
  ...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
- },
2083
+ ...buildTerminalSettingsMenuChoices({
2084
+ resetDefaultsDescription: "Restore the documented notification defaults for command-notify, sound, and Pushover settings.",
2085
+ }),
1156
2086
  ];
1157
2087
  }
1158
2088
 
1159
2089
  /**
1160
2090
  * @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.
2091
+ * @details Reuses the pi-usereq settings-menu renderer so sound-level selection remains stylistically aligned with the notification menu and appends subtree-local `Reset defaults` plus `Save and close` rows. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1162
2092
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1163
2093
  * @param[in] currentLevel {PiNotifySoundLevel} Currently selected sound level.
1164
- * @return {Promise<PiNotifySoundLevel | undefined>} Selected sound level or `undefined` when cancelled.
2094
+ * @return {Promise<PiNotifySoundLevel | "reset-defaults" | undefined>} Selected sound level, reset action, or `undefined` when cancelled.
1165
2095
  * @satisfies REQ-131, REQ-179, REQ-192
1166
2096
  */
1167
2097
  async function selectPiNotifySoundLevel(
1168
2098
  ctx: ExtensionCommandContext,
1169
2099
  currentLevel: PiNotifySoundLevel,
1170
- ): Promise<PiNotifySoundLevel | undefined> {
2100
+ ): Promise<PiNotifySoundLevel | "reset-defaults" | undefined> {
1171
2101
  const choice = await showPiUsereqSettingsMenu(ctx, "Enable sound", [
1172
2102
  {
1173
2103
  id: "none",
@@ -1193,21 +2123,32 @@ async function selectPiNotifySoundLevel(
1193
2123
  value: currentLevel === "high" ? "selected" : "",
1194
2124
  description: "Use the high-volume sound command when sound delivery is enabled for the current event.",
1195
2125
  },
2126
+ ...buildTerminalSettingsMenuChoices({
2127
+ resetDefaultsValue: getDefaultConfig("")["notify-sound"],
2128
+ resetDefaultsDescription: "Restore the documented default sound level.",
2129
+ }),
1196
2130
  ], { initialSelectedId: currentLevel });
1197
- return choice ? choice as PiNotifySoundLevel : undefined;
2131
+ if (!choice) {
2132
+ return undefined;
2133
+ }
2134
+ if (choice === "reset-defaults") {
2135
+ return "reset-defaults";
2136
+ }
2137
+ return choice as PiNotifySoundLevel;
1198
2138
  }
1199
2139
 
1200
2140
  /**
1201
2141
  * @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.
2142
+ * @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
2143
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1204
2144
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
1205
2145
  * @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
2146
+ * @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
2147
  */
1208
2148
  async function configurePiNotifyMenu(
1209
2149
  ctx: ExtensionCommandContext,
1210
2150
  config: UseReqConfig,
2151
+ onConfigChange: () => void,
1211
2152
  ): Promise<boolean> {
1212
2153
  const originalShortcut = config["notify-sound-toggle-shortcut"];
1213
2154
  let focusedChoiceId: string | undefined;
@@ -1218,12 +2159,18 @@ async function configurePiNotifyMenu(
1218
2159
  buildPiNotifyMenuChoices(config),
1219
2160
  { initialSelectedId: focusedChoiceId },
1220
2161
  );
1221
- if (!choice || choice === "save-and-close") {
2162
+ if (!choice) {
1222
2163
  return config["notify-sound-toggle-shortcut"] !== originalShortcut;
1223
2164
  }
1224
2165
  focusedChoiceId = choice;
1225
2166
  if (choice === "notify-enabled" || choice === "notify-pushover-enabled") {
2167
+ if (choice === "notify-pushover-enabled" && !hasPiNotifyPushoverCredentials(config)) {
2168
+ config["notify-pushover-enabled"] = false;
2169
+ ctx.ui.notify("Populate both Pushover credential fields before enabling Pushover", "info");
2170
+ continue;
2171
+ }
1226
2172
  const enabled = togglePiNotifyFlag(config, choice as PiNotifyBooleanConfigKey);
2173
+ onConfigChange();
1227
2174
  const labelMap: Record<string, string> = {
1228
2175
  "notify-enabled": "Notification",
1229
2176
  "notify-pushover-enabled": "Pushover",
@@ -1236,6 +2183,7 @@ async function configurePiNotifyMenu(
1236
2183
  ctx,
1237
2184
  config,
1238
2185
  PI_NOTIFY_EVENT_MENU_DEFINITIONS.notification,
2186
+ onConfigChange,
1239
2187
  );
1240
2188
  continue;
1241
2189
  }
@@ -1244,6 +2192,7 @@ async function configurePiNotifyMenu(
1244
2192
  ctx,
1245
2193
  config,
1246
2194
  PI_NOTIFY_EVENT_MENU_DEFINITIONS.sound,
2195
+ onConfigChange,
1247
2196
  );
1248
2197
  continue;
1249
2198
  }
@@ -1252,6 +2201,7 @@ async function configurePiNotifyMenu(
1252
2201
  ctx,
1253
2202
  config,
1254
2203
  PI_NOTIFY_EVENT_MENU_DEFINITIONS.pushover,
2204
+ onConfigChange,
1255
2205
  );
1256
2206
  continue;
1257
2207
  }
@@ -1259,14 +2209,33 @@ async function configurePiNotifyMenu(
1259
2209
  const value = await ctx.ui.input("Notify command", config.PI_NOTIFY_CMD);
1260
2210
  if (value !== undefined) {
1261
2211
  config.PI_NOTIFY_CMD = normalizePiNotifyCommand(value, DEFAULT_PI_NOTIFY_CMD);
2212
+ onConfigChange();
1262
2213
  ctx.ui.notify("Updated notify command", "info");
1263
2214
  }
1264
2215
  continue;
1265
2216
  }
1266
2217
  if (choice === "selected-sound-command") {
1267
2218
  const nextLevel = await selectPiNotifySoundLevel(ctx, config["notify-sound"]);
1268
- if (nextLevel !== undefined) {
2219
+ if (nextLevel === "reset-defaults") {
2220
+ const defaultSoundLevel = getDefaultConfig("")["notify-sound"];
2221
+ const approved = await confirmResetChanges(
2222
+ ctx,
2223
+ "Confirm sound reset",
2224
+ [{ label: "Enable sound", previousValue: config["notify-sound"], nextValue: defaultSoundLevel }]
2225
+ .filter((change) => change.previousValue !== change.nextValue),
2226
+ "Approve restoring the documented default sound level.",
2227
+ "Abort the sound reset and keep the current value.",
2228
+ );
2229
+ if (!approved) {
2230
+ ctx.ui.notify("Aborted sound reset", "info");
2231
+ } else {
2232
+ config["notify-sound"] = defaultSoundLevel;
2233
+ onConfigChange();
2234
+ ctx.ui.notify("Restored default sound level", "info");
2235
+ }
2236
+ } else if (nextLevel !== undefined) {
1269
2237
  config["notify-sound"] = nextLevel;
2238
+ onConfigChange();
1270
2239
  ctx.ui.notify(`Enable sound set to ${nextLevel}`, "info");
1271
2240
  }
1272
2241
  continue;
@@ -1278,6 +2247,7 @@ async function configurePiNotifyMenu(
1278
2247
  );
1279
2248
  if (value?.trim()) {
1280
2249
  config["notify-sound-toggle-shortcut"] = value.trim();
2250
+ onConfigChange();
1281
2251
  ctx.ui.notify(
1282
2252
  `Sound toggle hotkey bind set to ${config["notify-sound-toggle-shortcut"]}`,
1283
2253
  "info",
@@ -1292,6 +2262,7 @@ async function configurePiNotifyMenu(
1292
2262
  value,
1293
2263
  DEFAULT_PI_NOTIFY_SOUND_LOW_CMD,
1294
2264
  );
2265
+ onConfigChange();
1295
2266
  ctx.ui.notify("Updated sound command (low vol.)", "info");
1296
2267
  }
1297
2268
  continue;
@@ -1303,6 +2274,7 @@ async function configurePiNotifyMenu(
1303
2274
  value,
1304
2275
  DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
1305
2276
  );
2277
+ onConfigChange();
1306
2278
  ctx.ui.notify("Updated sound command (mid vol.)", "info");
1307
2279
  }
1308
2280
  continue;
@@ -1314,14 +2286,36 @@ async function configurePiNotifyMenu(
1314
2286
  value,
1315
2287
  DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD,
1316
2288
  );
2289
+ onConfigChange();
1317
2290
  ctx.ui.notify("Updated sound command (high vol.)", "info");
1318
2291
  }
1319
2292
  continue;
1320
2293
  }
1321
2294
  if (choice === "notify-pushover-priority") {
1322
2295
  const nextPriority = await selectPiNotifyPushoverPriority(ctx, config["notify-pushover-priority"]);
1323
- if (nextPriority !== undefined) {
2296
+ if (nextPriority === "reset-defaults") {
2297
+ const defaultPriority = getDefaultConfig("")["notify-pushover-priority"];
2298
+ const approved = await confirmResetChanges(
2299
+ ctx,
2300
+ "Confirm Pushover priority reset",
2301
+ [{
2302
+ label: "Pushover priority",
2303
+ previousValue: formatPiNotifyPushoverPriority(config["notify-pushover-priority"]),
2304
+ nextValue: formatPiNotifyPushoverPriority(defaultPriority),
2305
+ }].filter((change) => change.previousValue !== change.nextValue),
2306
+ "Approve restoring the documented default Pushover priority.",
2307
+ "Abort the Pushover priority reset and keep the current value.",
2308
+ );
2309
+ if (!approved) {
2310
+ ctx.ui.notify("Aborted Pushover priority reset", "info");
2311
+ } else {
2312
+ config["notify-pushover-priority"] = defaultPriority;
2313
+ onConfigChange();
2314
+ ctx.ui.notify("Restored default Pushover priority", "info");
2315
+ }
2316
+ } else if (nextPriority !== undefined) {
1324
2317
  config["notify-pushover-priority"] = nextPriority;
2318
+ onConfigChange();
1325
2319
  ctx.ui.notify(`Pushover priority set to ${formatPiNotifyPushoverPriority(nextPriority)}`, "info");
1326
2320
  }
1327
2321
  continue;
@@ -1333,17 +2327,22 @@ async function configurePiNotifyMenu(
1333
2327
  value,
1334
2328
  DEFAULT_PI_NOTIFY_PUSHOVER_TITLE,
1335
2329
  );
2330
+ onConfigChange();
1336
2331
  ctx.ui.notify("Updated Pushover title", "info");
1337
2332
  }
1338
2333
  continue;
1339
2334
  }
1340
2335
  if (choice === "notify-pushover-text") {
1341
- const value = await ctx.ui.input("Pushover text", config["notify-pushover-text"]);
2336
+ const value = await ctx.ui.input(
2337
+ "Pushover text",
2338
+ formatPiNotifyControlSequenceText(config["notify-pushover-text"]),
2339
+ );
1342
2340
  if (value !== undefined) {
1343
2341
  config["notify-pushover-text"] = normalizePiNotifyTemplateValue(
1344
- value,
2342
+ parsePiNotifyControlSequenceText(value),
1345
2343
  DEFAULT_PI_NOTIFY_PUSHOVER_TEXT,
1346
2344
  );
2345
+ onConfigChange();
1347
2346
  ctx.ui.notify("Updated Pushover text", "info");
1348
2347
  }
1349
2348
  continue;
@@ -1355,6 +2354,10 @@ async function configurePiNotifyMenu(
1355
2354
  );
1356
2355
  if (value !== undefined) {
1357
2356
  config["notify-pushover-user-key"] = normalizePiNotifyPushoverCredential(value);
2357
+ if (!hasPiNotifyPushoverCredentials(config)) {
2358
+ config["notify-pushover-enabled"] = false;
2359
+ }
2360
+ onConfigChange();
1358
2361
  ctx.ui.notify("Updated Pushover user key", "info");
1359
2362
  }
1360
2363
  continue;
@@ -1366,12 +2369,44 @@ async function configurePiNotifyMenu(
1366
2369
  );
1367
2370
  if (value !== undefined) {
1368
2371
  config["notify-pushover-api-token"] = normalizePiNotifyPushoverCredential(value);
2372
+ if (!hasPiNotifyPushoverCredentials(config)) {
2373
+ config["notify-pushover-enabled"] = false;
2374
+ }
2375
+ onConfigChange();
1369
2376
  ctx.ui.notify("Updated Pushover API token", "info");
1370
2377
  }
1371
2378
  continue;
1372
2379
  }
1373
2380
  if (choice === "reset-defaults") {
2381
+ const defaults = getDefaultConfig("");
2382
+ const resetPreview: ResetConfirmationChange[] = [
2383
+ { label: "Enable notification", previousValue: formatPiNotifyStatus(config), nextValue: formatPiNotifyStatus(defaults) },
2384
+ { label: "Enable sound", previousValue: config["notify-sound"], nextValue: defaults["notify-sound"] },
2385
+ { label: "Sound toggle hotkey bind", previousValue: config["notify-sound-toggle-shortcut"], nextValue: defaults["notify-sound-toggle-shortcut"] },
2386
+ { label: "Notify command", previousValue: config.PI_NOTIFY_CMD, nextValue: defaults.PI_NOTIFY_CMD },
2387
+ { label: "Sound command (low vol.)", previousValue: config.PI_NOTIFY_SOUND_LOW_CMD, nextValue: defaults.PI_NOTIFY_SOUND_LOW_CMD },
2388
+ { label: "Sound command (mid vol.)", previousValue: config.PI_NOTIFY_SOUND_MID_CMD, nextValue: defaults.PI_NOTIFY_SOUND_MID_CMD },
2389
+ { label: "Sound command (high vol.)", previousValue: config.PI_NOTIFY_SOUND_HIGH_CMD, nextValue: defaults.PI_NOTIFY_SOUND_HIGH_CMD },
2390
+ { label: "Enable pushover", previousValue: formatPiNotifyPushoverStatus(config), nextValue: formatPiNotifyPushoverStatus(defaults) },
2391
+ { label: "Pushover priority", previousValue: formatPiNotifyPushoverPriority(config["notify-pushover-priority"]), nextValue: formatPiNotifyPushoverPriority(defaults["notify-pushover-priority"]) },
2392
+ { label: "Pushover title", previousValue: config["notify-pushover-title"], nextValue: defaults["notify-pushover-title"] },
2393
+ { label: "Pushover text", previousValue: formatPiNotifyControlSequenceText(config["notify-pushover-text"]), nextValue: formatPiNotifyControlSequenceText(defaults["notify-pushover-text"]) },
2394
+ { label: "Pushover User Key/Delivery Group Key", previousValue: config["notify-pushover-user-key"] || "(empty)", nextValue: defaults["notify-pushover-user-key"] || "(empty)" },
2395
+ { label: "Pushover Token/API Token Key", previousValue: config["notify-pushover-api-token"] || "(empty)", nextValue: defaults["notify-pushover-api-token"] || "(empty)" },
2396
+ ].filter((change) => change.previousValue !== change.nextValue);
2397
+ const approved = await confirmResetChanges(
2398
+ ctx,
2399
+ "Confirm Notifications reset",
2400
+ resetPreview,
2401
+ "Approve restoring the documented notification defaults.",
2402
+ "Abort the notification reset and keep the current values.",
2403
+ );
2404
+ if (!approved) {
2405
+ ctx.ui.notify("Aborted notification reset", "info");
2406
+ continue;
2407
+ }
1374
2408
  resetPiNotifyConfigToDefaults(config);
2409
+ onConfigChange();
1375
2410
  ctx.ui.notify("Restored notification defaults", "info");
1376
2411
  continue;
1377
2412
  }
@@ -1399,7 +2434,7 @@ function registerPiNotifyShortcut(
1399
2434
  if (typeof shortcutRegistrar.registerShortcut !== "function") {
1400
2435
  return;
1401
2436
  }
1402
- const config = loadProjectConfig(process.cwd());
2437
+ const config = loadProjectConfig(getProcessCwdSafe());
1403
2438
  shortcutRegistrar.registerShortcut(config["notify-sound-toggle-shortcut"], {
1404
2439
  description: "Cycle pi-usereq notification sound level",
1405
2440
  handler: async (ctx) => {
@@ -1415,110 +2450,141 @@ function registerPiNotifyShortcut(
1415
2450
 
1416
2451
  /**
1417
2452
  * @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.
2453
+ * @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
2454
  * @param[in] pi {ExtensionAPI} Active extension API instance.
1420
2455
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
1421
2456
  * @return {void} No return value.
1422
- * @satisfies REQ-004, REQ-067, REQ-068, REQ-169
2457
+ * @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
2458
  */
1424
2459
  function registerPromptCommands(
1425
2460
  pi: ExtensionAPI,
1426
2461
  statusController: PiUsereqStatusController,
1427
2462
  ): void {
1428
- PROMPT_NAMES.forEach((promptName) => {
2463
+ PROMPT_COMMAND_NAMES.forEach((promptName) => {
1429
2464
  pi.registerCommand(`req-${promptName}`, {
1430
- description: `Run pi-usereq prompt ${promptName}`,
2465
+ description: resolvePromptCommandDescription(promptName),
1431
2466
  handler: async (args, ctx) => {
1432
- ensureBundledResourcesAccessible();
1433
- const projectBase = getProjectBase(ctx.cwd);
1434
- const config = loadProjectConfig(ctx.cwd);
1435
- statusController.state.pendingPromptRequest = {
2467
+ if (statusController.state.workflowState !== "idle") {
2468
+ const message = `ERROR: Prompt workflow state is ${statusController.state.workflowState}, expected idle.`;
2469
+ ctx.ui.notify(message, "error");
2470
+ throw new ReqError(message, 1);
2471
+ }
2472
+ const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
2473
+ syncContextCwdMirror(ctx, commandCwd);
2474
+ bootstrapRuntimePathState(commandCwd, {
2475
+ gitPath: resolveRuntimeGitPath(commandCwd),
2476
+ });
2477
+ const projectBase = getProjectBase(commandCwd);
2478
+ const config = loadProjectConfig(commandCwd);
2479
+ transitionPromptWorkflowState(
2480
+ statusController,
2481
+ ctx,
2482
+ projectBase,
2483
+ config,
1436
2484
  promptName,
1437
- promptArgs: args,
1438
- };
1439
- const content = renderPrompt(promptName, args, projectBase, config);
1440
- await deliverPromptCommand(pi, content);
1441
- },
1442
- });
1443
- });
1444
- }
1445
-
1446
-
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
- },
2485
+ "checking",
2486
+ );
2487
+ ensureBundledResourcesAccessible();
2488
+ let executionPlan: PromptCommandExecutionPlan | undefined;
2489
+ let promptContext = ctx;
2490
+ try {
2491
+ executionPlan = preparePromptCommandExecution(
2492
+ promptName,
2493
+ args,
2494
+ projectBase,
2495
+ config,
2496
+ ctx.sessionManager.getSessionFile(),
2497
+ ctx.sessionManager.getSessionDir?.(),
2498
+ ctx.sessionManager.getBranch?.(),
2499
+ {
2500
+ config,
2501
+ workflowState: statusController.state.workflowState,
2502
+ },
2503
+ );
2504
+ const content = renderPrompt(
2505
+ promptName,
2506
+ args,
2507
+ projectBase,
2508
+ config,
2509
+ executionPlan,
2510
+ );
2511
+ statusController.state.pendingPromptRequest = executionPlan;
2512
+ writePersistedPromptCommandRuntimeState({
2513
+ workflowState: statusController.state.workflowState,
2514
+ pendingPromptRequest: statusController.state.pendingPromptRequest,
2515
+ activePromptRequest: statusController.state.activePromptRequest,
2516
+ });
2517
+ promptContext = (await activatePromptCommandExecution(executionPlan, ctx) ?? ctx) as typeof ctx;
2518
+ logPromptWorkflowEvent(
2519
+ projectBase,
2520
+ config,
2521
+ statusController.state.workflowState,
2522
+ promptName,
2523
+ "workflow_activation",
2524
+ {
2525
+ execution_session_file: executionPlan.executionSessionFile,
2526
+ context_path: executionPlan.contextPath,
2527
+ },
2528
+ {
2529
+ success: true,
2530
+ context_path: promptContext.cwd ?? executionPlan.contextPath,
2531
+ prompt_context_has_switch_session: typeof promptContext.switchSession === "function",
2532
+ },
2533
+ );
2534
+ renderPiUsereqStatus(statusController, promptContext);
2535
+ const promptDelivery = deliverPromptCommand(pi, content, promptContext);
2536
+ transitionPromptWorkflowState(
2537
+ statusController,
2538
+ promptContext,
2539
+ projectBase,
2540
+ config,
2541
+ promptName,
2542
+ "running",
2543
+ );
2544
+ await promptDelivery;
2545
+ } catch (error) {
2546
+ if (shouldIgnoreLatePromptDeliveryFailure(error, statusController.state.workflowState, executionPlan)) {
2547
+ return;
2548
+ }
2549
+ promptContext = (getPromptCommandErrorContext(error) ?? promptContext) as typeof ctx;
2550
+ statusController.state.pendingPromptRequest = undefined;
2551
+ statusController.state.activePromptRequest = undefined;
2552
+ transitionPromptWorkflowState(
2553
+ statusController,
2554
+ promptContext,
2555
+ projectBase,
2556
+ config,
2557
+ promptName,
2558
+ "error",
2559
+ );
2560
+ if (executionPlan !== undefined) {
2561
+ const abortResult = await abortPromptCommandExecution(executionPlan, promptContext, {
2562
+ config,
2563
+ workflowState: statusController.state.workflowState,
2564
+ });
2565
+ promptContext = (abortResult.activeContext ?? promptContext) as typeof ctx;
2566
+ if (!abortResult.cleanupSucceeded && abortResult.errorMessage) {
2567
+ notifyContextSafely(promptContext, abortResult.errorMessage, "error");
2568
+ }
2569
+ }
2570
+ const message = error instanceof Error ? error.message : String(error);
2571
+ notifyContextSafely(promptContext, message, "error");
2572
+ throw error;
2573
+ }
2574
+ },
2575
+ });
1520
2576
  });
2577
+ }
1521
2578
 
2579
+
2580
+ /**
2581
+ * @brief Registers pi-usereq agent tools exposed to the model.
2582
+ * @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.
2583
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
2584
+ * @return {void} No return value.
2585
+ * @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
2586
+ */
2587
+ function registerAgentTools(pi: ExtensionAPI): void {
1522
2588
  const filesReferencesSchema = Type.Object(
1523
2589
  {
1524
2590
  files: Type.Array(
@@ -1527,7 +2593,7 @@ function registerAgentTools(pi: ExtensionAPI): void {
1527
2593
  ),
1528
2594
  },
1529
2595
  {
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.",
2596
+ 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
2597
  },
1532
2598
  );
1533
2599
  const multiFileSchema = Type.Object(
@@ -1538,7 +2604,7 @@ function registerAgentTools(pi: ExtensionAPI): void {
1538
2604
  ),
1539
2605
  },
1540
2606
  {
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.",
2607
+ description: "Input contract: files[]. Output contract: monolithic text in content[0].text plus details.execution diagnostics.",
1542
2608
  },
1543
2609
  );
1544
2610
  const filesTokensSchema = Type.Object(
@@ -1549,58 +2615,43 @@ function registerAgentTools(pi: ExtensionAPI): void {
1549
2615
  ),
1550
2616
  },
1551
2617
  {
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.",
2618
+ 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
2619
  },
1554
2620
  );
1555
2621
 
1556
2622
  pi.registerTool({
1557
2623
  name: "files-tokens",
1558
2624
  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.",
2625
+ description: "Scope: explicit files. Return the monolithic token pack summary in content[0].text and keep only execution metadata in details.execution.",
2626
+ promptSnippet: "Return the monolithic token summary for caller-selected files.",
1561
2627
  promptGuidelines: [
1562
2628
  "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.",
2629
+ "Output contract: monolithic pack-summary text in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2630
+ "Behavior contract: missing or non-file inputs surface through details.execution diagnostics; the tool fails when no processable files remain.",
1566
2631
  ],
2632
+ renderResult: buildStructuredToolRenderResult("files-tokens"),
1567
2633
  parameters: filesTokensSchema,
1568
2634
  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
- }
2635
+ return executeMonolithicTool(() => runFilesTokens(params.files));
1581
2636
  },
1582
2637
  });
1583
2638
 
1584
2639
  pi.registerTool({
1585
2640
  name: "files-references",
1586
2641
  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.",
2642
+ description: "Scope: explicit source files. Return the monolithic references markdown report in content[0].text and keep only execution metadata in details.execution.",
2643
+ promptSnippet: "Return the monolithic references markdown report for caller-selected source files.",
1589
2644
  promptGuidelines: [
1590
2645
  "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.",
2646
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2647
+ "Formatting contract: content matches the Python reference renderer used by `generate_markdown.py`.",
2648
+ "Behavior contract: missing inputs, non-file inputs, unsupported extensions, and analysis failures surface through details.execution diagnostics.",
1594
2649
  ],
2650
+ renderResult: buildStructuredToolRenderResult("files-references"),
1595
2651
  parameters: filesReferencesSchema,
1596
2652
  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);
2653
+ const contextPath = getRuntimeContextPath(process.cwd());
2654
+ return executeMonolithicTool(() => runFilesReferences(params.files, contextPath));
1604
2655
  },
1605
2656
  });
1606
2657
 
@@ -1610,38 +2661,37 @@ function registerAgentTools(pi: ExtensionAPI): void {
1610
2661
  Type.String({ description: "Project-relative or absolute source file path resolved from the current working directory when not already absolute" }),
1611
2662
  { description: "Explicit source-file list preserved in caller order" },
1612
2663
  ),
1613
- enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `compressed_source_text` and `compressed_lines[].display_text` include original source line-number prefixes" })),
2664
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, fenced code block lines include original source line-number prefixes" })),
1614
2665
  },
1615
2666
  {
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.",
2667
+ 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
2668
  },
1618
2669
  );
1619
2670
 
1620
2671
  pi.registerTool({
1621
2672
  name: "files-compress",
1622
2673
  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.",
2674
+ description: "Scope: explicit files. Return the monolithic compression markdown report in content[0].text and keep only execution metadata in details.execution.",
2675
+ promptSnippet: "Return the monolithic compression markdown report for caller-selected source files.",
1625
2676
  promptGuidelines: [
1626
2677
  "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.",
2678
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2679
+ "Formatting contract: output uses `@@@ <path> | <language>` headers, `> Lines:` metadata, and fenced code blocks matching `compress_files.py`.",
2680
+ "Line-number behavior: enableLineNumbers toggles original source line prefixes inside fenced code blocks.",
1630
2681
  ],
2682
+ renderResult: buildStructuredToolRenderResult("files-compress"),
1631
2683
  parameters: filesCompressSchema,
1632
2684
  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);
2685
+ const contextPath = getRuntimeContextPath(process.cwd());
2686
+ return executeMonolithicTool(() => runFilesCompress(
2687
+ params.files,
2688
+ contextPath,
2689
+ params.enableLineNumbers ?? false,
2690
+ ));
1641
2691
  },
1642
2692
  });
1643
2693
 
1644
- const filesFindSchema = Type.Object(
2694
+ const filesSearchSchema = Type.Object(
1645
2695
  {
1646
2696
  tag: Type.String({ description: "Pipe-separated construct-tag filter applied case-insensitively; unsupported tags are ignored" }),
1647
2697
  pattern: Type.String({ description: "JavaScript RegExp applied to construct names only; use ^...$ for exact-name matching" }),
@@ -1649,434 +2699,198 @@ function registerAgentTools(pi: ExtensionAPI): void {
1649
2699
  Type.String({ description: "Project-relative or absolute source file path resolved from the current working directory when not already absolute" }),
1650
2700
  { description: "Explicit source-file list preserved in caller order" },
1651
2701
  ),
1652
- enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `code_lines[].display_text` and `stripped_source_text` include original source line-number prefixes" })),
2702
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, fenced code block lines include original source line-number prefixes" })),
1653
2703
  },
1654
2704
  {
1655
- description: buildFindToolSchemaDescription("explicit-files"),
2705
+ description: buildSearchToolSchemaDescription("explicit-files"),
1656
2706
  },
1657
2707
  );
1658
2708
 
1659
2709
  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,
2710
+ name: "files-search",
2711
+ label: "files-search",
2712
+ description: "Scope: explicit source files. Return the monolithic construct-search markdown report in content[0].text and keep only execution metadata in details.execution.",
2713
+ promptSnippet: "Return the monolithic construct-search markdown report for caller-selected source files.",
2714
+ promptGuidelines: buildSearchToolPromptGuidelines("explicit-files"),
2715
+ renderResult: buildStructuredToolRenderResult("files-search"),
2716
+ parameters: filesSearchSchema,
1666
2717
  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);
2718
+ return executeMonolithicTool(() => runFilesSearch(
2719
+ [params.tag, params.pattern, ...params.files],
2720
+ params.enableLineNumbers ?? false,
2721
+ ));
1677
2722
  },
1678
2723
  });
1679
2724
 
1680
2725
  const referencesSchema = Type.Object(
1681
2726
  {},
1682
2727
  {
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.",
2728
+ 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
2729
  },
1685
2730
  );
1686
2731
  const tokensSchema = Type.Object(
1687
2732
  {},
1688
2733
  {
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.",
2734
+ 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
2735
  },
1691
2736
  );
1692
2737
 
1693
2738
  pi.registerTool({
1694
2739
  name: "references",
1695
2740
  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.",
2741
+ description: "Scope: configured project source directories. Return the monolithic references markdown report in content[0].text and keep only execution metadata in details.execution.",
2742
+ promptSnippet: "Return the monolithic project references markdown report from the configured source directories.",
1698
2743
  promptGuidelines: [
1699
2744
  "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.",
2745
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2746
+ "Formatting contract: content prepends the file-structure markdown block before the per-file markdown produced by `generate_markdown.py`.",
2747
+ "Configuration contract: output changes with cwd-derived project config and src-dir values; the tool does not accept explicit file overrides.",
1703
2748
  ],
2749
+ renderResult: buildStructuredToolRenderResult("references"),
1704
2750
  parameters: referencesSchema,
1705
2751
  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);
2752
+ const contextPath = getRuntimeContextPath(process.cwd());
2753
+ const projectBase = getProjectBase(contextPath);
2754
+ const config = loadProjectConfig(projectBase);
2755
+ return executeMonolithicTool(() => runReferences(contextPath, config));
1716
2756
  },
1717
2757
  });
1718
2758
 
1719
2759
  const compressSchema = Type.Object(
1720
2760
  {
1721
- enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `compressed_source_text` and `compressed_lines[].display_text` include original source line-number prefixes" })),
2761
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, fenced code block lines include original source line-number prefixes" })),
1722
2762
  },
1723
2763
  {
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.",
2764
+ 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
2765
  },
1726
2766
  );
1727
2767
 
1728
2768
  pi.registerTool({
1729
2769
  name: "compress",
1730
2770
  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.",
2771
+ description: "Scope: configured project source directories. Return the monolithic compression markdown report in content[0].text and keep only execution metadata in details.execution.",
2772
+ promptSnippet: "Return the monolithic project compression markdown report from the configured source directories.",
1733
2773
  promptGuidelines: [
1734
2774
  "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.",
2775
+ "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2776
+ "Formatting contract: output uses `@@@ <path> | <language>` headers, `> Lines:` metadata, and fenced code blocks matching `compress_files.py`.",
2777
+ "Line-number behavior: enableLineNumbers toggles original source line prefixes inside fenced code blocks.",
1738
2778
  ],
2779
+ renderResult: buildStructuredToolRenderResult("compress"),
1739
2780
  parameters: compressSchema,
1740
2781
  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);
2782
+ const contextPath = getRuntimeContextPath(process.cwd());
2783
+ const projectBase = getProjectBase(contextPath);
2784
+ const config = loadProjectConfig(projectBase);
2785
+ return executeMonolithicTool(() => runCompress(
2786
+ contextPath,
2787
+ config,
2788
+ params.enableLineNumbers ?? false,
2789
+ ));
1753
2790
  },
1754
2791
  });
1755
2792
 
1756
- const findSchema = Type.Object(
2793
+ const searchSchema = Type.Object(
1757
2794
  {
1758
2795
  tag: Type.String({ description: "Pipe-separated construct-tag filter applied case-insensitively; unsupported tags are ignored" }),
1759
2796
  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" })),
2797
+ enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, fenced code block lines include original source line-number prefixes" })),
1761
2798
  },
1762
2799
  {
1763
- description: buildFindToolSchemaDescription("configured-source-directories"),
2800
+ description: buildSearchToolSchemaDescription("configured-source-directories"),
1764
2801
  },
1765
2802
  );
1766
2803
 
1767
2804
  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,
2805
+ name: "search",
2806
+ label: "search",
2807
+ 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.",
2808
+ promptSnippet: "Return the monolithic construct-search markdown report from the configured source directories.",
2809
+ promptGuidelines: buildSearchToolPromptGuidelines("configured-source-directories"),
2810
+ renderResult: buildStructuredToolRenderResult("search"),
2811
+ parameters: searchSchema,
1774
2812
  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);
2813
+ const contextPath = getRuntimeContextPath(process.cwd());
2814
+ const projectBase = getProjectBase(contextPath);
2815
+ const config = loadProjectConfig(projectBase);
2816
+ return executeMonolithicTool(() => runSearch(
2817
+ contextPath,
2818
+ params.tag,
2819
+ params.pattern,
2820
+ config,
2821
+ params.enableLineNumbers ?? false,
2822
+ ));
1789
2823
  },
1790
2824
  });
1791
2825
 
1792
2826
  pi.registerTool({
1793
2827
  name: "tokens",
1794
2828
  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.",
2829
+ 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.",
2830
+ promptSnippet: "Return the monolithic token summary for canonical documentation files.",
1797
2831
  promptGuidelines: [
1798
2832
  "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.",
2833
+ "Output contract: monolithic pack-summary text in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2834
+ "Behavior contract: missing canonical docs surface through details.execution diagnostics; the tool fails when no processable canonical docs remain.",
1802
2835
  ],
2836
+ renderResult: buildStructuredToolRenderResult("tokens"),
1803
2837
  parameters: tokensSchema,
1804
2838
  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
- }
2839
+ const contextPath = getRuntimeContextPath(process.cwd());
2840
+ const projectBase = getProjectBase(contextPath);
2841
+ const config = loadProjectConfig(projectBase);
2842
+ return executeMonolithicTool(() => runTokens(contextPath, config));
1823
2843
  },
1824
2844
  });
1825
2845
 
1826
2846
  pi.registerTool({
1827
2847
  name: "files-static-check",
1828
2848
  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.",
2849
+ description: "Scope: explicit files. Return the monolithic static-check report in content[0].text and keep only execution metadata in details.execution.",
2850
+ promptSnippet: "Return the monolithic explicit-file static-check report for the current project configuration.",
1831
2851
  promptGuidelines: [
1832
2852
  "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.",
2853
+ "Output contract: monolithic text in content[0].text; details.execution preserves only exit code and residual diagnostics.",
1834
2854
  "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.",
2855
+ "Failure contract: execution diagnostics preserve failing checker output and skipped-input warnings.",
1836
2856
  ],
2857
+ renderResult: buildStructuredToolRenderResult("files-static-check"),
1837
2858
  parameters: multiFileSchema,
1838
2859
  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);
2860
+ const contextPath = getRuntimeContextPath(process.cwd());
2861
+ const projectBase = getProjectBase(contextPath);
2862
+ const config = loadProjectConfig(projectBase);
2863
+ return executeMonolithicTool(() => runFilesStaticCheck(params.files, projectBase, config));
1854
2864
  },
1855
2865
  });
1856
2866
 
1857
2867
  const staticCheckSchema = Type.Object(
1858
2868
  {},
1859
2869
  {
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.",
2870
+ 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
2871
  },
1862
2872
  );
1863
2873
  pi.registerTool({
1864
2874
  name: "static-check",
1865
2875
  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.",
2876
+ 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.",
2877
+ promptSnippet: "Return the monolithic project static-check report for the current configuration.",
1868
2878
  promptGuidelines: [
1869
2879
  "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.",
2880
+ "Output contract: monolithic text in content[0].text; details.execution preserves only exit code and residual diagnostics.",
1871
2881
  "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.",
2882
+ "Failure contract: execution diagnostics preserve failing checker output and skipped-selection warnings.",
1873
2883
  ],
2884
+ renderResult: buildStructuredToolRenderResult("static-check"),
1874
2885
  parameters: staticCheckSchema,
1875
2886
  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);
2887
+ const contextPath = getRuntimeContextPath(process.cwd());
2888
+ const projectBase = getProjectBase(contextPath);
2889
+ const config = loadProjectConfig(projectBase);
2890
+ return executeMonolithicTool(() => runProjectStaticCheck(contextPath, config));
2038
2891
  },
2039
2892
  });
2040
2893
 
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
2894
  }
2081
2895
 
2082
2896
  /**
@@ -2108,28 +2922,20 @@ function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig):
2108
2922
  value: `${tools.length} targets`,
2109
2923
  description: "Disable every configurable startup tool for future session starts.",
2110
2924
  },
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
- },
2925
+ ...buildTerminalSettingsMenuChoices({
2926
+ resetDefaultsValue: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
2927
+ resetDefaultsDescription: "Restore the documented default startup-tool selection.",
2928
+ }),
2123
2929
  ];
2124
2930
  }
2125
2931
 
2126
2932
  /**
2127
2933
  * @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.
2934
+ * @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 subtree-local `Reset defaults` plus `Save and close` rows. Runtime is O(t) in configurable-tool count. No external state is mutated.
2129
2935
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2130
2936
  * @param[in] config {UseReqConfig} Effective project configuration.
2131
2937
  * @return {PiUsereqSettingsMenuChoice[]} Ordered per-tool toggle choices.
2132
- * @satisfies REQ-007, REQ-151, REQ-152, REQ-153, REQ-154
2938
+ * @satisfies REQ-007, REQ-151, REQ-152, REQ-153, REQ-154, REQ-231, REQ-232
2133
2939
  */
2134
2940
  function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2135
2941
  const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
@@ -2140,25 +2946,28 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
2140
2946
  value: enabledTools.has(tool.name) ? "on" : "off",
2141
2947
  description: tool.description ?? `Toggle startup activation for ${tool.name}.`,
2142
2948
  })),
2143
- {
2144
- id: "back",
2145
- label: "Back",
2146
- value: "",
2147
- description: "Return to the Enable tools menu.",
2148
- },
2949
+ ...buildTerminalSettingsMenuChoices({
2950
+ resetDefaultsValue: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
2951
+ resetDefaultsDescription: "Restore the documented default startup-tool selection.",
2952
+ }),
2149
2953
  ];
2150
2954
  }
2151
2955
 
2152
2956
  /**
2153
2957
  * @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.
2958
+ * @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
2959
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2156
2960
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2157
2961
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
2158
2962
  * @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
2963
+ * @satisfies REQ-007, REQ-063, REQ-064, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193, REQ-231, REQ-232
2160
2964
  */
2161
- async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
2965
+ async function configurePiUsereqToolsMenu(
2966
+ pi: ExtensionAPI,
2967
+ ctx: ExtensionCommandContext,
2968
+ config: UseReqConfig,
2969
+ onConfigChange: () => void,
2970
+ ): Promise<void> {
2162
2971
  applyConfiguredPiUsereqTools(pi, config);
2163
2972
  let focusedChoiceId: string | undefined;
2164
2973
  while (true) {
@@ -2168,32 +2977,71 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
2168
2977
  initialSelectedId: focusedChoiceId,
2169
2978
  });
2170
2979
 
2171
- if (!choice || choice === "save-and-close") {
2980
+ if (!choice) {
2172
2981
  return;
2173
2982
  }
2174
2983
  focusedChoiceId = choice;
2175
2984
 
2176
2985
  if (choice === "enable-all-tools") {
2177
2986
  setConfiguredPiUsereqTools(pi, config, tools.map((tool) => tool.name));
2987
+ onConfigChange();
2178
2988
  ctx.ui.notify("Enabled all configurable active tools", "info");
2179
2989
  continue;
2180
2990
  }
2181
2991
 
2182
2992
  if (choice === "disable-all-tools") {
2183
2993
  setConfiguredPiUsereqTools(pi, config, []);
2994
+ onConfigChange();
2184
2995
  ctx.ui.notify("Disabled all configurable active tools", "info");
2185
2996
  continue;
2186
2997
  }
2187
2998
 
2188
2999
  if (choice === "reset-defaults") {
3000
+ const approved = await confirmResetChanges(
3001
+ ctx,
3002
+ "Confirm Enable tools reset",
3003
+ [{
3004
+ label: "Enable tools",
3005
+ previousValue: String(getConfiguredEnabledPiUsereqTools(config).length),
3006
+ nextValue: String(normalizeEnabledPiUsereqTools(undefined).length),
3007
+ }].filter((change) => change.previousValue !== change.nextValue),
3008
+ "Approve restoring the documented default startup-tool selection.",
3009
+ "Abort the startup-tool reset and keep the current values.",
3010
+ );
3011
+ if (!approved) {
3012
+ ctx.ui.notify("Aborted startup-tool reset", "info");
3013
+ continue;
3014
+ }
2189
3015
  setConfiguredPiUsereqTools(pi, config, normalizeEnabledPiUsereqTools(undefined));
3016
+ onConfigChange();
2190
3017
  ctx.ui.notify("Restored default configurable active tools", "info");
2191
3018
  continue;
2192
3019
  }
2193
3020
 
2194
3021
  if (choice === "enable-tools") {
2195
3022
  const selectedToolName = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolToggleChoices(pi, config));
2196
- if (!selectedToolName || selectedToolName === "back") {
3023
+ if (!selectedToolName) {
3024
+ continue;
3025
+ }
3026
+ if (selectedToolName === "reset-defaults") {
3027
+ const approved = await confirmResetChanges(
3028
+ ctx,
3029
+ "Confirm Enable tools reset",
3030
+ [{
3031
+ label: "Enable tools",
3032
+ previousValue: String(getConfiguredEnabledPiUsereqTools(config).length),
3033
+ nextValue: String(normalizeEnabledPiUsereqTools(undefined).length),
3034
+ }].filter((change) => change.previousValue !== change.nextValue),
3035
+ "Approve restoring the documented default startup-tool selection.",
3036
+ "Abort the startup-tool reset and keep the current values.",
3037
+ );
3038
+ if (!approved) {
3039
+ ctx.ui.notify("Aborted startup-tool reset", "info");
3040
+ continue;
3041
+ }
3042
+ setConfiguredPiUsereqTools(pi, config, normalizeEnabledPiUsereqTools(undefined));
3043
+ onConfigChange();
3044
+ ctx.ui.notify("Restored default configurable active tools", "info");
2197
3045
  continue;
2198
3046
  }
2199
3047
  if (enabledTools.has(selectedToolName)) {
@@ -2202,6 +3050,7 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
2202
3050
  enabledTools.add(selectedToolName);
2203
3051
  }
2204
3052
  setConfiguredPiUsereqTools(pi, config, tools.map((tool) => tool.name).filter((toolName) => enabledTools.has(toolName)));
3053
+ onConfigChange();
2205
3054
  ctx.ui.notify(
2206
3055
  `${enabledTools.has(selectedToolName) ? "Enabled" : "Disabled"} ${selectedToolName}`,
2207
3056
  "info",
@@ -2211,34 +3060,75 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
2211
3060
  }
2212
3061
 
2213
3062
  /**
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.
3063
+ * @brief Resolves one static-check language config for menu rendering.
3064
+ * @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.
3065
+ * @param[in] config {UseReqConfig} Effective project configuration.
3066
+ * @param[in] language {string} Canonical language name.
3067
+ * @return {StaticCheckLanguageConfig} Resolved per-language config object.
3068
+ */
3069
+ function getStaticCheckLanguageConfigForMenu(
3070
+ config: UseReqConfig,
3071
+ language: string,
3072
+ ): StaticCheckLanguageConfig {
3073
+ return config["static-check"][language] ?? createStaticCheckLanguageConfig([]);
3074
+ }
3075
+
3076
+ /**
3077
+ * @brief Counts languages that currently expose at least one configured checker.
3078
+ * @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.
3079
+ * @param[in] config {UseReqConfig} Effective project configuration.
3080
+ * @return {number} Number of languages with at least one configured checker.
3081
+ */
3082
+ function countConfiguredStaticCheckLanguages(config: UseReqConfig): number {
3083
+ return Object.values(config["static-check"]).filter((languageConfig) => languageConfig.checkers.length > 0).length;
3084
+ }
3085
+
3086
+ /**
3087
+ * @brief Counts languages whose static-check enable flag is on.
3088
+ * @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.
3089
+ * @param[in] config {UseReqConfig} Effective project configuration.
3090
+ * @return {number} Number of enabled languages.
3091
+ */
3092
+ function countEnabledStaticCheckLanguages(config: UseReqConfig): number {
3093
+ return Object.values(config["static-check"]).filter((languageConfig) => languageConfig.enabled === "enable").length;
3094
+ }
3095
+
3096
+ /**
3097
+ * @brief Restores the documented static-check default configuration.
3098
+ * @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`.
3099
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
3100
+ * @return {void} No return value.
3101
+ * @satisfies REQ-250, REQ-251, REQ-252
3102
+ */
3103
+ function resetStaticCheckConfig(config: UseReqConfig): void {
3104
+ config["static-check"] = getDefaultStaticCheckConfig();
3105
+ }
3106
+
3107
+ /**
3108
+ * @brief Summarizes enabled and configured static-check languages.
3109
+ * @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
3110
  * @param[in] config {UseReqConfig} Effective project configuration.
2217
- * @return {string} Compact summary string or `(none)`.
3111
+ * @return {string} Compact summary string.
2218
3112
  */
2219
3113
  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)";
3114
+ return `${countEnabledStaticCheckLanguages(config)} enabled • ${countConfiguredStaticCheckLanguages(config)} configured`;
2225
3115
  }
2226
3116
 
2227
3117
  /**
2228
3118
  * @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.
3119
+ * @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
3120
  * @param[in] config {UseReqConfig} Effective project configuration.
2231
3121
  * @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
3122
+ * @satisfies REQ-008, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-160, REQ-161, REQ-193, REQ-248
2233
3123
  */
2234
3124
  function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2235
- const supportedLanguageCount = getSupportedStaticCheckLanguageSupport().length;
2236
- const configuredLanguageCount = Object.values(config["static-check"]).filter((entries) => entries.length > 0).length;
3125
+ const supportedLanguages = getSupportedStaticCheckLanguageSupport();
3126
+ const configuredLanguageCount = countConfiguredStaticCheckLanguages(config);
2237
3127
  return [
2238
3128
  {
2239
3129
  id: "add-static-check-entry",
2240
3130
  label: "Add static code checker",
2241
- value: `${supportedLanguageCount} languages`,
3131
+ value: `${supportedLanguages.length} languages`,
2242
3132
  description: "Select a supported language, then configure one Command static-check executable.",
2243
3133
  },
2244
3134
  {
@@ -2247,96 +3137,137 @@ function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenu
2247
3137
  value: configuredLanguageCount > 0 ? `${configuredLanguageCount} configured` : "(none)",
2248
3138
  description: "Remove every configured static-check entry for one language.",
2249
3139
  },
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
- },
3140
+ ...supportedLanguages.map(({ language, extensions }) => {
3141
+ const languageConfig = getStaticCheckLanguageConfigForMenu(config, language);
3142
+ const configuredCount = languageConfig.checkers.length;
3143
+ const suffix = configuredCount === 1 ? "checker" : "checkers";
3144
+ return {
3145
+ id: `toggle-static-check-language:${language}`,
3146
+ label: language,
3147
+ value: languageConfig.enabled === "enable" ? "on" : "off",
3148
+ description: `Toggle static-check execution for ${language}. Configured ${configuredCount} ${suffix}. Supported extensions: ${extensions.join(", ")}.`,
3149
+ };
3150
+ }),
3151
+ ...buildTerminalSettingsMenuChoices({
3152
+ resetDefaultsValue: formatStaticCheckLanguagesSummary(config),
3153
+ resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3154
+ }),
2262
3155
  ];
2263
3156
  }
2264
3157
 
2265
3158
  /**
2266
3159
  * @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.
3160
+ * @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
3161
  * @param[in] config {UseReqConfig} Effective project configuration.
2269
3162
  * @return {PiUsereqSettingsMenuChoice[]} Ordered language-choice vector.
2270
3163
  */
2271
3164
  function buildSupportedStaticCheckLanguageChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2272
3165
  return [
2273
3166
  ...getSupportedStaticCheckLanguageSupport().map(({ language, extensions }) => {
2274
- const configuredCount = config["static-check"][language]?.length ?? 0;
3167
+ const languageConfig = getStaticCheckLanguageConfigForMenu(config, language);
3168
+ const configuredCount = languageConfig.checkers.length;
2275
3169
  const suffix = configuredCount === 1 ? "checker" : "checkers";
2276
3170
  return {
2277
3171
  id: language,
2278
3172
  label: language,
2279
- value: `${extensions.join(", ")} • ${configuredCount} ${suffix}`,
2280
- description: `Configure the Command static-check entry for ${language}. Supported extensions: ${extensions.join(", ")}.`,
3173
+ value: `${extensions.join(", ")} • ${languageConfig.enabled === "enable" ? "on" : "off"} • ${configuredCount} ${suffix}`,
3174
+ description: `Configure the Command static-check entries for ${language}. Supported extensions: ${extensions.join(", ")}.`,
2281
3175
  };
2282
3176
  }),
2283
- {
2284
- id: "back",
2285
- label: "Back",
2286
- value: "",
2287
- description: "Return to the Language static code checkers menu.",
2288
- },
3177
+ ...buildTerminalSettingsMenuChoices({
3178
+ resetDefaultsValue: formatStaticCheckLanguagesSummary(config),
3179
+ resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3180
+ }),
2289
3181
  ];
2290
3182
  }
2291
3183
 
2292
3184
  /**
2293
3185
  * @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.
3186
+ * @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
3187
  * @param[in] config {UseReqConfig} Effective project configuration.
2296
3188
  * @return {PiUsereqSettingsMenuChoice[]} Ordered configured-language vector.
2297
3189
  */
2298
3190
  function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2299
3191
  return [
2300
3192
  ...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
- },
3193
+ .filter(({ language }) => getStaticCheckLanguageConfigForMenu(config, language).checkers.length > 0)
3194
+ .map(({ language, extensions }) => {
3195
+ const languageConfig = getStaticCheckLanguageConfigForMenu(config, language);
3196
+ return {
3197
+ id: language,
3198
+ label: language,
3199
+ value: `${extensions.join(", ")} • ${languageConfig.checkers.length} configured`,
3200
+ description: `Remove every configured static-check entry for ${language}.`,
3201
+ };
3202
+ }),
3203
+ ...buildTerminalSettingsMenuChoices({
3204
+ resetDefaultsValue: formatStaticCheckLanguagesSummary(config),
3205
+ resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3206
+ }),
2314
3207
  ];
2315
3208
  }
2316
3209
 
2317
3210
  /**
2318
3211
  * @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.
3212
+ * @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
3213
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2321
3214
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
2322
3215
  * @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
3216
+ * @satisfies REQ-008, REQ-151, REQ-152, REQ-153, REQ-154, REQ-160, REQ-161, REQ-193, REQ-195, REQ-248, REQ-253
2324
3217
  */
2325
- async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
3218
+ async function configureStaticCheckMenu(
3219
+ ctx: ExtensionCommandContext,
3220
+ config: UseReqConfig,
3221
+ onConfigChange: () => void,
3222
+ ): Promise<void> {
2326
3223
  let focusedChoiceId: string | undefined;
2327
3224
  while (true) {
2328
3225
  const staticChoice = await showPiUsereqSettingsMenu(ctx, "Language static code checkers", buildStaticCheckMenuChoices(config), {
2329
3226
  initialSelectedId: focusedChoiceId,
2330
3227
  });
2331
3228
 
2332
- if (!staticChoice || staticChoice === "save-and-close") {
3229
+ if (!staticChoice) {
2333
3230
  return;
2334
3231
  }
2335
3232
  focusedChoiceId = staticChoice;
2336
3233
 
3234
+ if (staticChoice.startsWith("toggle-static-check-language:")) {
3235
+ const language = staticChoice.slice("toggle-static-check-language:".length);
3236
+ config["static-check"][language] ??= createStaticCheckLanguageConfig([]);
3237
+ const languageConfig = config["static-check"][language]!;
3238
+ languageConfig.enabled = languageConfig.enabled === "enable" ? "disable" : "enable";
3239
+ onConfigChange();
3240
+ ctx.ui.notify(
3241
+ `${languageConfig.enabled === "enable" ? "Enabled" : "Disabled"} static-check for ${language}`,
3242
+ "info",
3243
+ );
3244
+ continue;
3245
+ }
3246
+
2337
3247
  if (staticChoice === "add-static-check-entry") {
2338
3248
  const selectedLanguage = await showPiUsereqSettingsMenu(ctx, "static-check language", buildSupportedStaticCheckLanguageChoices(config));
2339
- if (!selectedLanguage || selectedLanguage === "back") {
3249
+ if (!selectedLanguage) {
3250
+ continue;
3251
+ }
3252
+ if (selectedLanguage === "reset-defaults") {
3253
+ const approved = await confirmResetChanges(
3254
+ ctx,
3255
+ "Confirm static-check reset",
3256
+ [{
3257
+ label: "Language static code checkers",
3258
+ previousValue: formatStaticCheckLanguagesSummary(config),
3259
+ nextValue: formatStaticCheckLanguagesSummary({ ...config, "static-check": getDefaultStaticCheckConfig() }),
3260
+ }].filter((change) => change.previousValue !== change.nextValue),
3261
+ "Approve restoring the documented per-language static-check defaults.",
3262
+ "Abort the static-check reset and keep the current values.",
3263
+ );
3264
+ if (!approved) {
3265
+ ctx.ui.notify("Aborted static-check reset", "info");
3266
+ continue;
3267
+ }
3268
+ resetStaticCheckConfig(config);
3269
+ onConfigChange();
3270
+ ctx.ui.notify("Restored default static code checker configuration", "info");
2340
3271
  continue;
2341
3272
  }
2342
3273
 
@@ -2356,23 +3287,64 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
2356
3287
  entry.params = params;
2357
3288
  }
2358
3289
 
2359
- config["static-check"][selectedLanguage] ??= [];
2360
- config["static-check"][selectedLanguage]!.push(entry);
3290
+ config["static-check"][selectedLanguage] ??= createStaticCheckLanguageConfig([]);
3291
+ config["static-check"][selectedLanguage]!.enabled = "enable";
3292
+ config["static-check"][selectedLanguage]!.checkers.push(entry);
3293
+ onConfigChange();
2361
3294
  ctx.ui.notify(`Added ${entry.module} checker for ${selectedLanguage}`, "info");
2362
3295
  continue;
2363
3296
  }
2364
3297
 
2365
3298
  if (staticChoice === "remove-static-check-entry") {
2366
3299
  const configuredLanguage = await showPiUsereqSettingsMenu(ctx, "Remove static code checker", buildConfiguredStaticCheckLanguageChoices(config));
2367
- if (!configuredLanguage || configuredLanguage === "back") {
3300
+ if (!configuredLanguage) {
2368
3301
  continue;
2369
3302
  }
2370
- delete config["static-check"][configuredLanguage];
3303
+ if (configuredLanguage === "reset-defaults") {
3304
+ const approved = await confirmResetChanges(
3305
+ ctx,
3306
+ "Confirm static-check reset",
3307
+ [{
3308
+ label: "Language static code checkers",
3309
+ previousValue: formatStaticCheckLanguagesSummary(config),
3310
+ nextValue: formatStaticCheckLanguagesSummary({ ...config, "static-check": getDefaultStaticCheckConfig() }),
3311
+ }].filter((change) => change.previousValue !== change.nextValue),
3312
+ "Approve restoring the documented per-language static-check defaults.",
3313
+ "Abort the static-check reset and keep the current values.",
3314
+ );
3315
+ if (!approved) {
3316
+ ctx.ui.notify("Aborted static-check reset", "info");
3317
+ continue;
3318
+ }
3319
+ resetStaticCheckConfig(config);
3320
+ onConfigChange();
3321
+ ctx.ui.notify("Restored default static code checker configuration", "info");
3322
+ continue;
3323
+ }
3324
+ config["static-check"][configuredLanguage] = createStaticCheckLanguageConfig([]);
3325
+ onConfigChange();
2371
3326
  ctx.ui.notify(`Removed static-check entries for ${configuredLanguage}`, "info");
2372
3327
  continue;
2373
3328
  }
3329
+
2374
3330
  if (staticChoice === "reset-defaults") {
2375
- config["static-check"] = {};
3331
+ const approved = await confirmResetChanges(
3332
+ ctx,
3333
+ "Confirm static-check reset",
3334
+ [{
3335
+ label: "Language static code checkers",
3336
+ previousValue: formatStaticCheckLanguagesSummary(config),
3337
+ nextValue: formatStaticCheckLanguagesSummary({ ...config, "static-check": getDefaultStaticCheckConfig() }),
3338
+ }].filter((change) => change.previousValue !== change.nextValue),
3339
+ "Approve restoring the documented per-language static-check defaults.",
3340
+ "Abort the static-check reset and keep the current values.",
3341
+ );
3342
+ if (!approved) {
3343
+ ctx.ui.notify("Aborted static-check reset", "info");
3344
+ continue;
3345
+ }
3346
+ resetStaticCheckConfig(config);
3347
+ onConfigChange();
2376
3348
  ctx.ui.notify("Restored default static code checker configuration", "info");
2377
3349
  }
2378
3350
  }
@@ -2380,16 +3352,21 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
2380
3352
 
2381
3353
  /**
2382
3354
  * @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.
3355
+ * @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
3356
  * @param[in] cwd {string} Current working directory.
2385
3357
  * @param[in] config {UseReqConfig} Effective project configuration.
2386
3358
  * @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
3359
+ * @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
3360
  */
2389
3361
  function buildPiUsereqMenuChoices(
2390
3362
  cwd: string,
2391
3363
  config: UseReqConfig,
2392
3364
  ): PiUsereqSettingsMenuChoice[] {
3365
+ const autoGitCommitDisabled = config.AUTO_GIT_COMMIT === "disable";
3366
+ const effectiveGitWorktreeEnabled = resolveEffectiveGitWorktreeEnabled(
3367
+ config.AUTO_GIT_COMMIT,
3368
+ config.GIT_WORKTREE_ENABLED,
3369
+ );
2393
3370
  return [
2394
3371
  {
2395
3372
  id: "docs-dir",
@@ -2409,11 +3386,37 @@ function buildPiUsereqMenuChoices(
2409
3386
  value: config["tests-dir"],
2410
3387
  description: "Edit the repository-relative directory used for project test assets and static-check selection.",
2411
3388
  },
3389
+ {
3390
+ id: "auto-git-commit",
3391
+ label: "Auto git commit",
3392
+ value: config.AUTO_GIT_COMMIT,
3393
+ description: "Select bundled `git_commit.md` or `git_read-only.md` for `%%COMMIT%%`; disabling also forces prompt-command worktrees off.",
3394
+ },
3395
+ {
3396
+ id: "git-worktree-enabled",
3397
+ label: "Git worktree",
3398
+ labelTone: autoGitCommitDisabled ? "dim" : undefined,
3399
+ value: effectiveGitWorktreeEnabled,
3400
+ valueTone: autoGitCommitDisabled ? "dim" : undefined,
3401
+ description: autoGitCommitDisabled
3402
+ ? "Forced to `disable` while `Auto git commit` is disabled."
3403
+ : "Enable or disable prompt-command worktree orchestration.",
3404
+ },
3405
+ {
3406
+ id: "git-worktree-prefix",
3407
+ label: "Worktree prefix",
3408
+ labelTone: autoGitCommitDisabled ? "dim" : undefined,
3409
+ value: config.GIT_WORKTREE_PREFIX,
3410
+ valueTone: autoGitCommitDisabled ? "dim" : undefined,
3411
+ description: autoGitCommitDisabled
3412
+ ? "Stored prefix is locked while `Auto git commit` is disabled."
3413
+ : "Edit the static prefix used by generated worktree names.",
3414
+ },
2412
3415
  {
2413
3416
  id: "static-check",
2414
3417
  label: "Language static code checkers",
2415
3418
  value: formatStaticCheckLanguagesSummary(config),
2416
- description: "Manage guided Command static-check entries by language.",
3419
+ description: "Manage guided Command static-check entries and per-language enable flags.",
2417
3420
  },
2418
3421
  {
2419
3422
  id: "startup-tools",
@@ -2427,25 +3430,22 @@ function buildPiUsereqMenuChoices(
2427
3430
  value: `notification:${formatPiNotifyStatus(config)} • sound:${config["notify-sound"]} • pushover:${formatPiNotifyPushoverStatus(config)}`,
2428
3431
  description: "Manage command-notify, sound, and Pushover settings with dedicated event submenus.",
2429
3432
  },
3433
+ {
3434
+ id: "debug",
3435
+ label: "Debug",
3436
+ value: formatDebugMenuSummary(config),
3437
+ description: "Manage debug logging for tools and `req-*` prompt orchestration.",
3438
+ },
2430
3439
  {
2431
3440
  id: "show-config",
2432
3441
  label: "Show configuration",
2433
3442
  value: formatProjectConfigPathForMenu(cwd),
2434
3443
  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.",
3444
+ description: "Persist the current project configuration file and write its exact text into the editor.",
2448
3445
  },
3446
+ ...buildTerminalSettingsMenuChoices({
3447
+ resetDefaultsDescription: "Restore the default pi-usereq configuration for the current project base.",
3448
+ }),
2449
3449
  ];
2450
3450
  }
2451
3451
 
@@ -2470,24 +3470,16 @@ function buildSrcDirMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoic
2470
3470
  value: config["src-dir"].join(", "),
2471
3471
  description: "Select one configured source directory to remove from the current configuration.",
2472
3472
  },
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
- },
3473
+ ...buildTerminalSettingsMenuChoices({
3474
+ resetDefaultsValue: `${DEFAULT_SRC_DIRS.join(", ")}`,
3475
+ resetDefaultsDescription: "Restore the documented default source-directory configuration.",
3476
+ }),
2485
3477
  ];
2486
3478
  }
2487
3479
 
2488
3480
  /**
2489
3481
  * @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.
3482
+ * @details Exposes every configured `src-dir` entry as one removable row and appends subtree-local `Reset defaults` plus `Save and close` rows. Runtime is O(s) in source-directory count. No external state is mutated.
2491
3483
  * @param[in] config {UseReqConfig} Effective project configuration.
2492
3484
  * @return {PiUsereqSettingsMenuChoice[]} Ordered removable source-directory choices.
2493
3485
  * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154
@@ -2500,34 +3492,36 @@ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuCh
2500
3492
  value: "remove",
2501
3493
  description: `Remove the source-directory entry ${entry} from the current configuration.`,
2502
3494
  })),
2503
- {
2504
- id: "back",
2505
- label: "Back",
2506
- value: "",
2507
- description: "Return to the source-directory menu.",
2508
- },
3495
+ ...buildTerminalSettingsMenuChoices({
3496
+ resetDefaultsValue: `${DEFAULT_SRC_DIRS.join(", ")}`,
3497
+ resetDefaultsDescription: "Restore the documented default source-directory configuration.",
3498
+ }),
2509
3499
  ];
2510
3500
  }
2511
3501
 
2512
3502
  /**
2513
3503
  * @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.
3504
+ * @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
3505
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2516
3506
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2517
3507
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2518
3508
  * @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
3509
+ * @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
3510
  */
2521
3511
  async function configurePiUsereq(
2522
3512
  pi: ExtensionAPI,
2523
3513
  ctx: ExtensionCommandContext,
2524
3514
  statusController: PiUsereqStatusController,
2525
3515
  ): Promise<void> {
3516
+ bootstrapRuntimePathState(ctx.cwd, {
3517
+ gitPath: resolveRuntimeGitPath(ctx.cwd),
3518
+ });
2526
3519
  let config = loadProjectConfig(ctx.cwd);
2527
3520
  const projectBase = getProjectBase(ctx.cwd);
2528
3521
  const initialShortcut = config["notify-sound-toggle-shortcut"];
2529
- const ensureSaved = () => saveProjectConfig(ctx.cwd, config);
2530
- const refreshStatus = () => {
3522
+ const persistConfigChange = () => {
3523
+ Object.assign(config, normalizeConfigPaths(projectBase, config));
3524
+ saveProjectConfig(ctx.cwd, config);
2531
3525
  setPiUsereqStatusConfig(statusController, config);
2532
3526
  renderPiUsereqStatus(statusController, ctx);
2533
3527
  };
@@ -2540,9 +3534,7 @@ async function configurePiUsereq(
2540
3534
  buildPiUsereqMenuChoices(ctx.cwd, config),
2541
3535
  { initialSelectedId: focusedChoiceId },
2542
3536
  );
2543
- if (!choice || choice === "save-and-close") {
2544
- ensureSaved();
2545
- refreshStatus();
3537
+ if (!choice) {
2546
3538
  if (config["notify-sound-toggle-shortcut"] !== initialShortcut) {
2547
3539
  ctx.ui.notify("Sound toggle hotkey bind updated; run /reload to apply the new binding", "info");
2548
3540
  }
@@ -2551,12 +3543,61 @@ async function configurePiUsereq(
2551
3543
  focusedChoiceId = choice;
2552
3544
  if (choice === "docs-dir") {
2553
3545
  const value = await ctx.ui.input("Document directory", config["docs-dir"]);
2554
- if (value?.trim()) config["docs-dir"] = value.trim();
3546
+ if (value?.trim()) {
3547
+ config["docs-dir"] = value.trim();
3548
+ persistConfigChange();
3549
+ }
2555
3550
  continue;
2556
3551
  }
2557
3552
  if (choice === "tests-dir") {
2558
3553
  const value = await ctx.ui.input("Unit tests directory", config["tests-dir"]);
2559
- if (value?.trim()) config["tests-dir"] = value.trim();
3554
+ if (value?.trim()) {
3555
+ config["tests-dir"] = value.trim();
3556
+ persistConfigChange();
3557
+ }
3558
+ continue;
3559
+ }
3560
+ if (choice === "auto-git-commit") {
3561
+ const nextAutoGitCommit = config.AUTO_GIT_COMMIT === "enable"
3562
+ ? "disable"
3563
+ : "enable";
3564
+ config.AUTO_GIT_COMMIT = nextAutoGitCommit;
3565
+ if (nextAutoGitCommit === "disable") {
3566
+ config.GIT_WORKTREE_ENABLED = "disable";
3567
+ persistConfigChange();
3568
+ ctx.ui.notify("Auto git commit disabled; Git worktree forced off", "info");
3569
+ } else {
3570
+ persistConfigChange();
3571
+ ctx.ui.notify("Auto git commit enabled", "info");
3572
+ }
3573
+ continue;
3574
+ }
3575
+ if (choice === "git-worktree-enabled") {
3576
+ if (config.AUTO_GIT_COMMIT === "disable") {
3577
+ ctx.ui.notify("Git worktree is locked while Auto git commit is disabled", "info");
3578
+ continue;
3579
+ }
3580
+ config.GIT_WORKTREE_ENABLED = config.GIT_WORKTREE_ENABLED === "enable"
3581
+ ? "disable"
3582
+ : "enable";
3583
+ persistConfigChange();
3584
+ ctx.ui.notify(
3585
+ `Git worktree ${resolveEffectiveGitWorktreeEnabled(config.AUTO_GIT_COMMIT, config.GIT_WORKTREE_ENABLED) === "enable" ? "enabled" : "disabled"}`,
3586
+ "info",
3587
+ );
3588
+ continue;
3589
+ }
3590
+ if (choice === "git-worktree-prefix") {
3591
+ if (config.AUTO_GIT_COMMIT === "disable") {
3592
+ ctx.ui.notify("Worktree prefix is locked while Auto git commit is disabled", "info");
3593
+ continue;
3594
+ }
3595
+ const value = await ctx.ui.input("Worktree prefix", config.GIT_WORKTREE_PREFIX);
3596
+ if (value !== undefined) {
3597
+ config.GIT_WORKTREE_PREFIX = value.trim() || DEFAULT_GIT_WORKTREE_PREFIX;
3598
+ persistConfigChange();
3599
+ ctx.ui.notify(`Worktree prefix set to ${config.GIT_WORKTREE_PREFIX}`, "info");
3600
+ }
2560
3601
  continue;
2561
3602
  }
2562
3603
  if (choice === "src-dir") {
@@ -2565,7 +3606,7 @@ async function configurePiUsereq(
2565
3606
  const srcAction = await showPiUsereqSettingsMenu(ctx, "Source-code directories", buildSrcDirMenuChoices(config), {
2566
3607
  initialSelectedId: srcFocusedChoiceId,
2567
3608
  });
2568
- if (!srcAction || srcAction === "save-and-close") {
3609
+ if (!srcAction) {
2569
3610
  break;
2570
3611
  }
2571
3612
  srcFocusedChoiceId = srcAction;
@@ -2573,47 +3614,118 @@ async function configurePiUsereq(
2573
3614
  const value = await ctx.ui.input("New source-code directory", "src");
2574
3615
  if (value?.trim()) {
2575
3616
  config["src-dir"] = [...config["src-dir"], value.trim()];
3617
+ persistConfigChange();
2576
3618
  }
2577
3619
  continue;
2578
3620
  }
2579
3621
  if (srcAction === "remove-src-dir-entry") {
2580
3622
  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"];
3623
+ if (!toRemove) {
3624
+ continue;
3625
+ }
3626
+ if (toRemove === "reset-defaults") {
3627
+ const approved = await confirmResetChanges(
3628
+ ctx,
3629
+ "Confirm source-directory reset",
3630
+ [{
3631
+ label: "Source-code directories",
3632
+ previousValue: config["src-dir"].join(", "),
3633
+ nextValue: DEFAULT_SRC_DIRS.join(", "),
3634
+ }].filter((change) => change.previousValue !== change.nextValue),
3635
+ "Approve restoring the documented default source-directory configuration.",
3636
+ "Abort the source-directory reset and keep the current values.",
3637
+ );
3638
+ if (!approved) {
3639
+ ctx.ui.notify("Aborted source-directory reset", "info");
3640
+ continue;
2585
3641
  }
3642
+ config["src-dir"] = [...DEFAULT_SRC_DIRS];
3643
+ persistConfigChange();
3644
+ ctx.ui.notify("Restored default source-code directories", "info");
3645
+ continue;
3646
+ }
3647
+ config["src-dir"] = config["src-dir"].filter((entry) => entry !== toRemove);
3648
+ if (config["src-dir"].length === 0) {
3649
+ config["src-dir"] = ["src"];
2586
3650
  }
3651
+ persistConfigChange();
2587
3652
  continue;
2588
3653
  }
2589
3654
  if (srcAction === "reset-defaults") {
3655
+ const approved = await confirmResetChanges(
3656
+ ctx,
3657
+ "Confirm source-directory reset",
3658
+ [{
3659
+ label: "Source-code directories",
3660
+ previousValue: config["src-dir"].join(", "),
3661
+ nextValue: DEFAULT_SRC_DIRS.join(", "),
3662
+ }].filter((change) => change.previousValue !== change.nextValue),
3663
+ "Approve restoring the documented default source-directory configuration.",
3664
+ "Abort the source-directory reset and keep the current values.",
3665
+ );
3666
+ if (!approved) {
3667
+ ctx.ui.notify("Aborted source-directory reset", "info");
3668
+ continue;
3669
+ }
2590
3670
  config["src-dir"] = [...DEFAULT_SRC_DIRS];
3671
+ persistConfigChange();
2591
3672
  ctx.ui.notify("Restored default source-code directories", "info");
2592
3673
  }
2593
3674
  }
2594
3675
  continue;
2595
3676
  }
2596
3677
  if (choice === "static-check") {
2597
- await configureStaticCheckMenu(ctx, config);
3678
+ await configureStaticCheckMenu(ctx, config, persistConfigChange);
2598
3679
  continue;
2599
3680
  }
2600
3681
  if (choice === "startup-tools") {
2601
- await configurePiUsereqToolsMenu(pi, ctx, config);
3682
+ await configurePiUsereqToolsMenu(pi, ctx, config, persistConfigChange);
2602
3683
  continue;
2603
3684
  }
2604
3685
  if (choice === "notifications") {
2605
- await configurePiNotifyMenu(ctx, config);
3686
+ await configurePiNotifyMenu(ctx, config, persistConfigChange);
3687
+ continue;
3688
+ }
3689
+ if (choice === "debug") {
3690
+ await configureDebugMenu(ctx, config, persistConfigChange);
2606
3691
  continue;
2607
3692
  }
2608
3693
  if (choice === "reset-defaults") {
2609
- config = getDefaultConfig(projectBase);
3694
+ const defaultConfig = getDefaultConfig(projectBase);
3695
+ const approved = await confirmResetChanges(
3696
+ ctx,
3697
+ "Confirm pi-usereq reset",
3698
+ [
3699
+ { label: "Document directory", previousValue: config["docs-dir"], nextValue: defaultConfig["docs-dir"] },
3700
+ { label: "Source-code directories", previousValue: config["src-dir"].join(", "), nextValue: defaultConfig["src-dir"].join(", ") },
3701
+ { label: "Unit tests directory", previousValue: config["tests-dir"], nextValue: defaultConfig["tests-dir"] },
3702
+ { label: "Auto git commit", previousValue: config.AUTO_GIT_COMMIT, nextValue: defaultConfig.AUTO_GIT_COMMIT },
3703
+ { label: "Git worktree", previousValue: config.GIT_WORKTREE_ENABLED, nextValue: defaultConfig.GIT_WORKTREE_ENABLED },
3704
+ { label: "Worktree prefix", previousValue: config.GIT_WORKTREE_PREFIX, nextValue: defaultConfig.GIT_WORKTREE_PREFIX },
3705
+ { label: "Enable tools", previousValue: String(getConfiguredEnabledPiUsereqTools(config).length), nextValue: String(getConfiguredEnabledPiUsereqTools(defaultConfig).length) },
3706
+ { label: "Notifications", previousValue: buildPiNotifyMenuChoices(config).length.toString(), nextValue: buildPiNotifyMenuChoices(defaultConfig).length.toString() },
3707
+ { label: "Debug", previousValue: formatDebugMenuSummary(config), nextValue: formatDebugMenuSummary(defaultConfig) },
3708
+ ].filter((change) => change.previousValue !== change.nextValue),
3709
+ "Approve restoring the default pi-usereq configuration.",
3710
+ "Abort the pi-usereq reset and keep the current values.",
3711
+ );
3712
+ if (!approved) {
3713
+ ctx.ui.notify("Aborted pi-usereq reset", "info");
3714
+ continue;
3715
+ }
3716
+ config = defaultConfig;
2610
3717
  applyConfiguredPiUsereqTools(pi, config);
3718
+ persistConfigChange();
2611
3719
  ctx.ui.notify("Restored all default configuration values", "info");
2612
3720
  continue;
2613
3721
  }
2614
3722
  if (choice === "show-config") {
2615
- ctx.ui.setEditorText(`${JSON.stringify(config, null, 2)}\n`);
2616
- continue;
3723
+ persistConfigChange();
3724
+ if (config["notify-sound-toggle-shortcut"] !== initialShortcut) {
3725
+ ctx.ui.notify("Sound toggle hotkey bind updated; run /reload to apply the new binding", "info");
3726
+ }
3727
+ writePersistedProjectConfigToEditor(ctx, ctx.cwd, config);
3728
+ return;
2617
3729
  }
2618
3730
  }
2619
3731
  }
@@ -2645,14 +3757,14 @@ function registerConfigCommands(
2645
3757
  * notification-sound shortcut when the runtime supports shortcuts, and
2646
3758
  * installs shared wrappers for all supported pi lifecycle hooks so status
2647
3759
  * 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
3760
+ * Pushover metadata, tool-result debug logging, and prompt-orchestration debug
3761
+ * effects remain synchronized with runtime events. Runtime is O(h) in hook
2650
3762
  * count during registration. Side effects include filesystem reads,
2651
- * command/tool/shortcut registration, UI updates, active-tool changes, and
2652
- * timer scheduling.
3763
+ * command/tool/shortcut registration, UI updates, active-tool changes,
3764
+ * optional debug-log writes, and timer scheduling.
2653
3765
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2654
3766
  * @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
3767
+ * @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-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, 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
3768
  */
2657
3769
  export default function piUsereqExtension(pi: ExtensionAPI): void {
2658
3770
  const statusController = createPiUsereqStatusController();