pi-usereq 0.5.0 → 0.7.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.
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.5.0"
11
+ export const VERSION = "0.7.0"
12
12
 
13
13
  import path from "node:path";
14
14
  import type {
@@ -52,6 +52,7 @@ import {
52
52
  type FindToolScope,
53
53
  } from "./core/find-payload.js";
54
54
  import {
55
+ DEFAULT_SRC_DIRS,
55
56
  getDefaultConfig,
56
57
  getProjectConfigPath,
57
58
  loadConfig,
@@ -61,21 +62,25 @@ import {
61
62
  type UseReqConfig,
62
63
  } from "./core/config.js";
63
64
  import {
65
+ DEFAULT_PI_NOTIFY_CMD,
66
+ DEFAULT_PI_NOTIFY_PUSHOVER_TEXT,
67
+ DEFAULT_PI_NOTIFY_PUSHOVER_TITLE,
68
+ DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD,
69
+ DEFAULT_PI_NOTIFY_SOUND_LOW_CMD,
70
+ DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
64
71
  cyclePiNotifySoundLevel,
65
- formatPiNotifyBeepStatus,
66
72
  formatPiNotifyPushoverStatus,
73
+ formatPiNotifyStatus,
74
+ normalizePiNotifyCommand,
67
75
  normalizePiNotifyPushoverCredential,
68
76
  normalizePiNotifyPushoverPriority,
77
+ normalizePiNotifyTemplateValue,
69
78
  runPiNotifyEffects,
79
+ type PiNotifyEventRequest,
70
80
  type PiNotifyPushoverPriority,
71
- type PiNotifyPushoverRequest,
72
81
  type PiNotifySoundLevel,
73
82
  } from "./core/pi-notify.js";
74
- import {
75
- buildRuntimePathContext,
76
- buildRuntimePathFacts,
77
- formatRuntimePathForDisplay,
78
- } from "./core/path-context.js";
83
+ import { formatRuntimePathForDisplay } from "./core/path-context.js";
79
84
  import { resolveRuntimeGitPath } from "./core/runtime-project-paths.js";
80
85
  import { showPiUsereqSettingsMenu, type PiUsereqSettingsMenuChoice } from "./core/settings-menu.js";
81
86
  import {
@@ -108,9 +113,7 @@ import {
108
113
  } from "./core/tool-runner.js";
109
114
  import { LANGUAGE_TAGS } from "./core/find-constructs.js";
110
115
  import {
111
- STATIC_CHECK_MODULES,
112
116
  getSupportedStaticCheckLanguageSupport,
113
- parseEnableStaticCheck,
114
117
  } from "./core/static-check.js";
115
118
  import { makeRelativeIfContainsProject, shellSplit } from "./core/utils.js";
116
119
 
@@ -164,20 +167,6 @@ function getProjectBase(cwd: string): string {
164
167
  return path.resolve(cwd);
165
168
  }
166
169
 
167
- /**
168
- * @brief Builds the shared runtime path facts for the current command or tool context.
169
- * @details Derives installation, execution, base, config, resource, docs, test, source, and optional git paths from the cwd-derived project configuration plus runtime-only repository probing, then converts them into prompt/tool-facing strings. Runtime is O(s + p) where s is configured source-directory count and p is aggregate path length. Side effects are limited to git subprocess execution.
170
- * @param[in] cwd {string} Current working directory.
171
- * @param[in] config {UseReqConfig} Effective project configuration.
172
- * @return {import("./core/path-context.js").RuntimePathFacts} Shared runtime path facts.
173
- * @satisfies REQ-145, REQ-146
174
- */
175
- function buildSharedRuntimePathFacts(cwd: string, config: UseReqConfig): import("./core/path-context.js").RuntimePathFacts {
176
- const projectBase = getProjectBase(cwd);
177
- const gitPath = resolveRuntimeGitPath(projectBase);
178
- return buildRuntimePathFacts(buildRuntimePathContext(projectBase, config, { gitPath }));
179
- }
180
-
181
170
  /**
182
171
  * @brief Loads project configuration for the extension runtime.
183
172
  * @details Resolves the project base, loads persisted config, and normalizes configured directory paths without reading or persisting runtime-derived `base-path` or `git-path` metadata. Runtime is dominated by config I/O. Side effects are limited to filesystem reads.
@@ -205,15 +194,17 @@ function saveProjectConfig(cwd: string, config: UseReqConfig): void {
205
194
 
206
195
  /**
207
196
  * @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.
197
+ * @details Resolves `<base-path>/.pi-usereq/config.json` from the cwd-derived project base, reuses the shared runtime-path formatter, and rewrites a leading POSIX `$HOME` token to `~` for the `Show configuration` row only. Runtime is O(p) in path length. No external state is mutated.
211
198
  * @param[in] cwd {string} Current working directory.
212
- * @return {string} User-home-relative or absolute config path display value.
199
+ * @return {string} `~`-relative or absolute config path display value.
213
200
  * @satisfies REQ-162
214
201
  */
215
202
  function formatProjectConfigPathForMenu(cwd: string): string {
216
- return formatRuntimePathForDisplay(getProjectConfigPath(getProjectBase(cwd)));
203
+ const displayPath = formatRuntimePathForDisplay(getProjectConfigPath(getProjectBase(cwd)));
204
+ if (displayPath === "$HOME") {
205
+ return "~";
206
+ }
207
+ return displayPath.startsWith("$HOME/") ? `~/${displayPath.slice("$HOME/".length)}` : displayPath;
217
208
  }
218
209
 
219
210
  /**
@@ -263,10 +254,12 @@ function collectProjectStaticCheckSelection(
263
254
  * @return {string} Newline-delimited execution diagnostics.
264
255
  */
265
256
  function buildTokenToolExecutionStderr(payload: TokenToolPayload): string {
266
- const skippedLines = payload.guidance.source_observations.skipped_inputs
267
- .map((entry) => `skipped: ${entry.canonical_path}: ${entry.reason}`);
268
- const errorLines = payload.guidance.source_observations.error_inputs
269
- .map((entry) => `error: ${entry.canonical_path}: ${entry.reason}`);
257
+ const skippedLines = payload.files
258
+ .filter((entry) => entry.status === "skipped" && entry.error_message)
259
+ .map((entry) => `skipped: ${entry.canonical_path}: ${entry.error_message!}`);
260
+ const errorLines = payload.files
261
+ .filter((entry) => entry.status === "error" && entry.error_message)
262
+ .map((entry) => `error: ${entry.canonical_path}: ${entry.error_message!}`);
270
263
  return [...skippedLines, ...errorLines].join("\n");
271
264
  }
272
265
 
@@ -284,10 +277,8 @@ function buildTokenToolExecuteResult(
284
277
  details: TokenToolPayload & { execution: { code: number; stderr: string } };
285
278
  } {
286
279
  const details = {
287
- request: payload.request,
288
280
  summary: payload.summary,
289
281
  files: payload.files,
290
- guidance: payload.guidance,
291
282
  execution: {
292
283
  code: payload.summary.counted_file_count > 0 ? 0 : 1,
293
284
  stderr: buildTokenToolExecutionStderr(payload),
@@ -296,6 +287,43 @@ function buildTokenToolExecuteResult(
296
287
  return buildStructuredToolExecuteResult(details);
297
288
  }
298
289
 
290
+ /**
291
+ * @brief Builds the fallback execute result returned when token counting fails before payload construction.
292
+ * @details Normalizes one thrown `ReqError` into the stable token-tool response shape so missing runtime dependencies or other pre-count failures do not abort extension tool execution. Runtime is O(1). No external state is mutated.
293
+ * @param[in] error {unknown} Thrown token-tool failure.
294
+ * @return {{ content: Array<{ type: "text"; text: string }>; details: TokenToolPayload & { execution: { code: number; stderr: string } } }} Structured token-tool failure result.
295
+ */
296
+ function buildFailedTokenToolExecuteResult(
297
+ error: unknown,
298
+ ): {
299
+ content: Array<{ type: "text"; text: string }>;
300
+ details: TokenToolPayload & { execution: { code: number; stderr: string } };
301
+ } {
302
+ const result = normalizeToolFailure(error);
303
+ const details = {
304
+ summary: {
305
+ processable_file_count: 0,
306
+ counted_file_count: 0,
307
+ error_file_count: 0,
308
+ skipped_file_count: 0,
309
+ total_token_count: 0,
310
+ total_character_count: 0,
311
+ total_byte_count: 0,
312
+ total_line_count: 0,
313
+ average_token_count_per_counted_file: 0,
314
+ average_character_count_per_counted_file: 0,
315
+ average_byte_count_per_counted_file: 0,
316
+ average_line_count_per_counted_file: 0,
317
+ },
318
+ files: [],
319
+ execution: {
320
+ code: result.code,
321
+ stderr: result.stderr,
322
+ },
323
+ };
324
+ return buildStructuredToolExecuteResult(details);
325
+ }
326
+
299
327
  /**
300
328
  * @brief Builds the agent-oriented execute result returned by references tools.
301
329
  * @details Mirrors the structured references payload into both the text `content` channel and the machine-readable `details` channel while isolating execution metadata under `execution`. Runtime is O(n) in payload size. No side effects occur.
@@ -310,7 +338,6 @@ function buildReferenceToolExecuteResult(
310
338
  details: ReferenceToolPayload & { execution: { code: number; stderr: string } };
311
339
  } {
312
340
  const details = {
313
- request: payload.request,
314
341
  summary: payload.summary,
315
342
  repository: payload.repository,
316
343
  files: payload.files,
@@ -336,7 +363,6 @@ function buildCompressionToolExecuteResult(
336
363
  details: CompressToolPayload & { execution: { code: number; stderr: string } };
337
364
  } {
338
365
  const details = {
339
- request: payload.request,
340
366
  summary: payload.summary,
341
367
  repository: payload.repository,
342
368
  files: payload.files,
@@ -423,7 +449,7 @@ function buildFindToolSchemaDescription(scope: FindToolScope): string {
423
449
  const inputContract = scope === "explicit-files"
424
450
  ? "Input contract: tag + pattern + files[] + optional enableLineNumbers."
425
451
  : "Input contract: tag + pattern + optional enableLineNumbers. Scope is the configured src-dir list resolved from the current project configuration.";
426
- return `${inputContract} Output contract: JSON object with request, summary, repository, files, and execution. Repository exposes file_canonical_paths and supported_tags_by_language. File entries expose path facts, supported_tags, structured statuses, file_doxygen, and match records with typed line ranges, stripped code lines, and structured Doxygen fields. Regex matches construct names only.`;
452
+ return `${inputContract} Output contract: JSON object with summary, repository, files, and execution. Static supported-tag matrices are documented in tool registration metadata instead of runtime responses. File entries expose structured statuses, file_doxygen, and match records with typed line ranges, stripped code lines, and structured Doxygen fields. Regex matches construct names only.`;
427
453
  }
428
454
 
429
455
  /**
@@ -437,13 +463,13 @@ function buildFindToolPromptGuidelines(scope: FindToolScope): string[] {
437
463
  ? "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute."
438
464
  : "Scope: resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.";
439
465
  const outputLine = scope === "explicit-files"
440
- ? "Output contract: request + summary + repository + files + execution. Repository exposes requested file scope and supported_tags_by_language; file entries expose status, supported_tags, line ranges, file_doxygen, and matches; match entries expose symbol_kind, signature_text, line ranges, code_lines, stripped_source_text, and structured Doxygen fields."
441
- : "Output contract: request + summary + repository + files + execution. Repository exposes source_directory_paths, file_canonical_paths, and supported_tags_by_language; file entries expose status, supported_tags, line ranges, file_doxygen, and matches; match entries expose symbol_kind, signature_text, line ranges, code_lines, stripped_source_text, and structured Doxygen fields.";
466
+ ? "Output contract: summary + repository + files + execution. Repository exposes requested file scope only when it adds dynamic search context; supported tags remain documented in registration metadata. File entries expose status, line ranges, file_doxygen, and matches; match entries expose symbol_kind, signature_text, line ranges, code_lines, stripped_source_text, and structured Doxygen fields."
467
+ : "Output contract: summary + repository + files + execution. Repository exposes source_directory_paths and file_canonical_paths when project-scope search context varies; supported tags remain documented in registration metadata. File entries expose status, line ranges, file_doxygen, and matches; match entries expose symbol_kind, signature_text, line ranges, code_lines, stripped_source_text, and structured Doxygen fields.";
442
468
  return [
443
469
  scopeLine,
444
470
  outputLine,
445
471
  "Regex rule: pattern is applied to construct names only with JavaScript RegExp search semantics; it never matches construct bodies; use ^...$ for exact-name matching.",
446
- "Tag rule: tag is pipe-separated and case-insensitive; unsupported tags are ignored; if no valid tag remains, request.tag_filter_status becomes invalid.",
472
+ "Tag rule: tag is pipe-separated and case-insensitive; unsupported tags are ignored; if no valid tag remains, the response search_status becomes invalid_tag_filter.",
447
473
  "Line-number behavior: enableLineNumbers changes only display_text and stripped_source_text rendering; numeric source_line_number and line_range facts remain dedicated fields.",
448
474
  "Failure contract: invalid tag filters, invalid regex patterns, unsupported extensions, unsupported tag-language combinations, no-match files, and analysis failures are surfaced as structured statuses plus optional execution.stderr diagnostics.",
449
475
  ...buildFindToolSupportedTagGuidelines(),
@@ -465,7 +491,6 @@ function buildFindToolExecuteResult(
465
491
  } {
466
492
  const stderr = buildFindToolExecutionStderr(payload);
467
493
  const details = {
468
- request: payload.request,
469
494
  summary: payload.summary,
470
495
  repository: payload.repository,
471
496
  files: payload.files,
@@ -515,19 +540,6 @@ function getConfiguredEnabledPiUsereqTools(config: UseReqConfig): string[] {
515
540
  return enabledTools;
516
541
  }
517
542
 
518
- /**
519
- * @brief Classifies one configurable tool as embedded or extension-owned.
520
- * @details Uses the runtime `sourceInfo.source` field plus the supported embedded-name subset to produce one stable UI label. Runtime is O(1). No external state is mutated.
521
- * @param[in] tool {ToolInfo} Runtime tool descriptor.
522
- * @return {"builtin" | "extension"} Stable tool-kind label.
523
- */
524
- function getPiUsereqToolKind(tool: ToolInfo): "builtin" | "extension" {
525
- if (tool.sourceInfo?.source === "builtin" && isPiUsereqEmbeddedToolName(tool.name)) {
526
- return "builtin";
527
- }
528
- return "extension";
529
- }
530
-
531
543
  /**
532
544
  * @brief Applies the configured active-tool enablement to the current session.
533
545
  * @details Preserves non-configurable active tools, removes every configurable tool from the active set, then re-adds only configured tools that exist in the current runtime inventory. Runtime is O(t). Side effects include `pi.setActiveTools(...)`.
@@ -560,20 +572,19 @@ function applyConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig): v
560
572
  * @details Applies session-start-specific resource validation, project-config
561
573
  * refresh, and startup-tool enablement before forwarding the originating hook
562
574
  * name and payload into the shared `updateExtensionStatus(...)` pipeline.
563
- * On `agent_end`, also dispatches configured pi-notify beep, sound, and
575
+ * On `agent_end`, also dispatches configured command-notify, sound, and
564
576
  * prompt-specific Pushover effects when the current run originates from a
565
577
  * bundled prompt command. Runtime is dominated by configuration loading during
566
578
  * `session_start`; all other hooks are O(1). Side effects include resource
567
579
  * checks, active-tool mutation, status updates, live-ticker disposal on
568
- * shutdown, stdout writes, optional child-process spawning, and outbound
569
- * HTTPS requests.
580
+ * shutdown, optional child-process spawning, and outbound HTTPS requests.
570
581
  * @param[in] pi {ExtensionAPI} Active extension API instance.
571
582
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
572
583
  * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
573
584
  * @param[in] event {unknown} Hook payload forwarded by pi.
574
585
  * @param[in] ctx {ExtensionContext} Active extension context.
575
586
  * @return {Promise<void>} Promise resolved when hook processing completes.
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
587
+ * @satisfies REQ-117, REQ-118, REQ-119, REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-176, REQ-178, REQ-184, REQ-185, REQ-186, REQ-187
577
588
  */
578
589
  async function handleExtensionStatusEvent(
579
590
  pi: ExtensionAPI,
@@ -582,6 +593,16 @@ async function handleExtensionStatusEvent(
582
593
  event: unknown,
583
594
  ctx: ExtensionContext,
584
595
  ): Promise<void> {
596
+ const notifyRequest: PiNotifyEventRequest | undefined = hookName === "agent_end"
597
+ && statusController.state.activePromptRequest !== undefined
598
+ && statusController.state.runStartTimeMs !== undefined
599
+ ? {
600
+ promptName: statusController.state.activePromptRequest.promptName,
601
+ promptArgs: statusController.state.activePromptRequest.promptArgs,
602
+ basePath: path.resolve(ctx.cwd),
603
+ completionTimeMs: Math.max(0, Date.now() - statusController.state.runStartTimeMs),
604
+ }
605
+ : undefined;
585
606
  if (hookName === "session_start") {
586
607
  ensureBundledResourcesAccessible();
587
608
  const config = loadProjectConfig(ctx.cwd);
@@ -591,19 +612,10 @@ async function handleExtensionStatusEvent(
591
612
  updateExtensionStatus(statusController, hookName, event, ctx);
592
613
  if (hookName === "agent_end") {
593
614
  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
615
  runPiNotifyEffects(
604
616
  statusController.config,
605
617
  event as { messages: AgentEndEvent["messages"] },
606
- pushoverRequest,
618
+ notifyRequest,
607
619
  );
608
620
  }
609
621
  statusController.state.activePromptRequest = undefined;
@@ -653,131 +665,397 @@ function setConfiguredPiUsereqTools(pi: ExtensionAPI, config: UseReqConfig, enab
653
665
  }
654
666
 
655
667
  /**
656
- * @brief Renders a textual reference for configurable-tool configuration and runtime state.
657
- * @details Lists every configurable tool with configured enablement, runtime activation, builtin-versus-extension classification, source metadata, and optional descriptions. Runtime is O(t). No side effects occur.
658
- * @param[in] pi {ExtensionAPI} Active extension API instance.
659
- * @param[in] config {UseReqConfig} Effective project configuration.
660
- * @return {string} Multiline tool-status report.
668
+ * @brief Represents one persisted boolean notification-setting key.
669
+ * @details Restricts menu toggles to the global enable flags and completed/interrupted/failed event toggles used by command-notify, sound, and Pushover configuration. Compile-time only and introduces no runtime cost.
661
670
  */
662
- function renderPiUsereqToolsReference(pi: ExtensionAPI, config: UseReqConfig): string {
663
- const tools = getPiUsereqStartupTools(pi);
664
- const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
665
- const activeTools = new Set(pi.getActiveTools());
666
- const lines = [
667
- "# configurable active tools",
668
- "",
669
- `Configured enabled tools: ${enabledTools.size}/${tools.length}`,
670
- `Currently active tools: ${tools.filter((tool) => activeTools.has(tool.name)).length}/${tools.length}`,
671
- "",
672
- "Tools:",
673
- ];
671
+ type PiNotifyBooleanConfigKey =
672
+ | "notify-enabled"
673
+ | "notify-on-completed"
674
+ | "notify-on-interrupted"
675
+ | "notify-on-failed"
676
+ | "notify-sound-on-completed"
677
+ | "notify-sound-on-interrupted"
678
+ | "notify-sound-on-failed"
679
+ | "notify-pushover-enabled"
680
+ | "notify-pushover-on-completed"
681
+ | "notify-pushover-on-interrupted"
682
+ | "notify-pushover-on-failed";
674
683
 
675
- for (const tool of tools) {
676
- const configured = enabledTools.has(tool.name) ? "enabled" : "disabled";
677
- const active = activeTools.has(tool.name) ? "active" : "inactive";
678
- const source = tool.sourceInfo ? `${tool.sourceInfo.source}:${tool.sourceInfo.path}` : "unknown";
679
- lines.push(`- ${tool.name}`);
680
- lines.push(` kind: ${getPiUsereqToolKind(tool)}`);
681
- lines.push(` configured: ${configured}`);
682
- lines.push(` runtime: ${active}`);
683
- lines.push(` source: ${source}`);
684
- if (tool.description) {
685
- lines.push(` description: ${tool.description}`);
686
- }
687
- }
684
+ /**
685
+ * @brief Represents one persisted boolean notification event-toggle key.
686
+ * @details Restricts shared event-submenu mutation helpers to completed/interrupted/failed toggles and excludes global enable flags. Compile-time only and introduces no runtime cost.
687
+ */
688
+ type PiNotifyEventBooleanConfigKey = Exclude<
689
+ PiNotifyBooleanConfigKey,
690
+ "notify-enabled" | "notify-pushover-enabled"
691
+ >;
688
692
 
689
- return `${lines.join("\n")}\n`;
693
+ /**
694
+ * @brief Represents one shared prompt-end event identifier used by notification menus.
695
+ * @details Restricts event-submenu rendering to the canonical completed/interrupted/failed domain shared by command-notify, sound, and Pushover routing. Compile-time only and introduces no runtime cost.
696
+ */
697
+ type PiNotifyEventId = "completed" | "interrupted" | "failed";
698
+
699
+ /**
700
+ * @brief Describes one shared prompt-end event row rendered inside notification event submenus.
701
+ * @details Binds one canonical event identifier to the human-readable label and terminal-outcome description reused across command-notify, sound, and Pushover event menus. The interface is compile-time only and introduces no runtime cost.
702
+ */
703
+ interface PiNotifyEventRowDefinition {
704
+ eventId: PiNotifyEventId;
705
+ label: string;
706
+ description: string;
690
707
  }
691
708
 
692
709
  /**
693
- * @brief Represents one persisted pi-notify beep flag key.
694
- * @details Restricts menu toggles to the three independent prompt-end beep
695
- * flags stored in project configuration. Compile-time only and introduces no
696
- * runtime cost.
710
+ * @brief Describes one notification-system event submenu contract.
711
+ * @details Binds the top-level launcher row, submenu title, toast prefix, and completed/interrupted/failed config keys for one notification transport. The interface is compile-time only and introduces no runtime cost.
697
712
  */
698
- type PiNotifyBeepConfigKey =
699
- | "notify-beep-on-end"
700
- | "notify-beep-on-esc"
701
- | "notify-beep-on-error";
713
+ interface PiNotifyEventMenuDefinition {
714
+ topLevelId: string;
715
+ topLevelLabel: string;
716
+ submenuTitle: string;
717
+ systemLabel: string;
718
+ keys: Record<PiNotifyEventId, PiNotifyEventBooleanConfigKey>;
719
+ }
702
720
 
703
721
  /**
704
- * @brief Flips one persisted pi-notify beep flag.
705
- * @details Negates the selected prompt-end beep flag in place and returns the resulting boolean value so callers can emit deterministic UI feedback. Runtime is O(1). Side effect: mutates `config`.
722
+ * @brief Flips one persisted boolean notification setting.
723
+ * @details Negates the selected configuration flag in place and returns the resulting boolean value so callers can emit deterministic UI feedback. Runtime is O(1). Side effect: mutates `config`.
706
724
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
707
- * @param[in] key {PiNotifyBeepConfigKey} Beep flag key to toggle.
725
+ * @param[in] key {PiNotifyBooleanConfigKey} Boolean configuration key to toggle.
708
726
  * @return {boolean} Next enabled state.
709
727
  */
710
- function togglePiNotifyBeepFlag(config: UseReqConfig, key: PiNotifyBeepConfigKey): boolean {
728
+ function togglePiNotifyFlag(config: UseReqConfig, key: PiNotifyBooleanConfigKey): boolean {
711
729
  config[key] = !config[key];
712
730
  return config[key];
713
731
  }
714
732
 
733
+ /**
734
+ * @brief Restores notification-related settings to their documented defaults.
735
+ * @details Copies the command-notify, sound, and Pushover configuration subtree from a fresh default config into the supplied mutable project config. Runtime is O(1). Side effect: mutates `config`.
736
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
737
+ * @return {void} No return value.
738
+ * @satisfies REQ-174, REQ-178, REQ-184, REQ-195, REQ-196
739
+ */
740
+ function resetPiNotifyConfigToDefaults(config: UseReqConfig): void {
741
+ const defaults = getDefaultConfig("");
742
+ config["notify-enabled"] = defaults["notify-enabled"];
743
+ config["notify-on-completed"] = defaults["notify-on-completed"];
744
+ config["notify-on-interrupted"] = defaults["notify-on-interrupted"];
745
+ config["notify-on-failed"] = defaults["notify-on-failed"];
746
+ config["notify-sound"] = defaults["notify-sound"];
747
+ config["notify-sound-on-completed"] = defaults["notify-sound-on-completed"];
748
+ config["notify-sound-on-interrupted"] = defaults["notify-sound-on-interrupted"];
749
+ config["notify-sound-on-failed"] = defaults["notify-sound-on-failed"];
750
+ config["notify-sound-toggle-shortcut"] = defaults["notify-sound-toggle-shortcut"];
751
+ config["notify-pushover-enabled"] = defaults["notify-pushover-enabled"];
752
+ config["notify-pushover-on-completed"] = defaults["notify-pushover-on-completed"];
753
+ config["notify-pushover-on-interrupted"] = defaults["notify-pushover-on-interrupted"];
754
+ config["notify-pushover-on-failed"] = defaults["notify-pushover-on-failed"];
755
+ config["notify-pushover-user-key"] = defaults["notify-pushover-user-key"];
756
+ config["notify-pushover-api-token"] = defaults["notify-pushover-api-token"];
757
+ config["notify-pushover-priority"] = defaults["notify-pushover-priority"];
758
+ config["notify-pushover-title"] = defaults["notify-pushover-title"];
759
+ config["notify-pushover-text"] = defaults["notify-pushover-text"];
760
+ config.PI_NOTIFY_CMD = defaults.PI_NOTIFY_CMD;
761
+ config.PI_NOTIFY_SOUND_LOW_CMD = defaults.PI_NOTIFY_SOUND_LOW_CMD;
762
+ config.PI_NOTIFY_SOUND_MID_CMD = defaults.PI_NOTIFY_SOUND_MID_CMD;
763
+ config.PI_NOTIFY_SOUND_HIGH_CMD = defaults.PI_NOTIFY_SOUND_HIGH_CMD;
764
+ }
765
+
715
766
  /**
716
767
  * @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.
768
+ * @details Maps the canonical `0|1` priority domain to deterministic `Normal|High` labels reused by the Pushover configuration UI. Runtime is O(1). No external state is mutated.
718
769
  * @param[in] priority {PiNotifyPushoverPriority} Persisted Pushover priority.
719
770
  * @return {string} Menu-display label.
720
- * @satisfies REQ-165
771
+ * @satisfies REQ-172
721
772
  */
722
773
  function formatPiNotifyPushoverPriority(priority: PiNotifyPushoverPriority): string {
723
- return priority === 1 ? "1=High Priority" : "0=Normal";
774
+ return priority === 1 ? "High" : "Normal";
775
+ }
776
+
777
+ /**
778
+ * @brief Defines the shared prompt-end event rows reused across notification submenus.
779
+ * @details Encodes the human-readable completed/interrupted/failed labels and terminal-state descriptions required by the command-notify, sound, and Pushover event menus. Access complexity is O(1).
780
+ * @satisfies REQ-188, REQ-198
781
+ */
782
+ const PI_NOTIFY_EVENT_ROW_DEFINITIONS: PiNotifyEventRowDefinition[] = [
783
+ {
784
+ eventId: "completed",
785
+ label: "Prompt completed",
786
+ description: "Toggle delivery when the prompt finishes without interruption or failure.",
787
+ },
788
+ {
789
+ eventId: "interrupted",
790
+ label: "Prompt interrupted",
791
+ description: "Toggle delivery when the prompt finishes with assistant stopReason `aborted`.",
792
+ },
793
+ {
794
+ eventId: "failed",
795
+ label: "Prompt failed",
796
+ description: "Toggle delivery when the prompt finishes with assistant stopReason `error`.",
797
+ },
798
+ ];
799
+
800
+ /**
801
+ * @brief Defines the shared event-submenu contracts for command-notify, sound, and Pushover.
802
+ * @details Binds each notification transport to its top-level launcher row, submenu title, toast prefix, and completed/interrupted/failed config-key set. Access complexity is O(1).
803
+ * @satisfies REQ-174, REQ-178, REQ-184, REQ-198
804
+ */
805
+ const PI_NOTIFY_EVENT_MENU_DEFINITIONS: Record<
806
+ "notification" | "sound" | "pushover",
807
+ PiNotifyEventMenuDefinition
808
+ > = {
809
+ notification: {
810
+ topLevelId: "notification-events",
811
+ topLevelLabel: "Notification events",
812
+ submenuTitle: "Notification events",
813
+ systemLabel: "Notification",
814
+ keys: {
815
+ completed: "notify-on-completed",
816
+ interrupted: "notify-on-interrupted",
817
+ failed: "notify-on-failed",
818
+ },
819
+ },
820
+ sound: {
821
+ topLevelId: "sound-events",
822
+ topLevelLabel: "Sound events",
823
+ submenuTitle: "Sound events",
824
+ systemLabel: "Sound",
825
+ keys: {
826
+ completed: "notify-sound-on-completed",
827
+ interrupted: "notify-sound-on-interrupted",
828
+ failed: "notify-sound-on-failed",
829
+ },
830
+ },
831
+ pushover: {
832
+ topLevelId: "pushover-events",
833
+ topLevelLabel: "Pushover events",
834
+ submenuTitle: "Pushover events",
835
+ systemLabel: "Pushover",
836
+ keys: {
837
+ completed: "notify-pushover-on-completed",
838
+ interrupted: "notify-pushover-on-interrupted",
839
+ failed: "notify-pushover-on-failed",
840
+ },
841
+ },
842
+ };
843
+
844
+ /**
845
+ * @brief Formats the top-level summary value for one notification event submenu.
846
+ * @details Counts enabled completed/interrupted/failed toggles for the selected transport and renders the result as `n/3 on` for right-aligned menu display. Runtime is O(1). No external state is mutated.
847
+ * @param[in] config {UseReqConfig} Effective project configuration.
848
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
849
+ * @return {string} Compact enabled-toggle summary.
850
+ * @satisfies REQ-198
851
+ */
852
+ function formatPiNotifyEventMenuSummary(
853
+ config: UseReqConfig,
854
+ eventMenu: PiNotifyEventMenuDefinition,
855
+ ): string {
856
+ const enabledCount = PI_NOTIFY_EVENT_ROW_DEFINITIONS.filter(
857
+ (row) => config[eventMenu.keys[row.eventId]] === true,
858
+ ).length;
859
+ return `${enabledCount}/3 on`;
724
860
  }
725
861
 
726
862
  /**
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.
863
+ * @brief Builds the top-level launcher row for one notification event submenu.
864
+ * @details Reuses the shared completed/interrupted/failed summary renderer so the `Notifications` menu can expose dedicated event editors for command-notify, sound, and Pushover in a uniform shape. Runtime is O(1). No external state is mutated.
729
865
  * @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
866
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
867
+ * @return {PiUsereqSettingsMenuChoice} Launcher row for the selected event submenu.
868
+ * @satisfies REQ-181, REQ-183, REQ-165, REQ-198
732
869
  */
733
- function buildPiNotifyPushoverMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
870
+ function buildPiNotifyEventLauncherChoice(
871
+ config: UseReqConfig,
872
+ eventMenu: PiNotifyEventMenuDefinition,
873
+ ): PiUsereqSettingsMenuChoice {
874
+ return {
875
+ id: eventMenu.topLevelId,
876
+ label: eventMenu.topLevelLabel,
877
+ value: formatPiNotifyEventMenuSummary(config, eventMenu),
878
+ description: `Open the ${eventMenu.submenuTitle} submenu for shared prompt-end delivery events.`,
879
+ };
880
+ }
881
+
882
+ /**
883
+ * @brief Builds the shared settings-menu choices for one notification event submenu.
884
+ * @details Serializes completed/interrupted/failed rows with right-aligned `on|off` values, then appends `Reset defaults` and `Save and close` for submenu-scoped mutation control. Runtime is O(1). No external state is mutated.
885
+ * @param[in] config {UseReqConfig} Effective project configuration.
886
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
887
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered event-submenu choice vector.
888
+ * @satisfies REQ-188, REQ-193, REQ-198
889
+ */
890
+ function buildPiNotifyEventMenuChoices(
891
+ config: UseReqConfig,
892
+ eventMenu: PiNotifyEventMenuDefinition,
893
+ ): PiUsereqSettingsMenuChoice[] {
734
894
  return [
895
+ ...PI_NOTIFY_EVENT_ROW_DEFINITIONS.map((row) => ({
896
+ id: eventMenu.keys[row.eventId],
897
+ label: row.label,
898
+ value: config[eventMenu.keys[row.eventId]] ? "on" : "off",
899
+ description: `${eventMenu.systemLabel}: ${row.description}`,
900
+ })),
735
901
  {
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.",
902
+ id: "reset-defaults",
903
+ label: "Reset defaults",
904
+ value: "",
905
+ description: `Restore the documented default ${eventMenu.systemLabel.toLowerCase()} event toggles.`,
906
+ },
907
+ {
908
+ id: "save-and-close",
909
+ label: "Save and close",
910
+ value: "",
911
+ description: "Return to the notifications menu.",
740
912
  },
913
+ ];
914
+ }
915
+
916
+ /**
917
+ * @brief Restores one notification event submenu to its documented defaults.
918
+ * @details Copies only the completed/interrupted/failed toggles referenced by the supplied submenu contract from a fresh default config into the mutable project config. Runtime is O(1). Side effect: mutates `config`.
919
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
920
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
921
+ * @return {void} No return value.
922
+ * @satisfies REQ-174, REQ-178, REQ-184, REQ-195
923
+ */
924
+ function resetPiNotifyEventMenuToDefaults(
925
+ config: UseReqConfig,
926
+ eventMenu: PiNotifyEventMenuDefinition,
927
+ ): void {
928
+ const defaults = getDefaultConfig("");
929
+ for (const key of Object.values(eventMenu.keys) as PiNotifyEventBooleanConfigKey[]) {
930
+ config[key] = defaults[key];
931
+ }
932
+ }
933
+
934
+ /**
935
+ * @brief Resolves the human-readable event label for one event-toggle config key.
936
+ * @details Matches the supplied config key against the submenu contract and returns the corresponding completed/interrupted/failed menu label for deterministic notification toasts. Runtime is O(1). No external state is mutated.
937
+ * @param[in] key {PiNotifyEventBooleanConfigKey} Event-toggle configuration key.
938
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
939
+ * @return {string} Human-readable event label.
940
+ * @satisfies REQ-188, REQ-198
941
+ */
942
+ function resolvePiNotifyEventLabel(
943
+ key: PiNotifyEventBooleanConfigKey,
944
+ eventMenu: PiNotifyEventMenuDefinition,
945
+ ): string {
946
+ return PI_NOTIFY_EVENT_ROW_DEFINITIONS.find(
947
+ (row) => eventMenu.keys[row.eventId] === key,
948
+ )?.label ?? key;
949
+ }
950
+
951
+ /**
952
+ * @brief Runs one dedicated notification event submenu.
953
+ * @details Reuses the shared settings-menu renderer to toggle completed/interrupted/failed delivery flags, preserve row focus, and apply submenu-scoped reset semantics for command-notify, sound, or Pushover events. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
954
+ * @param[in] ctx {ExtensionCommandContext} Active command context.
955
+ * @param[in,out] config {UseReqConfig} Mutable configuration object.
956
+ * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
957
+ * @return {Promise<void>} Promise resolved when the submenu closes.
958
+ * @satisfies REQ-188, REQ-192, REQ-193, REQ-195, REQ-198
959
+ */
960
+ async function configurePiNotifyEventMenu(
961
+ ctx: ExtensionCommandContext,
962
+ config: UseReqConfig,
963
+ eventMenu: PiNotifyEventMenuDefinition,
964
+ ): Promise<void> {
965
+ let focusedChoiceId: string | undefined;
966
+ while (true) {
967
+ const choice = await showPiUsereqSettingsMenu(
968
+ ctx,
969
+ eventMenu.submenuTitle,
970
+ buildPiNotifyEventMenuChoices(config, eventMenu),
971
+ { initialSelectedId: focusedChoiceId },
972
+ );
973
+ if (!choice || choice === "save-and-close") {
974
+ return;
975
+ }
976
+ focusedChoiceId = choice;
977
+ if (choice === "reset-defaults") {
978
+ resetPiNotifyEventMenuToDefaults(config, eventMenu);
979
+ ctx.ui.notify(
980
+ `Restored default ${eventMenu.systemLabel.toLowerCase()} events`,
981
+ "info",
982
+ );
983
+ continue;
984
+ }
985
+ const enabled = togglePiNotifyFlag(
986
+ config,
987
+ choice as PiNotifyEventBooleanConfigKey,
988
+ );
989
+ const eventLabel = resolvePiNotifyEventLabel(
990
+ choice as PiNotifyEventBooleanConfigKey,
991
+ eventMenu,
992
+ );
993
+ ctx.ui.notify(
994
+ `${eventMenu.systemLabel} ${eventLabel} ${enabled ? "enabled" : "disabled"}`,
995
+ "info",
996
+ );
997
+ }
998
+ }
999
+
1000
+ /**
1001
+ * @brief Builds the direct Pushover rows rendered inside `Notifications`.
1002
+ * @details Serializes the global enable flag, shared-event submenu launcher, priority, title, text, and credential rows into right-valued menu items appended after the sound-command rows. Runtime is O(1). No external state is mutated.
1003
+ * @param[in] config {UseReqConfig} Effective project configuration.
1004
+ * @return {PiUsereqSettingsMenuChoice[]} Ordered direct Pushover rows.
1005
+ * @satisfies REQ-163, REQ-165, REQ-172, REQ-184, REQ-185, REQ-198
1006
+ */
1007
+ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1008
+ return [
741
1009
  {
742
- id: "pushover-on-success",
743
- label: "Notify on success",
1010
+ id: "notify-pushover-enabled",
1011
+ label: "Enable pushover",
744
1012
  value: formatPiNotifyPushoverStatus(config),
745
- description: "Toggle Pushover delivery after successful prompt completion only.",
1013
+ description: "Enable or disable all Pushover delivery globally.",
746
1014
  },
1015
+ buildPiNotifyEventLauncherChoice(
1016
+ config,
1017
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.pushover,
1018
+ ),
747
1019
  {
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.",
1020
+ id: "notify-pushover-priority",
1021
+ label: "Pushover priority",
1022
+ value: formatPiNotifyPushoverPriority(config["notify-pushover-priority"]),
1023
+ description: "Select whether outbound Pushover messages use Normal or High priority.",
752
1024
  },
753
1025
  {
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.",
1026
+ id: "notify-pushover-title",
1027
+ label: "Pushover title",
1028
+ value: config["notify-pushover-title"],
1029
+ description: "Edit the title template used for outbound Pushover messages.",
758
1030
  },
759
1031
  {
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.",
1032
+ id: "notify-pushover-text",
1033
+ label: "Pushover text",
1034
+ value: config["notify-pushover-text"],
1035
+ description: "Edit the text template used for outbound Pushover messages.",
764
1036
  },
765
1037
  {
766
- id: "back",
767
- label: "Back",
768
- value: "",
769
- description: "Return to the notifications menu.",
1038
+ id: "notify-pushover-user-key",
1039
+ label: "Pushover User Key/Delivery Group Key",
1040
+ value: config["notify-pushover-user-key"] || "(empty)",
1041
+ description: "Edit the Pushover user key or delivery group key used for outbound requests.",
1042
+ },
1043
+ {
1044
+ id: "notify-pushover-api-token",
1045
+ label: "Pushover Token/API Token Key",
1046
+ value: config["notify-pushover-api-token"] || "(empty)",
1047
+ description: "Edit the Pushover application token used for outbound requests.",
770
1048
  },
771
1049
  ];
772
1050
  }
773
1051
 
774
1052
  /**
775
1053
  * @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.
1054
+ * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the notification menus and returns the chosen priority or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
777
1055
  * @param[in] ctx {ExtensionCommandContext} Active command context.
778
1056
  * @param[in] currentPriority {PiNotifyPushoverPriority} Persisted priority value.
779
1057
  * @return {Promise<PiNotifyPushoverPriority | undefined>} Selected priority or `undefined` when cancelled.
780
- * @satisfies REQ-165
1058
+ * @satisfies REQ-172, REQ-192
781
1059
  */
782
1060
  async function selectPiNotifyPushoverPriority(
783
1061
  ctx: ExtensionCommandContext,
@@ -786,148 +1064,92 @@ async function selectPiNotifyPushoverPriority(
786
1064
  const choice = await showPiUsereqSettingsMenu(ctx, "Pushover priority", [
787
1065
  {
788
1066
  id: "0",
789
- label: "0=Normal",
1067
+ label: "Normal",
790
1068
  value: currentPriority === 0 ? "selected" : "",
791
- description: "Send successful prompt notifications with normal Pushover priority.",
1069
+ description: "Send outbound Pushover messages with normal priority `0`.",
792
1070
  },
793
1071
  {
794
1072
  id: "1",
795
- label: "1=High Priority",
1073
+ label: "High",
796
1074
  value: currentPriority === 1 ? "selected" : "",
797
- description: "Send successful prompt notifications with high Pushover priority.",
1075
+ description: "Send outbound Pushover messages with high priority `1`.",
798
1076
  },
799
- ]);
1077
+ ], { initialSelectedId: String(currentPriority) });
800
1078
  if (!choice) {
801
1079
  return undefined;
802
1080
  }
803
1081
  return normalizePiNotifyPushoverPriority(choice);
804
1082
  }
805
1083
 
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
-
865
1084
  /**
866
1085
  * @brief Builds the shared settings-menu choices for notification configuration.
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.
1086
+ * @details Serializes command-notify, sound, and Pushover blocks with dedicated shared-event submenu launchers so the settings-menu renderer can expose one unified but modular configuration surface. Runtime is O(1) plus command-length formatting. No external state is mutated.
868
1087
  * @param[in] config {UseReqConfig} Effective project configuration.
869
1088
  * @return {PiUsereqSettingsMenuChoice[]} Ordered notification-menu choice vector.
870
- * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-163, REQ-164, REQ-165, REQ-166, REQ-172
1089
+ * @satisfies REQ-137, REQ-149, REQ-150, REQ-151, REQ-152, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-188, REQ-193, REQ-198
871
1090
  */
872
1091
  function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
873
1092
  return [
874
1093
  {
875
- id: "toggle-beep-on-success",
876
- label: "Toggle beep on success",
877
- value: config["notify-beep-on-end"] ? "on" : "off",
878
- description: "Toggle terminal beep delivery for successful prompt completion.",
1094
+ id: "notify-enabled",
1095
+ label: "Enable notification",
1096
+ value: formatPiNotifyStatus(config),
1097
+ description: "Enable or disable command-notify delivery globally.",
879
1098
  },
1099
+ buildPiNotifyEventLauncherChoice(
1100
+ config,
1101
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.notification,
1102
+ ),
880
1103
  {
881
- id: "toggle-beep-on-escape",
882
- label: "Toggle beep on escape",
883
- value: config["notify-beep-on-esc"] ? "on" : "off",
884
- description: "Toggle terminal beep delivery for escape-triggered prompt abortion.",
1104
+ id: "notify-command",
1105
+ label: "Notify command",
1106
+ value: config.PI_NOTIFY_CMD,
1107
+ description: "Edit the shell command used for command-notify delivery.",
885
1108
  },
886
1109
  {
887
- id: "toggle-beep-on-error",
888
- label: "Toggle beep on error",
889
- value: config["notify-beep-on-error"] ? "on" : "off",
890
- description: "Toggle terminal beep delivery for error-terminated prompt completion.",
891
- },
892
- {
893
- id: "selected-notify-command",
894
- label: "Selected notify command",
1110
+ id: "selected-sound-command",
1111
+ label: "Enable sound",
895
1112
  value: config["notify-sound"],
896
- description: "Select which notify command runs after successful prompt completion.",
1113
+ description: "Select which sound command level is currently active.",
897
1114
  },
1115
+ buildPiNotifyEventLauncherChoice(
1116
+ config,
1117
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.sound,
1118
+ ),
898
1119
  {
899
1120
  id: "sound-toggle-hotkey-bind",
900
1121
  label: "Sound toggle hotkey bind",
901
1122
  value: config["notify-sound-toggle-shortcut"],
902
- description: "Edit the keyboard shortcut that cycles the selected notify command.",
1123
+ description: "Edit the keyboard shortcut that cycles the selected sound command.",
903
1124
  },
904
1125
  {
905
- id: "notify-command-low",
906
- label: "Notify command (low vol.)",
1126
+ id: "sound-command-low",
1127
+ label: "Sound command (low vol.)",
907
1128
  value: config.PI_NOTIFY_SOUND_LOW_CMD,
908
- description: "Edit the shell command used when the selected notify command is `low`.",
1129
+ description: "Edit the shell command used when the selected sound command is `low`.",
909
1130
  },
910
1131
  {
911
- id: "notify-command-mid",
912
- label: "Notify command (mid vol.)",
1132
+ id: "sound-command-mid",
1133
+ label: "Sound command (mid vol.)",
913
1134
  value: config.PI_NOTIFY_SOUND_MID_CMD,
914
- description: "Edit the shell command used when the selected notify command is `mid`.",
1135
+ description: "Edit the shell command used when the selected sound command is `mid`.",
915
1136
  },
916
1137
  {
917
- id: "notify-command-high",
918
- label: "Notify command (high vol.)",
1138
+ id: "sound-command-high",
1139
+ label: "Sound command (high vol.)",
919
1140
  value: config.PI_NOTIFY_SOUND_HIGH_CMD,
920
- description: "Edit the shell command used when the selected notify command is `high`.",
1141
+ description: "Edit the shell command used when the selected sound command is `high`.",
921
1142
  },
1143
+ ...buildPiNotifyPushoverRows(config),
922
1144
  {
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.",
1145
+ id: "reset-defaults",
1146
+ label: "Reset defaults",
1147
+ value: "",
1148
+ description: "Restore the documented notification defaults for command-notify, sound, and Pushover settings.",
927
1149
  },
928
1150
  {
929
- id: "back",
930
- label: "Back",
1151
+ id: "save-and-close",
1152
+ label: "Save and close",
931
1153
  value: "",
932
1154
  description: "Return to the parent configuration menu.",
933
1155
  },
@@ -935,127 +1157,229 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
935
1157
  }
936
1158
 
937
1159
  /**
938
- * @brief Opens the shared settings-menu selector for the selected notify command.
939
- * @details Reuses the pi-usereq settings-menu renderer so notify-command selection remains stylistically aligned with the main configuration UI and returns the chosen sound level or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1160
+ * @brief Opens the shared settings-menu selector for the active sound level.
1161
+ * @details Reuses the pi-usereq settings-menu renderer so sound-level selection remains stylistically aligned with the notification menu and returns the chosen sound level or `undefined` on cancel. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
940
1162
  * @param[in] ctx {ExtensionCommandContext} Active command context.
941
- * @param[in] currentLevel {PiNotifySoundLevel} Currently selected notify command.
1163
+ * @param[in] currentLevel {PiNotifySoundLevel} Currently selected sound level.
942
1164
  * @return {Promise<PiNotifySoundLevel | undefined>} Selected sound level or `undefined` when cancelled.
943
- * @satisfies REQ-131, REQ-137, REQ-149, REQ-151, REQ-152, REQ-153, REQ-154
1165
+ * @satisfies REQ-131, REQ-179, REQ-192
944
1166
  */
945
1167
  async function selectPiNotifySoundLevel(
946
1168
  ctx: ExtensionCommandContext,
947
1169
  currentLevel: PiNotifySoundLevel,
948
1170
  ): Promise<PiNotifySoundLevel | undefined> {
949
- const choice = await showPiUsereqSettingsMenu(ctx, "Selected notify command", [
1171
+ const choice = await showPiUsereqSettingsMenu(ctx, "Enable sound", [
950
1172
  {
951
1173
  id: "none",
952
1174
  label: "none",
953
1175
  value: currentLevel === "none" ? "selected" : "",
954
- description: "Disable external notify-command execution.",
1176
+ description: "Disable sound-command delivery while preserving per-event sound toggles.",
955
1177
  },
956
1178
  {
957
1179
  id: "low",
958
1180
  label: "low",
959
1181
  value: currentLevel === "low" ? "selected" : "",
960
- description: "Run the low-volume notify command after successful completion.",
1182
+ description: "Use the low-volume sound command when sound delivery is enabled for the current event.",
961
1183
  },
962
1184
  {
963
1185
  id: "mid",
964
1186
  label: "mid",
965
1187
  value: currentLevel === "mid" ? "selected" : "",
966
- description: "Run the mid-volume notify command after successful completion.",
1188
+ description: "Use the mid-volume sound command when sound delivery is enabled for the current event.",
967
1189
  },
968
1190
  {
969
1191
  id: "high",
970
1192
  label: "high",
971
1193
  value: currentLevel === "high" ? "selected" : "",
972
- description: "Run the high-volume notify command after successful completion.",
1194
+ description: "Use the high-volume sound command when sound delivery is enabled for the current event.",
973
1195
  },
974
- ]);
1196
+ ], { initialSelectedId: currentLevel });
975
1197
  return choice ? choice as PiNotifySoundLevel : undefined;
976
1198
  }
977
1199
 
978
1200
  /**
979
1201
  * @brief Runs the interactive notification-configuration menu.
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.
1202
+ * @details Exposes command-notify, sound, and Pushover controls through the shared settings-menu renderer, delegates completed/interrupted/failed toggles to dedicated event submenus, and preserves row focus across menu re-renders. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
981
1203
  * @param[in] ctx {ExtensionCommandContext} Active command context.
982
1204
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
983
1205
  * @return {Promise<boolean>} `true` when the sound-toggle shortcut changed.
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
1206
+ * @satisfies REQ-131, REQ-133, REQ-134, REQ-137, REQ-163, REQ-164, REQ-165, REQ-172, REQ-179, REQ-181, REQ-183, REQ-184, REQ-188, REQ-192, REQ-193, REQ-195, REQ-196, REQ-198
985
1207
  */
986
1208
  async function configurePiNotifyMenu(
987
1209
  ctx: ExtensionCommandContext,
988
1210
  config: UseReqConfig,
989
1211
  ): Promise<boolean> {
990
1212
  const originalShortcut = config["notify-sound-toggle-shortcut"];
1213
+ let focusedChoiceId: string | undefined;
991
1214
  while (true) {
992
- const choice = await showPiUsereqSettingsMenu(ctx, "notifications", buildPiNotifyMenuChoices(config));
993
- if (!choice || choice === "back") {
1215
+ const choice = await showPiUsereqSettingsMenu(
1216
+ ctx,
1217
+ "Notifications",
1218
+ buildPiNotifyMenuChoices(config),
1219
+ { initialSelectedId: focusedChoiceId },
1220
+ );
1221
+ if (!choice || choice === "save-and-close") {
994
1222
  return config["notify-sound-toggle-shortcut"] !== originalShortcut;
995
1223
  }
996
- if (choice === "toggle-beep-on-success") {
997
- const enabled = togglePiNotifyBeepFlag(config, "notify-beep-on-end");
998
- ctx.ui.notify(`Beep on success ${enabled ? "enabled" : "disabled"}`, "info");
1224
+ focusedChoiceId = choice;
1225
+ if (choice === "notify-enabled" || choice === "notify-pushover-enabled") {
1226
+ const enabled = togglePiNotifyFlag(config, choice as PiNotifyBooleanConfigKey);
1227
+ const labelMap: Record<string, string> = {
1228
+ "notify-enabled": "Notification",
1229
+ "notify-pushover-enabled": "Pushover",
1230
+ };
1231
+ ctx.ui.notify(`${labelMap[choice]} ${enabled ? "enabled" : "disabled"}`, "info");
999
1232
  continue;
1000
1233
  }
1001
- if (choice === "toggle-beep-on-escape") {
1002
- const enabled = togglePiNotifyBeepFlag(config, "notify-beep-on-esc");
1003
- ctx.ui.notify(`Beep on escape ${enabled ? "enabled" : "disabled"}`, "info");
1234
+ if (choice === PI_NOTIFY_EVENT_MENU_DEFINITIONS.notification.topLevelId) {
1235
+ await configurePiNotifyEventMenu(
1236
+ ctx,
1237
+ config,
1238
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.notification,
1239
+ );
1004
1240
  continue;
1005
1241
  }
1006
- if (choice === "toggle-beep-on-error") {
1007
- const enabled = togglePiNotifyBeepFlag(config, "notify-beep-on-error");
1008
- ctx.ui.notify(`Beep on error ${enabled ? "enabled" : "disabled"}`, "info");
1242
+ if (choice === PI_NOTIFY_EVENT_MENU_DEFINITIONS.sound.topLevelId) {
1243
+ await configurePiNotifyEventMenu(
1244
+ ctx,
1245
+ config,
1246
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.sound,
1247
+ );
1009
1248
  continue;
1010
1249
  }
1011
- if (choice === "selected-notify-command") {
1250
+ if (choice === PI_NOTIFY_EVENT_MENU_DEFINITIONS.pushover.topLevelId) {
1251
+ await configurePiNotifyEventMenu(
1252
+ ctx,
1253
+ config,
1254
+ PI_NOTIFY_EVENT_MENU_DEFINITIONS.pushover,
1255
+ );
1256
+ continue;
1257
+ }
1258
+ if (choice === "notify-command") {
1259
+ const value = await ctx.ui.input("Notify command", config.PI_NOTIFY_CMD);
1260
+ if (value !== undefined) {
1261
+ config.PI_NOTIFY_CMD = normalizePiNotifyCommand(value, DEFAULT_PI_NOTIFY_CMD);
1262
+ ctx.ui.notify("Updated notify command", "info");
1263
+ }
1264
+ continue;
1265
+ }
1266
+ if (choice === "selected-sound-command") {
1012
1267
  const nextLevel = await selectPiNotifySoundLevel(ctx, config["notify-sound"]);
1013
- if (nextLevel) {
1268
+ if (nextLevel !== undefined) {
1014
1269
  config["notify-sound"] = nextLevel;
1015
- ctx.ui.notify(`Selected notify command set to ${nextLevel}`, "info");
1270
+ ctx.ui.notify(`Enable sound set to ${nextLevel}`, "info");
1016
1271
  }
1017
1272
  continue;
1018
1273
  }
1019
1274
  if (choice === "sound-toggle-hotkey-bind") {
1020
- const value = await ctx.ui.input("Sound toggle hotkey bind", config["notify-sound-toggle-shortcut"]);
1275
+ const value = await ctx.ui.input(
1276
+ "Sound toggle hotkey bind",
1277
+ config["notify-sound-toggle-shortcut"],
1278
+ );
1021
1279
  if (value?.trim()) {
1022
1280
  config["notify-sound-toggle-shortcut"] = value.trim();
1023
- ctx.ui.notify(`Sound toggle hotkey bind set to ${config["notify-sound-toggle-shortcut"]}`, "info");
1281
+ ctx.ui.notify(
1282
+ `Sound toggle hotkey bind set to ${config["notify-sound-toggle-shortcut"]}`,
1283
+ "info",
1284
+ );
1024
1285
  }
1025
1286
  continue;
1026
1287
  }
1027
- if (choice === "notify-command-low") {
1028
- const value = await ctx.ui.input("Notify command (low vol.)", config.PI_NOTIFY_SOUND_LOW_CMD);
1029
- if (value?.trim()) {
1030
- config.PI_NOTIFY_SOUND_LOW_CMD = value.trim();
1031
- ctx.ui.notify("Updated notify command (low vol.)", "info");
1288
+ if (choice === "sound-command-low") {
1289
+ const value = await ctx.ui.input("Sound command (low vol.)", config.PI_NOTIFY_SOUND_LOW_CMD);
1290
+ if (value !== undefined) {
1291
+ config.PI_NOTIFY_SOUND_LOW_CMD = normalizePiNotifyCommand(
1292
+ value,
1293
+ DEFAULT_PI_NOTIFY_SOUND_LOW_CMD,
1294
+ );
1295
+ ctx.ui.notify("Updated sound command (low vol.)", "info");
1032
1296
  }
1033
1297
  continue;
1034
1298
  }
1035
- if (choice === "notify-command-mid") {
1036
- const value = await ctx.ui.input("Notify command (mid vol.)", config.PI_NOTIFY_SOUND_MID_CMD);
1037
- if (value?.trim()) {
1038
- config.PI_NOTIFY_SOUND_MID_CMD = value.trim();
1039
- ctx.ui.notify("Updated notify command (mid vol.)", "info");
1299
+ if (choice === "sound-command-mid") {
1300
+ const value = await ctx.ui.input("Sound command (mid vol.)", config.PI_NOTIFY_SOUND_MID_CMD);
1301
+ if (value !== undefined) {
1302
+ config.PI_NOTIFY_SOUND_MID_CMD = normalizePiNotifyCommand(
1303
+ value,
1304
+ DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
1305
+ );
1306
+ ctx.ui.notify("Updated sound command (mid vol.)", "info");
1040
1307
  }
1041
1308
  continue;
1042
1309
  }
1043
- if (choice === "notify-command-high") {
1044
- const value = await ctx.ui.input("Notify command (high vol.)", config.PI_NOTIFY_SOUND_HIGH_CMD);
1045
- if (value?.trim()) {
1046
- config.PI_NOTIFY_SOUND_HIGH_CMD = value.trim();
1047
- ctx.ui.notify("Updated notify command (high vol.)", "info");
1310
+ if (choice === "sound-command-high") {
1311
+ const value = await ctx.ui.input("Sound command (high vol.)", config.PI_NOTIFY_SOUND_HIGH_CMD);
1312
+ if (value !== undefined) {
1313
+ config.PI_NOTIFY_SOUND_HIGH_CMD = normalizePiNotifyCommand(
1314
+ value,
1315
+ DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD,
1316
+ );
1317
+ ctx.ui.notify("Updated sound command (high vol.)", "info");
1318
+ }
1319
+ continue;
1320
+ }
1321
+ if (choice === "notify-pushover-priority") {
1322
+ const nextPriority = await selectPiNotifyPushoverPriority(ctx, config["notify-pushover-priority"]);
1323
+ if (nextPriority !== undefined) {
1324
+ config["notify-pushover-priority"] = nextPriority;
1325
+ ctx.ui.notify(`Pushover priority set to ${formatPiNotifyPushoverPriority(nextPriority)}`, "info");
1326
+ }
1327
+ continue;
1328
+ }
1329
+ if (choice === "notify-pushover-title") {
1330
+ const value = await ctx.ui.input("Pushover title", config["notify-pushover-title"]);
1331
+ if (value !== undefined) {
1332
+ config["notify-pushover-title"] = normalizePiNotifyTemplateValue(
1333
+ value,
1334
+ DEFAULT_PI_NOTIFY_PUSHOVER_TITLE,
1335
+ );
1336
+ ctx.ui.notify("Updated Pushover title", "info");
1337
+ }
1338
+ continue;
1339
+ }
1340
+ if (choice === "notify-pushover-text") {
1341
+ const value = await ctx.ui.input("Pushover text", config["notify-pushover-text"]);
1342
+ if (value !== undefined) {
1343
+ config["notify-pushover-text"] = normalizePiNotifyTemplateValue(
1344
+ value,
1345
+ DEFAULT_PI_NOTIFY_PUSHOVER_TEXT,
1346
+ );
1347
+ ctx.ui.notify("Updated Pushover text", "info");
1348
+ }
1349
+ continue;
1350
+ }
1351
+ if (choice === "notify-pushover-user-key") {
1352
+ const value = await ctx.ui.input(
1353
+ "Pushover User Key/Delivery Group Key",
1354
+ config["notify-pushover-user-key"],
1355
+ );
1356
+ if (value !== undefined) {
1357
+ config["notify-pushover-user-key"] = normalizePiNotifyPushoverCredential(value);
1358
+ ctx.ui.notify("Updated Pushover user key", "info");
1048
1359
  }
1049
1360
  continue;
1050
1361
  }
1051
- if (choice === "pushover-notifications") {
1052
- await configurePiNotifyPushoverMenu(ctx, config);
1362
+ if (choice === "notify-pushover-api-token") {
1363
+ const value = await ctx.ui.input(
1364
+ "Pushover Token/API Token Key",
1365
+ config["notify-pushover-api-token"],
1366
+ );
1367
+ if (value !== undefined) {
1368
+ config["notify-pushover-api-token"] = normalizePiNotifyPushoverCredential(value);
1369
+ ctx.ui.notify("Updated Pushover API token", "info");
1370
+ }
1371
+ continue;
1372
+ }
1373
+ if (choice === "reset-defaults") {
1374
+ resetPiNotifyConfigToDefaults(config);
1375
+ ctx.ui.notify("Restored notification defaults", "info");
1376
+ continue;
1053
1377
  }
1054
1378
  }
1055
1379
  }
1056
1380
 
1057
1381
  /**
1058
- * @brief Registers the configurable successful-run sound shortcut when supported.
1382
+ * @brief Registers the configurable notification-sound shortcut when supported.
1059
1383
  * @details Loads the current project config, registers one raw pi shortcut when
1060
1384
  * the runtime exposes `registerShortcut(...)`, cycles persisted sound state on
1061
1385
  * invocation, saves the config, refreshes the status bar, and emits one info
@@ -1065,7 +1389,7 @@ async function configurePiNotifyMenu(
1065
1389
  * @param[in] pi {ExtensionAPI} Active extension API instance.
1066
1390
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
1067
1391
  * @return {void} No return value.
1068
- * @satisfies REQ-131, REQ-134, REQ-136
1392
+ * @satisfies REQ-131, REQ-134, REQ-180
1069
1393
  */
1070
1394
  function registerPiNotifyShortcut(
1071
1395
  pi: ExtensionAPI,
@@ -1077,7 +1401,7 @@ function registerPiNotifyShortcut(
1077
1401
  }
1078
1402
  const config = loadProjectConfig(process.cwd());
1079
1403
  shortcutRegistrar.registerShortcut(config["notify-sound-toggle-shortcut"], {
1080
- description: "Cycle pi-usereq prompt-success sound level",
1404
+ description: "Cycle pi-usereq notification sound level",
1081
1405
  handler: async (ctx) => {
1082
1406
  const nextConfig = loadProjectConfig(ctx.cwd);
1083
1407
  nextConfig["notify-sound"] = cyclePiNotifySoundLevel(nextConfig["notify-sound"]);
@@ -1131,17 +1455,17 @@ function registerAgentTools(pi: ExtensionAPI): void {
1131
1455
  const gitPathSchema = Type.Object(
1132
1456
  {},
1133
1457
  {
1134
- description: "Input contract: no params. Output contract: JSON object with request, result, and execution. Result exposes path_key, path_value, and path_present for the cwd-derived runtime git root.",
1458
+ description: "Input contract: no params. Output contract: JSON object with result and execution. Result exposes path_value and path_present for the cwd-derived runtime git root.",
1135
1459
  },
1136
1460
  );
1137
1461
  pi.registerTool({
1138
1462
  name: "git-path",
1139
1463
  label: "git-path",
1140
- description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the resolved `git-path` value through direct-access fields instead of text-only output.",
1464
+ description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the resolved `git-path` value through direct-access fields without request echoes.",
1141
1465
  promptSnippet: "Return the structured runtime git-root payload for the current project.",
1142
1466
  promptGuidelines: [
1143
1467
  "Input contract: no params. Scope is the cwd-derived runtime path context.",
1144
- "Output contract: request + result + execution. Result exposes path_key, path_value, and path_present.",
1468
+ "Output contract: result + execution. Result exposes path_value and path_present.",
1145
1469
  "Behavior contract: git-path is derived at runtime from the current working directory and repository ancestry rules.",
1146
1470
  "Failure contract: configuration-loading failures surface through execution.code and execution.stderr_lines.",
1147
1471
  ],
@@ -1150,14 +1474,12 @@ function registerAgentTools(pi: ExtensionAPI): void {
1150
1474
  ensureBundledResourcesAccessible();
1151
1475
  const projectBase = getProjectBase(process.cwd());
1152
1476
  const config = loadProjectConfig(process.cwd());
1153
- const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1154
1477
  const result = runGitPath(projectBase, config);
1155
1478
  const payload = buildPathQueryToolPayload(
1156
1479
  "git-path",
1157
1480
  process.cwd(),
1158
1481
  projectBase,
1159
1482
  result.stdout.trimEnd(),
1160
- runtimePaths,
1161
1483
  buildToolExecutionSection(result),
1162
1484
  );
1163
1485
  return buildStructuredToolExecuteResult(payload);
@@ -1167,17 +1489,17 @@ function registerAgentTools(pi: ExtensionAPI): void {
1167
1489
  const basePathSchema = Type.Object(
1168
1490
  {},
1169
1491
  {
1170
- description: "Input contract: no params. Output contract: JSON object with request, result, and execution. Result exposes path_key, path_value, and path_present for the cwd-derived runtime base path.",
1492
+ description: "Input contract: no params. Output contract: JSON object with result and execution. Result exposes path_value and path_present for the cwd-derived runtime base path.",
1171
1493
  },
1172
1494
  );
1173
1495
  pi.registerTool({
1174
1496
  name: "get-base-path",
1175
1497
  label: "get-base-path",
1176
- description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the resolved `base-path` value through direct-access fields instead of text-only output.",
1498
+ description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the resolved `base-path` value through direct-access fields without request echoes.",
1177
1499
  promptSnippet: "Return the structured runtime project-base payload.",
1178
1500
  promptGuidelines: [
1179
1501
  "Input contract: no params. Scope is the cwd-derived runtime path context.",
1180
- "Output contract: request + result + execution. Result exposes path_key, path_value, and path_present.",
1502
+ "Output contract: result + execution. Result exposes path_value and path_present.",
1181
1503
  "Behavior contract: base-path equals the current working directory used by the extension command or tool.",
1182
1504
  "Failure contract: configuration-loading failures surface through execution.code and execution.stderr_lines.",
1183
1505
  ],
@@ -1185,14 +1507,12 @@ function registerAgentTools(pi: ExtensionAPI): void {
1185
1507
  async execute() {
1186
1508
  const projectBase = getProjectBase(process.cwd());
1187
1509
  const config = loadProjectConfig(process.cwd());
1188
- const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1189
1510
  const result = runGetBasePath(projectBase, config);
1190
1511
  const payload = buildPathQueryToolPayload(
1191
1512
  "get-base-path",
1192
1513
  process.cwd(),
1193
1514
  projectBase,
1194
1515
  result.stdout.trimEnd(),
1195
- runtimePaths,
1196
1516
  buildToolExecutionSection(result),
1197
1517
  );
1198
1518
  return buildStructuredToolExecuteResult(payload);
@@ -1207,7 +1527,7 @@ function registerAgentTools(pi: ExtensionAPI): void {
1207
1527
  ),
1208
1528
  },
1209
1529
  {
1210
- description: "Input contract: files[]. Output contract: JSON object with request, summary, repository, files, and execution. File entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, standalone comments, and structured status facts. Missing or unsupported inputs become skipped entries. The tool fails when no source file can be analyzed.",
1530
+ description: "Input contract: files[]. Output contract: JSON object with summary, repository, files, and execution. File entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, standalone comments, and structured status facts. Missing or unsupported inputs become skipped entries. The tool fails when no source file can be analyzed.",
1211
1531
  },
1212
1532
  );
1213
1533
  const multiFileSchema = Type.Object(
@@ -1218,7 +1538,7 @@ function registerAgentTools(pi: ExtensionAPI): void {
1218
1538
  ),
1219
1539
  },
1220
1540
  {
1221
- description: "Input contract: files[]. Output contract: JSON object with request, summary, files, and execution. File entries expose canonical paths, detected language, configured checker modules, selection status, and error facts.",
1541
+ description: "Input contract: files[]. Output contract: JSON object with summary, files, and execution. File entries expose canonical paths, detected language, configured checker modules, selection status, and error facts.",
1222
1542
  },
1223
1543
  );
1224
1544
  const filesTokensSchema = Type.Object(
@@ -1229,42 +1549,46 @@ function registerAgentTools(pi: ExtensionAPI): void {
1229
1549
  ),
1230
1550
  },
1231
1551
  {
1232
- description: "Input contract: files[]. Output contract: JSON object with request, summary, files, guidance, and execution. File entries expose path identifiers, access facts, line ranges, sizes, token metrics, and optional heading or Doxygen metadata. Missing or non-file inputs become skipped entries. The tool fails when no processable files remain.",
1552
+ description: "Input contract: files[]. Output contract: JSON object with summary, files, and execution. File entries expose direct-access facts, token metrics, and optional heading or Doxygen metadata. Missing or non-file inputs become skipped entries. The tool fails when no processable files remain.",
1233
1553
  },
1234
1554
  );
1235
1555
 
1236
1556
  pi.registerTool({
1237
1557
  name: "files-tokens",
1238
1558
  label: "files-tokens",
1239
- description: "Scope: explicit files. Return an LLM-oriented JSON payload with request, summary, files, guidance, and execution sections. File entries expose direct-access path facts, status, line ranges, sizes, token metrics, and optional heading or Doxygen metadata.",
1559
+ description: "Scope: explicit files. Return a token-optimized JSON payload with summary, files, and execution sections. File entries expose direct-access path facts, status, size metrics, and optional heading or Doxygen metadata.",
1240
1560
  promptSnippet: "Return the structured token-analysis payload for caller-selected files.",
1241
1561
  promptGuidelines: [
1242
1562
  "Scope: explicit files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
1243
- "Output contract: request + summary + files + guidance + execution. File entries expose canonical paths, absolute paths, existence, file status, line range, line count, byte count, character count, token count, shares, and optional primary-heading or Doxygen file metadata.",
1244
- "Numeric contract: counts, sizes, shares, and line ranges remain in dedicated numeric fields; descriptive text is limited to stable reasons and guidance labels.",
1563
+ "Output contract: summary + files + execution. File entries expose canonical paths, absolute paths, existence, file status, line range, line count, byte count, character count, token count, shares, and optional primary-heading or Doxygen file metadata.",
1564
+ "Numeric contract: counts, sizes, shares, and line ranges remain in dedicated numeric fields; static request metadata and derived guidance are omitted from runtime responses.",
1245
1565
  "Behavior contract: missing or non-file inputs become skipped entries; read failures become error entries; the tool fails only when no processable files remain.",
1246
1566
  ],
1247
1567
  parameters: filesTokensSchema,
1248
1568
  async execute(_toolCallId, params) {
1249
- const payload = buildTokenToolPayload({
1250
- toolName: "files-tokens",
1251
- scope: "explicit-files",
1252
- baseDir: process.cwd(),
1253
- requestedPaths: params.files,
1254
- encodingName: TOKEN_COUNTER_ENCODING,
1255
- });
1256
- return buildTokenToolExecuteResult(payload);
1569
+ try {
1570
+ const payload = buildTokenToolPayload({
1571
+ toolName: "files-tokens",
1572
+ scope: "explicit-files",
1573
+ baseDir: process.cwd(),
1574
+ requestedPaths: params.files,
1575
+ encodingName: TOKEN_COUNTER_ENCODING,
1576
+ });
1577
+ return buildTokenToolExecuteResult(payload);
1578
+ } catch (error) {
1579
+ return buildFailedTokenToolExecuteResult(error);
1580
+ }
1257
1581
  },
1258
1582
  });
1259
1583
 
1260
1584
  pi.registerTool({
1261
1585
  name: "files-references",
1262
1586
  label: "files-references",
1263
- description: "Scope: explicit source files. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, standalone comments, and structured status facts.",
1587
+ description: "Scope: explicit source files. Return a token-optimized JSON payload with summary, repository, files, and execution sections. File entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, standalone comments, and structured status facts.",
1264
1588
  promptSnippet: "Return the structured references payload for caller-selected source files.",
1265
1589
  promptGuidelines: [
1266
1590
  "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
1267
- "Output contract: request + summary + repository + files + execution. File entries expose canonical paths, absolute paths, file status, line counts, line ranges, imports, symbols, child relationships, standalone comments, and structured Doxygen metadata.",
1591
+ "Output contract: summary + repository + files + execution. File entries expose canonical paths, absolute paths, file status, line counts, line ranges, imports, symbols, child relationships, standalone comments, and structured Doxygen metadata.",
1268
1592
  "Numeric contract: line counts, line ranges, symbol counts, import counts, comment counts, and Doxygen counts remain in dedicated numeric fields; text is limited to residual comment or signature content that cannot be split safely.",
1269
1593
  "Behavior contract: missing inputs, non-file inputs, and unsupported extensions become structured skipped entries; analysis failures become structured error entries; the tool fails only when no source file can be analyzed.",
1270
1594
  ],
@@ -1289,18 +1613,18 @@ function registerAgentTools(pi: ExtensionAPI): void {
1289
1613
  enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `compressed_source_text` and `compressed_lines[].display_text` include original source line-number prefixes" })),
1290
1614
  },
1291
1615
  {
1292
- description: "Input contract: files[] plus optional enableLineNumbers. Output contract: JSON object with request, summary, repository, files, and execution. File entries expose path identifiers, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts. Missing, unsupported, or invalid inputs become structured skipped entries. The tool fails when no file is compressed.",
1616
+ description: "Input contract: files[] plus optional enableLineNumbers. Output contract: JSON object with summary, repository, files, and execution. File entries expose path identifiers, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts. Missing, unsupported, or invalid inputs become structured skipped entries. The tool fails when no file is compressed.",
1293
1617
  },
1294
1618
  );
1295
1619
 
1296
1620
  pi.registerTool({
1297
1621
  name: "files-compress",
1298
1622
  label: "files-compress",
1299
- description: "Scope: explicit files. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose canonical paths, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts.",
1623
+ description: "Scope: explicit files. Return a token-optimized JSON payload with summary, repository, files, and execution sections. File entries expose canonical paths, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts.",
1300
1624
  promptSnippet: "Return the structured compression payload for caller-selected source files.",
1301
1625
  promptGuidelines: [
1302
1626
  "Scope: explicit source files selected by files[]; caller order is preserved; each item may be project-relative or absolute.",
1303
- "Output contract: request + summary + repository + files + execution. File entries expose canonical paths, absolute paths, line_number_mode, source line counts, source line ranges, compressed line counts, removed line counts, compressed_lines, compressed_source_text, symbols, and file_doxygen.",
1627
+ "Output contract: summary + repository + files + execution. File entries expose canonical paths, absolute paths, line_number_mode, source line counts, source line ranges, compressed line counts, removed line counts, compressed_lines, compressed_source_text, symbols, and file_doxygen.",
1304
1628
  "Line-number behavior: enableLineNumbers changes only rendered display strings; numeric source_line_number facts remain dedicated fields on compressed_lines for direct access.",
1305
1629
  "Behavior contract: missing inputs, non-file inputs, and unsupported extensions become structured skipped entries; compression failures become structured error entries; symbol-analysis failures retain compressed output with symbol_analysis_status=error; the tool fails only when no file is compressed.",
1306
1630
  ],
@@ -1335,7 +1659,7 @@ function registerAgentTools(pi: ExtensionAPI): void {
1335
1659
  pi.registerTool({
1336
1660
  name: "files-find",
1337
1661
  label: "files-find",
1338
- description: "Scope: explicit source files. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose structured statuses and match records with typed location, symbol, stripped-code, and Doxygen facts.",
1662
+ description: "Scope: explicit source files. Return a token-optimized JSON payload with summary, repository, files, and execution sections. File entries expose structured statuses and match records with typed location, symbol, stripped-code, and Doxygen facts.",
1339
1663
  promptSnippet: "Return the structured construct-search payload for caller-selected source files.",
1340
1664
  promptGuidelines: buildFindToolPromptGuidelines("explicit-files"),
1341
1665
  parameters: filesFindSchema,
@@ -1356,24 +1680,24 @@ function registerAgentTools(pi: ExtensionAPI): void {
1356
1680
  const referencesSchema = Type.Object(
1357
1681
  {},
1358
1682
  {
1359
- description: "Input contract: no params. Scope is the configured src-dir list resolved from the current project configuration. Output contract: JSON object with request, summary, repository, files, and execution. Repository exposes the structured directory tree; file entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, and status facts. The tool fails when no configured source file can be analyzed.",
1683
+ description: "Input contract: no params. Scope is the configured src-dir list resolved from the current project configuration. Output contract: JSON object with summary, repository, files, and execution. Repository exposes the structured directory tree; file entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, and status facts. The tool fails when no configured source file can be analyzed.",
1360
1684
  },
1361
1685
  );
1362
1686
  const tokensSchema = Type.Object(
1363
1687
  {},
1364
1688
  {
1365
- description: "Input contract: no params. Scope is the configured docs-dir plus canonical docs REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md. Output contract: same structured JSON shape as files-tokens, plus docs_dir_path and canonical_doc_names in request. Missing canonical docs become skipped entries. The tool fails when no processable canonical docs remain.",
1689
+ description: "Input contract: no params. Scope is the configured docs-dir plus canonical docs REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md. Output contract: same token-optimized JSON shape as files-tokens. Missing canonical docs become skipped entries. The tool fails when no processable canonical docs remain.",
1366
1690
  },
1367
1691
  );
1368
1692
 
1369
1693
  pi.registerTool({
1370
1694
  name: "references",
1371
1695
  label: "references",
1372
- description: "Scope: configured project source directories. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. The repository section exposes the structured directory tree; file entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, standalone comments, and status facts.",
1696
+ description: "Scope: configured project source directories. Return a token-optimized JSON payload with summary, repository, files, and execution sections. The repository section exposes the structured directory tree; file entries expose canonical paths, numeric line ranges, imports, symbols, structured Doxygen fields, standalone comments, and status facts.",
1373
1697
  promptSnippet: "Return the structured project references payload from the configured source directories.",
1374
1698
  promptGuidelines: [
1375
1699
  "Scope: no params; resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.",
1376
- "Output contract: request + summary + repository + files + execution. Repository exposes source_directory_paths, file_canonical_paths, and directory_tree; file entries expose canonical paths, line counts, line ranges, imports, symbols, hierarchy, standalone comments, and structured Doxygen metadata.",
1700
+ "Output contract: summary + repository + files + execution. Repository exposes source_directory_paths, file_canonical_paths, and directory_tree; file entries expose canonical paths, line counts, line ranges, imports, symbols, hierarchy, standalone comments, and structured Doxygen metadata.",
1377
1701
  "Configuration contract: output changes with cwd-derived project config, src-dir values, and repository source discovery; the tool does not accept explicit file overrides.",
1378
1702
  "Behavior contract: configured source files are analyzed in deterministic order, analysis failures become structured error entries, and the tool fails when no configured source file can be analyzed.",
1379
1703
  ],
@@ -1397,18 +1721,18 @@ function registerAgentTools(pi: ExtensionAPI): void {
1397
1721
  enableLineNumbers: Type.Optional(Type.Boolean({ description: "When true, `compressed_source_text` and `compressed_lines[].display_text` include original source line-number prefixes" })),
1398
1722
  },
1399
1723
  {
1400
- description: "Input contract: optional enableLineNumbers boolean. Scope is the configured src-dir list resolved from the current project configuration. Output contract: JSON object with request, summary, repository, files, and execution. File entries expose path identifiers, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts. The tool fails when no configured source file is compressed.",
1724
+ description: "Input contract: optional enableLineNumbers boolean. Scope is the configured src-dir list resolved from the current project configuration. Output contract: JSON object with summary, repository, files, and execution. File entries expose path identifiers, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts. The tool fails when no configured source file is compressed.",
1401
1725
  },
1402
1726
  );
1403
1727
 
1404
1728
  pi.registerTool({
1405
1729
  name: "compress",
1406
1730
  label: "compress",
1407
- description: "Scope: configured project source directories. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose canonical paths, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts.",
1731
+ description: "Scope: configured project source directories. Return a token-optimized JSON payload with summary, repository, files, and execution sections. File entries expose canonical paths, source and compressed line metrics, structured compressed lines, symbols, structured Doxygen fields, and stable status facts.",
1408
1732
  promptSnippet: "Return the structured project compression payload from the configured source directories.",
1409
1733
  promptGuidelines: [
1410
1734
  "Scope: resolve src-dir from the current project configuration and scan the configured source surface from the current working directory.",
1411
- "Output contract: request + summary + repository + files + execution. Repository exposes source_directory_paths and file_canonical_paths; file entries expose line_number_mode, source line counts, source line ranges, compressed line counts, removed line counts, compressed_lines, compressed_source_text, symbols, and file_doxygen.",
1735
+ "Output contract: summary + repository + files + execution. Repository exposes source_directory_paths and file_canonical_paths; file entries expose line_number_mode, source line counts, source line ranges, compressed line counts, removed line counts, compressed_lines, compressed_source_text, symbols, and file_doxygen.",
1412
1736
  "Configuration contract: output changes with cwd-derived project config, src-dir values, and repository source discovery; the tool does not accept explicit file overrides.",
1413
1737
  "Behavior contract: configured source files are processed in deterministic order, compression failures become structured error entries, symbol-analysis failures retain compressed output with symbol_analysis_status=error, and the tool fails only when no configured source file is compressed.",
1414
1738
  ],
@@ -1443,7 +1767,7 @@ function registerAgentTools(pi: ExtensionAPI): void {
1443
1767
  pi.registerTool({
1444
1768
  name: "find",
1445
1769
  label: "find",
1446
- description: "Scope: configured project source directories. Return an LLM-oriented JSON payload with request, summary, repository, files, and execution sections. File entries expose structured statuses and match records with typed location, symbol, stripped-code, and Doxygen facts.",
1770
+ description: "Scope: configured project source directories. Return a token-optimized JSON payload with summary, repository, files, and execution sections. File entries expose structured statuses and match records with typed location, symbol, stripped-code, and Doxygen facts.",
1447
1771
  promptSnippet: "Return the structured construct-search payload from the configured source directories.",
1448
1772
  promptGuidelines: buildFindToolPromptGuidelines("configured-source-directories"),
1449
1773
  parameters: findSchema,
@@ -1468,41 +1792,45 @@ function registerAgentTools(pi: ExtensionAPI): void {
1468
1792
  pi.registerTool({
1469
1793
  name: "tokens",
1470
1794
  label: "tokens",
1471
- description: "Scope: canonical docs from the configured docs-dir. Return the same LLM-oriented JSON contract as files-tokens, plus canonical-doc selection metadata in request for REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md.",
1795
+ description: "Scope: canonical docs from the configured docs-dir. Return the same token-optimized JSON contract as files-tokens, omitting canonical-doc request echoes from runtime responses.",
1472
1796
  promptSnippet: "Return the structured token-analysis payload for canonical documentation files.",
1473
1797
  promptGuidelines: [
1474
1798
  "Scope: no params; resolve docs-dir from project config; target canonical docs REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md.",
1475
- "Output contract: request + summary + files + guidance + execution. Request includes docs_dir_path and canonical_doc_names; file entries expose direct-access path facts, line ranges, sizes, token metrics, and optional metadata.",
1476
- "Numeric contract: counts, sizes, shares, and line ranges remain in dedicated numeric fields; guidance separates source observations, derived recommendations, and actionable next-step hints.",
1799
+ "Output contract: summary + files + execution. Static docs-dir and canonical-doc selection facts remain documented in registration metadata; file entries expose direct-access path facts, line ranges, sizes, token metrics, and optional metadata.",
1800
+ "Numeric contract: counts, sizes, shares, and line ranges remain in dedicated numeric fields; derived guidance is omitted from runtime responses to reduce token cost.",
1477
1801
  "Behavior contract: missing canonical docs become skipped entries; read failures become error entries; the tool fails only when no processable canonical docs remain.",
1478
1802
  ],
1479
1803
  parameters: tokensSchema,
1480
1804
  async execute() {
1481
- const projectBase = getProjectBase(process.cwd());
1482
- const config = loadProjectConfig(process.cwd());
1483
- const docsDir = config["docs-dir"].replace(/[/\\]+$/, "");
1484
- const canonicalDocNames = ["REQUIREMENTS.md", "WORKFLOW.md", "REFERENCES.md"];
1485
- const payload = buildTokenToolPayload({
1486
- toolName: "tokens",
1487
- scope: "canonical-docs",
1488
- baseDir: projectBase,
1489
- requestedPaths: canonicalDocNames.map((name) => path.join(docsDir, name)),
1490
- docsDir,
1491
- canonicalDocNames,
1492
- encodingName: TOKEN_COUNTER_ENCODING,
1493
- });
1494
- return buildTokenToolExecuteResult(payload);
1805
+ try {
1806
+ const projectBase = getProjectBase(process.cwd());
1807
+ const config = loadProjectConfig(process.cwd());
1808
+ const docsDir = config["docs-dir"].replace(/[/\\]+$/, "");
1809
+ const canonicalDocNames = ["REQUIREMENTS.md", "WORKFLOW.md", "REFERENCES.md"];
1810
+ const payload = buildTokenToolPayload({
1811
+ toolName: "tokens",
1812
+ scope: "canonical-docs",
1813
+ baseDir: projectBase,
1814
+ requestedPaths: canonicalDocNames.map((name) => path.join(docsDir, name)),
1815
+ docsDir,
1816
+ canonicalDocNames,
1817
+ encodingName: TOKEN_COUNTER_ENCODING,
1818
+ });
1819
+ return buildTokenToolExecuteResult(payload);
1820
+ } catch (error) {
1821
+ return buildFailedTokenToolExecuteResult(error);
1822
+ }
1495
1823
  },
1496
1824
  });
1497
1825
 
1498
1826
  pi.registerTool({
1499
1827
  name: "files-static-check",
1500
1828
  label: "files-static-check",
1501
- description: "Scope: explicit files. Return a JSON-first payload with request, summary, files, and execution sections. File entries expose canonical paths, detected language, configured checker modules, selection status, and stable error facts.",
1829
+ description: "Scope: explicit files. Return a token-optimized JSON payload with summary, files, and execution sections. File entries expose canonical paths, detected language, configured checker modules, selection status, and stable error facts.",
1502
1830
  promptSnippet: "Return the structured explicit-file static-check payload for the current project configuration.",
1503
1831
  promptGuidelines: [
1504
1832
  "Input contract: files[]. Scope is explicit caller-selected files resolved from the current working directory.",
1505
- "Output contract: request + summary + files + execution. File entries expose canonical_path, language_name, configured_checker_modules, status, and error_message.",
1833
+ "Output contract: summary + files + execution. File entries expose canonical_path, language_name, configured_checker_modules, status, and error_message.",
1506
1834
  "Configuration contract: checker selection is derived from the cwd-resolved static-check configuration and file extensions only.",
1507
1835
  "Failure contract: execution.code mirrors aggregated checker failures; execution.stdout_lines and execution.stderr_lines preserve residual checker diagnostics.",
1508
1836
  ],
@@ -1510,7 +1838,6 @@ function registerAgentTools(pi: ExtensionAPI): void {
1510
1838
  async execute(_toolCallId, params) {
1511
1839
  const projectBase = getProjectBase(process.cwd());
1512
1840
  const config = loadProjectConfig(process.cwd());
1513
- const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1514
1841
  const staticCheckConfig = config["static-check"] ?? {};
1515
1842
  const result = runFilesStaticCheck(params.files, projectBase, config);
1516
1843
  const payload = buildStaticCheckToolPayload(
@@ -1521,7 +1848,6 @@ function registerAgentTools(pi: ExtensionAPI): void {
1521
1848
  [],
1522
1849
  [],
1523
1850
  staticCheckConfig,
1524
- runtimePaths,
1525
1851
  buildToolExecutionSection(result),
1526
1852
  );
1527
1853
  return buildStructuredToolExecuteResult(payload);
@@ -1531,17 +1857,17 @@ function registerAgentTools(pi: ExtensionAPI): void {
1531
1857
  const staticCheckSchema = Type.Object(
1532
1858
  {},
1533
1859
  {
1534
- description: "Input contract: no params. Scope is the configured src-dir plus tests-dir selection after fixture exclusion. Output contract: JSON object with request, summary, files, and execution.",
1860
+ description: "Input contract: no params. Scope is the configured src-dir plus tests-dir selection after fixture exclusion. Output contract: JSON object with summary, files, and execution.",
1535
1861
  },
1536
1862
  );
1537
1863
  pi.registerTool({
1538
1864
  name: "static-check",
1539
1865
  label: "static-check",
1540
- description: "Scope: configured source and test directories. Return a JSON-first payload with request, summary, files, and execution sections. File entries expose selected-path facts, checker coverage, selection status, and residual diagnostics metadata.",
1866
+ description: "Scope: configured source and test directories. Return a token-optimized JSON payload with summary, files, and execution sections. File entries expose selected-path facts, checker coverage, selection status, and residual diagnostics metadata.",
1541
1867
  promptSnippet: "Return the structured project static-check payload for the current configuration.",
1542
1868
  promptGuidelines: [
1543
1869
  "Input contract: no params. Scope is src-dir plus tests-dir from the cwd-derived project configuration.",
1544
- "Output contract: request + summary + files + execution. Request exposes selection_directory_paths and excluded_directory_paths; file entries expose configured_checker_modules and status.",
1870
+ "Output contract: summary + files + execution. Selection-directory rules remain documented in registration metadata; file entries expose configured_checker_modules and status.",
1545
1871
  "Selection contract: tests/fixtures and <tests-dir>/fixtures are excluded before checker dispatch.",
1546
1872
  "Failure contract: execution.code mirrors aggregated checker failures or selection failures; execution.stderr_lines preserve residual diagnostics.",
1547
1873
  ],
@@ -1549,7 +1875,6 @@ function registerAgentTools(pi: ExtensionAPI): void {
1549
1875
  async execute() {
1550
1876
  const projectBase = getProjectBase(process.cwd());
1551
1877
  const config = loadProjectConfig(process.cwd());
1552
- const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1553
1878
  const staticCheckConfig = config["static-check"] ?? {};
1554
1879
  const selectionDirectoryPaths = [...config["src-dir"], config["tests-dir"]];
1555
1880
  const testsDirRel = makeRelativeIfContainsProject(config["tests-dir"], projectBase)
@@ -1577,7 +1902,6 @@ function registerAgentTools(pi: ExtensionAPI): void {
1577
1902
  selectionDirectoryPaths,
1578
1903
  excludedDirectoryPaths,
1579
1904
  staticCheckConfig,
1580
- runtimePaths,
1581
1905
  execution,
1582
1906
  );
1583
1907
  return buildStructuredToolExecuteResult(payload);
@@ -1587,17 +1911,17 @@ function registerAgentTools(pi: ExtensionAPI): void {
1587
1911
  const gitCheckSchema = Type.Object(
1588
1912
  {},
1589
1913
  {
1590
- description: "Input contract: no params. Output contract: JSON object with request, result, and execution. Result exposes git-root presence plus clean-versus-error repository status fields.",
1914
+ description: "Input contract: no params. Output contract: JSON object with result and execution. Result exposes git-path presence plus aggregate repository status fields.",
1591
1915
  },
1592
1916
  );
1593
1917
  pi.registerTool({
1594
1918
  name: "git-check",
1595
1919
  label: "git-check",
1596
- description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes repository validation status through direct fields instead of empty-success text.",
1920
+ description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes repository validation status through direct fields without request echoes.",
1597
1921
  promptSnippet: "Return the structured git-validation payload for the runtime repository.",
1598
1922
  promptGuidelines: [
1599
1923
  "Input contract: no params. Scope is the cwd-derived runtime path context.",
1600
- "Output contract: request + result + execution. Result exposes git_path_present, status, worktree_status, and head_status.",
1924
+ "Output contract: result + execution. Result exposes git_path_present and aggregate status.",
1601
1925
  "Behavior contract: the tool checks work-tree membership, porcelain cleanliness, and symbolic-or-detached HEAD validity.",
1602
1926
  "Failure contract: execution.code and execution.stderr_lines surface git-path or repository-state errors.",
1603
1927
  ],
@@ -1605,14 +1929,13 @@ function registerAgentTools(pi: ExtensionAPI): void {
1605
1929
  async execute() {
1606
1930
  const projectBase = getProjectBase(process.cwd());
1607
1931
  const config = loadProjectConfig(process.cwd());
1608
- const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1609
1932
  let execution;
1610
1933
  try {
1611
1934
  execution = buildToolExecutionSection(runGitCheck(projectBase, config));
1612
1935
  } catch (error) {
1613
1936
  execution = buildToolExecutionSection(normalizeToolFailure(error));
1614
1937
  }
1615
- const payload = buildGitCheckToolPayload(projectBase, resolveRuntimeGitPath(projectBase), runtimePaths, execution);
1938
+ const payload = buildGitCheckToolPayload(projectBase, resolveRuntimeGitPath(projectBase), execution);
1616
1939
  return buildStructuredToolExecuteResult(payload);
1617
1940
  },
1618
1941
  });
@@ -1620,17 +1943,17 @@ function registerAgentTools(pi: ExtensionAPI): void {
1620
1943
  const docsCheckSchema = Type.Object(
1621
1944
  {},
1622
1945
  {
1623
- description: "Input contract: no params. Output contract: JSON object with request, summary, files, and execution. File entries expose canonical paths, prompt_command remediation, and presence status for canonical docs.",
1946
+ description: "Input contract: no params. Output contract: JSON object with summary, files, and execution. File entries expose canonical paths, prompt_command remediation, and presence status for canonical docs.",
1624
1947
  },
1625
1948
  );
1626
1949
  pi.registerTool({
1627
1950
  name: "docs-check",
1628
1951
  label: "docs-check",
1629
- description: "Scope: canonical docs. Return a JSON-first payload with request, summary, files, and execution sections. File entries expose remediation prompt commands and direct presence facts for REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md.",
1952
+ description: "Scope: canonical docs. Return a token-optimized JSON payload with summary, files, and execution sections. File entries expose remediation prompt commands and direct presence facts for REQUIREMENTS.md, WORKFLOW.md, and REFERENCES.md.",
1630
1953
  promptSnippet: "Return the structured canonical-document validation payload.",
1631
1954
  promptGuidelines: [
1632
1955
  "Input contract: no params. Scope is docs-dir from the cwd-derived project configuration.",
1633
- "Output contract: request + summary + files + execution. File entries expose file_name, canonical_path, prompt_command, and status.",
1956
+ "Output contract: summary + files + execution. File entries expose file_name, canonical_path, prompt_command, and status.",
1634
1957
  "Specialization trigger: remediation differs per missing canonical file through prompt_command.",
1635
1958
  "Failure contract: execution.code is non-zero when any canonical document is missing; execution.stderr_lines enumerate missing files.",
1636
1959
  ],
@@ -1638,8 +1961,7 @@ function registerAgentTools(pi: ExtensionAPI): void {
1638
1961
  async execute() {
1639
1962
  const projectBase = getProjectBase(process.cwd());
1640
1963
  const config = loadProjectConfig(process.cwd());
1641
- const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1642
- const payload = buildDocsCheckToolPayload(projectBase, config["docs-dir"], runtimePaths);
1964
+ const payload = buildDocsCheckToolPayload(projectBase, config["docs-dir"]);
1643
1965
  return buildStructuredToolExecuteResult(payload);
1644
1966
  },
1645
1967
  });
@@ -1647,17 +1969,17 @@ function registerAgentTools(pi: ExtensionAPI): void {
1647
1969
  const gitWtNameSchema = Type.Object(
1648
1970
  {},
1649
1971
  {
1650
- description: "Input contract: no params. Output contract: JSON object with request, result, and execution. Result exposes worktree_name and the normative useReq naming format string.",
1972
+ description: "Input contract: no params. Output contract: JSON object with result and execution. Result exposes worktree_name when generation succeeds.",
1651
1973
  },
1652
1974
  );
1653
1975
  pi.registerTool({
1654
1976
  name: "git-wt-name",
1655
1977
  label: "git-wt-name",
1656
- description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the generated worktree name plus its normative format as direct fields.",
1978
+ description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the generated worktree name while static naming rules remain in registration metadata.",
1657
1979
  promptSnippet: "Return the structured worktree-name generation payload.",
1658
1980
  promptGuidelines: [
1659
1981
  "Input contract: no params. Scope is the cwd-derived runtime path context.",
1660
- "Output contract: request + result + execution. Result exposes worktree_name and format_text.",
1982
+ "Output contract: result + execution. Result exposes worktree_name.",
1661
1983
  "Behavior contract: generation follows useReq-<project>-<sanitized-branch>-<YYYYMMDDHHMMSS>.",
1662
1984
  "Failure contract: execution.code and execution.stderr_lines surface git-path or branch-resolution errors.",
1663
1985
  ],
@@ -1665,14 +1987,13 @@ function registerAgentTools(pi: ExtensionAPI): void {
1665
1987
  async execute() {
1666
1988
  const projectBase = getProjectBase(process.cwd());
1667
1989
  const config = loadProjectConfig(process.cwd());
1668
- const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1669
1990
  let execution;
1670
1991
  try {
1671
1992
  execution = buildToolExecutionSection(runGitWtName(projectBase, config));
1672
1993
  } catch (error) {
1673
1994
  execution = buildToolExecutionSection(normalizeToolFailure(error));
1674
1995
  }
1675
- const payload = buildWorktreeNameToolPayload(projectBase, resolveRuntimeGitPath(projectBase), runtimePaths, execution);
1996
+ const payload = buildWorktreeNameToolPayload(projectBase, resolveRuntimeGitPath(projectBase), execution);
1676
1997
  return buildStructuredToolExecuteResult(payload);
1677
1998
  },
1678
1999
  });
@@ -1682,17 +2003,17 @@ function registerAgentTools(pi: ExtensionAPI): void {
1682
2003
  wtName: Type.String({ description: "Exact target worktree name and branch name" }),
1683
2004
  },
1684
2005
  {
1685
- description: "Input contract: wtName. Output contract: JSON object with request, result, and execution. Result exposes operation, worktree_name, branch_name, derived worktree_path, and status.",
2006
+ description: "Input contract: wtName. Output contract: JSON object with result and execution. Result exposes worktree_name and derived worktree_path.",
1686
2007
  },
1687
2008
  );
1688
2009
  pi.registerTool({
1689
2010
  name: "git-wt-create",
1690
2011
  label: "git-wt-create",
1691
- description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the requested create operation, exact worktree name, derived path, and mutation status.",
2012
+ description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the exact worktree name and derived path without static operation echoes.",
1692
2013
  promptSnippet: "Return the structured worktree-creation payload for the requested name.",
1693
2014
  promptGuidelines: [
1694
2015
  "Input contract: wtName is required and must match the exact worktree/branch name to create.",
1695
- "Output contract: request + result + execution. Result exposes operation=create, worktree_name, branch_name, worktree_path, and status.",
2016
+ "Output contract: result + execution. Result exposes worktree_name and worktree_path.",
1696
2017
  "Specialization trigger: worktree_path depends on the runtime git root parent directory.",
1697
2018
  "Failure contract: execution.code and execution.stderr_lines surface invalid-name, git, or finalization errors.",
1698
2019
  ],
@@ -1700,7 +2021,6 @@ function registerAgentTools(pi: ExtensionAPI): void {
1700
2021
  async execute(_toolCallId, params) {
1701
2022
  const projectBase = getProjectBase(process.cwd());
1702
2023
  const config = loadProjectConfig(process.cwd());
1703
- const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1704
2024
  let execution;
1705
2025
  try {
1706
2026
  execution = buildToolExecutionSection(runGitWtCreate(projectBase, params.wtName, config));
@@ -1712,7 +2032,6 @@ function registerAgentTools(pi: ExtensionAPI): void {
1712
2032
  projectBase,
1713
2033
  resolveRuntimeGitPath(projectBase),
1714
2034
  params.wtName,
1715
- runtimePaths,
1716
2035
  execution,
1717
2036
  );
1718
2037
  return buildStructuredToolExecuteResult(payload);
@@ -1724,17 +2043,17 @@ function registerAgentTools(pi: ExtensionAPI): void {
1724
2043
  wtName: Type.String({ description: "Exact target worktree name and branch name" }),
1725
2044
  },
1726
2045
  {
1727
- description: "Input contract: wtName. Output contract: JSON object with request, result, and execution. Result exposes operation, worktree_name, branch_name, derived worktree_path, and status.",
2046
+ description: "Input contract: wtName. Output contract: JSON object with result and execution. Result exposes worktree_name and derived worktree_path.",
1728
2047
  },
1729
2048
  );
1730
2049
  pi.registerTool({
1731
2050
  name: "git-wt-delete",
1732
2051
  label: "git-wt-delete",
1733
- description: "Scope: current runtime path. Return a JSON-first payload with request, result, and execution sections. Result exposes the requested delete operation, exact worktree name, derived path, and mutation status.",
2052
+ description: "Scope: current runtime path. Return a token-optimized JSON payload with result and execution sections. Result exposes the exact worktree name and derived path without static operation echoes.",
1734
2053
  promptSnippet: "Return the structured worktree-deletion payload for the requested name.",
1735
2054
  promptGuidelines: [
1736
2055
  "Input contract: wtName is required and must match the exact worktree/branch name to delete.",
1737
- "Output contract: request + result + execution. Result exposes operation=delete, worktree_name, branch_name, worktree_path, and status.",
2056
+ "Output contract: result + execution. Result exposes worktree_name and worktree_path.",
1738
2057
  "Specialization trigger: worktree_path depends on the runtime git root parent directory.",
1739
2058
  "Failure contract: execution.code and execution.stderr_lines surface missing-target or deletion errors.",
1740
2059
  ],
@@ -1742,7 +2061,6 @@ function registerAgentTools(pi: ExtensionAPI): void {
1742
2061
  async execute(_toolCallId, params) {
1743
2062
  const projectBase = getProjectBase(process.cwd());
1744
2063
  const config = loadProjectConfig(process.cwd());
1745
- const runtimePaths = buildSharedRuntimePathFacts(process.cwd(), config);
1746
2064
  let execution;
1747
2065
  try {
1748
2066
  execution = buildToolExecutionSection(runGitWtDelete(projectBase, params.wtName, config));
@@ -1754,7 +2072,6 @@ function registerAgentTools(pi: ExtensionAPI): void {
1754
2072
  projectBase,
1755
2073
  resolveRuntimeGitPath(projectBase),
1756
2074
  params.wtName,
1757
- runtimePaths,
1758
2075
  execution,
1759
2076
  );
1760
2077
  return buildStructuredToolExecuteResult(payload);
@@ -1764,26 +2081,20 @@ function registerAgentTools(pi: ExtensionAPI): void {
1764
2081
 
1765
2082
  /**
1766
2083
  * @brief Builds the shared settings-menu choices for startup-tool management.
1767
- * @details Serializes startup-tool actions into right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(t) in configurable-tool count. No external state is mutated.
2084
+ * @details Serializes startup-tool actions into right-valued menu rows consumed by the shared settings-menu renderer while omitting the removed status-reference action. Runtime is O(t) in configurable-tool count. No external state is mutated.
1768
2085
  * @param[in] pi {ExtensionAPI} Active extension API instance.
1769
2086
  * @param[in] config {UseReqConfig} Effective project configuration.
1770
2087
  * @return {PiUsereqSettingsMenuChoice[]} Ordered startup-tool menu choices.
1771
- * @satisfies REQ-007, REQ-151, REQ-152, REQ-153, REQ-154
2088
+ * @satisfies REQ-007, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
1772
2089
  */
1773
2090
  function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1774
2091
  const tools = getPiUsereqStartupTools(pi);
1775
2092
  return [
1776
2093
  {
1777
- id: "show-tool-status",
1778
- label: "Show tool status",
1779
- value: `${getConfiguredEnabledPiUsereqTools(config).length}/${tools.length} enabled`,
1780
- description: "Open the full startup-tool reference report in the editor.",
1781
- },
1782
- {
1783
- id: "toggle-tool",
1784
- label: "Toggle tool",
2094
+ id: "enable-tools",
2095
+ label: "Enable tools",
1785
2096
  value: `${getConfiguredEnabledPiUsereqTools(config).length} enabled`,
1786
- description: "Open the per-tool toggle menu for configurable startup tools.",
2097
+ description: "Open the per-tool enablement menu for configurable startup tools.",
1787
2098
  },
1788
2099
  {
1789
2100
  id: "enable-all-tools",
@@ -1798,14 +2109,14 @@ function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig):
1798
2109
  description: "Disable every configurable startup tool for future session starts.",
1799
2110
  },
1800
2111
  {
1801
- id: "reset-tool-defaults",
1802
- label: "Reset configurable-tool defaults",
2112
+ id: "reset-defaults",
2113
+ label: "Reset defaults",
1803
2114
  value: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
1804
2115
  description: "Restore the documented default startup-tool selection.",
1805
2116
  },
1806
2117
  {
1807
- id: "back",
1808
- label: "Back",
2118
+ id: "save-and-close",
2119
+ label: "Save and close",
1809
2120
  value: "",
1810
2121
  description: "Return to the parent configuration menu.",
1811
2122
  },
@@ -1833,7 +2144,7 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
1833
2144
  id: "back",
1834
2145
  label: "Back",
1835
2146
  value: "",
1836
- description: "Return to the startup-tools menu.",
2147
+ description: "Return to the Enable tools menu.",
1837
2148
  },
1838
2149
  ];
1839
2150
  }
@@ -1845,23 +2156,22 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
1845
2156
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1846
2157
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
1847
2158
  * @return {Promise<void>} Promise resolved when the menu closes.
1848
- * @satisfies REQ-007, REQ-063, REQ-064, REQ-151, REQ-152, REQ-153, REQ-154
2159
+ * @satisfies REQ-007, REQ-063, REQ-064, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
1849
2160
  */
1850
2161
  async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
1851
2162
  applyConfiguredPiUsereqTools(pi, config);
2163
+ let focusedChoiceId: string | undefined;
1852
2164
  while (true) {
1853
2165
  const tools = getPiUsereqStartupTools(pi);
1854
2166
  const enabledTools = new Set(getConfiguredEnabledPiUsereqTools(config));
1855
- const choice = await showPiUsereqSettingsMenu(ctx, "startup tools", buildPiUsereqToolsMenuChoices(pi, config));
2167
+ const choice = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolsMenuChoices(pi, config), {
2168
+ initialSelectedId: focusedChoiceId,
2169
+ });
1856
2170
 
1857
- if (!choice || choice === "back") {
2171
+ if (!choice || choice === "save-and-close") {
1858
2172
  return;
1859
2173
  }
1860
-
1861
- if (choice === "show-tool-status") {
1862
- ctx.ui.setEditorText(renderPiUsereqToolsReference(pi, config));
1863
- continue;
1864
- }
2174
+ focusedChoiceId = choice;
1865
2175
 
1866
2176
  if (choice === "enable-all-tools") {
1867
2177
  setConfiguredPiUsereqTools(pi, config, tools.map((tool) => tool.name));
@@ -1875,14 +2185,14 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
1875
2185
  continue;
1876
2186
  }
1877
2187
 
1878
- if (choice === "reset-tool-defaults") {
2188
+ if (choice === "reset-defaults") {
1879
2189
  setConfiguredPiUsereqTools(pi, config, normalizeEnabledPiUsereqTools(undefined));
1880
2190
  ctx.ui.notify("Restored default configurable active tools", "info");
1881
2191
  continue;
1882
2192
  }
1883
2193
 
1884
- if (choice === "toggle-tool") {
1885
- const selectedToolName = await showPiUsereqSettingsMenu(ctx, "toggle startup tool", buildPiUsereqToolToggleChoices(pi, config));
2194
+ if (choice === "enable-tools") {
2195
+ const selectedToolName = await showPiUsereqSettingsMenu(ctx, "Enable tools", buildPiUsereqToolToggleChoices(pi, config));
1886
2196
  if (!selectedToolName || selectedToolName === "back") {
1887
2197
  continue;
1888
2198
  }
@@ -1900,20 +2210,6 @@ async function configurePiUsereqToolsMenu(pi: ExtensionAPI, ctx: ExtensionComman
1900
2210
  }
1901
2211
  }
1902
2212
 
1903
- /**
1904
- * @brief Formats one static-check configuration entry for UI display.
1905
- * @details Renders command-backed entries as `Command(cmd args...)` and all other modules as `Module(args...)`. Runtime is O(n) in parameter count. No side effects occur.
1906
- * @param[in] entry {StaticCheckEntry} Static-check configuration entry.
1907
- * @return {string} Human-readable entry summary.
1908
- */
1909
- function formatStaticCheckEntry(entry: StaticCheckEntry): string {
1910
- const params = Array.isArray(entry.params) && entry.params.length > 0 ? ` ${entry.params.join(" ")}` : "";
1911
- if (entry.module === "Command") {
1912
- return `${entry.module}(${entry.cmd ?? "?"}${params})`;
1913
- }
1914
- return `${entry.module}${params ? `(${entry.params!.join(" ")})` : ""}`;
1915
- }
1916
-
1917
2213
  /**
1918
2214
  * @brief Summarizes configured static-check languages.
1919
2215
  * @details Keeps only languages with at least one configured checker, sorts them, and emits a compact `Language (count)` list. Runtime is O(l log l). No side effects occur.
@@ -1928,80 +2224,38 @@ function formatStaticCheckLanguagesSummary(config: UseReqConfig): string {
1928
2224
  return languages.join(", ") || "(none)";
1929
2225
  }
1930
2226
 
1931
- /**
1932
- * @brief Renders the static-check configuration reference view.
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.
1934
- * @param[in] config {UseReqConfig} Effective project configuration.
1935
- * @return {string} Reference text for the editor view.
1936
- */
1937
- function renderStaticCheckReference(config: UseReqConfig): string {
1938
- const lines = ["# Static-check configuration", "", `Configured languages: ${formatStaticCheckLanguagesSummary(config)}`, ""];
1939
- const configuredLanguages = Object.entries(config["static-check"])
1940
- .filter(([, entries]) => Array.isArray(entries) && entries.length > 0)
1941
- .sort(([left], [right]) => left.localeCompare(right));
1942
-
1943
- if (configuredLanguages.length === 0) {
1944
- lines.push("Configured entries: (none)");
1945
- } else {
1946
- lines.push("Configured entries:");
1947
- for (const [language, entries] of configuredLanguages) {
1948
- lines.push(`- ${language}: ${(entries as StaticCheckEntry[]).map(formatStaticCheckEntry).join(", ")}`);
1949
- }
1950
- }
1951
-
1952
- lines.push("", "Supported languages:");
1953
- for (const { language, extensions } of getSupportedStaticCheckLanguageSupport()) {
1954
- lines.push(`- ${language}: ${extensions.join(", ")}`);
1955
- }
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
- );
1964
- return `${lines.join("\n")}\n`;
1965
- }
1966
-
1967
2227
  /**
1968
2228
  * @brief Builds the shared settings-menu choices for static-check management.
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.
2229
+ * @details Serializes guided Command-oriented static-check actions into right-valued menu rows consumed by the shared settings-menu renderer while omitting raw-spec and reference-only actions. Runtime is O(1). No external state is mutated.
1970
2230
  * @param[in] config {UseReqConfig} Effective project configuration.
1971
2231
  * @return {PiUsereqSettingsMenuChoice[]} Ordered static-check menu choices.
1972
- * @satisfies REQ-008, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154
2232
+ * @satisfies REQ-008, REQ-150, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
1973
2233
  */
1974
2234
  function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
1975
2235
  const supportedLanguageCount = getSupportedStaticCheckLanguageSupport().length;
1976
2236
  const configuredLanguageCount = Object.values(config["static-check"]).filter((entries) => entries.length > 0).length;
1977
2237
  return [
1978
2238
  {
1979
- id: "add-entry-supported-language",
1980
- label: "Add entry for supported language",
2239
+ id: "add-static-check-entry",
2240
+ label: "Add static code checker",
1981
2241
  value: `${supportedLanguageCount} languages`,
1982
- description: "Select a supported language, then configure the Command static-check executable.",
2242
+ description: "Select a supported language, then configure one Command static-check executable.",
1983
2243
  },
1984
2244
  {
1985
- id: "add-entry-raw-spec",
1986
- label: "Add entry from LANG=MODULE[,CMD[,PARAM...]]",
1987
- value: "raw spec",
1988
- description: "Enter one raw Command-based static-check specification string in canonical CLI format.",
1989
- },
1990
- {
1991
- id: "remove-language-entry",
1992
- label: "Remove language entry",
2245
+ id: "remove-static-check-entry",
2246
+ label: "Remove static code checker",
1993
2247
  value: configuredLanguageCount > 0 ? `${configuredLanguageCount} configured` : "(none)",
1994
2248
  description: "Remove every configured static-check entry for one language.",
1995
2249
  },
1996
2250
  {
1997
- id: "show-supported-languages",
1998
- label: "Show supported languages",
1999
- value: `${supportedLanguageCount} languages`,
2000
- description: "Open the static-check reference report in the editor.",
2251
+ id: "reset-defaults",
2252
+ label: "Reset defaults",
2253
+ value: configuredLanguageCount > 0 ? `${configuredLanguageCount} configured` : "(none)",
2254
+ description: "Remove every configured static-check entry and restore the default empty static-check configuration.",
2001
2255
  },
2002
2256
  {
2003
- id: "back",
2004
- label: "Back",
2257
+ id: "save-and-close",
2258
+ label: "Save and close",
2005
2259
  value: "",
2006
2260
  description: "Return to the parent configuration menu.",
2007
2261
  },
@@ -2030,7 +2284,7 @@ function buildSupportedStaticCheckLanguageChoices(config: UseReqConfig): PiUsere
2030
2284
  id: "back",
2031
2285
  label: "Back",
2032
2286
  value: "",
2033
- description: "Return to the static-check menu.",
2287
+ description: "Return to the Language static code checkers menu.",
2034
2288
  },
2035
2289
  ];
2036
2290
  }
@@ -2055,33 +2309,32 @@ function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUser
2055
2309
  id: "back",
2056
2310
  label: "Back",
2057
2311
  value: "",
2058
- description: "Return to the static-check menu.",
2312
+ description: "Return to the Language static code checkers menu.",
2059
2313
  },
2060
2314
  ];
2061
2315
  }
2062
2316
 
2063
2317
  /**
2064
2318
  * @brief Runs the interactive static-check configuration menu.
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.
2319
+ * @details Lets the user add Command entries by guided prompts, remove configured language entries, and reset the static-check configuration through the shared settings-menu renderer until the user exits. Runtime depends on user interaction count. Side effects include UI updates and config mutation.
2066
2320
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2067
2321
  * @param[in,out] config {UseReqConfig} Mutable configuration object.
2068
2322
  * @return {Promise<void>} Promise resolved when the menu closes.
2069
- * @satisfies REQ-008, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154
2323
+ * @satisfies REQ-008, REQ-150, REQ-160, REQ-161, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193, REQ-195
2070
2324
  */
2071
2325
  async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: UseReqConfig): Promise<void> {
2326
+ let focusedChoiceId: string | undefined;
2072
2327
  while (true) {
2073
- const staticChoice = await showPiUsereqSettingsMenu(ctx, "static-check", buildStaticCheckMenuChoices(config));
2328
+ const staticChoice = await showPiUsereqSettingsMenu(ctx, "Language static code checkers", buildStaticCheckMenuChoices(config), {
2329
+ initialSelectedId: focusedChoiceId,
2330
+ });
2074
2331
 
2075
- if (!staticChoice || staticChoice === "back") {
2332
+ if (!staticChoice || staticChoice === "save-and-close") {
2076
2333
  return;
2077
2334
  }
2335
+ focusedChoiceId = staticChoice;
2078
2336
 
2079
- if (staticChoice === "show-supported-languages") {
2080
- ctx.ui.setEditorText(renderStaticCheckReference(config));
2081
- continue;
2082
- }
2083
-
2084
- if (staticChoice === "add-entry-supported-language") {
2337
+ if (staticChoice === "add-static-check-entry") {
2085
2338
  const selectedLanguage = await showPiUsereqSettingsMenu(ctx, "static-check language", buildSupportedStaticCheckLanguageChoices(config));
2086
2339
  if (!selectedLanguage || selectedLanguage === "back") {
2087
2340
  continue;
@@ -2109,29 +2362,18 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
2109
2362
  continue;
2110
2363
  }
2111
2364
 
2112
- if (staticChoice === "add-entry-raw-spec") {
2113
- const spec = await ctx.ui.input("Static-check spec", "Python=Command,true");
2114
- if (!spec?.trim()) {
2115
- continue;
2116
- }
2117
- try {
2118
- const [canonicalLang, entry] = parseEnableStaticCheck(spec.trim());
2119
- config["static-check"][canonicalLang] ??= [];
2120
- config["static-check"][canonicalLang]!.push(entry);
2121
- ctx.ui.notify(`Added ${entry.module} checker for ${canonicalLang}`, "info");
2122
- } catch (error) {
2123
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
2124
- }
2125
- continue;
2126
- }
2127
-
2128
- if (staticChoice === "remove-language-entry") {
2129
- const configuredLanguage = await showPiUsereqSettingsMenu(ctx, "remove static-check language", buildConfiguredStaticCheckLanguageChoices(config));
2365
+ if (staticChoice === "remove-static-check-entry") {
2366
+ const configuredLanguage = await showPiUsereqSettingsMenu(ctx, "Remove static code checker", buildConfiguredStaticCheckLanguageChoices(config));
2130
2367
  if (!configuredLanguage || configuredLanguage === "back") {
2131
2368
  continue;
2132
2369
  }
2133
2370
  delete config["static-check"][configuredLanguage];
2134
2371
  ctx.ui.notify(`Removed static-check entries for ${configuredLanguage}`, "info");
2372
+ continue;
2373
+ }
2374
+ if (staticChoice === "reset-defaults") {
2375
+ config["static-check"] = {};
2376
+ ctx.ui.notify("Restored default static code checker configuration", "info");
2135
2377
  }
2136
2378
  }
2137
2379
  }
@@ -2142,7 +2384,7 @@ async function configureStaticCheckMenu(ctx: ExtensionCommandContext, config: Us
2142
2384
  * @param[in] cwd {string} Current working directory.
2143
2385
  * @param[in] config {UseReqConfig} Effective project configuration.
2144
2386
  * @return {PiUsereqSettingsMenuChoice[]} Ordered top-level menu choices.
2145
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162
2387
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-162, REQ-190, REQ-191, REQ-197
2146
2388
  */
2147
2389
  function buildPiUsereqMenuChoices(
2148
2390
  cwd: string,
@@ -2151,43 +2393,43 @@ function buildPiUsereqMenuChoices(
2151
2393
  return [
2152
2394
  {
2153
2395
  id: "docs-dir",
2154
- label: "docs-dir",
2396
+ label: "Document directory",
2155
2397
  value: config["docs-dir"],
2156
2398
  description: "Edit the repository-relative directory that stores REQUIREMENTS, WORKFLOW, and REFERENCES documents.",
2157
2399
  },
2158
- {
2159
- id: "tests-dir",
2160
- label: "tests-dir",
2161
- value: config["tests-dir"],
2162
- description: "Edit the repository-relative directory used for project test assets and static-check selection.",
2163
- },
2164
2400
  {
2165
2401
  id: "src-dir",
2166
- label: "src-dir",
2402
+ label: "Source-code directories",
2167
2403
  value: config["src-dir"].join(", "),
2168
2404
  description: "Manage the repository-relative source directories scanned by project-scope tools.",
2169
2405
  },
2406
+ {
2407
+ id: "tests-dir",
2408
+ label: "Unit tests directory",
2409
+ value: config["tests-dir"],
2410
+ description: "Edit the repository-relative directory used for project test assets and static-check selection.",
2411
+ },
2170
2412
  {
2171
2413
  id: "static-check",
2172
- label: "static-check",
2414
+ label: "Language static code checkers",
2173
2415
  value: formatStaticCheckLanguagesSummary(config),
2174
- description: "Manage configured static-check entries and inspect supported languages and modules.",
2416
+ description: "Manage guided Command static-check entries by language.",
2175
2417
  },
2176
2418
  {
2177
2419
  id: "startup-tools",
2178
- label: "startup tools",
2420
+ label: "Enable tools",
2179
2421
  value: `${getConfiguredEnabledPiUsereqTools(config).length} enabled`,
2180
2422
  description: "Manage which configurable tools become active during session_start.",
2181
2423
  },
2182
2424
  {
2183
2425
  id: "notifications",
2184
- label: "notifications",
2185
- value: `beep:${formatPiNotifyBeepStatus(config)} • sound:${config["notify-sound"]}`,
2186
- description: "Manage terminal beep flags, selected notify command, hotkey bind, per-level notify commands, and the nested Pushover submenu.",
2426
+ label: "Notifications",
2427
+ value: `notification:${formatPiNotifyStatus(config)} • sound:${config["notify-sound"]} • pushover:${formatPiNotifyPushoverStatus(config)}`,
2428
+ description: "Manage command-notify, sound, and Pushover settings with dedicated event submenus.",
2187
2429
  },
2188
2430
  {
2189
2431
  id: "show-config",
2190
- label: "show-config",
2432
+ label: "Show configuration",
2191
2433
  value: formatProjectConfigPathForMenu(cwd),
2192
2434
  valueTone: "dim",
2193
2435
  description: "Write the current project configuration JSON into the editor without saving additional changes.",
@@ -2212,25 +2454,31 @@ function buildPiUsereqMenuChoices(
2212
2454
  * @details Exposes add and remove actions for `src-dir` entries through right-valued menu rows consumed by the shared settings-menu renderer. Runtime is O(s) in source-directory count. No external state is mutated.
2213
2455
  * @param[in] config {UseReqConfig} Effective project configuration.
2214
2456
  * @return {PiUsereqSettingsMenuChoice[]} Ordered source-directory management choices.
2215
- * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154
2457
+ * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154, REQ-193
2216
2458
  */
2217
2459
  function buildSrcDirMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice[] {
2218
2460
  return [
2219
2461
  {
2220
2462
  id: "add-src-dir-entry",
2221
- label: "Add src-dir entry",
2463
+ label: "Add source-code directory",
2222
2464
  value: `${config["src-dir"].length} configured`,
2223
2465
  description: "Append one repository-relative source directory to the current configuration.",
2224
2466
  },
2225
2467
  {
2226
2468
  id: "remove-src-dir-entry",
2227
- label: "Remove src-dir entry",
2469
+ label: "Remove source-code directory",
2228
2470
  value: config["src-dir"].join(", "),
2229
2471
  description: "Select one configured source directory to remove from the current configuration.",
2230
2472
  },
2231
2473
  {
2232
- id: "back",
2233
- label: "Back",
2474
+ id: "reset-defaults",
2475
+ label: "Reset defaults",
2476
+ value: `${DEFAULT_SRC_DIRS.join(", ")}`,
2477
+ description: "Restore the documented default source-directory configuration.",
2478
+ },
2479
+ {
2480
+ id: "save-and-close",
2481
+ label: "Save and close",
2234
2482
  value: "",
2235
2483
  description: "Return to the parent configuration menu.",
2236
2484
  },
@@ -2268,7 +2516,7 @@ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuCh
2268
2516
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2269
2517
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
2270
2518
  * @return {Promise<void>} Promise resolved when configuration is saved and the menu closes.
2271
- * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162
2519
+ * @satisfies REQ-006, REQ-031, REQ-137, REQ-150, REQ-151, REQ-152, REQ-153, REQ-154, REQ-162, REQ-190, REQ-191, REQ-192, REQ-194, REQ-195
2272
2520
  */
2273
2521
  async function configurePiUsereq(
2274
2522
  pi: ExtensionAPI,
@@ -2284,11 +2532,13 @@ async function configurePiUsereq(
2284
2532
  renderPiUsereqStatus(statusController, ctx);
2285
2533
  };
2286
2534
 
2535
+ let focusedChoiceId: string | undefined;
2287
2536
  while (true) {
2288
2537
  const choice = await showPiUsereqSettingsMenu(
2289
2538
  ctx,
2290
2539
  "pi-usereq",
2291
2540
  buildPiUsereqMenuChoices(ctx.cwd, config),
2541
+ { initialSelectedId: focusedChoiceId },
2292
2542
  );
2293
2543
  if (!choice || choice === "save-and-close") {
2294
2544
  ensureSaved();
@@ -2298,37 +2548,47 @@ async function configurePiUsereq(
2298
2548
  }
2299
2549
  return;
2300
2550
  }
2551
+ focusedChoiceId = choice;
2301
2552
  if (choice === "docs-dir") {
2302
- const value = await ctx.ui.input("docs-dir", config["docs-dir"]);
2553
+ const value = await ctx.ui.input("Document directory", config["docs-dir"]);
2303
2554
  if (value?.trim()) config["docs-dir"] = value.trim();
2304
2555
  continue;
2305
2556
  }
2306
2557
  if (choice === "tests-dir") {
2307
- const value = await ctx.ui.input("tests-dir", config["tests-dir"]);
2558
+ const value = await ctx.ui.input("Unit tests directory", config["tests-dir"]);
2308
2559
  if (value?.trim()) config["tests-dir"] = value.trim();
2309
2560
  continue;
2310
2561
  }
2311
2562
  if (choice === "src-dir") {
2563
+ let srcFocusedChoiceId: string | undefined;
2312
2564
  while (true) {
2313
- const srcAction = await showPiUsereqSettingsMenu(ctx, "src-dir", buildSrcDirMenuChoices(config));
2314
- if (!srcAction || srcAction === "back") {
2565
+ const srcAction = await showPiUsereqSettingsMenu(ctx, "Source-code directories", buildSrcDirMenuChoices(config), {
2566
+ initialSelectedId: srcFocusedChoiceId,
2567
+ });
2568
+ if (!srcAction || srcAction === "save-and-close") {
2315
2569
  break;
2316
2570
  }
2571
+ srcFocusedChoiceId = srcAction;
2317
2572
  if (srcAction === "add-src-dir-entry") {
2318
- const value = await ctx.ui.input("New src-dir entry", "src");
2573
+ const value = await ctx.ui.input("New source-code directory", "src");
2319
2574
  if (value?.trim()) {
2320
2575
  config["src-dir"] = [...config["src-dir"], value.trim()];
2321
2576
  }
2322
2577
  continue;
2323
2578
  }
2324
2579
  if (srcAction === "remove-src-dir-entry") {
2325
- const toRemove = await showPiUsereqSettingsMenu(ctx, "remove src-dir entry", buildSrcDirRemovalChoices(config));
2580
+ const toRemove = await showPiUsereqSettingsMenu(ctx, "Remove source-code directory", buildSrcDirRemovalChoices(config));
2326
2581
  if (toRemove && toRemove !== "back") {
2327
2582
  config["src-dir"] = config["src-dir"].filter((entry) => entry !== toRemove);
2328
2583
  if (config["src-dir"].length === 0) {
2329
2584
  config["src-dir"] = ["src"];
2330
2585
  }
2331
2586
  }
2587
+ continue;
2588
+ }
2589
+ if (srcAction === "reset-defaults") {
2590
+ config["src-dir"] = [...DEFAULT_SRC_DIRS];
2591
+ ctx.ui.notify("Restored default source-code directories", "info");
2332
2592
  }
2333
2593
  }
2334
2594
  continue;
@@ -2348,6 +2608,7 @@ async function configurePiUsereq(
2348
2608
  if (choice === "reset-defaults") {
2349
2609
  config = getDefaultConfig(projectBase);
2350
2610
  applyConfiguredPiUsereqTools(pi, config);
2611
+ ctx.ui.notify("Restored all default configuration values", "info");
2351
2612
  continue;
2352
2613
  }
2353
2614
  if (choice === "show-config") {
@@ -2381,7 +2642,7 @@ function registerConfigCommands(
2381
2642
  * @brief Registers the complete pi-usereq extension.
2382
2643
  * @details Validates installation-owned bundled resources, registers prompt and
2383
2644
  * configuration commands plus agent tools, registers the configurable
2384
- * successful-run sound shortcut when the runtime supports shortcuts, and
2645
+ * notification-sound shortcut when the runtime supports shortcuts, and
2385
2646
  * installs shared wrappers for all supported pi lifecycle hooks so status
2386
2647
  * telemetry, context usage, prompt timing, cumulative runtime, prompt-specific
2387
2648
  * Pushover metadata, and pi-notify effects remain synchronized with runtime
@@ -2391,7 +2652,7 @@ function registerConfigCommands(
2391
2652
  * timer scheduling.
2392
2653
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2393
2654
  * @return {void} No return value.
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
2655
+ * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-045, REQ-067, REQ-068, REQ-109, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-131, REQ-132, REQ-133, REQ-134, REQ-137, REQ-148, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-174, REQ-179, REQ-180, REQ-184, REQ-188, REQ-190, REQ-191, REQ-192, REQ-193, REQ-194, REQ-195, REQ-196, REQ-197
2395
2656
  */
2396
2657
  export default function piUsereqExtension(pi: ExtensionAPI): void {
2397
2658
  const statusController = createPiUsereqStatusController();