pi-usereq 0.4.0 → 0.5.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 (60) hide show
  1. package/.gitignore +3 -0
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +1 -1
  4. package/package.json +1 -1
  5. package/{req → pi-usereq}/docs/REFERENCES.md +196 -159
  6. package/{req → pi-usereq}/docs/REQUIREMENTS.md +65 -44
  7. package/{req → pi-usereq}/docs/WORKFLOW.md +66 -73
  8. package/src/core/config.ts +39 -12
  9. package/src/core/extension-status.ts +94 -58
  10. package/src/core/pi-notify.ts +198 -11
  11. package/src/core/runtime-project-paths.ts +3 -32
  12. package/src/core/settings-menu.ts +9 -4
  13. package/src/core/static-check.ts +35 -171
  14. package/src/index.ts +281 -86
  15. package/tests/attended-results-scenarios.ts +3 -13
  16. package/tests/cli-command-option-parity.test.ts +21 -12
  17. package/tests/debug-extension-harness.test.ts +11 -7
  18. package/tests/extension-registration.test.ts +371 -71
  19. package/tests/oracle-project.test.ts +8 -3
  20. package/tests/oracle-standalone.test.ts +7 -13
  21. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_c.c.json +0 -5
  22. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_cpp.cpp.json +0 -5
  23. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_csharp.cs.json +0 -5
  24. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_elixir.ex.json +0 -5
  25. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_go.go.json +0 -5
  26. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_haskell.hs.json +0 -5
  27. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_java.java.json +0 -5
  28. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_javascript.js.json +0 -5
  29. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_kotlin.kt.json +0 -5
  30. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_lua.lua.json +0 -5
  31. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_perl.pl.json +0 -5
  32. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_php.php.json +0 -5
  33. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_python.py.json +0 -5
  34. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_ruby.rb.json +0 -5
  35. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_rust.rs.json +0 -5
  36. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_scala.scala.json +0 -5
  37. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_shell.sh.json +0 -5
  38. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_swift.swift.json +0 -5
  39. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_typescript.ts.json +0 -5
  40. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_zig.zig.json +0 -5
  41. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_c.c.json +0 -5
  42. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_cpp.cpp.json +0 -5
  43. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_csharp.cs.json +0 -5
  44. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_elixir.ex.json +0 -5
  45. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_go.go.json +0 -5
  46. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_haskell.hs.json +0 -5
  47. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_java.java.json +0 -5
  48. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_javascript.js.json +0 -5
  49. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_kotlin.kt.json +0 -5
  50. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_lua.lua.json +0 -5
  51. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_perl.pl.json +0 -5
  52. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_php.php.json +0 -5
  53. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_python.py.json +0 -5
  54. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_ruby.rb.json +0 -5
  55. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_rust.rs.json +0 -5
  56. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_scala.scala.json +0 -5
  57. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_shell.sh.json +0 -5
  58. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_swift.swift.json +0 -5
  59. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_typescript.ts.json +0 -5
  60. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_zig.zig.json +0 -5
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.4.0"
11
+ export const VERSION = "0.5.0"
12
12
 
13
13
  import path from "node:path";
14
14
  import type {
@@ -53,6 +53,7 @@ import {
53
53
  } from "./core/find-payload.js";
54
54
  import {
55
55
  getDefaultConfig,
56
+ getProjectConfigPath,
56
57
  loadConfig,
57
58
  normalizeConfigPaths,
58
59
  saveConfig,
@@ -62,10 +63,19 @@ import {
62
63
  import {
63
64
  cyclePiNotifySoundLevel,
64
65
  formatPiNotifyBeepStatus,
66
+ formatPiNotifyPushoverStatus,
67
+ normalizePiNotifyPushoverCredential,
68
+ normalizePiNotifyPushoverPriority,
65
69
  runPiNotifyEffects,
70
+ type PiNotifyPushoverPriority,
71
+ type PiNotifyPushoverRequest,
66
72
  type PiNotifySoundLevel,
67
73
  } from "./core/pi-notify.js";
68
- import { buildRuntimePathContext, buildRuntimePathFacts } from "./core/path-context.js";
74
+ import {
75
+ buildRuntimePathContext,
76
+ buildRuntimePathFacts,
77
+ formatRuntimePathForDisplay,
78
+ } from "./core/path-context.js";
69
79
  import { resolveRuntimeGitPath } from "./core/runtime-project-paths.js";
70
80
  import { showPiUsereqSettingsMenu, type PiUsereqSettingsMenuChoice } from "./core/settings-menu.js";
71
81
  import {
@@ -193,6 +203,19 @@ function saveProjectConfig(cwd: string, config: UseReqConfig): void {
193
203
  saveConfig(projectBase, normalizeConfigPaths(projectBase, config));
194
204
  }
195
205
 
206
+ /**
207
+ * @brief Formats the current project config path for top-level menu display.
208
+ * @details Resolves `<base-path>/.pi-usereq/config.json` from the cwd-derived
209
+ * project base and formats it relative to the user home when possible. Runtime
210
+ * is O(p) in path length. No external state is mutated.
211
+ * @param[in] cwd {string} Current working directory.
212
+ * @return {string} User-home-relative or absolute config path display value.
213
+ * @satisfies REQ-162
214
+ */
215
+ function formatProjectConfigPathForMenu(cwd: string): string {
216
+ return formatRuntimePathForDisplay(getProjectConfigPath(getProjectBase(cwd)));
217
+ }
218
+
196
219
  /**
197
220
  * @brief Collects the project-scoped static-check selection used by the agent tool.
198
221
  * @details Resolves configured source plus test directories, reuses the same fixture-root exclusions as `runProjectStaticCheck`, and returns canonical relative file paths for structured payload emission. Runtime is O(F) plus project file-discovery cost. Side effects are limited to filesystem reads and git subprocesses delegated through `collectSourceFiles`.
@@ -537,18 +560,20 @@ function applyConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig): v
537
560
  * @details Applies session-start-specific resource validation, project-config
538
561
  * refresh, and startup-tool enablement before forwarding the originating hook
539
562
  * name and payload into the shared `updateExtensionStatus(...)` pipeline.
540
- * On `agent_end`, also dispatches configured pi-notify beep and sound effects.
541
- * Runtime is dominated by configuration loading during `session_start`; all
542
- * other hooks are O(1). Side effects include resource checks, active-tool
543
- * mutation, status updates, live-ticker disposal on shutdown, stdout writes,
544
- * and optional child-process spawning.
563
+ * On `agent_end`, also dispatches configured pi-notify beep, sound, and
564
+ * prompt-specific Pushover effects when the current run originates from a
565
+ * bundled prompt command. Runtime is dominated by configuration loading during
566
+ * `session_start`; all other hooks are O(1). Side effects include resource
567
+ * checks, active-tool mutation, status updates, live-ticker disposal on
568
+ * shutdown, stdout writes, optional child-process spawning, and outbound
569
+ * HTTPS requests.
545
570
  * @param[in] pi {ExtensionAPI} Active extension API instance.
546
571
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
547
572
  * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
548
573
  * @param[in] event {unknown} Hook payload forwarded by pi.
549
574
  * @param[in] ctx {ExtensionContext} Active extension context.
550
575
  * @return {Promise<void>} Promise resolved when hook processing completes.
551
- * @satisfies REQ-117, REQ-118, REQ-119, REQ-129, REQ-130, REQ-131, REQ-132, REQ-133
576
+ * @satisfies REQ-117, REQ-118, REQ-119, REQ-129, REQ-130, REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172
552
577
  */
553
578
  async function handleExtensionStatusEvent(
554
579
  pi: ExtensionAPI,
@@ -564,8 +589,24 @@ async function handleExtensionStatusEvent(
564
589
  setPiUsereqStatusConfig(statusController, config);
565
590
  }
566
591
  updateExtensionStatus(statusController, hookName, event, ctx);
567
- if (hookName === "agent_end" && statusController.config) {
568
- runPiNotifyEffects(statusController.config, event as { messages: AgentEndEvent["messages"] });
592
+ if (hookName === "agent_end") {
593
+ if (statusController.config) {
594
+ const pushoverRequest: PiNotifyPushoverRequest | undefined = statusController.state.activePromptRequest
595
+ && statusController.state.lastRunDurationMs !== undefined
596
+ ? {
597
+ promptName: statusController.state.activePromptRequest.promptName,
598
+ promptArgs: statusController.state.activePromptRequest.promptArgs,
599
+ basePath: path.resolve(ctx.cwd),
600
+ completionTimeMs: statusController.state.lastRunDurationMs,
601
+ }
602
+ : undefined;
603
+ runPiNotifyEffects(
604
+ statusController.config,
605
+ event as { messages: AgentEndEvent["messages"] },
606
+ pushoverRequest,
607
+ );
608
+ }
609
+ statusController.state.activePromptRequest = undefined;
569
610
  }
570
611
  if (hookName === "session_shutdown") {
571
612
  disposePiUsereqStatusController(statusController);
@@ -671,12 +712,162 @@ function togglePiNotifyBeepFlag(config: UseReqConfig, key: PiNotifyBeepConfigKey
671
712
  return config[key];
672
713
  }
673
714
 
715
+ /**
716
+ * @brief Formats one persisted Pushover priority for menu display.
717
+ * @details Maps the canonical `0|1` priority domain to deterministic menu text reused by the Pushover configuration UI. Runtime is O(1). No external state is mutated.
718
+ * @param[in] priority {PiNotifyPushoverPriority} Persisted Pushover priority.
719
+ * @return {string} Menu-display label.
720
+ * @satisfies REQ-165
721
+ */
722
+ function formatPiNotifyPushoverPriority(priority: PiNotifyPushoverPriority): string {
723
+ return priority === 1 ? "1=High Priority" : "0=Normal";
724
+ }
725
+
726
+ /**
727
+ * @brief Builds the shared settings-menu choices for Pushover configuration.
728
+ * @details Serializes the Pushover global-disable flag, successful-completion enable flag, credential strings, and priority value into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(1). No external state is mutated.
729
+ * @param[in] config {UseReqConfig} Effective project configuration.
730
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered Pushover-menu choice vector.
731
+ * @satisfies REQ-163, REQ-165, REQ-166, REQ-172
732
+ */
733
+ function buildPiNotifyPushoverMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
734
+ return [
735
+ {
736
+ id: "pushover-global-disable",
737
+ label: "Global disable",
738
+ value: config["notify-pushover-global-disable"] ? "on" : "off",
739
+ description: "Suppress all Pushover delivery without changing the successful-prompt enable flag.",
740
+ },
741
+ {
742
+ id: "pushover-on-success",
743
+ label: "Notify on success",
744
+ value: formatPiNotifyPushoverStatus(config),
745
+ description: "Toggle Pushover delivery after successful prompt completion only.",
746
+ },
747
+ {
748
+ id: "pushover-user-key",
749
+ label: "User Key/Delivery Group Key",
750
+ value: config["notify-pushover-user-key"] || "(empty)",
751
+ description: "Edit the Pushover user key or delivery group key sent with successful prompt notifications.",
752
+ },
753
+ {
754
+ id: "pushover-api-token",
755
+ label: "Token/API Token Key",
756
+ value: config["notify-pushover-api-token"] || "(empty)",
757
+ description: "Edit the Pushover application token used for successful prompt notifications.",
758
+ },
759
+ {
760
+ id: "pushover-priority",
761
+ label: "Priority",
762
+ value: formatPiNotifyPushoverPriority(config["notify-pushover-priority"]),
763
+ description: "Select whether successful prompt notifications use normal or high Pushover priority.",
764
+ },
765
+ {
766
+ id: "back",
767
+ label: "Back",
768
+ value: "",
769
+ description: "Return to the notifications menu.",
770
+ },
771
+ ];
772
+ }
773
+
774
+ /**
775
+ * @brief Opens the shared settings-menu selector for Pushover priority.
776
+ * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the existing notification menus and returns the chosen priority or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
777
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
778
+ * @param[in] currentPriority {PiNotifyPushoverPriority} Persisted priority value.
779
+ * @return {Promise<PiNotifyPushoverPriority | undefined>} Selected priority or `undefined` when cancelled.
780
+ * @satisfies REQ-165
781
+ */
782
+ async function selectPiNotifyPushoverPriority(
783
+ ctx: ExtensionCommandContext,
784
+ currentPriority: PiNotifyPushoverPriority,
785
+ ): Promise<PiNotifyPushoverPriority | undefined> {
786
+ const choice = await showPiUsereqSettingsMenu(ctx, "Pushover priority", [
787
+ {
788
+ id: "0",
789
+ label: "0=Normal",
790
+ value: currentPriority === 0 ? "selected" : "",
791
+ description: "Send successful prompt notifications with normal Pushover priority.",
792
+ },
793
+ {
794
+ id: "1",
795
+ label: "1=High Priority",
796
+ value: currentPriority === 1 ? "selected" : "",
797
+ description: "Send successful prompt notifications with high Pushover priority.",
798
+ },
799
+ ]);
800
+ if (!choice) {
801
+ return undefined;
802
+ }
803
+ return normalizePiNotifyPushoverPriority(choice);
804
+ }
805
+
806
+ /**
807
+ * @brief Runs the interactive Pushover-configuration menu.
808
+ * @details Exposes the Pushover global-disable flag, successful-completion enable flag, user key, API token, and priority selector through the shared settings-menu renderer. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
809
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
810
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
811
+ * @return {Promise<void>} Promise resolved when the menu closes.
812
+ * @satisfies REQ-163, REQ-165, REQ-166, REQ-172
813
+ */
814
+ async function configurePiNotifyPushoverMenu(
815
+ ctx: ExtensionCommandContext,
816
+ config: UseReqConfig,
817
+ ): Promise<void> {
818
+ while (true) {
819
+ const choice = await showPiUsereqSettingsMenu(ctx, "Pushover notifications", buildPiNotifyPushoverMenuChoices(config));
820
+ if (!choice || choice === "back") {
821
+ return;
822
+ }
823
+ if (choice === "pushover-global-disable") {
824
+ config["notify-pushover-global-disable"] = !config["notify-pushover-global-disable"];
825
+ ctx.ui.notify(`Pushover global disable ${config["notify-pushover-global-disable"] ? "enabled" : "disabled"}`, "info");
826
+ continue;
827
+ }
828
+ if (choice === "pushover-on-success") {
829
+ config["notify-pushover-on-success"] = !config["notify-pushover-on-success"];
830
+ ctx.ui.notify(`Pushover on success ${config["notify-pushover-on-success"] ? "enabled" : "disabled"}`, "info");
831
+ continue;
832
+ }
833
+ if (choice === "pushover-user-key") {
834
+ const value = await ctx.ui.input(
835
+ "User Key/Delivery Group Key",
836
+ config["notify-pushover-user-key"] || "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
837
+ );
838
+ if (value !== undefined) {
839
+ config["notify-pushover-user-key"] = normalizePiNotifyPushoverCredential(value);
840
+ ctx.ui.notify("Updated Pushover user key", "info");
841
+ }
842
+ continue;
843
+ }
844
+ if (choice === "pushover-api-token") {
845
+ const value = await ctx.ui.input(
846
+ "Token/API Token Key",
847
+ config["notify-pushover-api-token"] || "ah6bf5u2sj63mcvou6qamiabeoubbe",
848
+ );
849
+ if (value !== undefined) {
850
+ config["notify-pushover-api-token"] = normalizePiNotifyPushoverCredential(value);
851
+ ctx.ui.notify("Updated Pushover API token", "info");
852
+ }
853
+ continue;
854
+ }
855
+ if (choice === "pushover-priority") {
856
+ const nextPriority = await selectPiNotifyPushoverPriority(ctx, config["notify-pushover-priority"]);
857
+ if (nextPriority !== undefined) {
858
+ config["notify-pushover-priority"] = nextPriority;
859
+ ctx.ui.notify(`Pushover priority set to ${formatPiNotifyPushoverPriority(nextPriority)}`, "info");
860
+ }
861
+ }
862
+ }
863
+ }
864
+
674
865
  /**
675
866
  * @brief Builds the shared settings-menu choices for notification configuration.
676
- * @details Serializes the current beep flags, selected notify command, hotkey bind, and per-level notify commands into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(1) plus command-length formatting. No external state is mutated.
867
+ * @details Serializes the current beep flags, selected notify command, hotkey bind, per-level notify commands, and the Pushover submenu entry into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(1) plus command-length formatting. No external state is mutated.
677
868
  * @param[in] config {UseReqConfig} Effective project configuration.
678
869
  * @return {PiUsereqSettingsMenuChoice[]} Ordered notification-menu choice vector.
679
- * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152
870
+ * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-163, REQ-164, REQ-165, REQ-166, REQ-172
680
871
  */
681
872
  function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
682
873
  return [
@@ -728,6 +919,12 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
728
919
  value: config.PI_NOTIFY_SOUND_HIGH_CMD,
729
920
  description: "Edit the shell command used when the selected notify command is `high`.",
730
921
  },
922
+ {
923
+ id: "pushover-notifications",
924
+ label: "Pushover notifications",
925
+ value: formatPiNotifyPushoverStatus(config),
926
+ description: "Open the Pushover submenu for successful-completion delivery, credentials, priority, and global disable.",
927
+ },
731
928
  {
732
929
  id: "back",
733
930
  label: "Back",
@@ -780,11 +977,11 @@ async function selectPiNotifySoundLevel(
780
977
 
781
978
  /**
782
979
  * @brief Runs the interactive notification-configuration menu.
783
- * @details Exposes prompt-end beep toggles, selected notify-command selection, hotkey-bind editing, and per-level notify-command editors through the shared settings-menu renderer. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
980
+ * @details Exposes prompt-end beep toggles, selected notify-command selection, hotkey-bind editing, per-level notify-command editors, and the nested Pushover submenu through the shared settings-menu renderer. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
784
981
  * @param[in] ctx {ExtensionCommandContext} Active command context.
785
982
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
786
983
  * @return {Promise<boolean>} `true` when the sound-toggle shortcut changed.
787
- * @satisfies REQ-129, REQ-131, REQ-133, REQ-134, REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154
984
+ * @satisfies REQ-129, REQ-131, REQ-133, REQ-134, REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-163, REQ-164, REQ-165, REQ-166, REQ-172
788
985
  */
789
986
  async function configurePiNotifyMenu(
790
987
  ctx: ExtensionCommandContext,
@@ -849,6 +1046,10 @@ async function configurePiNotifyMenu(
849
1046
  config.PI_NOTIFY_SOUND_HIGH_CMD = value.trim();
850
1047
  ctx.ui.notify("Updated notify command (high vol.)", "info");
851
1048
  }
1049
+ continue;
1050
+ }
1051
+ if (choice === "pushover-notifications") {
1052
+ await configurePiNotifyPushoverMenu(ctx, config);
852
1053
  }
853
1054
  }
854
1055
  }
@@ -890,12 +1091,16 @@ function registerPiNotifyShortcut(
890
1091
 
891
1092
  /**
892
1093
  * @brief Registers bundled prompt commands with the extension.
893
- * @details Creates one `req-<prompt>` command per bundled prompt name. Each handler ensures resources exist, renders the prompt, and sends it into the current active session. Runtime is O(p) for registration; handler cost depends on prompt rendering plus prompt dispatch. Side effects include command registration and user-message delivery during execution.
1094
+ * @details Creates one `req-<prompt>` command per bundled prompt name. Each handler ensures resources exist, records the prompt metadata needed for successful completion notifications, renders the prompt, and sends it into the current active session. Runtime is O(p) for registration; handler cost depends on prompt rendering plus prompt dispatch. Side effects include command registration, status-controller mutation, and user-message delivery during execution.
894
1095
  * @param[in] pi {ExtensionAPI} Active extension API instance.
1096
+ * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
895
1097
  * @return {void} No return value.
896
- * @satisfies REQ-004, REQ-067, REQ-068
1098
+ * @satisfies REQ-004, REQ-067, REQ-068, REQ-169
897
1099
  */
898
- function registerPromptCommands(pi: ExtensionAPI): void {
1100
+ function registerPromptCommands(
1101
+ pi: ExtensionAPI,
1102
+ statusController: PiUsereqStatusController,
1103
+ ): void {
899
1104
  PROMPT_NAMES.forEach((promptName) => {
900
1105
  pi.registerCommand(`req-${promptName}`, {
901
1106
  description: `Run pi-usereq prompt ${promptName}`,
@@ -903,6 +1108,10 @@ function registerPromptCommands(pi: ExtensionAPI): void {
903
1108
  ensureBundledResourcesAccessible();
904
1109
  const projectBase = getProjectBase(ctx.cwd);
905
1110
  const config = loadProjectConfig(ctx.cwd);
1111
+ statusController.state.pendingPromptRequest = {
1112
+ promptName,
1113
+ promptArgs: args,
1114
+ };
906
1115
  const content = renderPrompt(promptName, args, projectBase, config);
907
1116
  await deliverPromptCommand(pi, content);
908
1117
  },
@@ -1721,7 +1930,7 @@ function formatStaticCheckLanguagesSummary(config: UseReqConfig): string {
1721
1930
 
1722
1931
  /**
1723
1932
  * @brief Renders the static-check configuration reference view.
1724
- * @details Produces a markdown-like summary containing configured entries, supported languages, supported modules, and example specifications. Runtime is O(l log l). No side effects occur.
1933
+ * @details Produces a markdown-like summary containing configured entries, supported languages, the Command-only user module surface, and canonical example specifications. Runtime is O(l log l). No side effects occur.
1725
1934
  * @param[in] config {UseReqConfig} Effective project configuration.
1726
1935
  * @return {string} Reference text for the editor view.
1727
1936
  */
@@ -1744,16 +1953,23 @@ function renderStaticCheckReference(config: UseReqConfig): string {
1744
1953
  for (const { language, extensions } of getSupportedStaticCheckLanguageSupport()) {
1745
1954
  lines.push(`- ${language}: ${extensions.join(", ")}`);
1746
1955
  }
1747
- lines.push("", `Supported modules: ${STATIC_CHECK_MODULES.join(", ")}`, "", "Examples:", "- Python=Ruff", "- Python=Command,mypy,--strict", "- TypeScript=Command,eslint,--max-warnings,0");
1956
+ lines.push(
1957
+ "",
1958
+ `Supported modules: ${STATIC_CHECK_MODULES.join(", ")}`,
1959
+ "",
1960
+ "Examples:",
1961
+ "- Python=Command,mypy,--strict",
1962
+ "- TypeScript=Command,eslint,--max-warnings,0",
1963
+ );
1748
1964
  return `${lines.join("\n")}\n`;
1749
1965
  }
1750
1966
 
1751
1967
  /**
1752
1968
  * @brief Builds the shared settings-menu choices for static-check management.
1753
- * @details Serializes static-check actions into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(1). No external state is mutated.
1969
+ * @details Serializes Command-oriented static-check actions into right-valued menu rows consumed by the shared settings-menu renderer while omitting user-facing module selection. Runtime is O(1). No external state is mutated.
1754
1970
  * @param[in] config {UseReqConfig} Effective project configuration.
1755
1971
  * @return {PiUsereqSettingsMenuChoice[]} Ordered static-check menu choices.
1756
- * @satisfies REQ-008, REQ-151, REQ-152, REQ-153, REQ-154
1972
+ * @satisfies REQ-008, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154
1757
1973
  */
1758
1974
  function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1759
1975
  const supportedLanguageCount = getSupportedStaticCheckLanguageSupport().length;
@@ -1763,13 +1979,13 @@ function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenu
1763
1979
  id: "add-entry-supported-language",
1764
1980
  label: "Add entry for supported language",
1765
1981
  value: `${supportedLanguageCount} languages`,
1766
- description: "Select a supported language, then select one static-check module to add.",
1982
+ description: "Select a supported language, then configure the Command static-check executable.",
1767
1983
  },
1768
1984
  {
1769
1985
  id: "add-entry-raw-spec",
1770
1986
  label: "Add entry from LANG=MODULE[,CMD[,PARAM...]]",
1771
1987
  value: "raw spec",
1772
- description: "Enter one raw static-check specification string in canonical CLI format.",
1988
+ description: "Enter one raw Command-based static-check specification string in canonical CLI format.",
1773
1989
  },
1774
1990
  {
1775
1991
  id: "remove-language-entry",
@@ -1794,7 +2010,7 @@ function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenu
1794
2010
 
1795
2011
  /**
1796
2012
  * @brief Builds the shared settings-menu choices for supported static-check languages.
1797
- * @details Exposes every supported language as one row whose right-side value reports extensions plus the current configured checker count. Runtime is O(l log l). No external state is mutated.
2013
+ * @details Exposes every supported language as one row whose right-side value reports extensions plus the current configured checker count for Command-oriented configuration flows. Runtime is O(l log l). No external state is mutated.
1798
2014
  * @param[in] config {UseReqConfig} Effective project configuration.
1799
2015
  * @return {PiUsereqSettingsMenuChoice[]} Ordered language-choice vector.
1800
2016
  */
@@ -1807,7 +2023,7 @@ function buildSupportedStaticCheckLanguageChoices(config: UseReqConfig): PiUsere
1807
2023
  id: language,
1808
2024
  label: language,
1809
2025
  value: `${extensions.join(", ")} • ${configuredCount} ${suffix}`,
1810
- description: `Configure static-check modules for ${language}. Supported extensions: ${extensions.join(", ")}.`,
2026
+ description: `Configure the Command static-check entry for ${language}. Supported extensions: ${extensions.join(", ")}.`,
1811
2027
  };
1812
2028
  }),
1813
2029
  {
@@ -1819,31 +2035,6 @@ function buildSupportedStaticCheckLanguageChoices(config: UseReqConfig): PiUsere
1819
2035
  ];
1820
2036
  }
1821
2037
 
1822
- /**
1823
- * @brief Builds the shared settings-menu choices for static-check modules.
1824
- * @details Exposes every supported static-check module as one selectable row with a concise execution description. Runtime is O(m) in module count. No external state is mutated.
1825
- * @param[in] language {string} Canonical selected language.
1826
- * @return {PiUsereqSettingsMenuChoice[]} Ordered module-choice vector.
1827
- */
1828
- function buildStaticCheckModuleChoices(language: string): PiUsereqSettingsMenuChoice[] {
1829
- return [
1830
- ...STATIC_CHECK_MODULES.map((moduleName) => ({
1831
- id: moduleName,
1832
- label: moduleName,
1833
- value: language,
1834
- description: moduleName === "Command"
1835
- ? `Run one explicit external command against ${language} files.`
1836
- : `Run the built-in ${moduleName} checker for ${language} files.`,
1837
- })),
1838
- {
1839
- id: "back",
1840
- label: "Back",
1841
- value: "",
1842
- description: "Return to language selection.",
1843
- },
1844
- ];
1845
- }
1846
-
1847
2038
  /**
1848
2039
  * @brief Builds the shared settings-menu choices for configured static-check languages.
1849
2040
  * @details Exposes only languages that currently have at least one configured checker so removal remains deterministic. Runtime is O(l log l). No external state is mutated.
@@ -1871,11 +2062,11 @@ function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUser
1871
2062
 
1872
2063
  /**
1873
2064
  * @brief Runs the interactive static-check configuration menu.
1874
- * @details Lets the user inspect support, add entries by guided prompts or raw spec strings, and remove configured language entries through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
2065
+ * @details Lets the user inspect support, add Command entries by guided prompts or raw spec strings, and remove configured language entries through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
1875
2066
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1876
2067
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
1877
2068
  * @return {Promise<void>} Promise resolved when the menu closes.
1878
- * @satisfies REQ-008, REQ-151, REQ-152, REQ-153, REQ-154
2069
+ * @satisfies REQ-008, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154
1879
2070
  */
1880
2071
  async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
1881
2072
  while (true) {
@@ -1895,23 +2086,16 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
1895
2086
  if (!selectedLanguage || selectedLanguage === "back") {
1896
2087
  continue;
1897
2088
  }
1898
- const moduleName = await showPiUsereqSettingsMenu(ctx, `static-check for ${selectedLanguage}`, buildStaticCheckModuleChoices(selectedLanguage));
1899
- if (!moduleName || moduleName === "back") {
1900
- continue;
1901
- }
1902
2089
 
1903
- const entry: StaticCheckEntry = { module: moduleName };
1904
- if (moduleName === "Command") {
1905
- const cmd = await ctx.ui.input(`Command executable for ${selectedLanguage}`, "");
1906
- if (!cmd?.trim()) {
1907
- ctx.ui.notify(`Command executable is required for ${selectedLanguage}`, "error");
1908
- continue;
1909
- }
1910
- entry.cmd = cmd.trim();
2090
+ const cmd = await ctx.ui.input(`Command executable for ${selectedLanguage}`, "");
2091
+ if (!cmd?.trim()) {
2092
+ ctx.ui.notify(`Command executable is required for ${selectedLanguage}`, "error");
2093
+ continue;
1911
2094
  }
1912
2095
 
2096
+ const entry: StaticCheckEntry = { module: "Command", cmd: cmd.trim() };
1913
2097
  const paramsInput = await ctx.ui.input(
1914
- `Additional parameters for ${moduleName} on ${selectedLanguage} (optional, shell-style)`,
2098
+ `Additional parameters for Command on ${selectedLanguage} (optional, shell-style)`,
1915
2099
  "",
1916
2100
  );
1917
2101
  const params = paramsInput?.trim() ? shellSplit(paramsInput.trim()) : [];
@@ -1926,7 +2110,7 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
1926
2110
  }
1927
2111
 
1928
2112
  if (staticChoice === "add-entry-raw-spec") {
1929
- const spec = await ctx.ui.input("Static-check spec", "Python=Ruff");
2113
+ const spec = await ctx.ui.input("Static-check spec", "Python=Command,true");
1930
2114
  if (!spec?.trim()) {
1931
2115
  continue;
1932
2116
  }
@@ -1954,12 +2138,16 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
1954
2138
 
1955
2139
  /**
1956
2140
  * @brief Builds the shared settings-menu choices for the top-level pi-usereq configuration UI.
1957
- * @details Serializes primary configuration actions into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(s) in source-directory count. No external state is mutated.
2141
+ * @details Serializes primary configuration actions into right-valued menu rows consumed by the shared settings-menu renderer, including the display-only config path beside `show-config`. Runtime is O(s) in source-directory count. No external state is mutated.
2142
+ * @param[in] cwd {string} Current working directory.
1958
2143
  * @param[in] config {UseReqConfig} Effective project configuration.
1959
2144
  * @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
1960
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152
2145
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162
1961
2146
  */
1962
- function buildPiUsereqMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2147
+ function buildPiUsereqMenuChoices(
2148
+ cwd: string,
2149
+ config: UseReqConfig,
2150
+ ): PiUsereqSettingsMenuChoice[] {
1963
2151
  return [
1964
2152
  {
1965
2153
  id: "docs-dir",
@@ -1995,7 +2183,14 @@ function buildPiUsereqMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
1995
2183
  id: "notifications",
1996
2184
  label: "notifications",
1997
2185
  value: `beep:${formatPiNotifyBeepStatus(config)} • sound:${config["notify-sound"]}`,
1998
- description: "Manage terminal beep flags, selected notify command, hotkey bind, and per-level notify commands.",
2186
+ description: "Manage terminal beep flags, selected notify command, hotkey bind, per-level notify commands, and the nested Pushover submenu.",
2187
+ },
2188
+ {
2189
+ id: "show-config",
2190
+ label: "show-config",
2191
+ value: formatProjectConfigPathForMenu(cwd),
2192
+ valueTone: "dim",
2193
+ description: "Write the current project configuration JSON into the editor without saving additional changes.",
1999
2194
  },
2000
2195
  {
2001
2196
  id: "reset-defaults",
@@ -2003,12 +2198,6 @@ function buildPiUsereqMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2003
2198
  value: "",
2004
2199
  description: "Restore the default pi-usereq configuration for the current project base.",
2005
2200
  },
2006
- {
2007
- id: "show-config",
2008
- label: "show-config",
2009
- value: "",
2010
- description: "Write the current project configuration JSON into the editor without saving additional changes.",
2011
- },
2012
2201
  {
2013
2202
  id: "save-and-close",
2014
2203
  label: "Save and close",
@@ -2074,12 +2263,12 @@ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuCh
2074
2263
 
2075
2264
  /**
2076
2265
  * @brief Runs the top-level pi-usereq configuration menu.
2077
- * @details Loads project config, exposes docs/test/source/static-check/startup-tool/notification actions through the shared settings-menu renderer, persists changes on exit, and refreshes the single-line status bar. Runtime depends on user interaction count. Side effects include UI updates, config writes, and active-tool changes.
2266
+ * @details Loads project config, exposes docs/test/source/static-check/startup-tool/notification actions through the shared settings-menu renderer, persists changes on exit, and refreshes the single-line status bar. Runtime depends on user interaction count. Side effects include UI updates, config writes, active-tool changes, and editor text updates.
2078
2267
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2079
2268
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2080
2269
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2081
2270
  * @return {Promise<void>} Promise resolved when configuration is saved and the menu closes.
2082
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154
2271
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162
2083
2272
  */
2084
2273
  async function configurePiUsereq(
2085
2274
  pi: ExtensionAPI,
@@ -2096,7 +2285,11 @@ async function configurePiUsereq(
2096
2285
  };
2097
2286
 
2098
2287
  while (true) {
2099
- const choice = await showPiUsereqSettingsMenu(ctx, "pi-usereq", buildPiUsereqMenuChoices(config));
2288
+ const choice = await showPiUsereqSettingsMenu(
2289
+ ctx,
2290
+ "pi-usereq",
2291
+ buildPiUsereqMenuChoices(ctx.cwd, config),
2292
+ );
2100
2293
  if (!choice || choice === "save-and-close") {
2101
2294
  ensureSaved();
2102
2295
  refreshStatus();
@@ -2190,18 +2383,20 @@ function registerConfigCommands(
2190
2383
  * configuration commands plus agent tools, registers the configurable
2191
2384
  * successful-run sound shortcut when the runtime supports shortcuts, and
2192
2385
  * installs shared wrappers for all supported pi lifecycle hooks so status
2193
- * telemetry, context usage, prompt timing, and pi-notify effects remain
2194
- * synchronized with runtime events. Runtime is O(h) in hook count during
2195
- * registration. Side effects include filesystem reads, command/tool/shortcut
2196
- * registration, UI updates, active-tool changes, and timer scheduling.
2386
+ * telemetry, context usage, prompt timing, cumulative runtime, prompt-specific
2387
+ * Pushover metadata, and pi-notify effects remain synchronized with runtime
2388
+ * events. Runtime is O(h) in hook
2389
+ * count during registration. Side effects include filesystem reads,
2390
+ * command/tool/shortcut registration, UI updates, active-tool changes, and
2391
+ * timer scheduling.
2197
2392
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2198
2393
  * @return {void} No return value.
2199
- * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-045, REQ-067, REQ-068, REQ-109, REQ-110, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-129, REQ-130, REQ-131, REQ-132, REQ-133, REQ-134, REQ-135, REQ-136, REQ-137
2394
+ * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-045, REQ-067, REQ-068, REQ-109, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-129, REQ-130, REQ-131, REQ-132, REQ-133, REQ-134, REQ-135, REQ-136, REQ-137, REQ-148, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-170, REQ-171, REQ-172
2200
2395
  */
2201
2396
  export default function piUsereqExtension(pi: ExtensionAPI): void {
2202
- const statusController = createPiUsereqStatusController(() => pi.getActiveTools());
2397
+ const statusController = createPiUsereqStatusController();
2203
2398
  ensureBundledResourcesAccessible();
2204
- registerPromptCommands(pi);
2399
+ registerPromptCommands(pi, statusController);
2205
2400
  registerAgentTools(pi);
2206
2401
  registerConfigCommands(pi, statusController);
2207
2402
  registerPiNotifyShortcut(pi, statusController);
@@ -357,16 +357,6 @@ function buildStandaloneScenarios(): AttendedScenario[] {
357
357
  path.posix.join("standalone", "test-static-check-dummy", `${fixtureName}.json`),
358
358
  ["--test-static-check", "dummy", relativeFixturePath],
359
359
  ),
360
- createStandaloneScenario(
361
- `standalone/test-static-check-pylance/${fixtureName}`,
362
- path.posix.join("standalone", "test-static-check-pylance", `${fixtureName}.json`),
363
- ["--test-static-check", "pylance", relativeFixturePath],
364
- ),
365
- createStandaloneScenario(
366
- `standalone/test-static-check-ruff/${fixtureName}`,
367
- path.posix.join("standalone", "test-static-check-ruff", `${fixtureName}.json`),
368
- ["--test-static-check", "ruff", relativeFixturePath],
369
- ),
370
360
  createStandaloneScenario(
371
361
  `standalone/test-static-check-command/${fixtureName}`,
372
362
  path.posix.join("standalone", "test-static-check-command", `${fixtureName}.json`),
@@ -470,14 +460,14 @@ function buildProjectScenarios(): AttendedScenario[] {
470
460
  "--provider",
471
461
  "claude:prompts",
472
462
  "--enable-static-check",
473
- "Python=Pylance",
463
+ "Python=Command,git,--version",
474
464
  ],
475
465
  cwd: projectBase,
476
466
  normalize: createProjectNormalizer(projectBase),
477
467
  cleanup: () => removePath(projectBase),
478
468
  postAssert: () => {
479
469
  const payload = JSON.parse(fs.readFileSync(getProjectConfigPath(projectBase), "utf8")) as UseReqConfig;
480
- assert.deepEqual(payload["static-check"].Python, [{ module: "Pylance" }]);
470
+ assert.deepEqual(payload["static-check"].Python, [{ module: "Command", cmd: "git", params: ["--version"] }]);
481
471
  },
482
472
  };
483
473
  },
@@ -488,7 +478,7 @@ function buildProjectScenarios(): AttendedScenario[] {
488
478
  const script = [
489
479
  "from pathlib import Path",
490
480
  "from usereq.cli import save_config",
491
- `save_config(Path(${JSON.stringify(projectBase)}), 'guidelines', 'docs', 'tests', ['src'], static_check_config={'Python': [{'module': 'Pylance'}]})`,
481
+ `save_config(Path(${JSON.stringify(projectBase)}), 'guidelines', 'docs', 'tests', ['src'], static_check_config={'Python': [{'module': 'Command', 'cmd': 'git', 'params': ['--version']} ]})`,
492
482
  ].join("\n");
493
483
  const result = runPythonInline(script, projectBase);
494
484
  return createProjectNormalizer(projectBase)(toArchivedCliResult(result));