pi-usereq 0.4.0 → 0.6.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 (65) hide show
  1. package/.gitignore +3 -0
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +1 -1
  4. package/package.json +1 -1
  5. package/{req → pi-usereq}/docs/REFERENCES.md +499 -491
  6. package/{req → pi-usereq}/docs/REQUIREMENTS.md +84 -62
  7. package/{req → pi-usereq}/docs/WORKFLOW.md +87 -102
  8. package/src/core/agent-tool-json.ts +42 -193
  9. package/src/core/compress-payload.ts +7 -15
  10. package/src/core/config.ts +39 -12
  11. package/src/core/extension-status.ts +94 -58
  12. package/src/core/find-payload.ts +21 -44
  13. package/src/core/pi-notify.ts +198 -11
  14. package/src/core/reference-payload.ts +6 -14
  15. package/src/core/runtime-project-paths.ts +3 -32
  16. package/src/core/settings-menu.ts +9 -4
  17. package/src/core/static-check.ts +35 -171
  18. package/src/core/token-counter.ts +3 -121
  19. package/src/index.ts +339 -180
  20. package/tests/attended-results-scenarios.ts +3 -13
  21. package/tests/cli-command-option-parity.test.ts +21 -12
  22. package/tests/debug-extension-harness.test.ts +20 -22
  23. package/tests/extension-registration.test.ts +394 -157
  24. package/tests/oracle-project.test.ts +8 -3
  25. package/tests/oracle-standalone.test.ts +7 -13
  26. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_c.c.json +0 -5
  27. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_cpp.cpp.json +0 -5
  28. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_csharp.cs.json +0 -5
  29. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_elixir.ex.json +0 -5
  30. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_go.go.json +0 -5
  31. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_haskell.hs.json +0 -5
  32. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_java.java.json +0 -5
  33. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_javascript.js.json +0 -5
  34. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_kotlin.kt.json +0 -5
  35. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_lua.lua.json +0 -5
  36. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_perl.pl.json +0 -5
  37. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_php.php.json +0 -5
  38. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_python.py.json +0 -5
  39. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_ruby.rb.json +0 -5
  40. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_rust.rs.json +0 -5
  41. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_scala.scala.json +0 -5
  42. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_shell.sh.json +0 -5
  43. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_swift.swift.json +0 -5
  44. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_typescript.ts.json +0 -5
  45. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_zig.zig.json +0 -5
  46. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_c.c.json +0 -5
  47. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_cpp.cpp.json +0 -5
  48. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_csharp.cs.json +0 -5
  49. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_elixir.ex.json +0 -5
  50. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_go.go.json +0 -5
  51. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_haskell.hs.json +0 -5
  52. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_java.java.json +0 -5
  53. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_javascript.js.json +0 -5
  54. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_kotlin.kt.json +0 -5
  55. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_lua.lua.json +0 -5
  56. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_perl.pl.json +0 -5
  57. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_php.php.json +0 -5
  58. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_python.py.json +0 -5
  59. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_ruby.rb.json +0 -5
  60. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_rust.rs.json +0 -5
  61. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_scala.scala.json +0 -5
  62. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_shell.sh.json +0 -5
  63. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_swift.swift.json +0 -5
  64. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_typescript.ts.json +0 -5
  65. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_zig.zig.json +0 -5
@@ -4,7 +4,13 @@ import { spawnSync } from "node:child_process";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import piUsereqExtension from "../src/index.ts";
7
- import { DEFAULT_DOCS_DIR, getProjectConfigPath } from "../src/core/config.js";
7
+ import {
8
+ DEFAULT_DOCS_DIR,
9
+ getDefaultConfig,
10
+ getProjectConfigPath,
11
+ } from "../src/core/config.js";
12
+ import { formatRuntimePathForDisplay } from "../src/core/path-context.js";
13
+ import { setPiNotifyHttpsRequestForTests } from "../src/core/pi-notify.js";
8
14
  import {
9
15
  PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES,
10
16
  PI_USEREQ_EMBEDDED_TOOL_NAMES,
@@ -284,9 +290,9 @@ function formatFakeThemeBackgroundFromForeground(color: string, text: string): s
284
290
 
285
291
  /**
286
292
  * @brief Builds the expected fake context-bar payload for assertions.
287
- * @details Resolves the empty-state `CLEAR` overlay for unavailable or
288
- * non-positive percent, resolves the high-water `FULL!` overlay for percent
289
- * above 90, and otherwise reconstructs the 5-cell bar. Runtime is O(1). No
293
+ * @details Resolves the empty-state `◀ CLEAR ▶ ` overlay for unavailable or
294
+ * non-positive percent, resolves the centered ` ◀ FULL ▶ ` overlay for percent
295
+ * above 90, and otherwise reconstructs the 10-cell bar. Runtime is O(1). No
290
296
  * external state is mutated.
291
297
  * @param[in] options {{ filledCells: number; percent?: number | null }} Expected context-bar facts.
292
298
  * @return {string} Encoded context-bar string.
@@ -298,16 +304,16 @@ function buildExpectedFakeContextBar(options: {
298
304
  if (options.percent === undefined || options.percent === null || options.percent <= 0) {
299
305
  return formatFakeThemeBackgroundFromForeground(
300
306
  "accent",
301
- formatFakeThemeForeground("warning", "CLEAR"),
307
+ formatFakeThemeForeground("warning", "◀ CLEAR ▶ "),
302
308
  );
303
309
  }
304
310
  if (options.percent > 90) {
305
311
  return formatFakeThemeBackgroundFromForeground(
306
312
  "warning",
307
- formatFakeThemeForeground("error", "FULL!"),
313
+ formatFakeThemeForeground("error", " ◀ FULL ▶ "),
308
314
  );
309
315
  }
310
- return Array.from({ length: 5 }, (_value, index) =>
316
+ return Array.from({ length: 10 }, (_value, index) =>
311
317
  formatFakeThemeBackgroundFromForeground(
312
318
  "accent",
313
319
  formatFakeThemeForeground(index < options.filledCells ? "warning" : "dim", "▓"),
@@ -317,23 +323,21 @@ function buildExpectedFakeContextBar(options: {
317
323
 
318
324
  /**
319
325
  * @brief Builds the expected fake pi-usereq status-bar string for assertions.
320
- * @details Reconstructs the field order, separators, context bar, and timing fields emitted by the extension using deterministic fake theme markers. Runtime is O(s) in source-path count. No external state is mutated.
321
- * @param[in] options {{ gitPath?: string; basePath?: string; docsDir: string; testsDir: string; srcDir: string[]; toolCount: number; contextFilledCells: number; contextPercent?: number | null; elapsed: string; last: string; beep?: string; sound?: string }} Expected status facts.
326
+ * @details Reconstructs the field order, separators, context bar, consolidated elapsed field, and Pushover enable field emitted by the extension using deterministic fake theme markers. Runtime is O(1). No external state is mutated.
327
+ * @param[in] options {{ basePath: string; contextFilledCells: number; contextPercent?: number | null; et: string; beep?: string; sound?: string; pushover?: string }} Expected status facts.
322
328
  * @return {string} Encoded status-bar string.
323
329
  */
324
330
  function buildExpectedFakeStatusText(options: {
325
- gitPath?: string;
326
- basePath?: string;
327
- docsDir: string;
328
- testsDir: string;
329
- srcDir: string[];
330
- toolCount: number;
331
+ basePath: string;
332
+ docsDir?: string;
333
+ testsDir?: string;
334
+ srcDir?: string[];
331
335
  contextFilledCells: number;
332
336
  contextPercent?: number | null;
333
- elapsed: string;
334
- last: string;
337
+ et: string;
335
338
  beep?: string;
336
339
  sound?: string;
340
+ pushover?: string;
337
341
  }): string {
338
342
  const buildField = (fieldName: string, value: string): string =>
339
343
  `${formatFakeThemeForeground("accent", `${fieldName}:`)}${formatFakeThemeForeground("warning", value)}`;
@@ -342,20 +346,27 @@ function buildExpectedFakeStatusText(options: {
342
346
  percent: options.contextPercent,
343
347
  });
344
348
  return [
345
- buildField("git", options.gitPath ?? ""),
346
- buildField("base", options.basePath ?? "."),
347
- buildField("docs", options.docsDir),
348
- buildField("tests", options.testsDir),
349
- buildField("src", options.srcDir.join(",")),
350
- buildField("tools", String(options.toolCount)),
349
+ buildField("base", options.basePath),
351
350
  `${formatFakeThemeForeground("accent", "context:")}${contextBar}`,
352
- buildField("elapsed", options.elapsed),
353
- buildField("last", options.last),
354
- buildField("beep", options.beep ?? "none"),
351
+ buildField("elapsed", options.et),
352
+ buildField("beep", options.beep ?? "end,esc,err"),
355
353
  buildField("sound", options.sound ?? "none"),
354
+ buildField("pushover", options.pushover ?? "off"),
356
355
  ].join(formatFakeThemeForeground("dim", " • "));
357
356
  }
358
357
 
358
+ /**
359
+ * @brief Formats one expected absolute base path for fake status assertions.
360
+ * @details Resolves the supplied cwd and normalizes path separators to `/` so
361
+ * fake status comparisons stay stable across operating systems. Runtime is O(p)
362
+ * in path length. No external state is mutated.
363
+ * @param[in] cwd {string} Runtime working directory.
364
+ * @return {string} Slash-normalized absolute base path.
365
+ */
366
+ function buildExpectedFakeBasePath(cwd: string): string {
367
+ return path.resolve(cwd).split(path.sep).join("/");
368
+ }
369
+
359
370
  /**
360
371
  * @brief Creates a fake extension command context with scripted UI interactions.
361
372
  * @details Materializes deterministic `select`, shared settings-menu `custom`, `input`, `notify`, `setStatus`, `setEditorText`, `waitForIdle`, `newSession`, `sessionManager`, `getContextUsage`, theme-color behaviors, and captured custom-menu render output backed by in-memory state so menu-oriented and prompt-command tests can assert side effects without a real UI. Optional `onNewSession` and theme overrides let tests emulate runtime replacement and strict token validation. Runtime is O(1) plus queued interaction count and delegated new-session setup cost. Side effects are limited to in-memory state mutation.
@@ -595,13 +606,13 @@ test("token tools register agent-oriented descriptions and schema details", () =
595
606
  assert.ok(filesTokens, "missing files-tokens tool");
596
607
  assert.ok(tokens, "missing tokens tool");
597
608
 
598
- assert.match(filesTokens.description ?? "", /LLM-oriented JSON payload/);
609
+ assert.match(filesTokens.description ?? "", /token-optimized JSON payload/);
599
610
  assert.ok(filesTokens.promptGuidelines?.some((line) => line.includes("Output contract:")));
600
- assert.match(String((filesTokens.parameters as { description?: string } | undefined)?.description ?? ""), /line ranges/);
611
+ assert.match(String((filesTokens.parameters as { description?: string } | undefined)?.description ?? ""), /token metrics/);
601
612
 
602
613
  assert.match(tokens.description ?? "", /canonical docs/);
603
- assert.ok(tokens.promptGuidelines?.some((line) => line.includes("docs_dir_path")));
604
- assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /canonical_doc_names/);
614
+ assert.ok(tokens.promptGuidelines?.some((line) => line.includes("registration metadata")));
615
+ assert.match(String((tokens.parameters as { description?: string } | undefined)?.description ?? ""), /token-optimized JSON shape/);
605
616
  });
606
617
 
607
618
  test("reference tools register agent-oriented descriptions and schema details", () => {
@@ -613,7 +624,7 @@ test("reference tools register agent-oriented descriptions and schema details",
613
624
  assert.ok(filesReferences, "missing files-references tool");
614
625
  assert.ok(references, "missing references tool");
615
626
 
616
- assert.match(filesReferences.description ?? "", /request, summary, repository, files, and execution sections/);
627
+ assert.match(filesReferences.description ?? "", /summary, repository, files, and execution sections/);
617
628
  assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Numeric contract:")));
618
629
  assert.ok(filesReferences.promptGuidelines?.some((line) => line.includes("Behavior contract:")));
619
630
  assert.match(String((filesReferences.parameters as { description?: string } | undefined)?.description ?? ""), /structured Doxygen fields/);
@@ -633,11 +644,11 @@ test("find tools register agent-oriented descriptions and schema details", () =>
633
644
  assert.ok(filesFind, "missing files-find tool");
634
645
  assert.ok(find, "missing find tool");
635
646
 
636
- assert.match(filesFind.description ?? "", /LLM-oriented JSON payload/);
647
+ assert.match(filesFind.description ?? "", /token-optimized JSON payload/);
637
648
  assert.ok(filesFind.promptGuidelines?.some((line) => line.includes("Regex rule:")));
638
649
  assert.ok(filesFind.promptGuidelines?.some((line) => line.includes("Supported tags [Typescript]:")));
639
650
  assert.ok(filesFind.promptGuidelines?.some((line) => line.includes("Supported tags [Python]:")));
640
- assert.match(String((filesFind.parameters as { description?: string } | undefined)?.description ?? ""), /supported_tags_by_language/);
651
+ assert.match(String((filesFind.parameters as { description?: string } | undefined)?.description ?? ""), /Static supported-tag matrices are documented in tool registration metadata/);
641
652
 
642
653
  assert.match(find.description ?? "", /configured project source directories/);
643
654
  assert.ok(find.promptGuidelines?.some((line) => line.includes("source_directory_paths")));
@@ -654,7 +665,7 @@ test("compression tools register agent-oriented descriptions and schema details"
654
665
  assert.ok(filesCompress, "missing files-compress tool");
655
666
  assert.ok(compress, "missing compress tool");
656
667
 
657
- assert.match(filesCompress.description ?? "", /request, summary, repository, files, and execution sections/);
668
+ assert.match(filesCompress.description ?? "", /summary, repository, files, and execution sections/);
658
669
  assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("Line-number behavior:")));
659
670
  assert.ok(filesCompress.promptGuidelines?.some((line) => line.includes("Behavior contract:")));
660
671
  assert.match(String((filesCompress.parameters as { description?: string } | undefined)?.description ?? ""), /structured compressed lines/);
@@ -665,7 +676,7 @@ test("compression tools register agent-oriented descriptions and schema details"
665
676
  assert.match(String((compress.parameters as { description?: string } | undefined)?.description ?? ""), /structured compressed lines/);
666
677
  });
667
678
 
668
- test("files-tokens returns structured source facts and separated guidance", async () => {
679
+ test("files-tokens returns token-optimized structured source facts", async () => {
669
680
  const { projectBase } = initFixtureRepo({
670
681
  fixtures: [],
671
682
  docs: {
@@ -682,10 +693,6 @@ test("files-tokens returns structured source facts and separated guidance", asyn
682
693
  }) as {
683
694
  content?: Array<{ type: string; text?: string }>;
684
695
  details?: {
685
- request: {
686
- base_dir_path: string;
687
- requested_input_paths: string[];
688
- };
689
696
  summary: {
690
697
  counted_file_count: number;
691
698
  skipped_file_count: number;
@@ -698,14 +705,8 @@ test("files-tokens returns structured source facts and separated guidance", asyn
698
705
  line_count: number;
699
706
  byte_count: number;
700
707
  primary_heading_text?: string;
708
+ error_message?: string;
701
709
  }>;
702
- guidance: {
703
- source_observations: {
704
- skipped_inputs: Array<{ input_path: string; canonical_path: string; reason: string }>;
705
- };
706
- derived_recommendations: Array<{ kind: string; ordered_paths: string[] }>;
707
- actionable_next_steps: Array<{ kind: string; ordered_paths: string[] }>;
708
- };
709
710
  execution: {
710
711
  stderr: string;
711
712
  };
@@ -714,8 +715,6 @@ test("files-tokens returns structured source facts and separated guidance", asyn
714
715
 
715
716
  const payload = result.details!;
716
717
  assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
717
- assert.equal(payload.request.base_dir_path, projectBase);
718
- assert.deepEqual(payload.request.requested_input_paths, [`${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`, `${DEFAULT_DOCS_DIR}/MISSING.md`]);
719
718
  assert.equal(payload.summary.counted_file_count, 1);
720
719
  assert.equal(payload.summary.skipped_file_count, 1);
721
720
  assert.equal(payload.files[0]?.canonical_path, `${DEFAULT_DOCS_DIR}/REQUIREMENTS.md`);
@@ -723,11 +722,8 @@ test("files-tokens returns structured source facts and separated guidance", asyn
723
722
  assert.equal(payload.files[0]?.end_line_number, payload.files[0]?.line_count);
724
723
  assert.ok((payload.files[0]?.byte_count ?? 0) > 0);
725
724
  assert.equal(payload.files[0]?.primary_heading_text, "Requirements");
726
- assert.deepEqual(payload.guidance.source_observations.skipped_inputs, [
727
- { input_path: `${DEFAULT_DOCS_DIR}/MISSING.md`, canonical_path: `${DEFAULT_DOCS_DIR}/MISSING.md`, reason: "not found" },
728
- ]);
729
- assert.equal(payload.guidance.derived_recommendations[0]?.kind, "prioritize_high_token_paths");
730
- assert.equal(payload.guidance.actionable_next_steps[0]?.kind, "read_top_token_paths_first");
725
+ assert.equal(payload.files[1]?.canonical_path, `${DEFAULT_DOCS_DIR}/MISSING.md`);
726
+ assert.equal(payload.files[1]?.error_message, "not found");
731
727
  assert.match(payload.execution.stderr, new RegExp(`skipped: ${DEFAULT_DOCS_DIR.replace("/", "\\/")}/MISSING\\.md: not found`));
732
728
  } finally {
733
729
  fs.rmSync(projectBase, { recursive: true, force: true });
@@ -760,9 +756,6 @@ export function buildSample(input: string): string {
760
756
  }) as {
761
757
  content?: Array<{ type: string; text?: string }>;
762
758
  details?: {
763
- request: {
764
- requested_input_paths: string[];
765
- };
766
759
  summary: {
767
760
  analyzed_file_count: number;
768
761
  skipped_file_count: number;
@@ -797,7 +790,6 @@ export function buildSample(input: string): string {
797
790
 
798
791
  const payload = result.details!;
799
792
  assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
800
- assert.deepEqual(payload.request.requested_input_paths, ["src/sample.ts", "src/missing.ts"]);
801
793
  assert.equal(payload.summary.analyzed_file_count, 1);
802
794
  assert.equal(payload.summary.skipped_file_count, 1);
803
795
  assert.deepEqual(payload.repository.file_canonical_paths, ["src/sample.ts"]);
@@ -901,10 +893,6 @@ export function buildSample(input: string): string {
901
893
  }) as {
902
894
  content?: Array<{ type: string; text?: string }>;
903
895
  details?: {
904
- request: {
905
- line_number_mode: string;
906
- requested_input_paths: string[];
907
- };
908
896
  summary: {
909
897
  compressed_file_count: number;
910
898
  skipped_file_count: number;
@@ -939,8 +927,6 @@ export function buildSample(input: string): string {
939
927
 
940
928
  const payload = result.details!;
941
929
  assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
942
- assert.equal(payload.request.line_number_mode, "enabled");
943
- assert.deepEqual(payload.request.requested_input_paths, ["src/sample.ts", "src/missing.ts"]);
944
930
  assert.equal(payload.summary.compressed_file_count, 1);
945
931
  assert.equal(payload.summary.skipped_file_count, 1);
946
932
  assert.equal(payload.summary.total_symbol_count, 1);
@@ -988,10 +974,6 @@ test("compress returns a structured repository-scoped compression payload", asyn
988
974
  }) as {
989
975
  content?: Array<{ type: string; text?: string }>;
990
976
  details?: {
991
- request: {
992
- scope: string;
993
- line_number_mode: string;
994
- };
995
977
  summary: {
996
978
  compressed_file_count: number;
997
979
  total_compressed_line_count: number;
@@ -1015,8 +997,6 @@ test("compress returns a structured repository-scoped compression payload", asyn
1015
997
 
1016
998
  const payload = result.details!;
1017
999
  assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
1018
- assert.equal(payload.request.scope, "configured-source-directories");
1019
- assert.equal(payload.request.line_number_mode, "disabled");
1020
1000
  assert.equal(payload.summary.compressed_file_count, 2);
1021
1001
  assert.ok((payload.summary.total_compressed_line_count ?? 0) >= 2);
1022
1002
  assert.deepEqual(payload.repository.source_directory_paths, ["src"]);
@@ -1060,12 +1040,6 @@ export function buildSample(input: string): string {
1060
1040
  }) as {
1061
1041
  content?: Array<{ type: string; text?: string }>;
1062
1042
  details?: {
1063
- request: {
1064
- tag_filter_values: string[];
1065
- tag_filter_status: string;
1066
- line_number_mode: string;
1067
- requested_input_paths: string[];
1068
- };
1069
1043
  summary: {
1070
1044
  search_status: string;
1071
1045
  matched_file_count: number;
@@ -1073,7 +1047,7 @@ export function buildSample(input: string): string {
1073
1047
  total_match_count: number;
1074
1048
  };
1075
1049
  repository: {
1076
- supported_tags_by_language: Record<string, string[]>;
1050
+ file_canonical_paths: string[];
1077
1051
  };
1078
1052
  files: Array<{
1079
1053
  canonical_path: string;
@@ -1106,15 +1080,11 @@ export function buildSample(input: string): string {
1106
1080
 
1107
1081
  const payload = result.details!;
1108
1082
  assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
1109
- assert.deepEqual(payload.request.tag_filter_values, ["FUNCTION"]);
1110
- assert.equal(payload.request.tag_filter_status, "valid");
1111
- assert.equal(payload.request.line_number_mode, "enabled");
1112
- assert.deepEqual(payload.request.requested_input_paths, ["src/sample.ts", "src/missing.ts"]);
1113
1083
  assert.equal(payload.summary.search_status, "matched");
1114
1084
  assert.equal(payload.summary.matched_file_count, 1);
1115
1085
  assert.equal(payload.summary.skipped_file_count, 1);
1116
1086
  assert.equal(payload.summary.total_match_count, 1);
1117
- assert.ok(payload.repository.supported_tags_by_language.typescript?.includes("FUNCTION"));
1087
+ assert.deepEqual(payload.repository.file_canonical_paths, ["src/sample.ts", "src/missing.ts"]);
1118
1088
  assert.deepEqual(payload.files.map((file) => file.status), ["matched", "skipped"]);
1119
1089
  assert.equal(payload.files[0]?.canonical_path, "src/sample.ts");
1120
1090
  assert.equal(payload.files[0]?.match_count, 1);
@@ -1171,11 +1141,6 @@ export function buildAlpha(): number {
1171
1141
  }) as {
1172
1142
  content?: Array<{ type: string; text?: string }>;
1173
1143
  details?: {
1174
- request: {
1175
- scope: string;
1176
- regex_status: string;
1177
- requested_file_count: number;
1178
- };
1179
1144
  summary: {
1180
1145
  search_status: string;
1181
1146
  matched_file_count: number;
@@ -1185,7 +1150,6 @@ export function buildAlpha(): number {
1185
1150
  repository: {
1186
1151
  source_directory_paths: string[];
1187
1152
  file_canonical_paths: string[];
1188
- supported_tags_by_language: Record<string, string[]>;
1189
1153
  };
1190
1154
  files: Array<{
1191
1155
  canonical_path: string;
@@ -1205,16 +1169,12 @@ export function buildAlpha(): number {
1205
1169
 
1206
1170
  const payload = result.details!;
1207
1171
  assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(payload)));
1208
- assert.equal(payload.request.scope, "configured-source-directories");
1209
- assert.equal(payload.request.regex_status, "valid");
1210
- assert.equal(payload.request.requested_file_count, 2);
1211
1172
  assert.equal(payload.summary.search_status, "matched");
1212
1173
  assert.equal(payload.summary.matched_file_count, 1);
1213
1174
  assert.equal(payload.summary.no_match_file_count, 1);
1214
1175
  assert.equal(payload.summary.total_match_count, 1);
1215
1176
  assert.deepEqual(payload.repository.source_directory_paths, ["src"]);
1216
1177
  assert.deepEqual(payload.repository.file_canonical_paths, ["src/alpha.ts", "src/nested/beta.ts"]);
1217
- assert.ok(payload.repository.supported_tags_by_language.typescript?.includes("FUNCTION"));
1218
1178
  assert.deepEqual(payload.files.map((file) => file.status), ["matched", "no_match"]);
1219
1179
  assert.equal(payload.files[0]?.matches[0]?.symbol_name, "buildAlpha");
1220
1180
  assert.match(payload.files[0]?.matches[0]?.code_lines[0]?.display_text ?? "", /^export function buildAlpha/);
@@ -1246,31 +1206,28 @@ test("path, static-check, git, docs, and worktree tools return structured JSON p
1246
1206
  content?: Array<{ text?: string }>;
1247
1207
  details?: {
1248
1208
  result: { path_value: string; path_present: boolean };
1249
- runtime_paths: { git_path: string };
1250
1209
  execution: { code: number };
1251
1210
  };
1252
1211
  };
1253
1212
  assert.deepEqual(JSON.parse(gitPathResult.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(gitPathResult.details)));
1254
- assert.equal(gitPathResult.details?.result.path_value, gitPathResult.details?.runtime_paths.git_path);
1213
+ assert.equal(gitPathResult.details?.result.path_value, projectBase);
1255
1214
  assert.equal(gitPathResult.details?.result.path_present, true);
1256
1215
  assert.equal(gitPathResult.details?.execution.code, 0);
1257
1216
 
1258
1217
  const basePathResult = await executeRegisteredTool(pi, "get-base-path", projectBase) as {
1259
1218
  details?: {
1260
1219
  result: { path_value: string };
1261
- runtime_paths: { base_path: string };
1262
1220
  execution: { code: number };
1263
1221
  };
1264
1222
  };
1265
- assert.equal(basePathResult.details?.result.path_value, basePathResult.details?.runtime_paths.base_path);
1223
+ assert.equal(basePathResult.details?.result.path_value, projectBase);
1266
1224
  assert.equal(basePathResult.details?.execution.code, 0);
1267
1225
 
1268
1226
  const gitCheckResult = await executeRegisteredTool(pi, "git-check", projectBase) as {
1269
- details?: { result: { status: string; worktree_status: string; head_status: string }; execution: { code: number } };
1227
+ details?: { result: { git_path_present: boolean; status: string }; execution: { code: number } };
1270
1228
  };
1229
+ assert.equal(gitCheckResult.details?.result.git_path_present, true);
1271
1230
  assert.equal(gitCheckResult.details?.result.status, "clean");
1272
- assert.equal(gitCheckResult.details?.result.worktree_status, "clean");
1273
- assert.equal(gitCheckResult.details?.result.head_status, "valid");
1274
1231
  assert.equal(gitCheckResult.details?.execution.code, 0);
1275
1232
 
1276
1233
  const docsCheckResult = await executeRegisteredTool(pi, "docs-check", projectBase) as {
@@ -1306,43 +1263,34 @@ test("path, static-check, git, docs, and worktree tools return structured JSON p
1306
1263
 
1307
1264
  const staticCheckResult = await executeRegisteredTool(pi, "static-check", projectBase) as {
1308
1265
  details?: {
1309
- request: { selection_directory_paths: string[]; excluded_directory_paths: string[] };
1310
1266
  summary: { selected_file_count: number };
1311
1267
  files: Array<{ canonical_path: string; status: string }>;
1312
1268
  execution: { code: number };
1313
1269
  };
1314
1270
  };
1315
- assert.deepEqual(staticCheckResult.details?.request.selection_directory_paths, ["src", "tests"]);
1316
- assert.ok(staticCheckResult.details?.request.excluded_directory_paths.includes("tests/fixtures"));
1317
1271
  assert.equal(staticCheckResult.details?.summary.selected_file_count, 1);
1318
1272
  assert.equal(staticCheckResult.details?.files[0]?.canonical_path, "src/check.ts");
1319
1273
  assert.equal(staticCheckResult.details?.files[0]?.status, "selected");
1320
1274
  assert.equal(staticCheckResult.details?.execution.code, 0);
1321
1275
 
1322
1276
  const gitWtNameResult = await executeRegisteredTool(pi, "git-wt-name", projectBase) as {
1323
- details?: { result: { worktree_name?: string; format_text: string; status: string }; execution: { code: number } };
1277
+ details?: { result: { worktree_name?: string }; execution: { code: number } };
1324
1278
  };
1325
1279
  const wtName = gitWtNameResult.details?.result.worktree_name ?? "";
1326
- assert.equal(gitWtNameResult.details?.result.status, "generated");
1327
- assert.equal(gitWtNameResult.details?.result.format_text, "useReq-<project>-<sanitized-branch>-<YYYYMMDDHHMMSS>");
1328
1280
  assert.match(wtName, /^useReq-/);
1329
1281
  assert.equal(gitWtNameResult.details?.execution.code, 0);
1330
1282
 
1331
1283
  const gitWtCreateResult = await executeRegisteredTool(pi, "git-wt-create", projectBase, { wtName }) as {
1332
- details?: { result: { status: string; worktree_name: string; branch_name: string; worktree_path: string }; execution: { code: number } };
1284
+ details?: { result: { worktree_name: string; worktree_path: string }; execution: { code: number } };
1333
1285
  };
1334
- assert.equal(gitWtCreateResult.details?.result.status, "created");
1335
1286
  assert.equal(gitWtCreateResult.details?.result.worktree_name, wtName);
1336
- assert.equal(gitWtCreateResult.details?.result.branch_name, wtName);
1337
1287
  assert.match(gitWtCreateResult.details?.result.worktree_path ?? "", new RegExp(`${wtName}$`));
1338
1288
  assert.equal(gitWtCreateResult.details?.execution.code, 0);
1339
1289
 
1340
1290
  const gitWtDeleteResult = await executeRegisteredTool(pi, "git-wt-delete", projectBase, { wtName }) as {
1341
- details?: { result: { status: string; worktree_name: string; branch_name: string; worktree_path: string }; execution: { code: number } };
1291
+ details?: { result: { worktree_name: string; worktree_path: string }; execution: { code: number } };
1342
1292
  };
1343
- assert.equal(gitWtDeleteResult.details?.result.status, "deleted");
1344
1293
  assert.equal(gitWtDeleteResult.details?.result.worktree_name, wtName);
1345
- assert.equal(gitWtDeleteResult.details?.result.branch_name, wtName);
1346
1294
  assert.match(gitWtDeleteResult.details?.result.worktree_path ?? "", new RegExp(`${wtName}$`));
1347
1295
  assert.equal(gitWtDeleteResult.details?.execution.code, 0);
1348
1296
  } finally {
@@ -1383,7 +1331,9 @@ test("configuration menu omits prompt-delivery controls and persisted config omi
1383
1331
  });
1384
1332
 
1385
1333
  test("configuration menus expose show-config ordering and omit overview or notification-reference rows", async () => {
1386
- const cwd = createTempDir("pi-usereq-menu-structure-");
1334
+ const cwd = fs.mkdtempSync(
1335
+ path.join(process.env.HOME ?? process.cwd(), "pi-usereq-menu-structure-"),
1336
+ );
1387
1337
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1388
1338
  const pi = createFakePi();
1389
1339
  piUsereqExtension(pi);
@@ -1393,6 +1343,7 @@ test("configuration menus expose show-config ordering and omit overview or notif
1393
1343
 
1394
1344
  await command!.handler("", ctx);
1395
1345
 
1346
+ const renderedMenu = (ctx.__state.customRenderLines[0] ?? []).join("\n");
1396
1347
  assert.deepEqual(ctx.__state.selectCalls[0]?.items ?? [], [
1397
1348
  "docs-dir",
1398
1349
  "tests-dir",
@@ -1400,10 +1351,11 @@ test("configuration menus expose show-config ordering and omit overview or notif
1400
1351
  "static-check",
1401
1352
  "startup tools",
1402
1353
  "notifications",
1403
- "Reset defaults",
1404
1354
  "show-config",
1355
+ "Reset defaults",
1405
1356
  "Save and close",
1406
1357
  ]);
1358
+ assert.ok(renderedMenu.includes(formatRuntimePathForDisplay(getProjectConfigPath(cwd))));
1407
1359
  assert.deepEqual(ctx.__state.selectCalls[2]?.items ?? [], [
1408
1360
  "Toggle beep on success",
1409
1361
  "Toggle beep on escape",
@@ -1413,6 +1365,7 @@ test("configuration menus expose show-config ordering and omit overview or notif
1413
1365
  "Notify command (low vol.)",
1414
1366
  "Notify command (mid vol.)",
1415
1367
  "Notify command (high vol.)",
1368
+ "Pushover notifications",
1416
1369
  "Back",
1417
1370
  ]);
1418
1371
  });
@@ -1493,13 +1446,12 @@ test("session_start applies configured pi-usereq startup tools", async () => {
1493
1446
  assert.equal(
1494
1447
  ctx.__state.statuses.get("pi-usereq"),
1495
1448
  buildExpectedFakeStatusText({
1449
+ basePath: buildExpectedFakeBasePath(cwd),
1496
1450
  docsDir: DEFAULT_DOCS_DIR,
1497
1451
  testsDir: "tests",
1498
1452
  srcDir: ["src", "foobar"],
1499
- toolCount: 2,
1500
1453
  contextFilledCells: 0,
1501
- elapsed: "idle",
1502
- last: "N/A",
1454
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1503
1455
  }),
1504
1456
  );
1505
1457
  });
@@ -1518,14 +1470,13 @@ test("session_start renders the CLEAR overlay when context usage is empty", asyn
1518
1470
  assert.equal(
1519
1471
  ctx.__state.statuses.get("pi-usereq"),
1520
1472
  buildExpectedFakeStatusText({
1473
+ basePath: buildExpectedFakeBasePath(cwd),
1521
1474
  docsDir: DEFAULT_DOCS_DIR,
1522
1475
  testsDir: "tests",
1523
1476
  srcDir: ["src"],
1524
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1525
1477
  contextFilledCells: 0,
1526
1478
  contextPercent: 0,
1527
- elapsed: "idle",
1528
- last: "N/A",
1479
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1529
1480
  }),
1530
1481
  );
1531
1482
  });
@@ -1586,14 +1537,13 @@ test("context hook refreshes context usage and rounds progress cells upward", as
1586
1537
  assert.equal(
1587
1538
  ctx.__state.statuses.get("pi-usereq"),
1588
1539
  buildExpectedFakeStatusText({
1540
+ basePath: buildExpectedFakeBasePath(cwd),
1589
1541
  docsDir: DEFAULT_DOCS_DIR,
1590
1542
  testsDir: "tests",
1591
1543
  srcDir: ["src"],
1592
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1593
- contextFilledCells: 1,
1544
+ contextFilledCells: 2,
1594
1545
  contextPercent: 19.1,
1595
- elapsed: "idle",
1596
- last: "N/A",
1546
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1597
1547
  }),
1598
1548
  );
1599
1549
  });
@@ -1614,14 +1564,13 @@ test("context hook renders the FULL! overlay when usage exceeds ninety percent",
1614
1564
  assert.equal(
1615
1565
  ctx.__state.statuses.get("pi-usereq"),
1616
1566
  buildExpectedFakeStatusText({
1567
+ basePath: buildExpectedFakeBasePath(cwd),
1617
1568
  docsDir: DEFAULT_DOCS_DIR,
1618
1569
  testsDir: "tests",
1619
1570
  srcDir: ["src"],
1620
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1621
- contextFilledCells: 5,
1571
+ contextFilledCells: 10,
1622
1572
  contextPercent: 91,
1623
- elapsed: "idle",
1624
- last: "N/A",
1573
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1625
1574
  }),
1626
1575
  );
1627
1576
  });
@@ -1657,19 +1606,18 @@ test("status FULL overlay uses only CLI-supported theme tokens", async () => {
1657
1606
  assert.equal(
1658
1607
  ctx.__state.statuses.get("pi-usereq"),
1659
1608
  buildExpectedFakeStatusText({
1609
+ basePath: buildExpectedFakeBasePath(cwd),
1660
1610
  docsDir: DEFAULT_DOCS_DIR,
1661
1611
  testsDir: "tests",
1662
1612
  srcDir: ["src"],
1663
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1664
- contextFilledCells: 5,
1613
+ contextFilledCells: 10,
1665
1614
  contextPercent: 91,
1666
- elapsed: "idle",
1667
- last: "N/A",
1615
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1668
1616
  }),
1669
1617
  );
1670
1618
  });
1671
1619
 
1672
- test("agent timing status updates elapsed and last while preserving aborted last values", async () => {
1620
+ test("agent timing status updates et while preserving accumulated completed runtime", async () => {
1673
1621
  const cwd = createTempDir("pi-usereq-timing-status-");
1674
1622
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1675
1623
 
@@ -1698,13 +1646,12 @@ test("agent timing status updates elapsed and last while preserving aborted last
1698
1646
  assert.equal(
1699
1647
  ctx.__state.statuses.get("pi-usereq"),
1700
1648
  buildExpectedFakeStatusText({
1649
+ basePath: buildExpectedFakeBasePath(cwd),
1701
1650
  docsDir: DEFAULT_DOCS_DIR,
1702
1651
  testsDir: "tests",
1703
1652
  srcDir: ["src"],
1704
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1705
1653
  contextFilledCells: 0,
1706
- elapsed: "0:00",
1707
- last: "N/A",
1654
+ et: " ⏱︎ 0:00 ⚑ --:-- ⌛︎ --:--",
1708
1655
  }),
1709
1656
  );
1710
1657
 
@@ -1713,13 +1660,12 @@ test("agent timing status updates elapsed and last while preserving aborted last
1713
1660
  assert.equal(
1714
1661
  ctx.__state.statuses.get("pi-usereq"),
1715
1662
  buildExpectedFakeStatusText({
1663
+ basePath: buildExpectedFakeBasePath(cwd),
1716
1664
  docsDir: DEFAULT_DOCS_DIR,
1717
1665
  testsDir: "tests",
1718
1666
  srcDir: ["src"],
1719
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1720
1667
  contextFilledCells: 0,
1721
- elapsed: "1:01",
1722
- last: "N/A",
1668
+ et: " ⏱︎ 1:01 ⚑ --:-- ⌛︎ --:--",
1723
1669
  }),
1724
1670
  );
1725
1671
 
@@ -1735,13 +1681,12 @@ test("agent timing status updates elapsed and last while preserving aborted last
1735
1681
  assert.equal(
1736
1682
  ctx.__state.statuses.get("pi-usereq"),
1737
1683
  buildExpectedFakeStatusText({
1684
+ basePath: buildExpectedFakeBasePath(cwd),
1738
1685
  docsDir: DEFAULT_DOCS_DIR,
1739
1686
  testsDir: "tests",
1740
1687
  srcDir: ["src"],
1741
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1742
1688
  contextFilledCells: 0,
1743
- elapsed: "idle",
1744
- last: "1:01",
1689
+ et: " ⏱︎ --:-- ⚑ 1:01 ⌛︎ 1:01",
1745
1690
  }),
1746
1691
  );
1747
1692
 
@@ -1760,13 +1705,12 @@ test("agent timing status updates elapsed and last while preserving aborted last
1760
1705
  assert.equal(
1761
1706
  ctx.__state.statuses.get("pi-usereq"),
1762
1707
  buildExpectedFakeStatusText({
1708
+ basePath: buildExpectedFakeBasePath(cwd),
1763
1709
  docsDir: DEFAULT_DOCS_DIR,
1764
1710
  testsDir: "tests",
1765
1711
  srcDir: ["src"],
1766
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1767
1712
  contextFilledCells: 0,
1768
- elapsed: "idle",
1769
- last: "1:01",
1713
+ et: " ⏱︎ --:-- ⚑ 1:01 ⌛︎ 1:01",
1770
1714
  }),
1771
1715
  );
1772
1716
  assert.ok(clearedHandles.length >= 2);
@@ -1794,6 +1738,14 @@ test("session_start enables default custom tools and default embedded tools only
1794
1738
  assert.equal(activeTools.has("ls"), false);
1795
1739
  });
1796
1740
 
1741
+ test("default configuration enables all pi-notify beep outcomes", () => {
1742
+ const config = getDefaultConfig(createTempDir("pi-usereq-default-notify-"));
1743
+
1744
+ assert.equal(config["notify-beep-on-end"], true);
1745
+ assert.equal(config["notify-beep-on-esc"], true);
1746
+ assert.equal(config["notify-beep-on-error"], true);
1747
+ });
1748
+
1797
1749
  test("configuration menu can enable embedded builtin tools", async () => {
1798
1750
  const cwd = createTempDir("pi-usereq-menu-embedded-");
1799
1751
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
@@ -1866,9 +1818,9 @@ test("configuration menu can persist pi-notify settings", async () => {
1866
1818
  await command!.handler("", ctx);
1867
1819
 
1868
1820
  const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1869
- assert.equal(config["notify-beep-on-end"], true);
1870
- assert.equal(config["notify-beep-on-error"], true);
1871
- assert.equal(config["notify-beep-on-esc"], false);
1821
+ assert.equal(config["notify-beep-on-end"], false);
1822
+ assert.equal(config["notify-beep-on-error"], false);
1823
+ assert.equal(config["notify-beep-on-esc"], true);
1872
1824
  assert.equal(config["notify-sound"], "high");
1873
1825
  assert.equal(config["notify-sound-toggle-shortcut"], "alt+shift+s");
1874
1826
  assert.equal(config.PI_NOTIFY_SOUND_LOW_CMD, "echo low %%INSTALLATION_PATH%%");
@@ -1877,19 +1829,285 @@ test("configuration menu can persist pi-notify settings", async () => {
1877
1829
  assert.equal(
1878
1830
  ctx.__state.statuses.get("pi-usereq"),
1879
1831
  buildExpectedFakeStatusText({
1832
+ basePath: buildExpectedFakeBasePath(cwd),
1880
1833
  docsDir: DEFAULT_DOCS_DIR,
1881
1834
  testsDir: "tests",
1882
1835
  srcDir: ["src"],
1883
- toolCount: pi.getActiveTools().length,
1884
1836
  contextFilledCells: 0,
1885
- elapsed: "idle",
1886
- last: "N/A",
1887
- beep: "end,err",
1837
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1838
+ beep: "esc",
1888
1839
  sound: "high",
1889
1840
  }),
1890
1841
  );
1891
1842
  });
1892
1843
 
1844
+ test("configuration menu can persist pushover settings", async () => {
1845
+ const cwd = createTempDir("pi-usereq-menu-pushover-");
1846
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1847
+ const pi = createFakePi();
1848
+ piUsereqExtension(pi);
1849
+ const command = pi.commands.get("pi-usereq");
1850
+ assert.ok(command);
1851
+ const ctx = createFakeCtx(cwd, {
1852
+ selects: [
1853
+ "notifications",
1854
+ "Pushover notifications",
1855
+ "Notify on success",
1856
+ "User Key/Delivery Group Key",
1857
+ "Token/API Token Key",
1858
+ "Priority",
1859
+ "1=High Priority",
1860
+ "Back",
1861
+ "Back",
1862
+ "Save and close",
1863
+ ],
1864
+ inputs: [
1865
+ "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
1866
+ "ah6bf5u2sj63mcvou6qamiabeoubbe",
1867
+ ],
1868
+ });
1869
+
1870
+ await command!.handler("", ctx);
1871
+
1872
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
1873
+ assert.equal(config["notify-pushover-global-disable"], false);
1874
+ assert.equal(config["notify-pushover-on-success"], true);
1875
+ assert.equal(config["notify-pushover-user-key"], "gzfjjvp1xxmhibqwzh9m7i1zwvf83j");
1876
+ assert.equal(config["notify-pushover-api-token"], "ah6bf5u2sj63mcvou6qamiabeoubbe");
1877
+ assert.equal(config["notify-pushover-priority"], 1);
1878
+ assert.equal(
1879
+ ctx.__state.statuses.get("pi-usereq"),
1880
+ buildExpectedFakeStatusText({
1881
+ basePath: buildExpectedFakeBasePath(cwd),
1882
+ docsDir: DEFAULT_DOCS_DIR,
1883
+ testsDir: "tests",
1884
+ srcDir: ["src"],
1885
+ contextFilledCells: 0,
1886
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1887
+ pushover: "on",
1888
+ }),
1889
+ );
1890
+ });
1891
+
1892
+ test("successful req prompt completion sends pushover requests and suppresses invalid scenarios", async () => {
1893
+ const cwd = createTempDir("pi-usereq-pushover-run-");
1894
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
1895
+ const configPath = getProjectConfigPath(cwd);
1896
+ const writeConfig = (overrides: Record<string, unknown>) => {
1897
+ const config = {
1898
+ ...getDefaultConfig(cwd),
1899
+ ...overrides,
1900
+ };
1901
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
1902
+ };
1903
+ const pi = createFakePi();
1904
+ piUsereqExtension(pi);
1905
+ const ctx = createFakeCtx(cwd);
1906
+ const originalDateNow = Date.now;
1907
+ const recordedRequests: Array<{ url: string; options: Record<string, unknown>; body: string }> = [];
1908
+ let nowMs = 0;
1909
+ setPiNotifyHttpsRequestForTests(((url: URL, options: Record<string, unknown>, callback?: (response: any) => void) => {
1910
+ const record = { url: url.toString(), options, body: "" };
1911
+ recordedRequests.push(record);
1912
+ const fakeResponse = {
1913
+ on(_eventName: string, _handler: (...args: any[]) => void) {
1914
+ return fakeResponse;
1915
+ },
1916
+ resume() {
1917
+ return undefined;
1918
+ },
1919
+ };
1920
+ callback?.(fakeResponse);
1921
+ const fakeRequest = {
1922
+ on(_eventName: string, _handler: (...args: any[]) => void) {
1923
+ return fakeRequest;
1924
+ },
1925
+ end(body?: string) {
1926
+ record.body = body ?? "";
1927
+ },
1928
+ };
1929
+ return fakeRequest as unknown as ReturnType<typeof import("node:https").request>;
1930
+ }) as typeof import("node:https").request);
1931
+ Date.now = () => nowMs;
1932
+
1933
+ try {
1934
+ writeConfig({
1935
+ "notify-pushover-on-success": true,
1936
+ "notify-pushover-user-key": "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
1937
+ "notify-pushover-api-token": "ah6bf5u2sj63mcvou6qamiabeoubbe",
1938
+ "notify-pushover-priority": 1,
1939
+ });
1940
+ await pi.emit("session_start", { reason: "startup" }, ctx);
1941
+ recordedRequests.length = 0;
1942
+ nowMs = 0;
1943
+ await pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx);
1944
+ await pi.emit("agent_start", {}, ctx);
1945
+ nowMs = 5_000;
1946
+ await pi.emit("agent_end", {
1947
+ messages: [
1948
+ {
1949
+ role: "assistant",
1950
+ stopReason: "stop",
1951
+ content: [],
1952
+ },
1953
+ ],
1954
+ }, ctx);
1955
+
1956
+ assert.equal(recordedRequests.length, 1);
1957
+ assert.equal(recordedRequests[0]?.url, "https://api.pushover.net/1/messages.json");
1958
+ const successParams = new URLSearchParams(recordedRequests[0]?.body ?? "");
1959
+ assert.equal(successParams.get("token"), "ah6bf5u2sj63mcvou6qamiabeoubbe");
1960
+ assert.equal(successParams.get("user"), "gzfjjvp1xxmhibqwzh9m7i1zwvf83j");
1961
+ assert.equal(successParams.get("priority"), "1");
1962
+ assert.equal(successParams.get("message"), "Inspect src/index.ts");
1963
+ assert.equal(successParams.get("title"), `analyze @ ${buildExpectedFakeBasePath(cwd)} [0:05]`);
1964
+
1965
+ recordedRequests.length = 0;
1966
+ nowMs = 10_000;
1967
+ await pi.commands.get("req-change")!.handler("Adjust docs", ctx);
1968
+ await pi.emit("agent_start", {}, ctx);
1969
+ nowMs = 14_000;
1970
+ await pi.emit("agent_end", {
1971
+ messages: [
1972
+ {
1973
+ role: "assistant",
1974
+ stopReason: "aborted",
1975
+ content: [],
1976
+ },
1977
+ ],
1978
+ }, ctx);
1979
+ assert.equal(recordedRequests.length, 0);
1980
+
1981
+ recordedRequests.length = 0;
1982
+ nowMs = 20_000;
1983
+ await pi.commands.get("req-create")!.handler("Create docs", ctx);
1984
+ await pi.emit("agent_start", {}, ctx);
1985
+ nowMs = 23_000;
1986
+ await pi.emit("agent_end", {
1987
+ messages: [
1988
+ {
1989
+ role: "assistant",
1990
+ stopReason: "error",
1991
+ content: [],
1992
+ },
1993
+ ],
1994
+ }, ctx);
1995
+ assert.equal(recordedRequests.length, 0);
1996
+
1997
+ writeConfig({
1998
+ "notify-pushover-on-success": true,
1999
+ "notify-pushover-user-key": "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
2000
+ "notify-pushover-api-token": "",
2001
+ "notify-pushover-priority": 1,
2002
+ });
2003
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2004
+ recordedRequests.length = 0;
2005
+ nowMs = 30_000;
2006
+ await pi.commands.get("req-write")!.handler("Write docs", ctx);
2007
+ await pi.emit("agent_start", {}, ctx);
2008
+ nowMs = 32_000;
2009
+ await pi.emit("agent_end", {
2010
+ messages: [
2011
+ {
2012
+ role: "assistant",
2013
+ stopReason: "stop",
2014
+ content: [],
2015
+ },
2016
+ ],
2017
+ }, ctx);
2018
+ assert.equal(recordedRequests.length, 0);
2019
+ } finally {
2020
+ setPiNotifyHttpsRequestForTests(undefined);
2021
+ Date.now = originalDateNow;
2022
+ }
2023
+ });
2024
+
2025
+ test("pushover global disable suppresses delivery without clearing enabled state", async () => {
2026
+ const cwd = createTempDir("pi-usereq-pushover-disabled-");
2027
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
2028
+ const pi = createFakePi();
2029
+ piUsereqExtension(pi);
2030
+ const command = pi.commands.get("pi-usereq");
2031
+ assert.ok(command);
2032
+ const ctx = createFakeCtx(cwd, {
2033
+ selects: [
2034
+ "notifications",
2035
+ "Pushover notifications",
2036
+ "Notify on success",
2037
+ "Global disable",
2038
+ "User Key/Delivery Group Key",
2039
+ "Token/API Token Key",
2040
+ "Back",
2041
+ "Back",
2042
+ "Save and close",
2043
+ ],
2044
+ inputs: [
2045
+ "gzfjjvp1xxmhibqwzh9m7i1zwvf83j",
2046
+ "ah6bf5u2sj63mcvou6qamiabeoubbe",
2047
+ ],
2048
+ });
2049
+
2050
+ await command!.handler("", ctx);
2051
+
2052
+ const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
2053
+ assert.equal(config["notify-pushover-on-success"], true);
2054
+ assert.equal(config["notify-pushover-global-disable"], true);
2055
+ assert.equal(
2056
+ ctx.__state.statuses.get("pi-usereq"),
2057
+ buildExpectedFakeStatusText({
2058
+ basePath: buildExpectedFakeBasePath(cwd),
2059
+ docsDir: DEFAULT_DOCS_DIR,
2060
+ testsDir: "tests",
2061
+ srcDir: ["src"],
2062
+ contextFilledCells: 0,
2063
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
2064
+ pushover: "on",
2065
+ }),
2066
+ );
2067
+
2068
+ const recordedRequests: string[] = [];
2069
+ setPiNotifyHttpsRequestForTests(((url: URL, options: Record<string, unknown>, callback?: (response: any) => void) => {
2070
+ void options;
2071
+ recordedRequests.push(url.toString());
2072
+ const fakeResponse = {
2073
+ on(_eventName: string, _handler: (...args: any[]) => void) {
2074
+ return fakeResponse;
2075
+ },
2076
+ resume() {
2077
+ return undefined;
2078
+ },
2079
+ };
2080
+ callback?.(fakeResponse);
2081
+ const fakeRequest = {
2082
+ on(_eventName: string, _handler: (...args: any[]) => void) {
2083
+ return fakeRequest;
2084
+ },
2085
+ end(_body?: string) {
2086
+ return undefined;
2087
+ },
2088
+ };
2089
+ return fakeRequest as unknown as ReturnType<typeof import("node:https").request>;
2090
+ }) as typeof import("node:https").request);
2091
+
2092
+ try {
2093
+ await pi.emit("session_start", { reason: "startup" }, ctx);
2094
+ await pi.commands.get("req-analyze")!.handler("Inspect src/index.ts", ctx);
2095
+ await pi.emit("agent_start", {}, ctx);
2096
+ await pi.emit("agent_end", {
2097
+ messages: [
2098
+ {
2099
+ role: "assistant",
2100
+ stopReason: "stop",
2101
+ content: [],
2102
+ },
2103
+ ],
2104
+ }, ctx);
2105
+ assert.deepEqual(recordedRequests, []);
2106
+ } finally {
2107
+ setPiNotifyHttpsRequestForTests(undefined);
2108
+ }
2109
+ });
2110
+
1893
2111
  test("sound toggle shortcut cycles persisted pi-notify sound levels", async () => {
1894
2112
  const cwd = createTempDir("pi-usereq-shortcut-notify-");
1895
2113
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
@@ -1914,13 +2132,12 @@ test("sound toggle shortcut cycles persisted pi-notify sound levels", async () =
1914
2132
  assert.equal(
1915
2133
  ctx.__state.statuses.get("pi-usereq"),
1916
2134
  buildExpectedFakeStatusText({
2135
+ basePath: buildExpectedFakeBasePath(cwd),
1917
2136
  docsDir: DEFAULT_DOCS_DIR,
1918
2137
  testsDir: "tests",
1919
2138
  srcDir: ["src"],
1920
- toolCount: PI_USEREQ_DEFAULT_ENABLED_TOOL_NAMES.length,
1921
2139
  contextFilledCells: 0,
1922
- elapsed: "idle",
1923
- last: "N/A",
2140
+ et: " ⏱︎ --:-- ⚑ --:-- ⌛︎ --:--",
1924
2141
  sound: expectedSound,
1925
2142
  }),
1926
2143
  );
@@ -1939,11 +2156,11 @@ test("git-path tool derives the repository root at runtime", async () => {
1939
2156
  piUsereqExtension(pi);
1940
2157
  const result = await executeRegisteredTool(pi, "git-path", projectBase) as {
1941
2158
  content?: Array<{ text?: string }>;
1942
- details?: { result: { path_value: string }; runtime_paths: { git_path: string } };
2159
+ details?: { result: { path_value: string } };
1943
2160
  };
1944
2161
 
1945
2162
  assert.deepEqual(JSON.parse(result.content?.[0]?.text ?? "{}"), JSON.parse(JSON.stringify(result.details)));
1946
- assert.equal(result.details?.result.path_value, result.details?.runtime_paths.git_path);
2163
+ assert.equal(result.details?.result.path_value, projectBase);
1947
2164
  } finally {
1948
2165
  fs.rmSync(projectBase, { recursive: true, force: true });
1949
2166
  }
@@ -1979,6 +2196,26 @@ test("configuration menu can add static-check entries from raw specs", async ()
1979
2196
  assert.deepEqual(config["static-check"].Python, [{ module: "Command", cmd: "true" }]);
1980
2197
  });
1981
2198
 
2199
+ test("configuration menu hides removed static-check modules from user-facing actions", async () => {
2200
+ const cwd = createTempDir("pi-usereq-menu-sc-hidden-");
2201
+ fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
2202
+ const pi = createFakePi();
2203
+ piUsereqExtension(pi);
2204
+ const command = pi.commands.get("pi-usereq");
2205
+ assert.ok(command);
2206
+ const ctx = createFakeCtx(cwd, {
2207
+ selects: ["static-check"],
2208
+ });
2209
+
2210
+ await command!.handler("", ctx);
2211
+
2212
+ const renderedStaticCheckMenu = (ctx.__state.customRenderLines[1] ?? []).join("\n");
2213
+ assert.match(renderedStaticCheckMenu, /Add entry for supported language/);
2214
+ assert.doesNotMatch(renderedStaticCheckMenu, /Pylance/);
2215
+ assert.doesNotMatch(renderedStaticCheckMenu, /Ruff/);
2216
+ assert.doesNotMatch(renderedStaticCheckMenu, /Dummy/);
2217
+ });
2218
+
1982
2219
  test("configuration menu can add guided static-check entries for explicit supported languages", async () => {
1983
2220
  const cwd = createTempDir("pi-usereq-menu-sc-guided-");
1984
2221
  fs.mkdirSync(path.dirname(getProjectConfigPath(cwd)), { recursive: true });
@@ -1994,13 +2231,13 @@ test("configuration menu can add guided static-check entries for explicit suppor
1994
2231
  "static-check",
1995
2232
  "Add entry for supported language",
1996
2233
  "Python",
1997
- "Ruff",
1998
2234
  "Back",
1999
2235
  "Save and close",
2000
2236
  ],
2237
+ inputs: ["true", ""],
2001
2238
  }),
2002
2239
  );
2003
2240
 
2004
2241
  const config = JSON.parse(fs.readFileSync(getProjectConfigPath(cwd), "utf8"));
2005
- assert.deepEqual(config["static-check"].Python, [{ module: "Ruff" }]);
2242
+ assert.deepEqual(config["static-check"].Python, [{ module: "Command", cmd: "true" }]);
2006
2243
  });