pi-usereq 0.11.0 → 0.13.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 (53) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +6 -6
  3. package/package.json +1 -1
  4. package/pi-usereq/docs/REFERENCES.md +1135 -834
  5. package/pi-usereq/docs/REQUIREMENTS.md +177 -110
  6. package/pi-usereq/docs/WORKFLOW.md +244 -79
  7. package/scripts/lib/extension-debug-harness.ts +2 -2
  8. package/scripts/tool-args-to-params.ts +2 -2
  9. package/src/cli.ts +12 -12
  10. package/src/core/config.ts +541 -180
  11. package/src/core/extension-status.ts +69 -12
  12. package/src/core/path-context.ts +19 -4
  13. package/src/core/pi-notify.ts +5 -5
  14. package/src/core/pi-usereq-tools.ts +4 -2
  15. package/src/core/prompt-command-catalog.ts +4 -5
  16. package/src/core/prompt-command-runtime.ts +183 -44
  17. package/src/core/prompts.ts +0 -2
  18. package/src/core/req-references-command.ts +175 -0
  19. package/src/core/req-reset-command.ts +323 -0
  20. package/src/core/resources.ts +6 -23
  21. package/src/core/settings-menu.ts +85 -28
  22. package/src/core/tool-runner.ts +26 -6
  23. package/src/index.ts +601 -116
  24. package/tests/attended-results-scenarios.ts +15 -9
  25. package/tests/cli-command-option-parity.test.ts +53 -35
  26. package/tests/debug-extension-harness.test.ts +8 -10
  27. package/tests/extension-registration.test.ts +1204 -205
  28. package/tests/helpers.ts +29 -6
  29. package/tests/oracle-project.test.ts +4 -4
  30. package/tests/oracle-standalone.test.ts +5 -5
  31. package/src/core/reference-payload.ts +0 -752
  32. package/src/resources/prompts/references.md +0 -64
  33. /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
  34. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
  35. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
  36. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
  37. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
  38. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
  39. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
  40. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
  41. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
  42. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
  43. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
  44. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
  45. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
  46. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
  47. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
  48. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
  49. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
  50. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
  51. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
  52. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
  53. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_zig.zig.json +0 -0
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
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.11.0";
11
+ export const VERSION = "0.13.0";
12
12
 
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
@@ -32,6 +32,7 @@ import {
32
32
  createStaticCheckLanguageConfig,
33
33
  getDefaultConfig,
34
34
  getDefaultStaticCheckConfig,
35
+ getGlobalConfigPath,
35
36
  getProjectConfigPath,
36
37
  loadConfig,
37
38
  normalizeConfigPaths,
@@ -91,6 +92,18 @@ import {
91
92
  restorePromptCommandExecution,
92
93
  type PromptCommandExecutionPlan,
93
94
  } from "./core/prompt-command-runtime.js";
95
+ import {
96
+ REQ_REFERENCES_COMMAND_DESCRIPTION,
97
+ executeReqReferencesCommandExecution,
98
+ prepareReqReferencesCommandExecution,
99
+ } from "./core/req-references-command.js";
100
+ import {
101
+ REQ_RESET_COMMAND_DESCRIPTION,
102
+ executeReqResetCommandExecution,
103
+ prepareReqResetCommandExecution,
104
+ type ReqResetCommandExecutionResult,
105
+ type ReqResetCommandPlan,
106
+ } from "./core/req-reset-command.js";
94
107
  import {
95
108
  DEBUG_PROMPT_NAMES,
96
109
  DEBUG_WORKFLOW_STATES,
@@ -114,6 +127,7 @@ import {
114
127
  import { PROMPT_COMMAND_NAMES } from "./core/prompt-command-catalog.js";
115
128
  import { resolveRuntimeGitPath } from "./core/runtime-project-paths.js";
116
129
  import {
130
+ clearPersistedPromptCommandRuntimeState,
117
131
  readPersistedPromptCommandRuntimeState,
118
132
  writePersistedPromptCommandRuntimeState,
119
133
  } from "./core/prompt-command-state.js";
@@ -123,8 +137,10 @@ import {
123
137
  PI_USEREQ_STATUS_HOOK_NAMES,
124
138
  createPiUsereqStatusController,
125
139
  disposePiUsereqStatusController,
140
+ getPiUsereqRuntimeSoundLevel,
126
141
  isStaleExtensionContextError,
127
142
  renderPiUsereqStatus,
143
+ setPiUsereqRuntimeSoundLevel,
128
144
  setPiUsereqStatusConfig,
129
145
  setPiUsereqWorkflowState,
130
146
  shouldPreservePromptCommandStateOnShutdown,
@@ -135,13 +151,14 @@ import {
135
151
  import {
136
152
  runCompress,
137
153
  runFilesCompress,
138
- runFilesReferences,
139
154
  runFilesSearch,
140
155
  runFilesStaticCheck,
156
+ runFilesSummarize,
141
157
  runFilesTokens,
142
158
  runProjectStaticCheck,
143
159
  runReferences,
144
160
  runSearch,
161
+ runSummarize,
145
162
  runTokens,
146
163
  type ToolResult,
147
164
  } from "./core/tool-runner.js";
@@ -252,12 +269,12 @@ function loadProjectConfig(cwd: string): UseReqConfig {
252
269
  }
253
270
 
254
271
  /**
255
- * @brief Persists project configuration from the extension runtime.
256
- * @details Resolves the project base, normalizes configured directory paths into project-relative form, and delegates persistence to `saveConfig` without serializing runtime-derived path metadata. Runtime is O(n) in config size. Side effects include config-file writes.
272
+ * @brief Persists effective project configuration from the extension runtime.
273
+ * @details Resolves the project base, normalizes configured local directory paths into project-relative form, and delegates split local/global persistence to `saveConfig` without serializing runtime-derived path metadata. Runtime is O(n) in config size. Side effects include config-file writes.
257
274
  * @param[in] cwd {string} Current working directory.
258
- * @param[in] config {UseReqConfig} Configuration to persist.
275
+ * @param[in] config {UseReqConfig} Effective configuration to persist.
259
276
  * @return {void} No return value.
260
- * @satisfies REQ-146
277
+ * @satisfies REQ-146, REQ-315
261
278
  */
262
279
  function saveProjectConfig(cwd: string, config: UseReqConfig): void {
263
280
  const projectBase = getProjectBase(cwd);
@@ -265,18 +282,28 @@ function saveProjectConfig(cwd: string, config: UseReqConfig): void {
265
282
  }
266
283
 
267
284
  /**
268
- * @brief Formats the current project config path for top-level menu display.
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.
285
+ * @brief Formats the current local config path for top-level menu display.
286
+ * @details Resolves `<base-path>/.pi-usereq.json` from the cwd-derived project base and reuses the shared runtime-path formatter so the `Show local configuration` row uses the documented `~`-relative display contract. Runtime is O(p) in path length. No external state is mutated.
270
287
  * @param[in] cwd {string} Current working directory.
271
- * @return {string} `~`-relative or absolute config path display value.
288
+ * @return {string} `~`-relative or absolute local config-path display value.
272
289
  * @satisfies REQ-162
273
290
  */
274
- function formatProjectConfigPathForMenu(cwd: string): string {
291
+ function formatLocalConfigPathForMenu(cwd: string): string {
275
292
  return formatRuntimePathForDisplay(
276
293
  getProjectConfigPath(getProjectBase(cwd)),
277
294
  );
278
295
  }
279
296
 
297
+ /**
298
+ * @brief Formats the current global config path for top-level menu display.
299
+ * @details Resolves `~/.config/pi-usereq/config.json` through the shared runtime-path formatter so the `Show global configuration` row uses the documented `~`-relative display contract. Runtime is O(p) in path length. No external state is mutated.
300
+ * @return {string} `~`-relative or absolute global config-path display value.
301
+ * @satisfies REQ-319
302
+ */
303
+ function formatGlobalConfigPathForMenu(): string {
304
+ return formatRuntimePathForDisplay(getGlobalConfigPath());
305
+ }
306
+
280
307
  /**
281
308
  * @brief Builds the standardized terminal rows appended to every configuration menu.
282
309
  * @details Returns the canonical value-less `Reset defaults` row so all configuration menus and descendant selector menus share the same terminal ordering contract without rendering `Save and close`. Runtime is O(1). No external state is mutated.
@@ -393,21 +420,46 @@ async function confirmResetChanges(
393
420
  }
394
421
 
395
422
  /**
396
- * @brief Writes the already-persisted project configuration file text into the editor.
397
- * @details Reads the current `.pi-usereq.json` file content from disk after the caller has saved any pending configuration changes and forwards that exact persisted text into the editor. Runtime is O(n) in serialized config size. Side effects include filesystem reads and editor-text mutation.
423
+ * @brief Writes one already-persisted config file text into the editor.
424
+ * @details Reads the target config file from disk after the caller has saved any pending changes and forwards the exact persisted text into the editor. Runtime is O(n) in serialized config size. Side effects include filesystem reads and editor-text mutation.
425
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
426
+ * @param[in] configPath {string} Absolute persisted config path.
427
+ * @return {void} No return value.
428
+ */
429
+ function writePersistedConfigToEditor(
430
+ ctx: ExtensionCommandContext,
431
+ configPath: string,
432
+ ): void {
433
+ ctx.ui.setEditorText(fs.readFileSync(configPath, "utf8"));
434
+ }
435
+
436
+ /**
437
+ * @brief Writes the already-persisted local configuration file text into the editor.
438
+ * @details Reads `<base-path>/.pi-usereq.json` from disk after the caller has saved any pending local and global configuration changes, then forwards that exact persisted text into the editor. Runtime is O(n) in serialized config size. Side effects include filesystem reads and editor-text mutation.
398
439
  * @param[in] ctx {ExtensionCommandContext} Active command context.
399
440
  * @param[in] cwd {string} Current working directory.
400
- * @param[in] _config {UseReqConfig} Unused effective project configuration retained for stable call-site shape.
401
441
  * @return {void} No return value.
402
442
  * @satisfies REQ-031
403
443
  */
404
- function writePersistedProjectConfigToEditor(
444
+ function writePersistedLocalConfigToEditor(
405
445
  ctx: ExtensionCommandContext,
406
446
  cwd: string,
407
- _config: UseReqConfig,
408
447
  ): void {
409
448
  const projectBase = getProjectBase(cwd);
410
- ctx.ui.setEditorText(fs.readFileSync(getProjectConfigPath(projectBase), "utf8"));
449
+ writePersistedConfigToEditor(ctx, getProjectConfigPath(projectBase));
450
+ }
451
+
452
+ /**
453
+ * @brief Writes the already-persisted global configuration file text into the editor.
454
+ * @details Reads `~/.config/pi-usereq/config.json` from disk after the caller has saved any pending local and global configuration changes, then 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.
455
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
456
+ * @return {void} No return value.
457
+ * @satisfies REQ-318
458
+ */
459
+ function writePersistedGlobalConfigToEditor(
460
+ ctx: ExtensionCommandContext,
461
+ ): void {
462
+ writePersistedConfigToEditor(ctx, getGlobalConfigPath());
411
463
  }
412
464
 
413
465
  /**
@@ -686,6 +738,44 @@ function executeMonolithicTool(operation: () => ToolResult): ReturnType<typeof b
686
738
  }
687
739
  }
688
740
 
741
+ /**
742
+ * @brief Executes one CLI-style runner for a status-only agent tool.
743
+ * @details Reuses the standalone tool-runner contract, preserves `content[0].text` as the status-only `success` or `error: <diagnostic>` payload, and strips success-path `stdout_lines` so `details.execution` stays limited to the numeric code plus optional residual stderr diagnostics. Runtime is dominated by the delegated runner. Side effects depend on the selected tool.
744
+ * @param[in] operation {() => ToolResult} Runner callback.
745
+ * @return {ReturnType<typeof buildMonolithicToolExecuteResult>} Status-only tool execute result.
746
+ * @satisfies REQ-294, REQ-295, REQ-296
747
+ */
748
+ function executeStatusTool(operation: () => ToolResult): ReturnType<typeof buildMonolithicToolExecuteResult> {
749
+ const normalizeExecution = (
750
+ payload: ReturnType<typeof buildMonolithicToolExecuteResult>,
751
+ ): ReturnType<typeof buildMonolithicToolExecuteResult> => {
752
+ const execution = payload.details.execution;
753
+ return {
754
+ content: payload.content,
755
+ details: {
756
+ execution: {
757
+ code: execution.code,
758
+ ...(Array.isArray(execution.stderr_lines) && execution.stderr_lines.length > 0
759
+ ? { stderr_lines: execution.stderr_lines }
760
+ : {}),
761
+ },
762
+ },
763
+ };
764
+ };
765
+
766
+ try {
767
+ return normalizeExecution(buildMonolithicToolExecuteResult(operation()));
768
+ } catch (error) {
769
+ const failure = normalizeToolFailure(error);
770
+ const diagnostic = failure.stderr.trim().replace(/^(Error|error):\s*/u, "") || "unknown failure";
771
+ return normalizeExecution(buildMonolithicToolExecuteResult({
772
+ stdout: "",
773
+ stderr: `error: ${diagnostic}`,
774
+ code: failure.code,
775
+ }));
776
+ }
777
+ }
778
+
689
779
  /**
690
780
  * @brief Starts delivery of one rendered prompt into the current active session.
691
781
  * @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.
@@ -838,7 +928,7 @@ function transitionPromptWorkflowState(
838
928
 
839
929
  /**
840
930
  * @brief Resolves the runtime slash-command description for one bundled prompt.
841
- * @details Reads the bundled prompt front matter, extracts its normalized `description` field, and falls back to the historical generated label when the prompt metadata omits a description. Runtime is O(n) in prompt length. Side effects are limited to filesystem reads.
931
+ * @details Reads the bundled prompt markdown, extracts the first `# ` heading payload, and falls back to the historical generated label when the prompt omits a level-one heading. Runtime is O(n) in prompt length. Side effects are limited to filesystem reads.
842
932
  * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
843
933
  * @return {string} Runtime command description.
844
934
  */
@@ -889,6 +979,38 @@ function notifyContextSafely(
889
979
  }
890
980
  }
891
981
 
982
+ /**
983
+ * @brief Rejects one non-`idle` req-command invocation and records the workflow error state.
984
+ * @details Builds a deterministic busy-state diagnostic from the current workflow state, transitions the shared workflow state to `error`, preserves any pending or active prompt execution metadata for later closure handling, emits an error notification, and throws `ReqError`. Bundled prompt commands reuse `transitionPromptWorkflowState(...)` when cached configuration is available so prompt debug logging captures the actual state transition; specialized non-prompt commands fall back to direct status mutation. Runtime is O(1). Side effects include workflow-state mutation, status-bar rendering, optional debug-log writes, and user notification delivery.
985
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
986
+ * @param[in] ctx {ExtensionContext | ExtensionCommandContext} Active extension context.
987
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName | undefined} Optional bundled prompt name used for prompt debug logging.
988
+ * @return {never} This helper always throws a deterministic `ReqError`.
989
+ * @throws {ReqError} Always throws because non-`idle` req commands are rejected.
990
+ * @satisfies REQ-224
991
+ */
992
+ function rejectNonIdleReqCommand(
993
+ statusController: PiUsereqStatusController,
994
+ ctx: ExtensionContext | ExtensionCommandContext,
995
+ promptName?: import("./core/prompt-command-catalog.js").PromptCommandName,
996
+ ): never {
997
+ const message = `ERROR: Prompt workflow state is ${statusController.state.workflowState}, expected idle.`;
998
+ if (promptName !== undefined && statusController.config !== undefined) {
999
+ transitionPromptWorkflowState(
1000
+ statusController,
1001
+ ctx,
1002
+ resolveDebugProjectBase(ctx.cwd, statusController),
1003
+ statusController.config,
1004
+ promptName,
1005
+ "error",
1006
+ );
1007
+ } else {
1008
+ setPiUsereqWorkflowState(statusController, "error", ctx);
1009
+ }
1010
+ notifyContextSafely(ctx, message, "error");
1011
+ throw new ReqError(message, 1);
1012
+ }
1013
+
892
1014
  /**
893
1015
  * @brief Returns the configurable active-tool inventory visible to the extension.
894
1016
  * @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.
@@ -947,14 +1069,14 @@ function applyConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig): v
947
1069
 
948
1070
  /**
949
1071
  * @brief Handles one intercepted pi lifecycle hook for pi-usereq status updates.
950
- * @details Applies session-start-specific resource validation, project-config refresh, startup-tool enablement, and selected debug-tool logging before forwarding the originating hook name and payload into the shared `updateExtensionStatus(...)` pipeline. Before `agent_start`, re-verifies any prepared prompt execution session switch. On `agent_end`, dispatches configured command-notify, sound, and prompt-specific Pushover effects, logs dedicated workflow-closure diagnostics, restores the original session-backed `base-path` for every matched worktree-backed completion by reusing persisted replacement-session command contexts when event contexts omit `switchSession()`, merges and deletes the worktree only for matched successful completions, tolerates stale replacement-session notification contexts after session replacement, retains the worktree plus notifies closure failure for interrupted or failed outcomes, logs selected prompt workflow transitions, and transitions workflow state through `merging`, `error`, and `idle` as required. On `session_shutdown`, captures pre-update prompt snapshots so workflow-shutdown diagnostics and same-runtime command continuation preserve the active prompt workflow state across switch-triggered rebinding, then disposes the shared controller. Runtime is dominated by configuration loading during `session_start` and git finalization during matched successful `agent_end` handling; all other hooks are O(1). Side effects include resource checks, active-tool mutation, active-session replacement, status updates, live-ticker disposal on shutdown, optional child-process spawning, outbound HTTPS requests, branch merges, worktree deletion, and optional debug-log writes.
1072
+ * @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()`, executes the stash-assisted merge-and-delete finalization path for every matched successful worktree-backed completion even when a later busy-command rejection already moved workflow state to `error`, emits a warning-only notification when restored `base-path` changes are reapplied after merge, 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.
951
1073
  * @param[in] pi {ExtensionAPI} Active extension API instance.
952
1074
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
953
1075
  * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
954
1076
  * @param[in] event {unknown} Hook payload forwarded by pi.
955
1077
  * @param[in] ctx {ExtensionContext} Active extension context.
956
1078
  * @return {Promise<void>} Promise resolved when hook processing completes.
957
- * @satisfies REQ-117, REQ-118, REQ-119, REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-176, REQ-178, REQ-184, REQ-185, REQ-186, REQ-187, REQ-208, REQ-209, REQ-221, REQ-228, REQ-229, REQ-230, REQ-244, REQ-245, REQ-246, REQ-247, REQ-276, REQ-277, REQ-278, REQ-279, REQ-280
1079
+ * @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, REQ-291, REQ-292
958
1080
  */
959
1081
  async function handleExtensionStatusEvent(
960
1082
  pi: ExtensionAPI,
@@ -1025,7 +1147,10 @@ async function handleExtensionStatusEvent(
1025
1147
  if (hookName === "agent_end") {
1026
1148
  if (statusController.config) {
1027
1149
  runPiNotifyEffects(
1028
- statusController.config,
1150
+ {
1151
+ ...statusController.config,
1152
+ "notify-sound": getPiUsereqRuntimeSoundLevel(statusController),
1153
+ },
1029
1154
  event as { messages: AgentEndEvent["messages"] },
1030
1155
  notifyRequest,
1031
1156
  );
@@ -1041,7 +1166,6 @@ async function handleExtensionStatusEvent(
1041
1166
  ? `ERROR: Prompt closure retained worktree ${activePromptRequest.worktreeDir} after ${outcome} outcome.`
1042
1167
  : undefined;
1043
1168
  const shouldFinalizeMatchedSuccess = outcome === "completed"
1044
- && statusController.state.workflowState === "running"
1045
1169
  && activePromptRequest.worktreeDir !== undefined;
1046
1170
  if (debugConfig) {
1047
1171
  logPromptWorkflowEvent(
@@ -1080,6 +1204,7 @@ async function handleExtensionStatusEvent(
1080
1204
  mergeSucceeded: boolean;
1081
1205
  cleanupSucceeded: boolean;
1082
1206
  errorMessage?: string;
1207
+ warningMessage?: string;
1083
1208
  activeContext?: unknown;
1084
1209
  }
1085
1210
  | undefined;
@@ -1134,6 +1259,14 @@ async function handleExtensionStatusEvent(
1134
1259
  }
1135
1260
  notifyContextSafely(promptContext, finalization.errorMessage, "error");
1136
1261
  }
1262
+ if (
1263
+ finalization.warningMessage
1264
+ && finalization.cleanupSucceeded
1265
+ && finalization.mergeSucceeded
1266
+ && !finalization.errorMessage
1267
+ ) {
1268
+ notifyContextSafely(promptContext, finalization.warningMessage, "info");
1269
+ }
1137
1270
  } else {
1138
1271
  try {
1139
1272
  promptContext = (await restorePromptCommandExecution(
@@ -1383,6 +1516,7 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1383
1516
  id: "debug-enabled",
1384
1517
  label: "Debug",
1385
1518
  value: config.DEBUG_ENABLED,
1519
+ values: ["enable", "disable"],
1386
1520
  description: "Enable or disable all debug logging behavior and unlock the remaining Debug rows.",
1387
1521
  },
1388
1522
  buildDebugMenuChoice(
@@ -1408,6 +1542,7 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1408
1542
  id: "debug-status-changes",
1409
1543
  label: "Status changes",
1410
1544
  value: normalizeDebugStatusChanges(config.DEBUG_STATUS_CHANGES),
1545
+ values: ["enable", "disable"],
1411
1546
  description: "Enable or disable `workflow_state` debug entries for prompt-orchestration transitions.",
1412
1547
  },
1413
1548
  debugEnabled,
@@ -1417,6 +1552,7 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1417
1552
  id: "debug-workflow-events",
1418
1553
  label: "Workflow events",
1419
1554
  value: normalizeDebugWorkflowEvents(config.DEBUG_WORKFLOW_EVENTS),
1555
+ values: ["enable", "disable"],
1420
1556
  description: "Enable or disable dedicated workflow debug entries for activation, restoration, closure, and session-shutdown diagnostics.",
1421
1557
  },
1422
1558
  debugEnabled,
@@ -1426,6 +1562,7 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1426
1562
  id: `debug-tool:${toolName}`,
1427
1563
  label: toolName,
1428
1564
  value: enabledTools.has(toolName) ? "enable" : "disable",
1565
+ values: ["enable", "disable"],
1429
1566
  description: PI_USEREQ_CUSTOM_TOOL_NAMES.includes(toolName as never)
1430
1567
  ? `Toggle debug logging for custom tool ${toolName}.`
1431
1568
  : `Toggle debug logging for embedded tool ${toolName}.`,
@@ -1437,6 +1574,7 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1437
1574
  id: `debug-prompt:${promptName}`,
1438
1575
  label: promptName,
1439
1576
  value: enabledPrompts.has(promptName) ? "enable" : "disable",
1577
+ values: ["enable", "disable"],
1440
1578
  description: `Toggle prompt-orchestration debug logging for /${promptName}.`,
1441
1579
  },
1442
1580
  debugEnabled,
@@ -1464,6 +1602,58 @@ async function configureDebugMenu(
1464
1602
  while (true) {
1465
1603
  const choice = await showPiUsereqSettingsMenu(ctx, "Debug", buildDebugMenuChoices(config), {
1466
1604
  initialSelectedId: focusedChoiceId,
1605
+ getChoices: () => buildDebugMenuChoices(config),
1606
+ onChange: (choiceId, newValue) => {
1607
+ if (choiceId === "debug-enabled") {
1608
+ config.DEBUG_ENABLED = newValue === "enable" ? "enable" : "disable";
1609
+ onConfigChange();
1610
+ ctx.ui.notify(`Debug ${config.DEBUG_ENABLED}`, "info");
1611
+ return;
1612
+ }
1613
+ if (choiceId === "debug-status-changes") {
1614
+ config.DEBUG_STATUS_CHANGES = normalizeDebugStatusChanges(newValue);
1615
+ onConfigChange();
1616
+ ctx.ui.notify(`Debug status-change logging ${config.DEBUG_STATUS_CHANGES}`, "info");
1617
+ return;
1618
+ }
1619
+ if (choiceId === "debug-workflow-events") {
1620
+ config.DEBUG_WORKFLOW_EVENTS = normalizeDebugWorkflowEvents(newValue);
1621
+ onConfigChange();
1622
+ ctx.ui.notify(`Debug workflow-event logging ${config.DEBUG_WORKFLOW_EVENTS}`, "info");
1623
+ return;
1624
+ }
1625
+ if (choiceId.startsWith("debug-tool:")) {
1626
+ const toolName = choiceId.slice("debug-tool:".length) as PiUsereqStartupToolName;
1627
+ const enabledTools = new Set(normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS));
1628
+ if (newValue === "enable") {
1629
+ enabledTools.add(toolName);
1630
+ } else {
1631
+ enabledTools.delete(toolName);
1632
+ }
1633
+ config.DEBUG_ENABLED_TOOLS = getDebugToolToggleNames().filter((name) => enabledTools.has(name));
1634
+ onConfigChange();
1635
+ ctx.ui.notify(
1636
+ `${newValue === "enable" ? "Enabled" : "Disabled"} debug logging for ${toolName}`,
1637
+ "info",
1638
+ );
1639
+ return;
1640
+ }
1641
+ if (choiceId.startsWith("debug-prompt:")) {
1642
+ const promptName = choiceId.slice("debug-prompt:".length) as (typeof DEBUG_PROMPT_NAMES)[number];
1643
+ const enabledPrompts = new Set(normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS));
1644
+ if (newValue === "enable") {
1645
+ enabledPrompts.add(promptName);
1646
+ } else {
1647
+ enabledPrompts.delete(promptName);
1648
+ }
1649
+ config.DEBUG_ENABLED_PROMPTS = DEBUG_PROMPT_NAMES.filter((name) => enabledPrompts.has(name));
1650
+ onConfigChange();
1651
+ ctx.ui.notify(
1652
+ `${newValue === "enable" ? "Enabled" : "Disabled"} debug logging for ${promptName}`,
1653
+ "info",
1654
+ );
1655
+ }
1656
+ },
1467
1657
  });
1468
1658
  if (!choice) {
1469
1659
  return;
@@ -1751,6 +1941,13 @@ const PI_NOTIFY_EVENT_MENU_DEFINITIONS: Record<
1751
1941
  },
1752
1942
  };
1753
1943
 
1944
+ /**
1945
+ * @brief Defines the canonical label used for persisted boot-sound menu rows.
1946
+ * @details Reuses one shared string literal across notification menu rows, selectors, reset previews, and tests so the persisted boot-sound terminology remains stable. Access complexity is O(1).
1947
+ * @satisfies REQ-149, REQ-179
1948
+ */
1949
+ const PI_NOTIFY_BOOT_SOUND_LABEL = "Enable sound (boot value)";
1950
+
1754
1951
  /**
1755
1952
  * @brief Formats the top-level summary value for one notification event submenu.
1756
1953
  * @details Counts enabled completed/interrupted/failed toggles for the selected transport and renders the result as `n/3 on` for right-aligned menu display. Runtime is O(1). No external state is mutated.
@@ -1806,6 +2003,7 @@ function buildPiNotifyEventMenuChoices(
1806
2003
  id: eventMenu.keys[row.eventId],
1807
2004
  label: row.label,
1808
2005
  value: config[eventMenu.keys[row.eventId]] ? "on" : "off",
2006
+ values: ["on", "off"],
1809
2007
  description: `${eventMenu.systemLabel}: ${row.description}`,
1810
2008
  })),
1811
2009
  ...buildTerminalSettingsMenuChoices({
@@ -1870,7 +2068,23 @@ async function configurePiNotifyEventMenu(
1870
2068
  ctx,
1871
2069
  eventMenu.submenuTitle,
1872
2070
  buildPiNotifyEventMenuChoices(config, eventMenu),
1873
- { initialSelectedId: focusedChoiceId },
2071
+ {
2072
+ initialSelectedId: focusedChoiceId,
2073
+ getChoices: () => buildPiNotifyEventMenuChoices(config, eventMenu),
2074
+ onChange: (choiceId, newValue) => {
2075
+ const enabled = newValue === "on";
2076
+ config[choiceId as PiNotifyEventBooleanConfigKey] = enabled;
2077
+ onConfigChange();
2078
+ const eventLabel = resolvePiNotifyEventLabel(
2079
+ choiceId as PiNotifyEventBooleanConfigKey,
2080
+ eventMenu,
2081
+ );
2082
+ ctx.ui.notify(
2083
+ `${eventMenu.systemLabel} ${eventLabel} ${enabled ? "enabled" : "disabled"}`,
2084
+ "info",
2085
+ );
2086
+ },
2087
+ },
1874
2088
  );
1875
2089
  if (!choice) {
1876
2090
  return;
@@ -1922,7 +2136,7 @@ async function configurePiNotifyEventMenu(
1922
2136
 
1923
2137
  /**
1924
2138
  * @brief Builds the direct Pushover rows rendered inside `Notifications`.
1925
- * @details Serializes the global enable flag, shared-event submenu launcher, priority, title, text, and credential rows into right-valued menu items appended after the sound-command rows, dims and disables the enable row until both credentials are populated, and escapes control characters for the single-line `Pushover text` value. Runtime is O(n) in the rendered text-template length. No external state is mutated.
2139
+ * @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, renders the locked value as `configure user/token keys first`, 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.
1926
2140
  * @param[in] config {UseReqConfig} Effective project configuration.
1927
2141
  * @return {PiUsereqSettingsMenuChoice[]} Ordered direct Pushover rows.
1928
2142
  * @satisfies REQ-163, REQ-165, REQ-172, REQ-184, REQ-185, REQ-198, REQ-234, REQ-235
@@ -1934,8 +2148,9 @@ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuCh
1934
2148
  id: "notify-pushover-enabled",
1935
2149
  label: "Enable pushover",
1936
2150
  labelTone: pushoverCredentialsReady ? undefined : "dim",
1937
- value: pushoverCredentialsReady ? formatPiNotifyPushoverStatus(config) : "off",
2151
+ value: pushoverCredentialsReady ? formatPiNotifyPushoverStatus(config) : "configure user/token keys first",
1938
2152
  valueTone: pushoverCredentialsReady ? undefined : "dim",
2153
+ values: ["on", "off"],
1939
2154
  disabled: !pushoverCredentialsReady,
1940
2155
  description: pushoverCredentialsReady
1941
2156
  ? "Enable or disable all Pushover delivery globally."
@@ -2018,10 +2233,10 @@ async function selectPiNotifyPushoverPriority(
2018
2233
 
2019
2234
  /**
2020
2235
  * @brief Builds the shared settings-menu choices for notification configuration.
2021
- * @details Serializes command-notify, sound, and Pushover blocks with dedicated shared-event submenu launchers so the settings-menu renderer can expose one unified but modular configuration surface, including locked Pushover enablement and escaped single-line rendering for `Pushover text`. Runtime is O(n) in the longest rendered command or text field. No external state is mutated.
2236
+ * @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, persisted boot-sound rows that stay decoupled from the active runtime sound level, 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.
2022
2237
  * @param[in] config {UseReqConfig} Effective project configuration.
2023
2238
  * @return {PiUsereqSettingsMenuChoice[]} Ordered notification-menu choice vector.
2024
- * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-188, REQ-193, REQ-198, REQ-234, REQ-235
2239
+ * @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, REQ-289
2025
2240
  */
2026
2241
  function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2027
2242
  return [
@@ -2029,6 +2244,7 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2029
2244
  id: "notify-enabled",
2030
2245
  label: "Enable notification",
2031
2246
  value: formatPiNotifyStatus(config),
2247
+ values: ["on", "off"],
2032
2248
  description: "Enable or disable command-notify delivery globally.",
2033
2249
  },
2034
2250
  buildPiNotifyEventLauncherChoice(
@@ -2043,9 +2259,9 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2043
2259
  },
2044
2260
  {
2045
2261
  id: "selected-sound-command",
2046
- label: "Enable sound",
2262
+ label: PI_NOTIFY_BOOT_SOUND_LABEL,
2047
2263
  value: config["notify-sound"],
2048
- description: "Select which sound command level is currently active.",
2264
+ description: "Edit the persisted boot sound level without changing the active runtime sound level.",
2049
2265
  },
2050
2266
  buildPiNotifyEventLauncherChoice(
2051
2267
  config,
@@ -2055,25 +2271,25 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2055
2271
  id: "sound-toggle-hotkey-bind",
2056
2272
  label: "Sound toggle hotkey bind",
2057
2273
  value: config["notify-sound-toggle-shortcut"],
2058
- description: "Edit the keyboard shortcut that cycles the selected sound command.",
2274
+ description: "Edit the keyboard shortcut that cycles the active runtime sound level.",
2059
2275
  },
2060
2276
  {
2061
2277
  id: "sound-command-low",
2062
2278
  label: "Sound command (low vol.)",
2063
2279
  value: config.PI_NOTIFY_SOUND_LOW_CMD,
2064
- description: "Edit the shell command used when the selected sound command is `low`.",
2280
+ description: "Edit the shell command used when the active runtime sound level is `low`.",
2065
2281
  },
2066
2282
  {
2067
2283
  id: "sound-command-mid",
2068
2284
  label: "Sound command (mid vol.)",
2069
2285
  value: config.PI_NOTIFY_SOUND_MID_CMD,
2070
- description: "Edit the shell command used when the selected sound command is `mid`.",
2286
+ description: "Edit the shell command used when the active runtime sound level is `mid`.",
2071
2287
  },
2072
2288
  {
2073
2289
  id: "sound-command-high",
2074
2290
  label: "Sound command (high vol.)",
2075
2291
  value: config.PI_NOTIFY_SOUND_HIGH_CMD,
2076
- description: "Edit the shell command used when the selected sound command is `high`.",
2292
+ description: "Edit the shell command used when the active runtime sound level is `high`.",
2077
2293
  },
2078
2294
  ...buildPiNotifyPushoverRows(config),
2079
2295
  ...buildTerminalSettingsMenuChoices({
@@ -2083,44 +2299,44 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2083
2299
  }
2084
2300
 
2085
2301
  /**
2086
- * @brief Opens the shared settings-menu selector for the active sound level.
2087
- * @details Reuses the pi-usereq settings-menu renderer so sound-level selection remains stylistically aligned with the notification menu and appends a value-less subtree-local `Reset defaults` row. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
2302
+ * @brief Opens the shared settings-menu selector for the persisted boot sound level.
2303
+ * @details Reuses the pi-usereq settings-menu renderer so boot-sound selection remains stylistically aligned with the notification menu, keeps the active runtime sound level unchanged, and appends a value-less subtree-local `Reset defaults` row. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
2088
2304
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2089
- * @param[in] currentLevel {PiNotifySoundLevel} Currently selected sound level.
2090
- * @return {Promise<PiNotifySoundLevel | "reset-defaults" | undefined>} Selected sound level, reset action, or `undefined` when cancelled.
2091
- * @satisfies REQ-131, REQ-179, REQ-192
2305
+ * @param[in] currentLevel {PiNotifySoundLevel} Persisted boot sound level.
2306
+ * @return {Promise<PiNotifySoundLevel | "reset-defaults" | undefined>} Selected boot sound level, reset action, or `undefined` when cancelled.
2307
+ * @satisfies REQ-131, REQ-179, REQ-192, REQ-289
2092
2308
  */
2093
2309
  async function selectPiNotifySoundLevel(
2094
2310
  ctx: ExtensionCommandContext,
2095
2311
  currentLevel: PiNotifySoundLevel,
2096
2312
  ): Promise<PiNotifySoundLevel | "reset-defaults" | undefined> {
2097
- const choice = await showPiUsereqSettingsMenu(ctx, "Enable sound", [
2313
+ const choice = await showPiUsereqSettingsMenu(ctx, PI_NOTIFY_BOOT_SOUND_LABEL, [
2098
2314
  {
2099
2315
  id: "none",
2100
2316
  label: "none",
2101
2317
  value: currentLevel === "none" ? "selected" : "",
2102
- description: "Disable sound-command delivery while preserving per-event sound toggles.",
2318
+ description: "Persist `none` as the boot sound level loaded during the next session start.",
2103
2319
  },
2104
2320
  {
2105
2321
  id: "low",
2106
2322
  label: "low",
2107
2323
  value: currentLevel === "low" ? "selected" : "",
2108
- description: "Use the low-volume sound command when sound delivery is enabled for the current event.",
2324
+ description: "Persist `low` as the boot sound level loaded during the next session start.",
2109
2325
  },
2110
2326
  {
2111
2327
  id: "mid",
2112
2328
  label: "mid",
2113
2329
  value: currentLevel === "mid" ? "selected" : "",
2114
- description: "Use the mid-volume sound command when sound delivery is enabled for the current event.",
2330
+ description: "Persist `mid` as the boot sound level loaded during the next session start.",
2115
2331
  },
2116
2332
  {
2117
2333
  id: "high",
2118
2334
  label: "high",
2119
2335
  value: currentLevel === "high" ? "selected" : "",
2120
- description: "Use the high-volume sound command when sound delivery is enabled for the current event.",
2336
+ description: "Persist `high` as the boot sound level loaded during the next session start.",
2121
2337
  },
2122
2338
  ...buildTerminalSettingsMenuChoices({
2123
- resetDefaultsDescription: "Restore the documented default sound level.",
2339
+ resetDefaultsDescription: "Restore the documented default boot sound level.",
2124
2340
  }),
2125
2341
  ], { initialSelectedId: currentLevel });
2126
2342
  if (!choice) {
@@ -2134,11 +2350,11 @@ async function selectPiNotifySoundLevel(
2134
2350
 
2135
2351
  /**
2136
2352
  * @brief Runs the interactive notification-configuration menu.
2137
- * @details Exposes command-notify, sound, and Pushover controls through the shared settings-menu renderer, delegates completed/interrupted/failed toggles to dedicated event submenus, keeps `Enable pushover` locked until both credentials are populated, decodes escaped control-sequence input for `Pushover text`, and preserves row focus across menu re-renders. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
2353
+ * @details Exposes command-notify, sound, and Pushover controls through the shared settings-menu renderer, persists every notification subtree mutation into global configuration, delegates completed/interrupted/failed toggles to dedicated event submenus, preserves boot-sound edits without altering the active runtime sound level, 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.
2138
2354
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2139
2355
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
2140
2356
  * @return {Promise<boolean>} `true` when the sound-toggle shortcut changed.
2141
- * @satisfies REQ-131, REQ-133, REQ-134, REQ-137, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-184, REQ-188, REQ-192, REQ-193, REQ-195, REQ-196, REQ-198, REQ-234, REQ-235
2357
+ * @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, REQ-288, REQ-289
2142
2358
  */
2143
2359
  async function configurePiNotifyMenu(
2144
2360
  ctx: ExtensionCommandContext,
@@ -2152,7 +2368,27 @@ async function configurePiNotifyMenu(
2152
2368
  ctx,
2153
2369
  "Notifications",
2154
2370
  buildPiNotifyMenuChoices(config),
2155
- { initialSelectedId: focusedChoiceId },
2371
+ {
2372
+ initialSelectedId: focusedChoiceId,
2373
+ getChoices: () => buildPiNotifyMenuChoices(config),
2374
+ onChange: (choiceId, newValue) => {
2375
+ if (choiceId === "notify-enabled" || choiceId === "notify-pushover-enabled") {
2376
+ if (choiceId === "notify-pushover-enabled" && !hasPiNotifyPushoverCredentials(config)) {
2377
+ config["notify-pushover-enabled"] = false;
2378
+ ctx.ui.notify("Populate both Pushover credential fields before enabling Pushover", "info");
2379
+ return;
2380
+ }
2381
+ const enabled = newValue === "on";
2382
+ config[choiceId as PiNotifyBooleanConfigKey] = enabled;
2383
+ onConfigChange();
2384
+ const labelMap: Record<string, string> = {
2385
+ "notify-enabled": "Notification",
2386
+ "notify-pushover-enabled": "Pushover",
2387
+ };
2388
+ ctx.ui.notify(`${labelMap[choiceId]} ${enabled ? "enabled" : "disabled"}`, "info");
2389
+ }
2390
+ },
2391
+ },
2156
2392
  );
2157
2393
  if (!choice) {
2158
2394
  return config["notify-sound-toggle-shortcut"] !== originalShortcut;
@@ -2216,22 +2452,22 @@ async function configurePiNotifyMenu(
2216
2452
  const approved = await confirmResetChanges(
2217
2453
  ctx,
2218
2454
  "Confirm sound reset",
2219
- [{ label: "Enable sound", previousValue: config["notify-sound"], nextValue: defaultSoundLevel }]
2455
+ [{ label: PI_NOTIFY_BOOT_SOUND_LABEL, previousValue: config["notify-sound"], nextValue: defaultSoundLevel }]
2220
2456
  .filter((change) => change.previousValue !== change.nextValue),
2221
- "Approve restoring the documented default sound level.",
2222
- "Abort the sound reset and keep the current value.",
2457
+ "Approve restoring the documented default boot sound level.",
2458
+ "Abort the boot sound reset and keep the current value.",
2223
2459
  );
2224
2460
  if (!approved) {
2225
- ctx.ui.notify("Aborted sound reset", "info");
2461
+ ctx.ui.notify("Aborted boot sound reset", "info");
2226
2462
  } else {
2227
2463
  config["notify-sound"] = defaultSoundLevel;
2228
2464
  onConfigChange();
2229
- ctx.ui.notify("Restored default sound level", "info");
2465
+ ctx.ui.notify("Restored default boot sound level; active runtime sound is unchanged", "info");
2230
2466
  }
2231
2467
  } else if (nextLevel !== undefined) {
2232
2468
  config["notify-sound"] = nextLevel;
2233
2469
  onConfigChange();
2234
- ctx.ui.notify(`Enable sound set to ${nextLevel}`, "info");
2470
+ ctx.ui.notify(`Stored ${PI_NOTIFY_BOOT_SOUND_LABEL.toLowerCase()} as ${nextLevel}; active runtime sound is unchanged`, "info");
2235
2471
  }
2236
2472
  continue;
2237
2473
  }
@@ -2376,7 +2612,7 @@ async function configurePiNotifyMenu(
2376
2612
  const defaults = getDefaultConfig("");
2377
2613
  const resetPreview: ResetConfirmationChange[] = [
2378
2614
  { label: "Enable notification", previousValue: formatPiNotifyStatus(config), nextValue: formatPiNotifyStatus(defaults) },
2379
- { label: "Enable sound", previousValue: config["notify-sound"], nextValue: defaults["notify-sound"] },
2615
+ { label: PI_NOTIFY_BOOT_SOUND_LABEL, previousValue: config["notify-sound"], nextValue: defaults["notify-sound"] },
2380
2616
  { label: "Sound toggle hotkey bind", previousValue: config["notify-sound-toggle-shortcut"], nextValue: defaults["notify-sound-toggle-shortcut"] },
2381
2617
  { label: "Notify command", previousValue: config.PI_NOTIFY_CMD, nextValue: defaults.PI_NOTIFY_CMD },
2382
2618
  { label: "Sound command (low vol.)", previousValue: config.PI_NOTIFY_SOUND_LOW_CMD, nextValue: defaults.PI_NOTIFY_SOUND_LOW_CMD },
@@ -2410,16 +2646,16 @@ async function configurePiNotifyMenu(
2410
2646
 
2411
2647
  /**
2412
2648
  * @brief Registers the configurable notification-sound shortcut when supported.
2413
- * @details Loads the current project config, registers one raw pi shortcut when
2414
- * the runtime exposes `registerShortcut(...)`, cycles persisted sound state on
2415
- * invocation, saves the config, refreshes the status bar, and emits one info
2416
- * notification. Runtime is O(1) for registration plus config I/O per shortcut
2417
- * use. Side effects include shortcut registration, config writes, and status
2418
- * updates.
2649
+ * @details Loads the current effective config, registers one raw pi shortcut when
2650
+ * the runtime exposes `registerShortcut(...)`, cycles only the active runtime
2651
+ * sound level on invocation, leaves persisted local and global configuration
2652
+ * unchanged, refreshes the status bar, and emits one info notification. Runtime is O(1) for registration
2653
+ * plus one status update per shortcut use. Side effects include shortcut
2654
+ * registration and status updates.
2419
2655
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2420
2656
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2421
2657
  * @return {void} No return value.
2422
- * @satisfies REQ-131, REQ-134, REQ-180
2658
+ * @satisfies REQ-134, REQ-180, REQ-286, REQ-287
2423
2659
  */
2424
2660
  function registerPiNotifyShortcut(
2425
2661
  pi: ExtensionAPI,
@@ -2433,19 +2669,170 @@ function registerPiNotifyShortcut(
2433
2669
  shortcutRegistrar.registerShortcut(config["notify-sound-toggle-shortcut"], {
2434
2670
  description: "Cycle pi-usereq notification sound level",
2435
2671
  handler: async (ctx) => {
2436
- const nextConfig = loadProjectConfig(ctx.cwd);
2437
- nextConfig["notify-sound"] = cyclePiNotifySoundLevel(nextConfig["notify-sound"]);
2438
- saveProjectConfig(ctx.cwd, nextConfig);
2439
- setPiUsereqStatusConfig(statusController, nextConfig);
2440
- renderPiUsereqStatus(statusController, ctx);
2441
- ctx.ui.notify(`pi-usereq sound:${nextConfig["notify-sound"]}`, "info");
2672
+ const nextRuntimeSoundLevel = cyclePiNotifySoundLevel(
2673
+ getPiUsereqRuntimeSoundLevel(statusController),
2674
+ );
2675
+ setPiUsereqRuntimeSoundLevel(
2676
+ statusController,
2677
+ nextRuntimeSoundLevel,
2678
+ ctx,
2679
+ );
2680
+ ctx.ui.notify(`pi-usereq sound:${nextRuntimeSoundLevel}`, "info");
2681
+ },
2682
+ });
2683
+ }
2684
+
2685
+ /**
2686
+ * @brief Resolves the prompt execution plan targeted by `req-reset` recovery.
2687
+ * @details Prefers the current in-memory active request, then the current in-memory pending request, then the process-scoped persisted prompt runtime state so the dedicated reset command can recover from same-host unclean prompt termination after session replacement. Runtime is O(1). No external state is mutated.
2688
+ * @param[in] statusController {PiUsereqStatusController} Mutable status controller.
2689
+ * @return {PromptCommandExecutionPlan | undefined} Recoverable prompt execution plan when one remains available.
2690
+ */
2691
+ function resolveReqResetPromptRequest(
2692
+ statusController: PiUsereqStatusController,
2693
+ ): PromptCommandExecutionPlan | undefined {
2694
+ const isWorktreeBacked = (request: PromptCommandExecutionPlan | undefined): request is PromptCommandExecutionPlan =>
2695
+ request?.worktreeDir !== undefined
2696
+ && request.worktreeRootPath !== undefined
2697
+ && request.worktreePath !== undefined;
2698
+ const inMemoryActiveRequest = statusController.state.activePromptRequest;
2699
+ if (isWorktreeBacked(inMemoryActiveRequest)) {
2700
+ return inMemoryActiveRequest;
2701
+ }
2702
+ const inMemoryPendingRequest = statusController.state.pendingPromptRequest;
2703
+ if (isWorktreeBacked(inMemoryPendingRequest)) {
2704
+ return inMemoryPendingRequest;
2705
+ }
2706
+ const persistedRuntimeState = readPersistedPromptCommandRuntimeState();
2707
+ if (isWorktreeBacked(persistedRuntimeState.activePromptRequest)) {
2708
+ return persistedRuntimeState.activePromptRequest;
2709
+ }
2710
+ if (isWorktreeBacked(persistedRuntimeState.pendingPromptRequest)) {
2711
+ return persistedRuntimeState.pendingPromptRequest;
2712
+ }
2713
+ return inMemoryActiveRequest
2714
+ ?? inMemoryPendingRequest
2715
+ ?? persistedRuntimeState.activePromptRequest
2716
+ ?? persistedRuntimeState.pendingPromptRequest;
2717
+ }
2718
+
2719
+ /**
2720
+ * @brief Registers the specialized `req-reset` slash command.
2721
+ * @details Registers the non-agentic prompt-recovery command that accepts any current workflow state, reuses persisted prompt runtime state when available, restores the original session-backed `base-path`, force-removes matching generated worktrees plus branches, clears recoverable prompt state when restoration succeeds, and notifies pi without starting an LLM session or creating a worktree. Runtime is dominated by session restoration plus git cleanup. Side effects include command registration, status-controller mutation, active-session replacement, worktree deletion, branch deletion, and user notifications.
2722
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
2723
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2724
+ * @return {void} No return value.
2725
+ * @satisfies REQ-304, REQ-305, REQ-306, REQ-307, REQ-308, REQ-309, REQ-310, REQ-311, REQ-312, REQ-313
2726
+ */
2727
+ function registerReqResetCommand(
2728
+ pi: ExtensionAPI,
2729
+ statusController: PiUsereqStatusController,
2730
+ ): void {
2731
+ pi.registerCommand("req-reset", {
2732
+ description: REQ_RESET_COMMAND_DESCRIPTION,
2733
+ handler: async (_args, ctx) => {
2734
+ const promptRequest = resolveReqResetPromptRequest(statusController);
2735
+ const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
2736
+ syncContextCwdMirror(ctx, commandCwd);
2737
+ const projectBase = path.resolve(promptRequest?.basePath ?? getProjectBase(commandCwd));
2738
+ bootstrapRuntimePathState(projectBase, {
2739
+ gitPath: resolveRuntimeGitPath(projectBase),
2740
+ });
2741
+ const config = loadProjectConfig(projectBase);
2742
+ let executionPlan: ReqResetCommandPlan;
2743
+ let executionResult: ReqResetCommandExecutionResult;
2744
+ let resetContext = ctx;
2745
+
2746
+ setPiUsereqStatusConfig(statusController, config);
2747
+ setPiUsereqWorkflowState(statusController, "running", ctx);
2748
+
2749
+ try {
2750
+ executionPlan = prepareReqResetCommandExecution(projectBase, config, promptRequest);
2751
+ executionResult = await executeReqResetCommandExecution(executionPlan, ctx);
2752
+ resetContext = (executionResult.activeContext ?? resetContext) as typeof ctx;
2753
+ } catch (error) {
2754
+ const message = error instanceof Error ? error.message : String(error);
2755
+ setPiUsereqWorkflowState(statusController, "error", resetContext);
2756
+ notifyContextSafely(resetContext, message, "error");
2757
+ throw error;
2758
+ }
2759
+
2760
+ const shouldClearPromptState = executionResult.restoredBasePath
2761
+ || executionPlan.promptRequest === undefined;
2762
+ if (shouldClearPromptState) {
2763
+ statusController.state.pendingPromptRequest = undefined;
2764
+ statusController.state.activePromptRequest = undefined;
2765
+ clearPersistedPromptCommandRuntimeState();
2766
+ }
2767
+
2768
+ if (executionResult.errorMessage) {
2769
+ setPiUsereqWorkflowState(statusController, "error", resetContext);
2770
+ notifyContextSafely(resetContext, executionResult.errorMessage, "error");
2771
+ throw new ReqError(executionResult.errorMessage, 1);
2772
+ }
2773
+
2774
+ setPiUsereqWorkflowState(statusController, "idle", resetContext);
2775
+ const successMessage = executionPlan.promptRequest !== undefined
2776
+ ? `SUCCESS: req-reset restored base-path and removed ${executionResult.removedWorktreeDirs.length} worktree(s) plus ${executionResult.removedBranchNames.length} branch(es).`
2777
+ : `SUCCESS: req-reset removed ${executionResult.removedWorktreeDirs.length} worktree(s) plus ${executionResult.removedBranchNames.length} branch(es) and restored idle state.`;
2778
+ notifyContextSafely(resetContext, successMessage, "info");
2442
2779
  },
2443
2780
  });
2444
2781
  }
2445
2782
 
2446
2783
  /**
2447
- * @brief Registers bundled prompt commands with the extension.
2448
- * @details Creates one `req-<prompt>` command per bundled prompt name. Each handler rejects non-`idle` workflow state, transitions the shared workflow state through `checking`, `error`, and `running`, runs dedicated prompt-command git and required-doc preflight checks, optionally prepares a dedicated worktree execution plan using the active session directory, persists the prompt metadata needed for switch-triggered rebinding, switches the active session to the verified execution cwd before prompt handoff, logs dedicated workflow-activation diagnostics, renders the prompt, starts prompt delivery into the forked active session, records `running` immediately after delivery handoff begins, and then awaits the wrapped prompt-delivery promise whose stale post-restore rejections are suppressed. Runtime is O(p) for registration; handler cost depends on prompt preflight, worktree preparation, session switching, prompt rendering, prompt dispatch, and optional debug logging. Side effects include command registration, status-controller mutation, worktree creation, active-session replacement, optional worktree rollback, user-message delivery during execution, and optional debug-log writes.
2784
+ * @brief Registers the specialized `req-references` slash command.
2785
+ * @details Registers the non-agentic references-maintenance command that rejects non-`idle` invocations by transitioning workflow state to `error` before direct execution, otherwise reuses slash-command-owned git validation, transitions workflow state through `checking|running|idle`, regenerates `REFERENCES.md` directly from configured source directories, stages only the generated file, creates the fixed-message git commit, verifies repository cleanliness, and notifies pi without starting an LLM session or creating a worktree. Runtime is dominated by git subprocess execution plus source-summary generation. Side effects include command registration, status-controller mutation, filesystem writes, git index/history mutation, and user notifications.
2786
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
2787
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2788
+ * @return {void} No return value.
2789
+ * @satisfies REQ-200, REQ-221, REQ-224, REQ-298, REQ-299, REQ-300, REQ-301, REQ-302, REQ-303
2790
+ */
2791
+ function registerReqReferencesCommand(
2792
+ pi: ExtensionAPI,
2793
+ statusController: PiUsereqStatusController,
2794
+ ): void {
2795
+ pi.registerCommand("req-references", {
2796
+ description: REQ_REFERENCES_COMMAND_DESCRIPTION,
2797
+ handler: async (_args, ctx) => {
2798
+ if (statusController.state.workflowState !== "idle") {
2799
+ rejectNonIdleReqCommand(statusController, ctx);
2800
+ }
2801
+ const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
2802
+ syncContextCwdMirror(ctx, commandCwd);
2803
+ bootstrapRuntimePathState(commandCwd, {
2804
+ gitPath: resolveRuntimeGitPath(commandCwd),
2805
+ });
2806
+ const projectBase = getProjectBase(commandCwd);
2807
+ const config = loadProjectConfig(commandCwd);
2808
+ setPiUsereqStatusConfig(statusController, config);
2809
+ statusController.state.pendingPromptRequest = undefined;
2810
+ statusController.state.activePromptRequest = undefined;
2811
+ setPiUsereqWorkflowState(statusController, "checking", ctx);
2812
+ try {
2813
+ const executionPlan = prepareReqReferencesCommandExecution(projectBase, config);
2814
+ setPiUsereqWorkflowState(statusController, "running", ctx);
2815
+ executeReqReferencesCommandExecution(executionPlan, config);
2816
+ setPiUsereqWorkflowState(statusController, "idle", ctx);
2817
+ ctx.ui.notify(
2818
+ `SUCCESS: Updated ${formatRuntimePathForDisplay(executionPlan.referencesPath)} and committed changes.`,
2819
+ "info",
2820
+ );
2821
+ } catch (error) {
2822
+ statusController.state.pendingPromptRequest = undefined;
2823
+ statusController.state.activePromptRequest = undefined;
2824
+ setPiUsereqWorkflowState(statusController, "error", ctx);
2825
+ const message = error instanceof Error ? error.message : String(error);
2826
+ ctx.ui.notify(message, "error");
2827
+ throw error;
2828
+ }
2829
+ },
2830
+ });
2831
+ }
2832
+
2833
+ /**
2834
+ * @brief Registers bundled prompt-backed commands with the extension.
2835
+ * @details Creates one prompt-template-backed `req-<prompt>` command per bundled prompt name. Each handler rejects non-`idle` workflow state by transitioning the shared workflow state to `error` before command-side preflight, otherwise 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.
2449
2836
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2450
2837
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2451
2838
  * @return {void} No return value.
@@ -2460,9 +2847,7 @@ function registerPromptCommands(
2460
2847
  description: resolvePromptCommandDescription(promptName),
2461
2848
  handler: async (args, ctx) => {
2462
2849
  if (statusController.state.workflowState !== "idle") {
2463
- const message = `ERROR: Prompt workflow state is ${statusController.state.workflowState}, expected idle.`;
2464
- ctx.ui.notify(message, "error");
2465
- throw new ReqError(message, 1);
2850
+ rejectNonIdleReqCommand(statusController, ctx, promptName);
2466
2851
  }
2467
2852
  const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
2468
2853
  syncContextCwdMirror(ctx, commandCwd);
@@ -2577,10 +2962,10 @@ function registerPromptCommands(
2577
2962
  * @details Defines the tool schemas, prompt metadata, and execution handlers that bridge extension tool calls into tool-runner operations without registering duplicate custom slash commands for the same capabilities. Runtime is O(t) for registration; execution cost depends on the selected tool. Side effects include tool registration.
2578
2963
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2579
2964
  * @return {void} No return value.
2580
- * @satisfies REQ-005, REQ-010, REQ-011, REQ-014, REQ-017, REQ-044, REQ-069, REQ-070, REQ-071, REQ-072, REQ-073, REQ-074, REQ-075, REQ-076, REQ-077, REQ-078, REQ-079, REQ-080, REQ-089, REQ-090, REQ-091, REQ-092, REQ-093, REQ-094, REQ-095, REQ-096, REQ-097, REQ-098, REQ-099, REQ-100, REQ-101, REQ-102
2965
+ * @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, REQ-293, REQ-294, REQ-295, REQ-296, REQ-297
2581
2966
  */
2582
2967
  function registerAgentTools(pi: ExtensionAPI): void {
2583
- const filesReferencesSchema = Type.Object(
2968
+ const filesSummarizeSchema = Type.Object(
2584
2969
  {
2585
2970
  files: Type.Array(
2586
2971
  Type.String({ description: "Project-relative or absolute source file path resolved from the current working directory when not already absolute" }),
@@ -2632,21 +3017,21 @@ function registerAgentTools(pi: ExtensionAPI): void {
2632
3017
  });
2633
3018
 
2634
3019
  pi.registerTool({
2635
- name: "files-references",
2636
- label: "files-references",
2637
- description: "Scope: explicit source files. Return the monolithic references markdown report in content[0].text and keep only execution metadata in details.execution.",
2638
- promptSnippet: "Return the monolithic references markdown report for caller-selected source files.",
3020
+ name: "files-summarize",
3021
+ label: "files-summarize",
3022
+ description: "Scope: explicit source files. Return the monolithic summary markdown report in content[0].text and keep only execution metadata in details.execution.",
3023
+ promptSnippet: "Return the monolithic summary markdown report for caller-selected source files.",
2639
3024
  promptGuidelines: [
2640
3025
  "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
2641
3026
  "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2642
- "Formatting contract: content matches the Python reference renderer used by `generate_markdown.py`.",
3027
+ "Formatting contract: content matches the Python summary renderer used by `generate_markdown.py`.",
2643
3028
  "Behavior contract: missing inputs, non-file inputs, unsupported extensions, and analysis failures surface through details.execution diagnostics.",
2644
3029
  ],
2645
- renderResult: buildStructuredToolRenderResult("files-references"),
2646
- parameters: filesReferencesSchema,
3030
+ renderResult: buildStructuredToolRenderResult("files-summarize"),
3031
+ parameters: filesSummarizeSchema,
2647
3032
  async execute(_toolCallId, params) {
2648
3033
  const contextPath = getRuntimeContextPath(process.cwd());
2649
- return executeMonolithicTool(() => runFilesReferences(params.files, contextPath));
3034
+ return executeMonolithicTool(() => runFilesSummarize(params.files, contextPath));
2650
3035
  },
2651
3036
  });
2652
3037
 
@@ -2717,12 +3102,18 @@ function registerAgentTools(pi: ExtensionAPI): void {
2717
3102
  },
2718
3103
  });
2719
3104
 
2720
- const referencesSchema = Type.Object(
3105
+ const summarizeSchema = Type.Object(
2721
3106
  {},
2722
3107
  {
2723
3108
  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.",
2724
3109
  },
2725
3110
  );
3111
+ const referencesSchema = Type.Object(
3112
+ {},
3113
+ {
3114
+ description: "Input contract: no params. Scope is the configured src-dir list plus configured docs-dir resolved from the current project configuration. Output contract: content[0].text returns only `success` or `error: <diagnostic>`; the tool overwrites `<docs-dir>/REFERENCES.md` and keeps details.execution diagnostics.",
3115
+ },
3116
+ );
2726
3117
  const tokensSchema = Type.Object(
2727
3118
  {},
2728
3119
  {
@@ -2731,23 +3122,44 @@ function registerAgentTools(pi: ExtensionAPI): void {
2731
3122
  );
2732
3123
 
2733
3124
  pi.registerTool({
2734
- name: "references",
2735
- label: "references",
2736
- description: "Scope: configured project source directories. Return the monolithic references markdown report in content[0].text and keep only execution metadata in details.execution.",
2737
- promptSnippet: "Return the monolithic project references markdown report from the configured source directories.",
3125
+ name: "summarize",
3126
+ label: "summarize",
3127
+ description: "Scope: configured project source directories. Return the monolithic summary markdown report in content[0].text and keep only execution metadata in details.execution.",
3128
+ promptSnippet: "Return the monolithic project summary markdown report from the configured source directories.",
2738
3129
  promptGuidelines: [
2739
3130
  "Scope: no params; resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.",
2740
3131
  "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2741
3132
  "Formatting contract: content prepends the file-structure markdown block before the per-file markdown produced by `generate_markdown.py`.",
2742
3133
  "Configuration contract: output changes with cwd-derived project config and src-dir values; the tool does not accept explicit file overrides.",
2743
3134
  ],
3135
+ renderResult: buildStructuredToolRenderResult("summarize"),
3136
+ parameters: summarizeSchema,
3137
+ async execute() {
3138
+ const contextPath = getRuntimeContextPath(process.cwd());
3139
+ const projectBase = getProjectBase(contextPath);
3140
+ const config = loadProjectConfig(projectBase);
3141
+ return executeMonolithicTool(() => runSummarize(contextPath, config));
3142
+ },
3143
+ });
3144
+
3145
+ pi.registerTool({
3146
+ name: "references",
3147
+ label: "references",
3148
+ description: "Scope: configured project source directories and configured docs-dir. Overwrite REFERENCES.md and return only success or error in content[0].text while keeping execution metadata in details.execution.",
3149
+ promptSnippet: "Generate REFERENCES.md from the configured source directories without returning the generated markdown.",
3150
+ promptGuidelines: [
3151
+ "Scope: no params; resolve src-dir and docs-dir from the current project configuration and current working directory.",
3152
+ "Behavior contract: generate the same file-structure-plus-summary markdown as `summarize` and overwrite `<docs-dir>/REFERENCES.md`.",
3153
+ "Output contract: `content[0].text` is `success` on success or `error: <diagnostic>` on failure; generated markdown is never returned to the LLM.",
3154
+ "Failure contract: source discovery, markdown generation, and file-write errors surface through the status text plus details.execution diagnostics.",
3155
+ ],
2744
3156
  renderResult: buildStructuredToolRenderResult("references"),
2745
3157
  parameters: referencesSchema,
2746
3158
  async execute() {
2747
3159
  const contextPath = getRuntimeContextPath(process.cwd());
2748
3160
  const projectBase = getProjectBase(contextPath);
2749
3161
  const config = loadProjectConfig(projectBase);
2750
- return executeMonolithicTool(() => runReferences(contextPath, config));
3162
+ return executeStatusTool(() => runReferences(contextPath, config));
2751
3163
  },
2752
3164
  });
2753
3165
 
@@ -2938,6 +3350,7 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
2938
3350
  id: tool.name,
2939
3351
  label: tool.name,
2940
3352
  value: enabledTools.has(tool.name) ? "on" : "off",
3353
+ values: ["on", "off"],
2941
3354
  description: tool.description ?? `Toggle startup activation for ${tool.name}.`,
2942
3355
  })),
2943
3356
  ...buildTerminalSettingsMenuChoices({
@@ -2948,7 +3361,7 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
2948
3361
 
2949
3362
  /**
2950
3363
  * @brief Runs the interactive active-tool configuration menu.
2951
- * @details Synchronizes runtime active tools with persisted config, renders startup-tool actions through the shared settings-menu UI, preserves the documented per-tool ordering, and updates configuration state in response to selections until the user exits. Runtime depends on user interaction count. Side effects include UI updates, active-tool changes, and config mutation.
3364
+ * @details Synchronizes runtime active tools with the effective config, renders startup-tool actions through the shared settings-menu UI, persists enablement changes into global configuration, 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.
2952
3365
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2953
3366
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2954
3367
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
@@ -3012,7 +3425,29 @@ async function configurePiUsereqToolsMenu(
3012
3425
  }
3013
3426
 
3014
3427
  if (choice === "enable-tools") {
3015
- const selectedToolName = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolToggleChoices(pi, config));
3428
+ const selectedToolName = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolToggleChoices(pi, config), {
3429
+ getChoices: () => buildPiUsereqToolToggleChoices(pi, config),
3430
+ onChange: (toolName, newValue) => {
3431
+ const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
3432
+ if (newValue === "on") {
3433
+ enabledTools.add(toolName as PiUsereqStartupToolName);
3434
+ } else {
3435
+ enabledTools.delete(toolName as PiUsereqStartupToolName);
3436
+ }
3437
+ setConfiguredPiUsereqTools(
3438
+ pi,
3439
+ config,
3440
+ getPiUsereqStartupTools(pi)
3441
+ .map((tool) => tool.name)
3442
+ .filter((currentToolName) => enabledTools.has(currentToolName)),
3443
+ );
3444
+ onConfigChange();
3445
+ ctx.ui.notify(
3446
+ `${newValue === "on" ? "Enabled" : "Disabled"} ${toolName}`,
3447
+ "info",
3448
+ );
3449
+ },
3450
+ });
3016
3451
  if (!selectedToolName) {
3017
3452
  continue;
3018
3453
  }
@@ -3138,6 +3573,7 @@ function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenu
3138
3573
  id: `toggle-static-check-language:${language}`,
3139
3574
  label: language,
3140
3575
  value: languageConfig.enabled === "enable" ? "on" : "off",
3576
+ values: ["on", "off"],
3141
3577
  description: `Toggle static-check execution for ${language}. Configured ${configuredCount} ${suffix}. Supported extensions: ${extensions.join(", ")}.`,
3142
3578
  };
3143
3579
  }),
@@ -3199,7 +3635,7 @@ function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUser
3199
3635
 
3200
3636
  /**
3201
3637
  * @brief Runs the interactive static-check configuration menu.
3202
- * @details Lets the user add Command entries by guided prompts, remove configured language entries, toggle direct per-language enable flags, and reset the subtree to documented defaults through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
3638
+ * @details Lets the user add and remove global Command entries, toggle direct local 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.
3203
3639
  * @param[in] ctx {ExtensionCommandContext} Active command context.
3204
3640
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
3205
3641
  * @return {Promise<void>} Promise resolved when the menu closes.
@@ -3214,6 +3650,19 @@ async function configureStaticCheckMenu(
3214
3650
  while (true) {
3215
3651
  const staticChoice = await showPiUsereqSettingsMenu(ctx, "Language static code checkers", buildStaticCheckMenuChoices(config), {
3216
3652
  initialSelectedId: focusedChoiceId,
3653
+ getChoices: () => buildStaticCheckMenuChoices(config),
3654
+ onChange: (choiceId, newValue) => {
3655
+ if (choiceId.startsWith("toggle-static-check-language:")) {
3656
+ const language = choiceId.slice("toggle-static-check-language:".length);
3657
+ config["static-check"][language] ??= createStaticCheckLanguageConfig([]);
3658
+ config["static-check"][language]!.enabled = newValue === "on" ? "enable" : "disable";
3659
+ onConfigChange();
3660
+ ctx.ui.notify(
3661
+ `${newValue === "on" ? "Enabled" : "Disabled"} static-check for ${language}`,
3662
+ "info",
3663
+ );
3664
+ }
3665
+ },
3217
3666
  });
3218
3667
 
3219
3668
  if (!staticChoice) {
@@ -3342,11 +3791,11 @@ async function configureStaticCheckMenu(
3342
3791
 
3343
3792
  /**
3344
3793
  * @brief Builds the shared settings-menu choices for the top-level pi-usereq configuration UI.
3345
- * @details Serializes primary configuration actions into right-valued menu rows consumed by the shared settings-menu renderer, including automatic git-commit mode, effective prompt-command worktree state, notification summary, debug summary, locked worktree rows when automatic git commit is disabled, and the display-only config path beside `show-config`. Runtime is O(s) in source-directory count. No external state is mutated.
3794
+ * @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 display-only local plus global config paths. Runtime is O(s) in source-directory count. No external state is mutated.
3346
3795
  * @param[in] cwd {string} Current working directory.
3347
3796
  * @param[in] config {UseReqConfig} Effective project configuration.
3348
3797
  * @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
3349
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240
3798
+ * @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, REQ-314, REQ-318, REQ-319, REQ-320
3350
3799
  */
3351
3800
  function buildPiUsereqMenuChoices(
3352
3801
  cwd: string,
@@ -3380,6 +3829,7 @@ function buildPiUsereqMenuChoices(
3380
3829
  id: "auto-git-commit",
3381
3830
  label: "Auto git commit",
3382
3831
  value: config.AUTO_GIT_COMMIT,
3832
+ values: ["enable", "disable"],
3383
3833
  description: "Select bundled `git_commit.md` or `git_read-only.md` for `%%COMMIT%%`; disabling also forces prompt-command worktrees off.",
3384
3834
  },
3385
3835
  {
@@ -3388,6 +3838,8 @@ function buildPiUsereqMenuChoices(
3388
3838
  labelTone: autoGitCommitDisabled ? "dim" : undefined,
3389
3839
  value: effectiveGitWorktreeEnabled,
3390
3840
  valueTone: autoGitCommitDisabled ? "dim" : undefined,
3841
+ values: ["enable", "disable"],
3842
+ disabled: autoGitCommitDisabled,
3391
3843
  description: autoGitCommitDisabled
3392
3844
  ? "Forced to `disable` while `Auto git commit` is disabled."
3393
3845
  : "Enable or disable prompt-command worktree orchestration.",
@@ -3406,32 +3858,39 @@ function buildPiUsereqMenuChoices(
3406
3858
  id: "static-check",
3407
3859
  label: "Language static code checkers",
3408
3860
  value: formatStaticCheckLanguagesSummary(config),
3409
- description: "Manage guided Command static-check entries and per-language enable flags.",
3861
+ description: "Manage global Command static-check entries and local per-language enable flags.",
3410
3862
  },
3411
3863
  {
3412
3864
  id: "startup-tools",
3413
3865
  label: "Enable tools",
3414
3866
  value: `${getConfiguredEnabledPiUsereqTools(config).length} enabled`,
3415
- description: "Manage which configurable tools become active during session_start.",
3867
+ description: "Manage the global configurable tool set activated during session_start.",
3416
3868
  },
3417
3869
  {
3418
3870
  id: "notifications",
3419
3871
  label: "Notifications",
3420
3872
  value: `notification:${formatPiNotifyStatus(config)} • sound:${config["notify-sound"]} • pushover:${formatPiNotifyPushoverStatus(config)}`,
3421
- description: "Manage command-notify, sound, and Pushover settings with dedicated event submenus.",
3873
+ description: "Manage global command-notify, sound, and Pushover settings with dedicated event submenus.",
3422
3874
  },
3423
3875
  {
3424
3876
  id: "debug",
3425
3877
  label: "Debug",
3426
3878
  value: formatDebugMenuSummary(config),
3427
- description: "Manage debug logging for tools and `req-*` prompt orchestration.",
3879
+ description: "Manage project-local debug logging for tools and `req-*` prompt orchestration.",
3880
+ },
3881
+ {
3882
+ id: "show-local-config",
3883
+ label: "Show local configuration",
3884
+ value: formatLocalConfigPathForMenu(cwd),
3885
+ valueTone: "dim",
3886
+ description: "Persist pending configuration changes and write the exact local config file text into the editor.",
3428
3887
  },
3429
3888
  {
3430
- id: "show-config",
3431
- label: "Show configuration",
3432
- value: formatProjectConfigPathForMenu(cwd),
3889
+ id: "show-global-config",
3890
+ label: "Show global configuration",
3891
+ value: formatGlobalConfigPathForMenu(),
3433
3892
  valueTone: "dim",
3434
- description: "Persist the current project configuration file and write its exact text into the editor.",
3893
+ description: "Persist pending configuration changes and write the exact global config file text into the editor.",
3435
3894
  },
3436
3895
  ...buildTerminalSettingsMenuChoices({
3437
3896
  resetDefaultsDescription: "Restore the default pi-usereq configuration for the current project base.",
@@ -3489,12 +3948,12 @@ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuCh
3489
3948
 
3490
3949
  /**
3491
3950
  * @brief Runs the top-level pi-usereq configuration menu.
3492
- * @details Loads project config, exposes docs/test/source/automatic-commit/worktree/static-check/startup-tool/notification/debug actions through the shared settings-menu renderer, forces worktree disablement when automatic git commit is disabled, prevents locked row edits, persists changes on exit, closes immediately after `Show configuration`, and refreshes the single-line status bar. Runtime depends on user interaction count. Side effects include UI updates, config writes, active-tool changes, and editor text updates.
3951
+ * @details Loads the effective merged 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 local configuration` or `Show global 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.
3493
3952
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3494
3953
  * @param[in] ctx {ExtensionCommandContext} Active command context.
3495
3954
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
3496
3955
  * @return {Promise<void>} Promise resolved when configuration is saved and the menu closes.
3497
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162, REQ-190, REQ-191, REQ-192, REQ-194, REQ-195, REQ-204, REQ-205, REQ-212, REQ-215, REQ-216, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243
3956
+ * @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, REQ-314, REQ-318, REQ-319, REQ-320
3498
3957
  */
3499
3958
  async function configurePiUsereq(
3500
3959
  pi: ExtensionAPI,
@@ -3520,7 +3979,36 @@ async function configurePiUsereq(
3520
3979
  ctx,
3521
3980
  "pi-usereq",
3522
3981
  buildPiUsereqMenuChoices(ctx.cwd, config),
3523
- { initialSelectedId: focusedChoiceId },
3982
+ {
3983
+ initialSelectedId: focusedChoiceId,
3984
+ getChoices: () => buildPiUsereqMenuChoices(ctx.cwd, config),
3985
+ onChange: (choiceId, newValue) => {
3986
+ if (choiceId === "auto-git-commit") {
3987
+ config.AUTO_GIT_COMMIT = newValue === "enable" ? "enable" : "disable";
3988
+ if (config.AUTO_GIT_COMMIT === "disable") {
3989
+ config.GIT_WORKTREE_ENABLED = "disable";
3990
+ persistConfigChange();
3991
+ ctx.ui.notify("Auto git commit disabled; Git worktree forced off", "info");
3992
+ } else {
3993
+ persistConfigChange();
3994
+ ctx.ui.notify("Auto git commit enabled", "info");
3995
+ }
3996
+ return;
3997
+ }
3998
+ if (choiceId === "git-worktree-enabled") {
3999
+ if (config.AUTO_GIT_COMMIT === "disable") {
4000
+ ctx.ui.notify("Git worktree is locked while Auto git commit is disabled", "info");
4001
+ return;
4002
+ }
4003
+ config.GIT_WORKTREE_ENABLED = newValue === "enable" ? "enable" : "disable";
4004
+ persistConfigChange();
4005
+ ctx.ui.notify(
4006
+ `Git worktree ${resolveEffectiveGitWorktreeEnabled(config.AUTO_GIT_COMMIT, config.GIT_WORKTREE_ENABLED) === "enable" ? "enabled" : "disabled"}`,
4007
+ "info",
4008
+ );
4009
+ }
4010
+ },
4011
+ },
3524
4012
  );
3525
4013
  if (!choice) {
3526
4014
  if (config["notify-sound-toggle-shortcut"] !== initialShortcut) {
@@ -3707,12 +4195,16 @@ async function configurePiUsereq(
3707
4195
  ctx.ui.notify("Restored all default configuration values", "info");
3708
4196
  continue;
3709
4197
  }
3710
- if (choice === "show-config") {
4198
+ if (choice === "show-local-config" || choice === "show-global-config") {
3711
4199
  persistConfigChange();
3712
4200
  if (config["notify-sound-toggle-shortcut"] !== initialShortcut) {
3713
4201
  ctx.ui.notify("Sound toggle hotkey bind updated; run /reload to apply the new binding", "info");
3714
4202
  }
3715
- writePersistedProjectConfigToEditor(ctx, ctx.cwd, config);
4203
+ if (choice === "show-local-config") {
4204
+ writePersistedLocalConfigToEditor(ctx, ctx.cwd);
4205
+ } else {
4206
+ writePersistedGlobalConfigToEditor(ctx);
4207
+ }
3716
4208
  return;
3717
4209
  }
3718
4210
  }
@@ -3740,23 +4232,16 @@ function registerConfigCommands(
3740
4232
 
3741
4233
  /**
3742
4234
  * @brief Registers the complete pi-usereq extension.
3743
- * @details Validates installation-owned bundled resources, registers prompt and
3744
- * configuration commands plus agent tools, registers the configurable
3745
- * notification-sound shortcut when the runtime supports shortcuts, and
3746
- * installs shared wrappers for all supported pi lifecycle hooks so status
3747
- * telemetry, context usage, prompt timing, cumulative runtime, prompt-specific
3748
- * Pushover metadata, tool-result debug logging, and prompt-orchestration debug
3749
- * effects remain synchronized with runtime events. Runtime is O(h) in hook
3750
- * count during registration. Side effects include filesystem reads,
3751
- * command/tool/shortcut registration, UI updates, active-tool changes,
3752
- * optional debug-log writes, and timer scheduling.
4235
+ * @details Validates installation-owned bundled resources, registers the specialized `req-reset` and `req-references` commands plus bundled prompt-backed commands and agent tools, registers configuration commands, registers the configurable notification-sound shortcut when the runtime supports shortcuts, and installs shared wrappers for all supported pi lifecycle hooks so status telemetry, context usage, prompt timing, cumulative runtime, prompt-specific Pushover metadata, tool-result debug logging, and prompt-orchestration effects remain synchronized with runtime events. Runtime is O(h) in hook count during registration. Side effects include filesystem reads, command/tool/shortcut registration, UI updates, active-tool changes, optional debug-log writes, and timer scheduling.
3753
4236
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3754
4237
  * @return {void} No return value.
3755
- * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-067, REQ-068, REQ-109, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-131, REQ-132, REQ-133, REQ-134, REQ-137, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-174, REQ-179, REQ-180, REQ-184, REQ-188, REQ-190, REQ-191, REQ-192, REQ-193, REQ-194, REQ-195, REQ-196, REQ-197, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243, REQ-244, REQ-245, REQ-246, REQ-247
4238
+ * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-067, REQ-068, REQ-109, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-131, REQ-132, REQ-133, REQ-134, REQ-137, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-174, REQ-179, REQ-180, REQ-184, REQ-188, REQ-190, REQ-191, REQ-192, REQ-193, REQ-194, REQ-195, REQ-196, REQ-197, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243, REQ-244, REQ-245, REQ-246, REQ-247, REQ-298, REQ-299, REQ-300, REQ-301, REQ-302, REQ-303, REQ-304, REQ-305, REQ-306, REQ-312, REQ-313
3756
4239
  */
3757
4240
  export default function piUsereqExtension(pi: ExtensionAPI): void {
3758
4241
  const statusController = createPiUsereqStatusController();
3759
4242
  ensureBundledResourcesAccessible();
4243
+ registerReqResetCommand(pi, statusController);
4244
+ registerReqReferencesCommand(pi, statusController);
3760
4245
  registerPromptCommands(pi, statusController);
3761
4246
  registerAgentTools(pi);
3762
4247
  registerConfigCommands(pi, statusController);