pi-usereq 0.10.0 → 0.12.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 (52) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +6 -6
  3. package/package.json +1 -1
  4. package/pi-usereq/docs/REFERENCES.md +861 -704
  5. package/pi-usereq/docs/REQUIREMENTS.md +152 -95
  6. package/pi-usereq/docs/WORKFLOW.md +228 -65
  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/debug-runtime.ts +2 -2
  11. package/src/core/extension-status.ts +98 -36
  12. package/src/core/pi-notify.ts +5 -5
  13. package/src/core/pi-usereq-tools.ts +4 -2
  14. package/src/core/prompt-command-catalog.ts +4 -5
  15. package/src/core/prompt-command-runtime.ts +347 -42
  16. package/src/core/prompts.ts +0 -2
  17. package/src/core/req-references-command.ts +175 -0
  18. package/src/core/req-reset-command.ts +323 -0
  19. package/src/core/resources.ts +6 -23
  20. package/src/core/runtime-project-paths.ts +21 -1
  21. package/src/core/settings-menu.ts +85 -28
  22. package/src/core/tool-runner.ts +26 -6
  23. package/src/index.ts +530 -104
  24. package/tests/attended-results-scenarios.ts +5 -5
  25. package/tests/cli-command-option-parity.test.ts +25 -25
  26. package/tests/debug-extension-harness.test.ts +8 -2
  27. package/tests/extension-registration.test.ts +1109 -76
  28. package/tests/oracle-project.test.ts +4 -4
  29. package/tests/oracle-standalone.test.ts +5 -5
  30. package/src/core/reference-payload.ts +0 -752
  31. package/src/resources/prompts/references.md +0 -64
  32. /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
  33. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
  34. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
  35. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
  36. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
  37. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
  38. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
  39. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
  40. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
  41. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
  42. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
  43. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
  44. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
  45. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
  46. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
  47. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
  48. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
  49. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
  50. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
  51. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
  52. /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.10.0";
11
+ export const VERSION = "0.12.0";
12
12
 
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
@@ -91,6 +91,18 @@ import {
91
91
  restorePromptCommandExecution,
92
92
  type PromptCommandExecutionPlan,
93
93
  } from "./core/prompt-command-runtime.js";
94
+ import {
95
+ REQ_REFERENCES_COMMAND_DESCRIPTION,
96
+ executeReqReferencesCommandExecution,
97
+ prepareReqReferencesCommandExecution,
98
+ } from "./core/req-references-command.js";
99
+ import {
100
+ REQ_RESET_COMMAND_DESCRIPTION,
101
+ executeReqResetCommandExecution,
102
+ prepareReqResetCommandExecution,
103
+ type ReqResetCommandExecutionResult,
104
+ type ReqResetCommandPlan,
105
+ } from "./core/req-reset-command.js";
94
106
  import {
95
107
  DEBUG_PROMPT_NAMES,
96
108
  DEBUG_WORKFLOW_STATES,
@@ -114,6 +126,7 @@ import {
114
126
  import { PROMPT_COMMAND_NAMES } from "./core/prompt-command-catalog.js";
115
127
  import { resolveRuntimeGitPath } from "./core/runtime-project-paths.js";
116
128
  import {
129
+ clearPersistedPromptCommandRuntimeState,
117
130
  readPersistedPromptCommandRuntimeState,
118
131
  writePersistedPromptCommandRuntimeState,
119
132
  } from "./core/prompt-command-state.js";
@@ -123,8 +136,10 @@ import {
123
136
  PI_USEREQ_STATUS_HOOK_NAMES,
124
137
  createPiUsereqStatusController,
125
138
  disposePiUsereqStatusController,
139
+ getPiUsereqRuntimeSoundLevel,
126
140
  isStaleExtensionContextError,
127
141
  renderPiUsereqStatus,
142
+ setPiUsereqRuntimeSoundLevel,
128
143
  setPiUsereqStatusConfig,
129
144
  setPiUsereqWorkflowState,
130
145
  shouldPreservePromptCommandStateOnShutdown,
@@ -135,13 +150,14 @@ import {
135
150
  import {
136
151
  runCompress,
137
152
  runFilesCompress,
138
- runFilesReferences,
139
153
  runFilesSearch,
140
154
  runFilesStaticCheck,
155
+ runFilesSummarize,
141
156
  runFilesTokens,
142
157
  runProjectStaticCheck,
143
158
  runReferences,
144
159
  runSearch,
160
+ runSummarize,
145
161
  runTokens,
146
162
  type ToolResult,
147
163
  } from "./core/tool-runner.js";
@@ -279,20 +295,19 @@ function formatProjectConfigPathForMenu(cwd: string): string {
279
295
 
280
296
  /**
281
297
  * @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.
298
+ * @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.
299
+ * @param[in] options {{ resetDefaultsDescription: string }} Menu-specific terminal-row metadata.
284
300
  * @return {PiUsereqSettingsMenuChoice[]} Ordered terminal menu rows.
285
301
  * @satisfies REQ-193
286
302
  */
287
303
  function buildTerminalSettingsMenuChoices(options: {
288
304
  resetDefaultsDescription: string;
289
- resetDefaultsValue?: string;
290
305
  }): PiUsereqSettingsMenuChoice[] {
291
306
  return [
292
307
  {
293
308
  id: "reset-defaults",
294
309
  label: "Reset defaults",
295
- value: options.resetDefaultsValue ?? "",
310
+ value: "",
296
311
  description: options.resetDefaultsDescription,
297
312
  },
298
313
  ];
@@ -687,6 +702,44 @@ function executeMonolithicTool(operation: () => ToolResult): ReturnType<typeof b
687
702
  }
688
703
  }
689
704
 
705
+ /**
706
+ * @brief Executes one CLI-style runner for a status-only agent tool.
707
+ * @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.
708
+ * @param[in] operation {() => ToolResult} Runner callback.
709
+ * @return {ReturnType<typeof buildMonolithicToolExecuteResult>} Status-only tool execute result.
710
+ * @satisfies REQ-294, REQ-295, REQ-296
711
+ */
712
+ function executeStatusTool(operation: () => ToolResult): ReturnType<typeof buildMonolithicToolExecuteResult> {
713
+ const normalizeExecution = (
714
+ payload: ReturnType<typeof buildMonolithicToolExecuteResult>,
715
+ ): ReturnType<typeof buildMonolithicToolExecuteResult> => {
716
+ const execution = payload.details.execution;
717
+ return {
718
+ content: payload.content,
719
+ details: {
720
+ execution: {
721
+ code: execution.code,
722
+ ...(Array.isArray(execution.stderr_lines) && execution.stderr_lines.length > 0
723
+ ? { stderr_lines: execution.stderr_lines }
724
+ : {}),
725
+ },
726
+ },
727
+ };
728
+ };
729
+
730
+ try {
731
+ return normalizeExecution(buildMonolithicToolExecuteResult(operation()));
732
+ } catch (error) {
733
+ const failure = normalizeToolFailure(error);
734
+ const diagnostic = failure.stderr.trim().replace(/^(Error|error):\s*/u, "") || "unknown failure";
735
+ return normalizeExecution(buildMonolithicToolExecuteResult({
736
+ stdout: "",
737
+ stderr: `error: ${diagnostic}`,
738
+ code: failure.code,
739
+ }));
740
+ }
741
+ }
742
+
690
743
  /**
691
744
  * @brief Starts delivery of one rendered prompt into the current active session.
692
745
  * @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.
@@ -839,7 +892,7 @@ function transitionPromptWorkflowState(
839
892
 
840
893
  /**
841
894
  * @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.
895
+ * @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.
843
896
  * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName} Bundled prompt name.
844
897
  * @return {string} Runtime command description.
845
898
  */
@@ -890,6 +943,38 @@ function notifyContextSafely(
890
943
  }
891
944
  }
892
945
 
946
+ /**
947
+ * @brief Rejects one non-`idle` req-command invocation and records the workflow error state.
948
+ * @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.
949
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
950
+ * @param[in] ctx {ExtensionContext | ExtensionCommandContext} Active extension context.
951
+ * @param[in] promptName {import("./core/prompt-command-catalog.js").PromptCommandName | undefined} Optional bundled prompt name used for prompt debug logging.
952
+ * @return {never} This helper always throws a deterministic `ReqError`.
953
+ * @throws {ReqError} Always throws because non-`idle` req commands are rejected.
954
+ * @satisfies REQ-224
955
+ */
956
+ function rejectNonIdleReqCommand(
957
+ statusController: PiUsereqStatusController,
958
+ ctx: ExtensionContext | ExtensionCommandContext,
959
+ promptName?: import("./core/prompt-command-catalog.js").PromptCommandName,
960
+ ): never {
961
+ const message = `ERROR: Prompt workflow state is ${statusController.state.workflowState}, expected idle.`;
962
+ if (promptName !== undefined && statusController.config !== undefined) {
963
+ transitionPromptWorkflowState(
964
+ statusController,
965
+ ctx,
966
+ resolveDebugProjectBase(ctx.cwd, statusController),
967
+ statusController.config,
968
+ promptName,
969
+ "error",
970
+ );
971
+ } else {
972
+ setPiUsereqWorkflowState(statusController, "error", ctx);
973
+ }
974
+ notifyContextSafely(ctx, message, "error");
975
+ throw new ReqError(message, 1);
976
+ }
977
+
893
978
  /**
894
979
  * @brief Returns the configurable active-tool inventory visible to the extension.
895
980
  * @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.
@@ -948,14 +1033,14 @@ function applyConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig): v
948
1033
 
949
1034
  /**
950
1035
  * @brief Handles one intercepted pi lifecycle hook for pi-usereq status updates.
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.
1036
+ * @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.
952
1037
  * @param[in] pi {ExtensionAPI} Active extension API instance.
953
1038
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
954
1039
  * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
955
1040
  * @param[in] event {unknown} Hook payload forwarded by pi.
956
1041
  * @param[in] ctx {ExtensionContext} Active extension context.
957
1042
  * @return {Promise<void>} Promise resolved when hook processing completes.
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
1043
+ * @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
959
1044
  */
960
1045
  async function handleExtensionStatusEvent(
961
1046
  pi: ExtensionAPI,
@@ -1026,7 +1111,10 @@ async function handleExtensionStatusEvent(
1026
1111
  if (hookName === "agent_end") {
1027
1112
  if (statusController.config) {
1028
1113
  runPiNotifyEffects(
1029
- statusController.config,
1114
+ {
1115
+ ...statusController.config,
1116
+ "notify-sound": getPiUsereqRuntimeSoundLevel(statusController),
1117
+ },
1030
1118
  event as { messages: AgentEndEvent["messages"] },
1031
1119
  notifyRequest,
1032
1120
  );
@@ -1042,7 +1130,6 @@ async function handleExtensionStatusEvent(
1042
1130
  ? `ERROR: Prompt closure retained worktree ${activePromptRequest.worktreeDir} after ${outcome} outcome.`
1043
1131
  : undefined;
1044
1132
  const shouldFinalizeMatchedSuccess = outcome === "completed"
1045
- && statusController.state.workflowState === "running"
1046
1133
  && activePromptRequest.worktreeDir !== undefined;
1047
1134
  if (debugConfig) {
1048
1135
  logPromptWorkflowEvent(
@@ -1081,6 +1168,7 @@ async function handleExtensionStatusEvent(
1081
1168
  mergeSucceeded: boolean;
1082
1169
  cleanupSucceeded: boolean;
1083
1170
  errorMessage?: string;
1171
+ warningMessage?: string;
1084
1172
  activeContext?: unknown;
1085
1173
  }
1086
1174
  | undefined;
@@ -1135,6 +1223,14 @@ async function handleExtensionStatusEvent(
1135
1223
  }
1136
1224
  notifyContextSafely(promptContext, finalization.errorMessage, "error");
1137
1225
  }
1226
+ if (
1227
+ finalization.warningMessage
1228
+ && finalization.cleanupSucceeded
1229
+ && finalization.mergeSucceeded
1230
+ && !finalization.errorMessage
1231
+ ) {
1232
+ notifyContextSafely(promptContext, finalization.warningMessage, "info");
1233
+ }
1138
1234
  } else {
1139
1235
  try {
1140
1236
  promptContext = (await restorePromptCommandExecution(
@@ -1356,7 +1452,6 @@ async function selectDebugLogOnStatus(
1356
1452
  description: `Write matching debug entries only while workflow state is ${workflowState}.`,
1357
1453
  })),
1358
1454
  ...buildTerminalSettingsMenuChoices({
1359
- resetDefaultsValue: DEFAULT_DEBUG_LOG_ON_STATUS,
1360
1455
  resetDefaultsDescription: "Restore the documented default workflow-state filter.",
1361
1456
  }),
1362
1457
  ], { initialSelectedId: currentValue });
@@ -1385,6 +1480,7 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1385
1480
  id: "debug-enabled",
1386
1481
  label: "Debug",
1387
1482
  value: config.DEBUG_ENABLED,
1483
+ values: ["enable", "disable"],
1388
1484
  description: "Enable or disable all debug logging behavior and unlock the remaining Debug rows.",
1389
1485
  },
1390
1486
  buildDebugMenuChoice(
@@ -1410,6 +1506,7 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1410
1506
  id: "debug-status-changes",
1411
1507
  label: "Status changes",
1412
1508
  value: normalizeDebugStatusChanges(config.DEBUG_STATUS_CHANGES),
1509
+ values: ["enable", "disable"],
1413
1510
  description: "Enable or disable `workflow_state` debug entries for prompt-orchestration transitions.",
1414
1511
  },
1415
1512
  debugEnabled,
@@ -1419,6 +1516,7 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1419
1516
  id: "debug-workflow-events",
1420
1517
  label: "Workflow events",
1421
1518
  value: normalizeDebugWorkflowEvents(config.DEBUG_WORKFLOW_EVENTS),
1519
+ values: ["enable", "disable"],
1422
1520
  description: "Enable or disable dedicated workflow debug entries for activation, restoration, closure, and session-shutdown diagnostics.",
1423
1521
  },
1424
1522
  debugEnabled,
@@ -1428,6 +1526,7 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1428
1526
  id: `debug-tool:${toolName}`,
1429
1527
  label: toolName,
1430
1528
  value: enabledTools.has(toolName) ? "enable" : "disable",
1529
+ values: ["enable", "disable"],
1431
1530
  description: PI_USEREQ_CUSTOM_TOOL_NAMES.includes(toolName as never)
1432
1531
  ? `Toggle debug logging for custom tool ${toolName}.`
1433
1532
  : `Toggle debug logging for embedded tool ${toolName}.`,
@@ -1439,12 +1538,12 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1439
1538
  id: `debug-prompt:${promptName}`,
1440
1539
  label: promptName,
1441
1540
  value: enabledPrompts.has(promptName) ? "enable" : "disable",
1541
+ values: ["enable", "disable"],
1442
1542
  description: `Toggle prompt-orchestration debug logging for /${promptName}.`,
1443
1543
  },
1444
1544
  debugEnabled,
1445
1545
  )),
1446
1546
  ...buildTerminalSettingsMenuChoices({
1447
- resetDefaultsValue: DEFAULT_DEBUG_LOG_ON_STATUS,
1448
1547
  resetDefaultsDescription: "Restore the documented default Debug configuration.",
1449
1548
  }),
1450
1549
  ];
@@ -1467,6 +1566,58 @@ async function configureDebugMenu(
1467
1566
  while (true) {
1468
1567
  const choice = await showPiUsereqSettingsMenu(ctx, "Debug", buildDebugMenuChoices(config), {
1469
1568
  initialSelectedId: focusedChoiceId,
1569
+ getChoices: () => buildDebugMenuChoices(config),
1570
+ onChange: (choiceId, newValue) => {
1571
+ if (choiceId === "debug-enabled") {
1572
+ config.DEBUG_ENABLED = newValue === "enable" ? "enable" : "disable";
1573
+ onConfigChange();
1574
+ ctx.ui.notify(`Debug ${config.DEBUG_ENABLED}`, "info");
1575
+ return;
1576
+ }
1577
+ if (choiceId === "debug-status-changes") {
1578
+ config.DEBUG_STATUS_CHANGES = normalizeDebugStatusChanges(newValue);
1579
+ onConfigChange();
1580
+ ctx.ui.notify(`Debug status-change logging ${config.DEBUG_STATUS_CHANGES}`, "info");
1581
+ return;
1582
+ }
1583
+ if (choiceId === "debug-workflow-events") {
1584
+ config.DEBUG_WORKFLOW_EVENTS = normalizeDebugWorkflowEvents(newValue);
1585
+ onConfigChange();
1586
+ ctx.ui.notify(`Debug workflow-event logging ${config.DEBUG_WORKFLOW_EVENTS}`, "info");
1587
+ return;
1588
+ }
1589
+ if (choiceId.startsWith("debug-tool:")) {
1590
+ const toolName = choiceId.slice("debug-tool:".length) as PiUsereqStartupToolName;
1591
+ const enabledTools = new Set(normalizeDebugEnabledTools(config.DEBUG_ENABLED_TOOLS));
1592
+ if (newValue === "enable") {
1593
+ enabledTools.add(toolName);
1594
+ } else {
1595
+ enabledTools.delete(toolName);
1596
+ }
1597
+ config.DEBUG_ENABLED_TOOLS = getDebugToolToggleNames().filter((name) => enabledTools.has(name));
1598
+ onConfigChange();
1599
+ ctx.ui.notify(
1600
+ `${newValue === "enable" ? "Enabled" : "Disabled"} debug logging for ${toolName}`,
1601
+ "info",
1602
+ );
1603
+ return;
1604
+ }
1605
+ if (choiceId.startsWith("debug-prompt:")) {
1606
+ const promptName = choiceId.slice("debug-prompt:".length) as (typeof DEBUG_PROMPT_NAMES)[number];
1607
+ const enabledPrompts = new Set(normalizeDebugEnabledPrompts(config.DEBUG_ENABLED_PROMPTS));
1608
+ if (newValue === "enable") {
1609
+ enabledPrompts.add(promptName);
1610
+ } else {
1611
+ enabledPrompts.delete(promptName);
1612
+ }
1613
+ config.DEBUG_ENABLED_PROMPTS = DEBUG_PROMPT_NAMES.filter((name) => enabledPrompts.has(name));
1614
+ onConfigChange();
1615
+ ctx.ui.notify(
1616
+ `${newValue === "enable" ? "Enabled" : "Disabled"} debug logging for ${promptName}`,
1617
+ "info",
1618
+ );
1619
+ }
1620
+ },
1470
1621
  });
1471
1622
  if (!choice) {
1472
1623
  return;
@@ -1754,6 +1905,13 @@ const PI_NOTIFY_EVENT_MENU_DEFINITIONS: Record<
1754
1905
  },
1755
1906
  };
1756
1907
 
1908
+ /**
1909
+ * @brief Defines the canonical label used for persisted boot-sound menu rows.
1910
+ * @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).
1911
+ * @satisfies REQ-149, REQ-179
1912
+ */
1913
+ const PI_NOTIFY_BOOT_SOUND_LABEL = "Enable sound (boot value)";
1914
+
1757
1915
  /**
1758
1916
  * @brief Formats the top-level summary value for one notification event submenu.
1759
1917
  * @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.
@@ -1794,7 +1952,7 @@ function buildPiNotifyEventLauncherChoice(
1794
1952
 
1795
1953
  /**
1796
1954
  * @brief Builds the shared settings-menu choices for one notification event submenu.
1797
- * @details Serializes completed/interrupted/failed rows with right-aligned `on|off` values, then appends `Reset defaults` and `Save and close` for submenu-scoped mutation control. Runtime is O(1). No external state is mutated.
1955
+ * @details Serializes completed/interrupted/failed rows with right-aligned `on|off` values, then appends a value-less `Reset defaults` row for submenu-scoped mutation control. Runtime is O(1). No external state is mutated.
1798
1956
  * @param[in] config {UseReqConfig} Effective project configuration.
1799
1957
  * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
1800
1958
  * @return {PiUsereqSettingsMenuChoice[]} Ordered event-submenu choice vector.
@@ -1809,6 +1967,7 @@ function buildPiNotifyEventMenuChoices(
1809
1967
  id: eventMenu.keys[row.eventId],
1810
1968
  label: row.label,
1811
1969
  value: config[eventMenu.keys[row.eventId]] ? "on" : "off",
1970
+ values: ["on", "off"],
1812
1971
  description: `${eventMenu.systemLabel}: ${row.description}`,
1813
1972
  })),
1814
1973
  ...buildTerminalSettingsMenuChoices({
@@ -1873,7 +2032,23 @@ async function configurePiNotifyEventMenu(
1873
2032
  ctx,
1874
2033
  eventMenu.submenuTitle,
1875
2034
  buildPiNotifyEventMenuChoices(config, eventMenu),
1876
- { initialSelectedId: focusedChoiceId },
2035
+ {
2036
+ initialSelectedId: focusedChoiceId,
2037
+ getChoices: () => buildPiNotifyEventMenuChoices(config, eventMenu),
2038
+ onChange: (choiceId, newValue) => {
2039
+ const enabled = newValue === "on";
2040
+ config[choiceId as PiNotifyEventBooleanConfigKey] = enabled;
2041
+ onConfigChange();
2042
+ const eventLabel = resolvePiNotifyEventLabel(
2043
+ choiceId as PiNotifyEventBooleanConfigKey,
2044
+ eventMenu,
2045
+ );
2046
+ ctx.ui.notify(
2047
+ `${eventMenu.systemLabel} ${eventLabel} ${enabled ? "enabled" : "disabled"}`,
2048
+ "info",
2049
+ );
2050
+ },
2051
+ },
1877
2052
  );
1878
2053
  if (!choice) {
1879
2054
  return;
@@ -1925,7 +2100,7 @@ async function configurePiNotifyEventMenu(
1925
2100
 
1926
2101
  /**
1927
2102
  * @brief Builds the direct Pushover rows rendered inside `Notifications`.
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.
2103
+ * @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.
1929
2104
  * @param[in] config {UseReqConfig} Effective project configuration.
1930
2105
  * @return {PiUsereqSettingsMenuChoice[]} Ordered direct Pushover rows.
1931
2106
  * @satisfies REQ-163, REQ-165, REQ-172, REQ-184, REQ-185, REQ-198, REQ-234, REQ-235
@@ -1937,8 +2112,9 @@ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuCh
1937
2112
  id: "notify-pushover-enabled",
1938
2113
  label: "Enable pushover",
1939
2114
  labelTone: pushoverCredentialsReady ? undefined : "dim",
1940
- value: pushoverCredentialsReady ? formatPiNotifyPushoverStatus(config) : "off",
2115
+ value: pushoverCredentialsReady ? formatPiNotifyPushoverStatus(config) : "configure user/token keys first",
1941
2116
  valueTone: pushoverCredentialsReady ? undefined : "dim",
2117
+ values: ["on", "off"],
1942
2118
  disabled: !pushoverCredentialsReady,
1943
2119
  description: pushoverCredentialsReady
1944
2120
  ? "Enable or disable all Pushover delivery globally."
@@ -1983,7 +2159,7 @@ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuCh
1983
2159
 
1984
2160
  /**
1985
2161
  * @brief Opens the shared settings-menu selector for Pushover priority.
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.
2162
+ * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the notification menus and appends a value-less subtree-local `Reset defaults` row. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1987
2163
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1988
2164
  * @param[in] currentPriority {PiNotifyPushoverPriority} Persisted priority value.
1989
2165
  * @return {Promise<PiNotifyPushoverPriority | "reset-defaults" | undefined>} Selected priority, reset action, or `undefined` when cancelled.
@@ -2007,7 +2183,6 @@ async function selectPiNotifyPushoverPriority(
2007
2183
  description: "Send outbound Pushover messages with high priority `1`.",
2008
2184
  },
2009
2185
  ...buildTerminalSettingsMenuChoices({
2010
- resetDefaultsValue: formatPiNotifyPushoverPriority(getDefaultConfig("")["notify-pushover-priority"]),
2011
2186
  resetDefaultsDescription: "Restore the documented default Pushover priority.",
2012
2187
  }),
2013
2188
  ], { initialSelectedId: String(currentPriority) });
@@ -2022,10 +2197,10 @@ async function selectPiNotifyPushoverPriority(
2022
2197
 
2023
2198
  /**
2024
2199
  * @brief Builds the shared settings-menu choices for notification configuration.
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.
2200
+ * @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.
2026
2201
  * @param[in] config {UseReqConfig} Effective project configuration.
2027
2202
  * @return {PiUsereqSettingsMenuChoice[]} Ordered notification-menu choice vector.
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
2203
+ * @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
2029
2204
  */
2030
2205
  function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2031
2206
  return [
@@ -2033,6 +2208,7 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2033
2208
  id: "notify-enabled",
2034
2209
  label: "Enable notification",
2035
2210
  value: formatPiNotifyStatus(config),
2211
+ values: ["on", "off"],
2036
2212
  description: "Enable or disable command-notify delivery globally.",
2037
2213
  },
2038
2214
  buildPiNotifyEventLauncherChoice(
@@ -2047,9 +2223,9 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2047
2223
  },
2048
2224
  {
2049
2225
  id: "selected-sound-command",
2050
- label: "Enable sound",
2226
+ label: PI_NOTIFY_BOOT_SOUND_LABEL,
2051
2227
  value: config["notify-sound"],
2052
- description: "Select which sound command level is currently active.",
2228
+ description: "Edit the persisted boot sound level without changing the active runtime sound level.",
2053
2229
  },
2054
2230
  buildPiNotifyEventLauncherChoice(
2055
2231
  config,
@@ -2059,25 +2235,25 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2059
2235
  id: "sound-toggle-hotkey-bind",
2060
2236
  label: "Sound toggle hotkey bind",
2061
2237
  value: config["notify-sound-toggle-shortcut"],
2062
- description: "Edit the keyboard shortcut that cycles the selected sound command.",
2238
+ description: "Edit the keyboard shortcut that cycles the active runtime sound level.",
2063
2239
  },
2064
2240
  {
2065
2241
  id: "sound-command-low",
2066
2242
  label: "Sound command (low vol.)",
2067
2243
  value: config.PI_NOTIFY_SOUND_LOW_CMD,
2068
- description: "Edit the shell command used when the selected sound command is `low`.",
2244
+ description: "Edit the shell command used when the active runtime sound level is `low`.",
2069
2245
  },
2070
2246
  {
2071
2247
  id: "sound-command-mid",
2072
2248
  label: "Sound command (mid vol.)",
2073
2249
  value: config.PI_NOTIFY_SOUND_MID_CMD,
2074
- description: "Edit the shell command used when the selected sound command is `mid`.",
2250
+ description: "Edit the shell command used when the active runtime sound level is `mid`.",
2075
2251
  },
2076
2252
  {
2077
2253
  id: "sound-command-high",
2078
2254
  label: "Sound command (high vol.)",
2079
2255
  value: config.PI_NOTIFY_SOUND_HIGH_CMD,
2080
- description: "Edit the shell command used when the selected sound command is `high`.",
2256
+ description: "Edit the shell command used when the active runtime sound level is `high`.",
2081
2257
  },
2082
2258
  ...buildPiNotifyPushoverRows(config),
2083
2259
  ...buildTerminalSettingsMenuChoices({
@@ -2087,45 +2263,44 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2087
2263
  }
2088
2264
 
2089
2265
  /**
2090
- * @brief Opens the shared settings-menu selector for the active sound level.
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.
2266
+ * @brief Opens the shared settings-menu selector for the persisted boot sound level.
2267
+ * @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.
2092
2268
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2093
- * @param[in] currentLevel {PiNotifySoundLevel} Currently selected sound level.
2094
- * @return {Promise<PiNotifySoundLevel | "reset-defaults" | undefined>} Selected sound level, reset action, or `undefined` when cancelled.
2095
- * @satisfies REQ-131, REQ-179, REQ-192
2269
+ * @param[in] currentLevel {PiNotifySoundLevel} Persisted boot sound level.
2270
+ * @return {Promise<PiNotifySoundLevel | "reset-defaults" | undefined>} Selected boot sound level, reset action, or `undefined` when cancelled.
2271
+ * @satisfies REQ-131, REQ-179, REQ-192, REQ-289
2096
2272
  */
2097
2273
  async function selectPiNotifySoundLevel(
2098
2274
  ctx: ExtensionCommandContext,
2099
2275
  currentLevel: PiNotifySoundLevel,
2100
2276
  ): Promise<PiNotifySoundLevel | "reset-defaults" | undefined> {
2101
- const choice = await showPiUsereqSettingsMenu(ctx, "Enable sound", [
2277
+ const choice = await showPiUsereqSettingsMenu(ctx, PI_NOTIFY_BOOT_SOUND_LABEL, [
2102
2278
  {
2103
2279
  id: "none",
2104
2280
  label: "none",
2105
2281
  value: currentLevel === "none" ? "selected" : "",
2106
- description: "Disable sound-command delivery while preserving per-event sound toggles.",
2282
+ description: "Persist `none` as the boot sound level loaded during the next session start.",
2107
2283
  },
2108
2284
  {
2109
2285
  id: "low",
2110
2286
  label: "low",
2111
2287
  value: currentLevel === "low" ? "selected" : "",
2112
- description: "Use the low-volume sound command when sound delivery is enabled for the current event.",
2288
+ description: "Persist `low` as the boot sound level loaded during the next session start.",
2113
2289
  },
2114
2290
  {
2115
2291
  id: "mid",
2116
2292
  label: "mid",
2117
2293
  value: currentLevel === "mid" ? "selected" : "",
2118
- description: "Use the mid-volume sound command when sound delivery is enabled for the current event.",
2294
+ description: "Persist `mid` as the boot sound level loaded during the next session start.",
2119
2295
  },
2120
2296
  {
2121
2297
  id: "high",
2122
2298
  label: "high",
2123
2299
  value: currentLevel === "high" ? "selected" : "",
2124
- description: "Use the high-volume sound command when sound delivery is enabled for the current event.",
2300
+ description: "Persist `high` as the boot sound level loaded during the next session start.",
2125
2301
  },
2126
2302
  ...buildTerminalSettingsMenuChoices({
2127
- resetDefaultsValue: getDefaultConfig("")["notify-sound"],
2128
- resetDefaultsDescription: "Restore the documented default sound level.",
2303
+ resetDefaultsDescription: "Restore the documented default boot sound level.",
2129
2304
  }),
2130
2305
  ], { initialSelectedId: currentLevel });
2131
2306
  if (!choice) {
@@ -2139,11 +2314,11 @@ async function selectPiNotifySoundLevel(
2139
2314
 
2140
2315
  /**
2141
2316
  * @brief Runs the interactive notification-configuration menu.
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.
2317
+ * @details Exposes command-notify, sound, and Pushover controls through the shared settings-menu renderer, delegates completed/interrupted/failed toggles to dedicated event submenus, persists boot-sound changes 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.
2143
2318
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2144
2319
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
2145
2320
  * @return {Promise<boolean>} `true` when the sound-toggle shortcut changed.
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
2321
+ * @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
2147
2322
  */
2148
2323
  async function configurePiNotifyMenu(
2149
2324
  ctx: ExtensionCommandContext,
@@ -2157,7 +2332,27 @@ async function configurePiNotifyMenu(
2157
2332
  ctx,
2158
2333
  "Notifications",
2159
2334
  buildPiNotifyMenuChoices(config),
2160
- { initialSelectedId: focusedChoiceId },
2335
+ {
2336
+ initialSelectedId: focusedChoiceId,
2337
+ getChoices: () => buildPiNotifyMenuChoices(config),
2338
+ onChange: (choiceId, newValue) => {
2339
+ if (choiceId === "notify-enabled" || choiceId === "notify-pushover-enabled") {
2340
+ if (choiceId === "notify-pushover-enabled" && !hasPiNotifyPushoverCredentials(config)) {
2341
+ config["notify-pushover-enabled"] = false;
2342
+ ctx.ui.notify("Populate both Pushover credential fields before enabling Pushover", "info");
2343
+ return;
2344
+ }
2345
+ const enabled = newValue === "on";
2346
+ config[choiceId as PiNotifyBooleanConfigKey] = enabled;
2347
+ onConfigChange();
2348
+ const labelMap: Record<string, string> = {
2349
+ "notify-enabled": "Notification",
2350
+ "notify-pushover-enabled": "Pushover",
2351
+ };
2352
+ ctx.ui.notify(`${labelMap[choiceId]} ${enabled ? "enabled" : "disabled"}`, "info");
2353
+ }
2354
+ },
2355
+ },
2161
2356
  );
2162
2357
  if (!choice) {
2163
2358
  return config["notify-sound-toggle-shortcut"] !== originalShortcut;
@@ -2221,22 +2416,22 @@ async function configurePiNotifyMenu(
2221
2416
  const approved = await confirmResetChanges(
2222
2417
  ctx,
2223
2418
  "Confirm sound reset",
2224
- [{ label: "Enable sound", previousValue: config["notify-sound"], nextValue: defaultSoundLevel }]
2419
+ [{ label: PI_NOTIFY_BOOT_SOUND_LABEL, previousValue: config["notify-sound"], nextValue: defaultSoundLevel }]
2225
2420
  .filter((change) => change.previousValue !== change.nextValue),
2226
- "Approve restoring the documented default sound level.",
2227
- "Abort the sound reset and keep the current value.",
2421
+ "Approve restoring the documented default boot sound level.",
2422
+ "Abort the boot sound reset and keep the current value.",
2228
2423
  );
2229
2424
  if (!approved) {
2230
- ctx.ui.notify("Aborted sound reset", "info");
2425
+ ctx.ui.notify("Aborted boot sound reset", "info");
2231
2426
  } else {
2232
2427
  config["notify-sound"] = defaultSoundLevel;
2233
2428
  onConfigChange();
2234
- ctx.ui.notify("Restored default sound level", "info");
2429
+ ctx.ui.notify("Restored default boot sound level; active runtime sound is unchanged", "info");
2235
2430
  }
2236
2431
  } else if (nextLevel !== undefined) {
2237
2432
  config["notify-sound"] = nextLevel;
2238
2433
  onConfigChange();
2239
- ctx.ui.notify(`Enable sound set to ${nextLevel}`, "info");
2434
+ ctx.ui.notify(`Stored ${PI_NOTIFY_BOOT_SOUND_LABEL.toLowerCase()} as ${nextLevel}; active runtime sound is unchanged`, "info");
2240
2435
  }
2241
2436
  continue;
2242
2437
  }
@@ -2381,7 +2576,7 @@ async function configurePiNotifyMenu(
2381
2576
  const defaults = getDefaultConfig("");
2382
2577
  const resetPreview: ResetConfirmationChange[] = [
2383
2578
  { label: "Enable notification", previousValue: formatPiNotifyStatus(config), nextValue: formatPiNotifyStatus(defaults) },
2384
- { label: "Enable sound", previousValue: config["notify-sound"], nextValue: defaults["notify-sound"] },
2579
+ { label: PI_NOTIFY_BOOT_SOUND_LABEL, previousValue: config["notify-sound"], nextValue: defaults["notify-sound"] },
2385
2580
  { label: "Sound toggle hotkey bind", previousValue: config["notify-sound-toggle-shortcut"], nextValue: defaults["notify-sound-toggle-shortcut"] },
2386
2581
  { label: "Notify command", previousValue: config.PI_NOTIFY_CMD, nextValue: defaults.PI_NOTIFY_CMD },
2387
2582
  { label: "Sound command (low vol.)", previousValue: config.PI_NOTIFY_SOUND_LOW_CMD, nextValue: defaults.PI_NOTIFY_SOUND_LOW_CMD },
@@ -2416,15 +2611,15 @@ async function configurePiNotifyMenu(
2416
2611
  /**
2417
2612
  * @brief Registers the configurable notification-sound shortcut when supported.
2418
2613
  * @details Loads the current project config, registers one raw pi shortcut when
2419
- * the runtime exposes `registerShortcut(...)`, cycles persisted sound state on
2420
- * invocation, saves the config, refreshes the status bar, and emits one info
2421
- * notification. Runtime is O(1) for registration plus config I/O per shortcut
2422
- * use. Side effects include shortcut registration, config writes, and status
2423
- * updates.
2614
+ * the runtime exposes `registerShortcut(...)`, cycles only the active runtime
2615
+ * sound level on invocation, leaves `.pi-usereq.json` unchanged, refreshes the
2616
+ * status bar, and emits one info notification. Runtime is O(1) for registration
2617
+ * plus one status update per shortcut use. Side effects include shortcut
2618
+ * registration and status updates.
2424
2619
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2425
2620
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2426
2621
  * @return {void} No return value.
2427
- * @satisfies REQ-131, REQ-134, REQ-180
2622
+ * @satisfies REQ-134, REQ-180, REQ-286, REQ-287
2428
2623
  */
2429
2624
  function registerPiNotifyShortcut(
2430
2625
  pi: ExtensionAPI,
@@ -2438,19 +2633,170 @@ function registerPiNotifyShortcut(
2438
2633
  shortcutRegistrar.registerShortcut(config["notify-sound-toggle-shortcut"], {
2439
2634
  description: "Cycle pi-usereq notification sound level",
2440
2635
  handler: async (ctx) => {
2441
- const nextConfig = loadProjectConfig(ctx.cwd);
2442
- nextConfig["notify-sound"] = cyclePiNotifySoundLevel(nextConfig["notify-sound"]);
2443
- saveProjectConfig(ctx.cwd, nextConfig);
2444
- setPiUsereqStatusConfig(statusController, nextConfig);
2445
- renderPiUsereqStatus(statusController, ctx);
2446
- ctx.ui.notify(`pi-usereq sound:${nextConfig["notify-sound"]}`, "info");
2636
+ const nextRuntimeSoundLevel = cyclePiNotifySoundLevel(
2637
+ getPiUsereqRuntimeSoundLevel(statusController),
2638
+ );
2639
+ setPiUsereqRuntimeSoundLevel(
2640
+ statusController,
2641
+ nextRuntimeSoundLevel,
2642
+ ctx,
2643
+ );
2644
+ ctx.ui.notify(`pi-usereq sound:${nextRuntimeSoundLevel}`, "info");
2447
2645
  },
2448
2646
  });
2449
2647
  }
2450
2648
 
2451
2649
  /**
2452
- * @brief Registers bundled prompt commands with the extension.
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.
2650
+ * @brief Resolves the prompt execution plan targeted by `req-reset` recovery.
2651
+ * @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.
2652
+ * @param[in] statusController {PiUsereqStatusController} Mutable status controller.
2653
+ * @return {PromptCommandExecutionPlan | undefined} Recoverable prompt execution plan when one remains available.
2654
+ */
2655
+ function resolveReqResetPromptRequest(
2656
+ statusController: PiUsereqStatusController,
2657
+ ): PromptCommandExecutionPlan | undefined {
2658
+ const isWorktreeBacked = (request: PromptCommandExecutionPlan | undefined): request is PromptCommandExecutionPlan =>
2659
+ request?.worktreeDir !== undefined
2660
+ && request.worktreeRootPath !== undefined
2661
+ && request.worktreePath !== undefined;
2662
+ const inMemoryActiveRequest = statusController.state.activePromptRequest;
2663
+ if (isWorktreeBacked(inMemoryActiveRequest)) {
2664
+ return inMemoryActiveRequest;
2665
+ }
2666
+ const inMemoryPendingRequest = statusController.state.pendingPromptRequest;
2667
+ if (isWorktreeBacked(inMemoryPendingRequest)) {
2668
+ return inMemoryPendingRequest;
2669
+ }
2670
+ const persistedRuntimeState = readPersistedPromptCommandRuntimeState();
2671
+ if (isWorktreeBacked(persistedRuntimeState.activePromptRequest)) {
2672
+ return persistedRuntimeState.activePromptRequest;
2673
+ }
2674
+ if (isWorktreeBacked(persistedRuntimeState.pendingPromptRequest)) {
2675
+ return persistedRuntimeState.pendingPromptRequest;
2676
+ }
2677
+ return inMemoryActiveRequest
2678
+ ?? inMemoryPendingRequest
2679
+ ?? persistedRuntimeState.activePromptRequest
2680
+ ?? persistedRuntimeState.pendingPromptRequest;
2681
+ }
2682
+
2683
+ /**
2684
+ * @brief Registers the specialized `req-reset` slash command.
2685
+ * @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.
2686
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
2687
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2688
+ * @return {void} No return value.
2689
+ * @satisfies REQ-304, REQ-305, REQ-306, REQ-307, REQ-308, REQ-309, REQ-310, REQ-311, REQ-312, REQ-313
2690
+ */
2691
+ function registerReqResetCommand(
2692
+ pi: ExtensionAPI,
2693
+ statusController: PiUsereqStatusController,
2694
+ ): void {
2695
+ pi.registerCommand("req-reset", {
2696
+ description: REQ_RESET_COMMAND_DESCRIPTION,
2697
+ handler: async (_args, ctx) => {
2698
+ const promptRequest = resolveReqResetPromptRequest(statusController);
2699
+ const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
2700
+ syncContextCwdMirror(ctx, commandCwd);
2701
+ const projectBase = path.resolve(promptRequest?.basePath ?? getProjectBase(commandCwd));
2702
+ bootstrapRuntimePathState(projectBase, {
2703
+ gitPath: resolveRuntimeGitPath(projectBase),
2704
+ });
2705
+ const config = loadProjectConfig(projectBase);
2706
+ let executionPlan: ReqResetCommandPlan;
2707
+ let executionResult: ReqResetCommandExecutionResult;
2708
+ let resetContext = ctx;
2709
+
2710
+ setPiUsereqStatusConfig(statusController, config);
2711
+ setPiUsereqWorkflowState(statusController, "running", ctx);
2712
+
2713
+ try {
2714
+ executionPlan = prepareReqResetCommandExecution(projectBase, config, promptRequest);
2715
+ executionResult = await executeReqResetCommandExecution(executionPlan, ctx);
2716
+ resetContext = (executionResult.activeContext ?? resetContext) as typeof ctx;
2717
+ } catch (error) {
2718
+ const message = error instanceof Error ? error.message : String(error);
2719
+ setPiUsereqWorkflowState(statusController, "error", resetContext);
2720
+ notifyContextSafely(resetContext, message, "error");
2721
+ throw error;
2722
+ }
2723
+
2724
+ const shouldClearPromptState = executionResult.restoredBasePath
2725
+ || executionPlan.promptRequest === undefined;
2726
+ if (shouldClearPromptState) {
2727
+ statusController.state.pendingPromptRequest = undefined;
2728
+ statusController.state.activePromptRequest = undefined;
2729
+ clearPersistedPromptCommandRuntimeState();
2730
+ }
2731
+
2732
+ if (executionResult.errorMessage) {
2733
+ setPiUsereqWorkflowState(statusController, "error", resetContext);
2734
+ notifyContextSafely(resetContext, executionResult.errorMessage, "error");
2735
+ throw new ReqError(executionResult.errorMessage, 1);
2736
+ }
2737
+
2738
+ setPiUsereqWorkflowState(statusController, "idle", resetContext);
2739
+ const successMessage = executionPlan.promptRequest !== undefined
2740
+ ? `SUCCESS: req-reset restored base-path and removed ${executionResult.removedWorktreeDirs.length} worktree(s) plus ${executionResult.removedBranchNames.length} branch(es).`
2741
+ : `SUCCESS: req-reset removed ${executionResult.removedWorktreeDirs.length} worktree(s) plus ${executionResult.removedBranchNames.length} branch(es) and restored idle state.`;
2742
+ notifyContextSafely(resetContext, successMessage, "info");
2743
+ },
2744
+ });
2745
+ }
2746
+
2747
+ /**
2748
+ * @brief Registers the specialized `req-references` slash command.
2749
+ * @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.
2750
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
2751
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2752
+ * @return {void} No return value.
2753
+ * @satisfies REQ-200, REQ-221, REQ-224, REQ-298, REQ-299, REQ-300, REQ-301, REQ-302, REQ-303
2754
+ */
2755
+ function registerReqReferencesCommand(
2756
+ pi: ExtensionAPI,
2757
+ statusController: PiUsereqStatusController,
2758
+ ): void {
2759
+ pi.registerCommand("req-references", {
2760
+ description: REQ_REFERENCES_COMMAND_DESCRIPTION,
2761
+ handler: async (_args, ctx) => {
2762
+ if (statusController.state.workflowState !== "idle") {
2763
+ rejectNonIdleReqCommand(statusController, ctx);
2764
+ }
2765
+ const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
2766
+ syncContextCwdMirror(ctx, commandCwd);
2767
+ bootstrapRuntimePathState(commandCwd, {
2768
+ gitPath: resolveRuntimeGitPath(commandCwd),
2769
+ });
2770
+ const projectBase = getProjectBase(commandCwd);
2771
+ const config = loadProjectConfig(commandCwd);
2772
+ setPiUsereqStatusConfig(statusController, config);
2773
+ statusController.state.pendingPromptRequest = undefined;
2774
+ statusController.state.activePromptRequest = undefined;
2775
+ setPiUsereqWorkflowState(statusController, "checking", ctx);
2776
+ try {
2777
+ const executionPlan = prepareReqReferencesCommandExecution(projectBase, config);
2778
+ setPiUsereqWorkflowState(statusController, "running", ctx);
2779
+ executeReqReferencesCommandExecution(executionPlan, config);
2780
+ setPiUsereqWorkflowState(statusController, "idle", ctx);
2781
+ ctx.ui.notify(
2782
+ `SUCCESS: Updated ${formatRuntimePathForDisplay(executionPlan.referencesPath)} and committed changes.`,
2783
+ "info",
2784
+ );
2785
+ } catch (error) {
2786
+ statusController.state.pendingPromptRequest = undefined;
2787
+ statusController.state.activePromptRequest = undefined;
2788
+ setPiUsereqWorkflowState(statusController, "error", ctx);
2789
+ const message = error instanceof Error ? error.message : String(error);
2790
+ ctx.ui.notify(message, "error");
2791
+ throw error;
2792
+ }
2793
+ },
2794
+ });
2795
+ }
2796
+
2797
+ /**
2798
+ * @brief Registers bundled prompt-backed commands with the extension.
2799
+ * @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.
2454
2800
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2455
2801
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2456
2802
  * @return {void} No return value.
@@ -2465,9 +2811,7 @@ function registerPromptCommands(
2465
2811
  description: resolvePromptCommandDescription(promptName),
2466
2812
  handler: async (args, ctx) => {
2467
2813
  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);
2814
+ rejectNonIdleReqCommand(statusController, ctx, promptName);
2471
2815
  }
2472
2816
  const commandCwd = resolveLiveBootstrapCwd(ctx.cwd);
2473
2817
  syncContextCwdMirror(ctx, commandCwd);
@@ -2582,10 +2926,10 @@ function registerPromptCommands(
2582
2926
  * @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
2927
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2584
2928
  * @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
2929
+ * @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
2586
2930
  */
2587
2931
  function registerAgentTools(pi: ExtensionAPI): void {
2588
- const filesReferencesSchema = Type.Object(
2932
+ const filesSummarizeSchema = Type.Object(
2589
2933
  {
2590
2934
  files: Type.Array(
2591
2935
  Type.String({ description: "Project-relative or absolute source file path resolved from the current working directory when not already absolute" }),
@@ -2637,21 +2981,21 @@ function registerAgentTools(pi: ExtensionAPI): void {
2637
2981
  });
2638
2982
 
2639
2983
  pi.registerTool({
2640
- name: "files-references",
2641
- label: "files-references",
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.",
2984
+ name: "files-summarize",
2985
+ label: "files-summarize",
2986
+ description: "Scope: explicit source files. Return the monolithic summary markdown report in content[0].text and keep only execution metadata in details.execution.",
2987
+ promptSnippet: "Return the monolithic summary markdown report for caller-selected source files.",
2644
2988
  promptGuidelines: [
2645
2989
  "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
2646
2990
  "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`.",
2991
+ "Formatting contract: content matches the Python summary renderer used by `generate_markdown.py`.",
2648
2992
  "Behavior contract: missing inputs, non-file inputs, unsupported extensions, and analysis failures surface through details.execution diagnostics.",
2649
2993
  ],
2650
- renderResult: buildStructuredToolRenderResult("files-references"),
2651
- parameters: filesReferencesSchema,
2994
+ renderResult: buildStructuredToolRenderResult("files-summarize"),
2995
+ parameters: filesSummarizeSchema,
2652
2996
  async execute(_toolCallId, params) {
2653
2997
  const contextPath = getRuntimeContextPath(process.cwd());
2654
- return executeMonolithicTool(() => runFilesReferences(params.files, contextPath));
2998
+ return executeMonolithicTool(() => runFilesSummarize(params.files, contextPath));
2655
2999
  },
2656
3000
  });
2657
3001
 
@@ -2722,12 +3066,18 @@ function registerAgentTools(pi: ExtensionAPI): void {
2722
3066
  },
2723
3067
  });
2724
3068
 
2725
- const referencesSchema = Type.Object(
3069
+ const summarizeSchema = Type.Object(
2726
3070
  {},
2727
3071
  {
2728
3072
  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.",
2729
3073
  },
2730
3074
  );
3075
+ const referencesSchema = Type.Object(
3076
+ {},
3077
+ {
3078
+ 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.",
3079
+ },
3080
+ );
2731
3081
  const tokensSchema = Type.Object(
2732
3082
  {},
2733
3083
  {
@@ -2736,23 +3086,44 @@ function registerAgentTools(pi: ExtensionAPI): void {
2736
3086
  );
2737
3087
 
2738
3088
  pi.registerTool({
2739
- name: "references",
2740
- label: "references",
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.",
3089
+ name: "summarize",
3090
+ label: "summarize",
3091
+ description: "Scope: configured project source directories. Return the monolithic summary markdown report in content[0].text and keep only execution metadata in details.execution.",
3092
+ promptSnippet: "Return the monolithic project summary markdown report from the configured source directories.",
2743
3093
  promptGuidelines: [
2744
3094
  "Scope: no params; resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.",
2745
3095
  "Output contract: monolithic markdown in content[0].text; details.execution preserves only exit code and residual diagnostics.",
2746
3096
  "Formatting contract: content prepends the file-structure markdown block before the per-file markdown produced by `generate_markdown.py`.",
2747
3097
  "Configuration contract: output changes with cwd-derived project config and src-dir values; the tool does not accept explicit file overrides.",
2748
3098
  ],
3099
+ renderResult: buildStructuredToolRenderResult("summarize"),
3100
+ parameters: summarizeSchema,
3101
+ async execute() {
3102
+ const contextPath = getRuntimeContextPath(process.cwd());
3103
+ const projectBase = getProjectBase(contextPath);
3104
+ const config = loadProjectConfig(projectBase);
3105
+ return executeMonolithicTool(() => runSummarize(contextPath, config));
3106
+ },
3107
+ });
3108
+
3109
+ pi.registerTool({
3110
+ name: "references",
3111
+ label: "references",
3112
+ 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.",
3113
+ promptSnippet: "Generate REFERENCES.md from the configured source directories without returning the generated markdown.",
3114
+ promptGuidelines: [
3115
+ "Scope: no params; resolve src-dir and docs-dir from the current project configuration and current working directory.",
3116
+ "Behavior contract: generate the same file-structure-plus-summary markdown as `summarize` and overwrite `<docs-dir>/REFERENCES.md`.",
3117
+ "Output contract: `content[0].text` is `success` on success or `error: <diagnostic>` on failure; generated markdown is never returned to the LLM.",
3118
+ "Failure contract: source discovery, markdown generation, and file-write errors surface through the status text plus details.execution diagnostics.",
3119
+ ],
2749
3120
  renderResult: buildStructuredToolRenderResult("references"),
2750
3121
  parameters: referencesSchema,
2751
3122
  async execute() {
2752
3123
  const contextPath = getRuntimeContextPath(process.cwd());
2753
3124
  const projectBase = getProjectBase(contextPath);
2754
3125
  const config = loadProjectConfig(projectBase);
2755
- return executeMonolithicTool(() => runReferences(contextPath, config));
3126
+ return executeStatusTool(() => runReferences(contextPath, config));
2756
3127
  },
2757
3128
  });
2758
3129
 
@@ -2923,7 +3294,6 @@ function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig):
2923
3294
  description: "Disable every configurable startup tool for future session starts.",
2924
3295
  },
2925
3296
  ...buildTerminalSettingsMenuChoices({
2926
- resetDefaultsValue: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
2927
3297
  resetDefaultsDescription: "Restore the documented default startup-tool selection.",
2928
3298
  }),
2929
3299
  ];
@@ -2931,7 +3301,7 @@ function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig):
2931
3301
 
2932
3302
  /**
2933
3303
  * @brief Builds the shared settings-menu choices for per-tool startup toggles.
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.
3304
+ * @details Exposes every configurable startup tool as one row whose right-side value reports the current enabled state, preserves the documented custom/files/embedded/default-disabled ordering, and appends a value-less subtree-local `Reset defaults` row. Runtime is O(t) in configurable-tool count. No external state is mutated.
2935
3305
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2936
3306
  * @param[in] config {UseReqConfig} Effective project configuration.
2937
3307
  * @return {PiUsereqSettingsMenuChoice[]} Ordered per-tool toggle choices.
@@ -2944,10 +3314,10 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
2944
3314
  id: tool.name,
2945
3315
  label: tool.name,
2946
3316
  value: enabledTools.has(tool.name) ? "on" : "off",
3317
+ values: ["on", "off"],
2947
3318
  description: tool.description ?? `Toggle startup activation for ${tool.name}.`,
2948
3319
  })),
2949
3320
  ...buildTerminalSettingsMenuChoices({
2950
- resetDefaultsValue: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
2951
3321
  resetDefaultsDescription: "Restore the documented default startup-tool selection.",
2952
3322
  }),
2953
3323
  ];
@@ -3019,7 +3389,29 @@ async function configurePiUsereqToolsMenu(
3019
3389
  }
3020
3390
 
3021
3391
  if (choice === "enable-tools") {
3022
- const selectedToolName = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolToggleChoices(pi, config));
3392
+ const selectedToolName = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolToggleChoices(pi, config), {
3393
+ getChoices: () => buildPiUsereqToolToggleChoices(pi, config),
3394
+ onChange: (toolName, newValue) => {
3395
+ const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
3396
+ if (newValue === "on") {
3397
+ enabledTools.add(toolName as PiUsereqStartupToolName);
3398
+ } else {
3399
+ enabledTools.delete(toolName as PiUsereqStartupToolName);
3400
+ }
3401
+ setConfiguredPiUsereqTools(
3402
+ pi,
3403
+ config,
3404
+ getPiUsereqStartupTools(pi)
3405
+ .map((tool) => tool.name)
3406
+ .filter((currentToolName) => enabledTools.has(currentToolName)),
3407
+ );
3408
+ onConfigChange();
3409
+ ctx.ui.notify(
3410
+ `${newValue === "on" ? "Enabled" : "Disabled"} ${toolName}`,
3411
+ "info",
3412
+ );
3413
+ },
3414
+ });
3023
3415
  if (!selectedToolName) {
3024
3416
  continue;
3025
3417
  }
@@ -3145,11 +3537,11 @@ function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenu
3145
3537
  id: `toggle-static-check-language:${language}`,
3146
3538
  label: language,
3147
3539
  value: languageConfig.enabled === "enable" ? "on" : "off",
3540
+ values: ["on", "off"],
3148
3541
  description: `Toggle static-check execution for ${language}. Configured ${configuredCount} ${suffix}. Supported extensions: ${extensions.join(", ")}.`,
3149
3542
  };
3150
3543
  }),
3151
3544
  ...buildTerminalSettingsMenuChoices({
3152
- resetDefaultsValue: formatStaticCheckLanguagesSummary(config),
3153
3545
  resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3154
3546
  }),
3155
3547
  ];
@@ -3175,7 +3567,6 @@ function buildSupportedStaticCheckLanguageChoices(config: UseReqConfig): PiUsere
3175
3567
  };
3176
3568
  }),
3177
3569
  ...buildTerminalSettingsMenuChoices({
3178
- resetDefaultsValue: formatStaticCheckLanguagesSummary(config),
3179
3570
  resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3180
3571
  }),
3181
3572
  ];
@@ -3201,7 +3592,6 @@ function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUser
3201
3592
  };
3202
3593
  }),
3203
3594
  ...buildTerminalSettingsMenuChoices({
3204
- resetDefaultsValue: formatStaticCheckLanguagesSummary(config),
3205
3595
  resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3206
3596
  }),
3207
3597
  ];
@@ -3224,6 +3614,19 @@ async function configureStaticCheckMenu(
3224
3614
  while (true) {
3225
3615
  const staticChoice = await showPiUsereqSettingsMenu(ctx, "Language static code checkers", buildStaticCheckMenuChoices(config), {
3226
3616
  initialSelectedId: focusedChoiceId,
3617
+ getChoices: () => buildStaticCheckMenuChoices(config),
3618
+ onChange: (choiceId, newValue) => {
3619
+ if (choiceId.startsWith("toggle-static-check-language:")) {
3620
+ const language = choiceId.slice("toggle-static-check-language:".length);
3621
+ config["static-check"][language] ??= createStaticCheckLanguageConfig([]);
3622
+ config["static-check"][language]!.enabled = newValue === "on" ? "enable" : "disable";
3623
+ onConfigChange();
3624
+ ctx.ui.notify(
3625
+ `${newValue === "on" ? "Enabled" : "Disabled"} static-check for ${language}`,
3626
+ "info",
3627
+ );
3628
+ }
3629
+ },
3227
3630
  });
3228
3631
 
3229
3632
  if (!staticChoice) {
@@ -3390,6 +3793,7 @@ function buildPiUsereqMenuChoices(
3390
3793
  id: "auto-git-commit",
3391
3794
  label: "Auto git commit",
3392
3795
  value: config.AUTO_GIT_COMMIT,
3796
+ values: ["enable", "disable"],
3393
3797
  description: "Select bundled `git_commit.md` or `git_read-only.md` for `%%COMMIT%%`; disabling also forces prompt-command worktrees off.",
3394
3798
  },
3395
3799
  {
@@ -3398,6 +3802,8 @@ function buildPiUsereqMenuChoices(
3398
3802
  labelTone: autoGitCommitDisabled ? "dim" : undefined,
3399
3803
  value: effectiveGitWorktreeEnabled,
3400
3804
  valueTone: autoGitCommitDisabled ? "dim" : undefined,
3805
+ values: ["enable", "disable"],
3806
+ disabled: autoGitCommitDisabled,
3401
3807
  description: autoGitCommitDisabled
3402
3808
  ? "Forced to `disable` while `Auto git commit` is disabled."
3403
3809
  : "Enable or disable prompt-command worktree orchestration.",
@@ -3471,7 +3877,6 @@ function buildSrcDirMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoic
3471
3877
  description: "Select one configured source directory to remove from the current configuration.",
3472
3878
  },
3473
3879
  ...buildTerminalSettingsMenuChoices({
3474
- resetDefaultsValue: `${DEFAULT_SRC_DIRS.join(", ")}`,
3475
3880
  resetDefaultsDescription: "Restore the documented default source-directory configuration.",
3476
3881
  }),
3477
3882
  ];
@@ -3479,7 +3884,7 @@ function buildSrcDirMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoic
3479
3884
 
3480
3885
  /**
3481
3886
  * @brief Builds the shared settings-menu choices for removing one source-directory entry.
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.
3887
+ * @details Exposes every configured `src-dir` entry as one removable row and appends a value-less subtree-local `Reset defaults` row. Runtime is O(s) in source-directory count. No external state is mutated.
3483
3888
  * @param[in] config {UseReqConfig} Effective project configuration.
3484
3889
  * @return {PiUsereqSettingsMenuChoice[]} Ordered removable source-directory choices.
3485
3890
  * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154
@@ -3493,7 +3898,6 @@ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuCh
3493
3898
  description: `Remove the source-directory entry ${entry} from the current configuration.`,
3494
3899
  })),
3495
3900
  ...buildTerminalSettingsMenuChoices({
3496
- resetDefaultsValue: `${DEFAULT_SRC_DIRS.join(", ")}`,
3497
3901
  resetDefaultsDescription: "Restore the documented default source-directory configuration.",
3498
3902
  }),
3499
3903
  ];
@@ -3532,7 +3936,36 @@ async function configurePiUsereq(
3532
3936
  ctx,
3533
3937
  "pi-usereq",
3534
3938
  buildPiUsereqMenuChoices(ctx.cwd, config),
3535
- { initialSelectedId: focusedChoiceId },
3939
+ {
3940
+ initialSelectedId: focusedChoiceId,
3941
+ getChoices: () => buildPiUsereqMenuChoices(ctx.cwd, config),
3942
+ onChange: (choiceId, newValue) => {
3943
+ if (choiceId === "auto-git-commit") {
3944
+ config.AUTO_GIT_COMMIT = newValue === "enable" ? "enable" : "disable";
3945
+ if (config.AUTO_GIT_COMMIT === "disable") {
3946
+ config.GIT_WORKTREE_ENABLED = "disable";
3947
+ persistConfigChange();
3948
+ ctx.ui.notify("Auto git commit disabled; Git worktree forced off", "info");
3949
+ } else {
3950
+ persistConfigChange();
3951
+ ctx.ui.notify("Auto git commit enabled", "info");
3952
+ }
3953
+ return;
3954
+ }
3955
+ if (choiceId === "git-worktree-enabled") {
3956
+ if (config.AUTO_GIT_COMMIT === "disable") {
3957
+ ctx.ui.notify("Git worktree is locked while Auto git commit is disabled", "info");
3958
+ return;
3959
+ }
3960
+ config.GIT_WORKTREE_ENABLED = newValue === "enable" ? "enable" : "disable";
3961
+ persistConfigChange();
3962
+ ctx.ui.notify(
3963
+ `Git worktree ${resolveEffectiveGitWorktreeEnabled(config.AUTO_GIT_COMMIT, config.GIT_WORKTREE_ENABLED) === "enable" ? "enabled" : "disabled"}`,
3964
+ "info",
3965
+ );
3966
+ }
3967
+ },
3968
+ },
3536
3969
  );
3537
3970
  if (!choice) {
3538
3971
  if (config["notify-sound-toggle-shortcut"] !== initialShortcut) {
@@ -3752,23 +4185,16 @@ function registerConfigCommands(
3752
4185
 
3753
4186
  /**
3754
4187
  * @brief Registers the complete pi-usereq extension.
3755
- * @details Validates installation-owned bundled resources, registers prompt and
3756
- * configuration commands plus agent tools, registers the configurable
3757
- * notification-sound shortcut when the runtime supports shortcuts, and
3758
- * installs shared wrappers for all supported pi lifecycle hooks so status
3759
- * telemetry, context usage, prompt timing, cumulative runtime, prompt-specific
3760
- * Pushover metadata, tool-result debug logging, and prompt-orchestration debug
3761
- * effects remain synchronized with runtime events. Runtime is O(h) in hook
3762
- * count during registration. Side effects include filesystem reads,
3763
- * command/tool/shortcut registration, UI updates, active-tool changes,
3764
- * optional debug-log writes, and timer scheduling.
4188
+ * @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.
3765
4189
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3766
4190
  * @return {void} No return value.
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
4191
+ * @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
3768
4192
  */
3769
4193
  export default function piUsereqExtension(pi: ExtensionAPI): void {
3770
4194
  const statusController = createPiUsereqStatusController();
3771
4195
  ensureBundledResourcesAccessible();
4196
+ registerReqResetCommand(pi, statusController);
4197
+ registerReqReferencesCommand(pi, statusController);
3772
4198
  registerPromptCommands(pi, statusController);
3773
4199
  registerAgentTools(pi);
3774
4200
  registerConfigCommands(pi, statusController);