pi-usereq 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.gitignore +3 -0
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +1 -1
  4. package/package.json +1 -1
  5. package/{req → pi-usereq}/docs/REFERENCES.md +499 -491
  6. package/{req → pi-usereq}/docs/REQUIREMENTS.md +84 -62
  7. package/{req → pi-usereq}/docs/WORKFLOW.md +87 -102
  8. package/src/core/agent-tool-json.ts +42 -193
  9. package/src/core/compress-payload.ts +7 -15
  10. package/src/core/config.ts +39 -12
  11. package/src/core/extension-status.ts +94 -58
  12. package/src/core/find-payload.ts +21 -44
  13. package/src/core/pi-notify.ts +198 -11
  14. package/src/core/reference-payload.ts +6 -14
  15. package/src/core/runtime-project-paths.ts +3 -32
  16. package/src/core/settings-menu.ts +9 -4
  17. package/src/core/static-check.ts +35 -171
  18. package/src/core/token-counter.ts +3 -121
  19. package/src/index.ts +339 -180
  20. package/tests/attended-results-scenarios.ts +3 -13
  21. package/tests/cli-command-option-parity.test.ts +21 -12
  22. package/tests/debug-extension-harness.test.ts +20 -22
  23. package/tests/extension-registration.test.ts +394 -157
  24. package/tests/oracle-project.test.ts +8 -3
  25. package/tests/oracle-standalone.test.ts +7 -13
  26. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_c.c.json +0 -5
  27. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_cpp.cpp.json +0 -5
  28. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_csharp.cs.json +0 -5
  29. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_elixir.ex.json +0 -5
  30. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_go.go.json +0 -5
  31. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_haskell.hs.json +0 -5
  32. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_java.java.json +0 -5
  33. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_javascript.js.json +0 -5
  34. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_kotlin.kt.json +0 -5
  35. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_lua.lua.json +0 -5
  36. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_perl.pl.json +0 -5
  37. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_php.php.json +0 -5
  38. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_python.py.json +0 -5
  39. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_ruby.rb.json +0 -5
  40. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_rust.rs.json +0 -5
  41. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_scala.scala.json +0 -5
  42. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_shell.sh.json +0 -5
  43. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_swift.swift.json +0 -5
  44. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_typescript.ts.json +0 -5
  45. package/tests/fixtures_attended_results/standalone/test-static-check-pylance/fixture_zig.zig.json +0 -5
  46. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_c.c.json +0 -5
  47. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_cpp.cpp.json +0 -5
  48. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_csharp.cs.json +0 -5
  49. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_elixir.ex.json +0 -5
  50. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_go.go.json +0 -5
  51. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_haskell.hs.json +0 -5
  52. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_java.java.json +0 -5
  53. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_javascript.js.json +0 -5
  54. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_kotlin.kt.json +0 -5
  55. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_lua.lua.json +0 -5
  56. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_perl.pl.json +0 -5
  57. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_php.php.json +0 -5
  58. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_python.py.json +0 -5
  59. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_ruby.rb.json +0 -5
  60. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_rust.rs.json +0 -5
  61. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_scala.scala.json +0 -5
  62. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_shell.sh.json +0 -5
  63. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_swift.swift.json +0 -5
  64. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_typescript.ts.json +0 -5
  65. package/tests/fixtures_attended_results/standalone/test-static-check-ruff/fixture_zig.zig.json +0 -5
@@ -19,6 +19,8 @@ import {
19
19
  DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
20
20
  DEFAULT_PI_NOTIFY_SOUND_TOGGLE_SHORTCUT,
21
21
  normalizePiNotifyCommand,
22
+ normalizePiNotifyPushoverCredential,
23
+ normalizePiNotifyPushoverPriority,
22
24
  normalizePiNotifyShortcut,
23
25
  normalizePiNotifySoundLevel,
24
26
  } from "./pi-notify.js";
@@ -50,6 +52,11 @@ export interface UseReqConfig {
50
52
  "notify-beep-on-error": boolean;
51
53
  "notify-sound": "none" | "low" | "mid" | "high";
52
54
  "notify-sound-toggle-shortcut": string;
55
+ "notify-pushover-global-disable": boolean;
56
+ "notify-pushover-on-success": boolean;
57
+ "notify-pushover-user-key": string;
58
+ "notify-pushover-api-token": string;
59
+ "notify-pushover-priority": 0 | 1;
53
60
  PI_NOTIFY_SOUND_LOW_CMD: string;
54
61
  PI_NOTIFY_SOUND_MID_CMD: string;
55
62
  PI_NOTIFY_SOUND_HIGH_CMD: string;
@@ -83,10 +90,10 @@ export function getProjectConfigPath(projectBase: string): string {
83
90
 
84
91
  /**
85
92
  * @brief Builds the default project configuration.
86
- * @details Populates canonical docs/test/source directories, the default startup tool set, and default pi-notify fields while excluding runtime-derived path metadata. Time complexity is O(n) in default tool count. No filesystem side effects occur.
93
+ * @details Populates canonical docs/test/source directories, the default startup tool set, default pi-notify fields, and default Pushover settings while excluding runtime-derived path metadata. Time complexity is O(n) in default tool count. No filesystem side effects occur.
87
94
  * @param[in] projectBase {string} Absolute project root path.
88
95
  * @return {UseReqConfig} Fresh default configuration object.
89
- * @satisfies CTN-001, CTN-012, REQ-066, REQ-146
96
+ * @satisfies CTN-001, CTN-012, REQ-066, REQ-129, REQ-146, REQ-163
90
97
  */
91
98
  export function getDefaultConfig(_projectBase: string): UseReqConfig {
92
99
  return {
@@ -95,11 +102,16 @@ export function getDefaultConfig(_projectBase: string): UseReqConfig {
95
102
  "src-dir": [...DEFAULT_SRC_DIRS],
96
103
  "static-check": {},
97
104
  "enabled-tools": normalizeEnabledPiUsereqTools(undefined),
98
- "notify-beep-on-end": false,
99
- "notify-beep-on-esc": false,
100
- "notify-beep-on-error": false,
105
+ "notify-beep-on-end": true,
106
+ "notify-beep-on-esc": true,
107
+ "notify-beep-on-error": true,
101
108
  "notify-sound": "none",
102
109
  "notify-sound-toggle-shortcut": DEFAULT_PI_NOTIFY_SOUND_TOGGLE_SHORTCUT,
110
+ "notify-pushover-global-disable": false,
111
+ "notify-pushover-on-success": false,
112
+ "notify-pushover-user-key": "",
113
+ "notify-pushover-api-token": "",
114
+ "notify-pushover-priority": 0,
103
115
  PI_NOTIFY_SOUND_LOW_CMD: DEFAULT_PI_NOTIFY_SOUND_LOW_CMD,
104
116
  PI_NOTIFY_SOUND_MID_CMD: DEFAULT_PI_NOTIFY_SOUND_MID_CMD,
105
117
  PI_NOTIFY_SOUND_HIGH_CMD: DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD,
@@ -108,11 +120,11 @@ export function getDefaultConfig(_projectBase: string): UseReqConfig {
108
120
 
109
121
  /**
110
122
  * @brief Loads and sanitizes the persisted project configuration.
111
- * @details Returns defaults when the config file does not exist. Otherwise parses JSON, validates directory and static-check field shapes, normalizes enabled tool names and pi-notify fields, and ignores removed or runtime-derived path metadata. Runtime is O(n) in config size. Side effects are limited to filesystem reads.
123
+ * @details Returns defaults when the config file does not exist. Otherwise parses JSON, validates directory and static-check field shapes, normalizes enabled tool names plus pi-notify and Pushover fields, applies enabled beep defaults for missing flag payloads, and ignores removed or runtime-derived path metadata. Runtime is O(n) in config size. Side effects are limited to filesystem reads.
112
124
  * @param[in] projectBase {string} Absolute project root path.
113
125
  * @return {UseReqConfig} Sanitized effective configuration.
114
126
  * @throws {ReqError} Throws with exit code `11` when the config file contains invalid JSON or a non-object payload.
115
- * @satisfies CTN-012, REQ-066, REQ-146
127
+ * @satisfies CTN-012, REQ-066, REQ-129, REQ-146, REQ-163
116
128
  */
117
129
  export function loadConfig(projectBase: string): UseReqConfig {
118
130
  const configPath = getProjectConfigPath(projectBase);
@@ -140,11 +152,16 @@ export function loadConfig(projectBase: string): UseReqConfig {
140
152
  ? (data["static-check"] as Record<string, StaticCheckEntry[]>)
141
153
  : {};
142
154
  const enabledTools = normalizeEnabledPiUsereqTools(data["enabled-tools"]);
143
- const notifyBeepOnEnd = data["notify-beep-on-end"] === true;
144
- const notifyBeepOnEsc = data["notify-beep-on-esc"] === true;
145
- const notifyBeepOnError = data["notify-beep-on-error"] === true;
155
+ const notifyBeepOnEnd = data["notify-beep-on-end"] !== false;
156
+ const notifyBeepOnEsc = data["notify-beep-on-esc"] !== false;
157
+ const notifyBeepOnError = data["notify-beep-on-error"] !== false;
146
158
  const notifySound = normalizePiNotifySoundLevel(data["notify-sound"]);
147
159
  const notifySoundToggleShortcut = normalizePiNotifyShortcut(data["notify-sound-toggle-shortcut"]);
160
+ const pushoverGlobalDisable = data["notify-pushover-global-disable"] === true;
161
+ const pushoverOnSuccess = data["notify-pushover-on-success"] === true;
162
+ const pushoverUserKey = normalizePiNotifyPushoverCredential(data["notify-pushover-user-key"]);
163
+ const pushoverApiToken = normalizePiNotifyPushoverCredential(data["notify-pushover-api-token"]);
164
+ const pushoverPriority = normalizePiNotifyPushoverPriority(data["notify-pushover-priority"]);
148
165
  const lowSoundCommand = normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_LOW_CMD, DEFAULT_PI_NOTIFY_SOUND_LOW_CMD);
149
166
  const midSoundCommand = normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_MID_CMD, DEFAULT_PI_NOTIFY_SOUND_MID_CMD);
150
167
  const highSoundCommand = normalizePiNotifyCommand(data.PI_NOTIFY_SOUND_HIGH_CMD, DEFAULT_PI_NOTIFY_SOUND_HIGH_CMD);
@@ -160,6 +177,11 @@ export function loadConfig(projectBase: string): UseReqConfig {
160
177
  "notify-beep-on-error": notifyBeepOnError,
161
178
  "notify-sound": notifySound,
162
179
  "notify-sound-toggle-shortcut": notifySoundToggleShortcut,
180
+ "notify-pushover-global-disable": pushoverGlobalDisable,
181
+ "notify-pushover-on-success": pushoverOnSuccess,
182
+ "notify-pushover-user-key": pushoverUserKey,
183
+ "notify-pushover-api-token": pushoverApiToken,
184
+ "notify-pushover-priority": pushoverPriority,
163
185
  PI_NOTIFY_SOUND_LOW_CMD: lowSoundCommand,
164
186
  PI_NOTIFY_SOUND_MID_CMD: midSoundCommand,
165
187
  PI_NOTIFY_SOUND_HIGH_CMD: highSoundCommand,
@@ -168,10 +190,10 @@ export function loadConfig(projectBase: string): UseReqConfig {
168
190
 
169
191
  /**
170
192
  * @brief Builds the persisted configuration payload that excludes runtime-derived fields.
171
- * @details Copies only the canonical persisted configuration keys into a fresh object so runtime-derived metadata such as `base-path` and `git-path` can never be written to disk. Runtime is O(n) in config size. No external state is mutated.
193
+ * @details Copies only the canonical persisted configuration keys into a fresh object so runtime-derived metadata such as `base-path` and `git-path` can never be written to disk while preserving notification and Pushover settings verbatim. Runtime is O(n) in config size. No external state is mutated.
172
194
  * @param[in] config {UseReqConfig} Effective configuration object.
173
195
  * @return {UseReqConfig} Persistable configuration payload.
174
- * @satisfies CTN-012, REQ-146
196
+ * @satisfies CTN-012, REQ-146, REQ-163
175
197
  */
176
198
  function buildPersistedConfig(config: UseReqConfig): UseReqConfig {
177
199
  return {
@@ -194,6 +216,11 @@ function buildPersistedConfig(config: UseReqConfig): UseReqConfig {
194
216
  "notify-beep-on-error": config["notify-beep-on-error"],
195
217
  "notify-sound": config["notify-sound"],
196
218
  "notify-sound-toggle-shortcut": config["notify-sound-toggle-shortcut"],
219
+ "notify-pushover-global-disable": config["notify-pushover-global-disable"],
220
+ "notify-pushover-on-success": config["notify-pushover-on-success"],
221
+ "notify-pushover-user-key": config["notify-pushover-user-key"],
222
+ "notify-pushover-api-token": config["notify-pushover-api-token"],
223
+ "notify-pushover-priority": config["notify-pushover-priority"],
197
224
  PI_NOTIFY_SOUND_LOW_CMD: config.PI_NOTIFY_SOUND_LOW_CMD,
198
225
  PI_NOTIFY_SOUND_MID_CMD: config.PI_NOTIFY_SOUND_MID_CMD,
199
226
  PI_NOTIFY_SOUND_HIGH_CMD: config.PI_NOTIFY_SOUND_HIGH_CMD,
@@ -8,6 +8,7 @@
8
8
  * scheduling through exported controller helpers.
9
9
  */
10
10
 
11
+ import path from "node:path";
11
12
  import type {
12
13
  AgentEndEvent,
13
14
  ContextUsage,
@@ -15,8 +16,8 @@ import type {
15
16
  ThemeColor,
16
17
  } from "@mariozechner/pi-coding-agent";
17
18
  import type { UseReqConfig } from "./config.js";
18
- import { formatPiNotifyBeepStatus } from "./pi-notify.js";
19
- import { formatAbsoluteGitPath, formatBasePathRelativeToGitPath, resolveRuntimeGitPath } from "./runtime-project-paths.js";
19
+ import { normalizePathSlashes } from "./path-context.js";
20
+ import { formatPiNotifyBeepStatus, formatPiNotifyPushoverStatus } from "./pi-notify.js";
20
21
 
21
22
  /**
22
23
  * @brief Enumerates the CLI-supported theme tokens consumed by status rendering.
@@ -66,7 +67,7 @@ interface StatusThemeAdapter {
66
67
  interface ContextUsageOverlaySpec {
67
68
  backgroundColor: StatusForegroundColor;
68
69
  foregroundColor: StatusForegroundColor;
69
- text: "CLEAR" | "FULL!";
70
+ text: "◀ CLEAR ▶ " | " ◀ FULL ▶ ";
70
71
  }
71
72
 
72
73
  /**
@@ -112,28 +113,36 @@ export const PI_USEREQ_STATUS_HOOK_NAMES = [
112
113
  */
113
114
  export type PiUsereqStatusHookName = (typeof PI_USEREQ_STATUS_HOOK_NAMES)[number];
114
115
 
116
+ /**
117
+ * @brief Describes one prompt request tracked across extension command delivery and runtime execution.
118
+ * @details Stores the bundled prompt command name plus the raw argument string substituted into `%%ARGS%%` so later successful-run side effects can reconstruct prompt-specific completion notifications. The interface is compile-time only and introduces no runtime cost.
119
+ */
120
+ export interface PiUsereqPromptRequest {
121
+ promptName: string;
122
+ promptArgs: string;
123
+ }
124
+
115
125
  /**
116
126
  * @brief Stores the mutable runtime facts displayed by the status bar.
117
- * @details Persists the latest context-usage snapshot, the active run start
118
- * timestamp, and the most recent normally completed run duration. Runtime state
119
- * is mutated in-place by controller helpers. Compile-time only and introduces
120
- * no runtime cost.
127
+ * @details Persists the latest context-usage snapshot, the active run start timestamp, the most recent normally completed run duration, the accumulated duration of all normally completed runs, and prompt-request metadata carried from command dispatch into the next runtime execution. Runtime state is mutated in-place by controller helpers. Compile-time only and introduces no runtime cost.
121
128
  */
122
129
  export interface PiUsereqStatusState {
123
130
  contextUsage: ContextUsage | undefined;
124
131
  runStartTimeMs: number | undefined;
125
132
  lastRunDurationMs: number | undefined;
133
+ totalRunDurationMs: number | undefined;
134
+ pendingPromptRequest: PiUsereqPromptRequest | undefined;
135
+ activePromptRequest: PiUsereqPromptRequest | undefined;
126
136
  }
127
137
 
128
138
  /**
129
139
  * @brief Stores the controller state required for event-driven status updates.
130
140
  * @details Keeps the mutable status snapshot, the current configuration, the
131
- * latest extension context used for rendering, the active-tools provider, and
132
- * the interval handle used for live elapsed-time refreshes. Compile-time only
133
- * and introduces no runtime cost.
141
+ * latest extension context used for rendering, and the interval handle used
142
+ * for live elapsed-time refreshes. Compile-time only and introduces no runtime
143
+ * cost.
134
144
  */
135
145
  export interface PiUsereqStatusController {
136
- readonly getActiveTools: () => readonly string[];
137
146
  config: UseReqConfig | undefined;
138
147
  latestContext: ExtensionContext | undefined;
139
148
  state: PiUsereqStatusState;
@@ -264,12 +273,12 @@ function refreshContextUsage(
264
273
  }
265
274
 
266
275
  /**
267
- * @brief Counts the filled cells rendered by the 5-cell context bar.
276
+ * @brief Counts the filled cells rendered by the 10-cell context bar.
268
277
  * @details Uses ceiling semantics for positive percentages so any non-zero
269
278
  * usage occupies at least one cell and zero usage occupies none. Runtime is
270
279
  * O(1). No external state is mutated.
271
280
  * @param[in] contextUsage {ContextUsage | undefined} Normalized context snapshot.
272
- * @return {number} Filled-cell count in the inclusive range `[0, 5]`.
281
+ * @return {number} Filled-cell count in the inclusive range `[0, 10]`.
273
282
  * @satisfies REQ-122
274
283
  */
275
284
  function countFilledContextCells(
@@ -279,13 +288,13 @@ function countFilledContextCells(
279
288
  if (percent === null || percent === undefined || percent <= 0) {
280
289
  return 0;
281
290
  }
282
- return Math.min(5, Math.ceil((percent * 5) / 100));
291
+ return Math.min(10, Math.ceil((percent * 10) / 100));
283
292
  }
284
293
 
285
294
  /**
286
295
  * @brief Resolves the threshold-specific context-bar overlay when required.
287
- * @details Returns the empty-state `CLEAR` overlay when normalized context
288
- * usage is unavailable or non-positive and returns the high-water `FULL!`
296
+ * @details Returns the empty-state `◀ CLEAR ▶ ` overlay when normalized context
297
+ * usage is unavailable or non-positive and returns the centered ` ◀ FULL ▶ `
289
298
  * overlay with the active theme `error` token when usage exceeds 90 percent.
290
299
  * Runtime is O(1). No external state is mutated.
291
300
  * @param[in] contextUsage {ContextUsage | undefined} Normalized context snapshot.
@@ -300,14 +309,14 @@ function resolveContextUsageOverlay(
300
309
  return {
301
310
  backgroundColor: "accent",
302
311
  foregroundColor: "warning",
303
- text: "CLEAR",
312
+ text: "◀ CLEAR ▶ ",
304
313
  };
305
314
  }
306
315
  if (percent > 90) {
307
316
  return {
308
317
  backgroundColor: "warning",
309
318
  foregroundColor: "error",
310
- text: "FULL!",
319
+ text: " ◀ FULL ▶ ",
311
320
  };
312
321
  }
313
322
  return undefined;
@@ -335,7 +344,7 @@ function formatContextUsageOverlay(
335
344
  }
336
345
 
337
346
  /**
338
- * @brief Formats one 5-cell context-usage bar.
347
+ * @brief Formats one 10-cell context-usage bar.
339
348
  * @details Renders threshold-specific overlays for empty and high-water states;
340
349
  * otherwise renders filled cells with the theme `warning` token on an
341
350
  * accent-derived background and unfilled cells in `dim` on the same background
@@ -343,7 +352,7 @@ function formatContextUsageOverlay(
343
352
  * mutated.
344
353
  * @param[in] theme {StatusThemeAdapter} Normalized status theme.
345
354
  * @param[in] contextUsage {ContextUsage | undefined} Normalized context snapshot.
346
- * @return {string} Rendered 5-cell bar or overlay.
355
+ * @return {string} Rendered 10-cell bar or overlay.
347
356
  * @satisfies REQ-121, REQ-122, REQ-126, REQ-127, REQ-128
348
357
  */
349
358
  function formatContextUsageBar(
@@ -355,7 +364,7 @@ function formatContextUsageBar(
355
364
  return formatContextUsageOverlay(theme, overlay);
356
365
  }
357
366
  const filledCells = countFilledContextCells(contextUsage);
358
- return Array.from({ length: 5 }, (_value, index) =>
367
+ return Array.from({ length: 10 }, (_value, index) =>
359
368
  index < filledCells ? theme.filledContextCell : theme.emptyContextCell,
360
369
  ).join("");
361
370
  }
@@ -376,6 +385,44 @@ function formatStatusDuration(durationMs: number): string {
376
385
  return `${minutes}:${String(seconds).padStart(2, "0")}`;
377
386
  }
378
387
 
388
+ /**
389
+ * @brief Formats one optional completed-duration value.
390
+ * @details Returns the canonical unset placeholder `--:--` until the supplied
391
+ * timer receives a normally completed prompt duration, then delegates to
392
+ * `formatStatusDuration(...)`. Runtime is O(1). No external state is mutated.
393
+ * @param[in] durationMs {number | undefined} Optional completed-duration value.
394
+ * @return {string} Rendered duration or unset placeholder.
395
+ * @satisfies REQ-124
396
+ */
397
+ function formatCompletedStatusDuration(
398
+ durationMs: number | undefined,
399
+ ): string {
400
+ return durationMs === undefined ? "--:--" : formatStatusDuration(durationMs);
401
+ }
402
+
403
+ /**
404
+ * @brief Formats the consolidated `elapsed` status-bar value.
405
+ * @details Emits the active prompt segment `⏱︎ <active>`, the latest normally
406
+ * completed segment `⚑ <last>`, and the accumulated successful-runtime segment
407
+ * `⌛︎ <total>` with fixed spacing. Runtime is O(1). No external state is
408
+ * mutated.
409
+ * @param[in] state {PiUsereqStatusState} Mutable status state snapshot.
410
+ * @param[in] nowMs {number} Current wall-clock time in milliseconds.
411
+ * @return {string} Consolidated `elapsed` field value.
412
+ * @satisfies REQ-123, REQ-124, REQ-125, REQ-159
413
+ */
414
+ function formatElapsedStatusValue(
415
+ state: PiUsereqStatusState,
416
+ nowMs: number,
417
+ ): string {
418
+ const activeText = state.runStartTimeMs === undefined
419
+ ? "--:--"
420
+ : formatStatusDuration(nowMs - state.runStartTimeMs);
421
+ const lastText = formatCompletedStatusDuration(state.lastRunDurationMs);
422
+ const totalText = formatCompletedStatusDuration(state.totalRunDurationMs);
423
+ return ` ⏱︎ ${activeText} ⚑ ${lastText} ⌛︎ ${totalText}`;
424
+ }
425
+
379
426
  /**
380
427
  * @brief Formats one standard status-bar field.
381
428
  * @details Renders the field label in accent color and the value in warning
@@ -431,52 +478,38 @@ function didAgentEndAbort(messages: AgentEndEvent["messages"]): boolean {
431
478
 
432
479
  /**
433
480
  * @brief Builds the full single-line pi-usereq status-bar payload.
434
- * @details Renders git, base, docs, tests, src, tools, context, elapsed, last, beep, and sound fields in the canonical order with dim bullet separators and threshold-specific context-bar overlays. Runtime is O(s) in configured source-path count plus runtime git probing. No external state is mutated.
435
- * @param[in] cwd {string} Runtime working directory used for git/base path derivation.
481
+ * @details Renders base, context, elapsed, beep, sound, and pushover fields in the canonical order with dim bullet separators and threshold-specific context-bar overlays. Runtime is O(1). No external state is mutated.
482
+ * @param[in] cwd {string} Runtime working directory used for base-path derivation.
436
483
  * @param[in] config {UseReqConfig} Effective project configuration.
437
- * @param[in] activeTools {readonly string[]} Active runtime tool names.
438
484
  * @param[in] theme {StatusThemeAdapter} Normalized status theme.
439
485
  * @param[in] state {PiUsereqStatusState} Mutable status state snapshot.
440
486
  * @param[in] nowMs {number} Current wall-clock time in milliseconds.
441
487
  * @return {string} Single-line status-bar text.
442
- * @satisfies REQ-109, REQ-112, REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-135, REQ-136, REQ-147, REQ-148, REQ-156
488
+ * @satisfies REQ-109, REQ-112, REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-135, REQ-136, REQ-148, REQ-156, REQ-159, REQ-170, REQ-171
443
489
  */
444
490
  function buildPiUsereqStatusText(
445
491
  cwd: string,
446
492
  config: UseReqConfig,
447
- activeTools: readonly string[],
448
493
  theme: StatusThemeAdapter,
449
494
  state: PiUsereqStatusState,
450
495
  nowMs: number,
451
496
  ): string {
452
- const gitPath = resolveRuntimeGitPath(cwd);
453
- const gitText = formatAbsoluteGitPath(gitPath);
454
- const baseText = formatBasePathRelativeToGitPath(cwd, gitPath);
455
- const sourcePaths = config["src-dir"].join(",");
456
- const elapsedText = state.runStartTimeMs === undefined
457
- ? "idle"
458
- : formatStatusDuration(nowMs - state.runStartTimeMs);
459
- const lastText = state.lastRunDurationMs === undefined
460
- ? "N/A"
461
- : formatStatusDuration(state.lastRunDurationMs);
497
+ const baseText = normalizePathSlashes(path.resolve(cwd));
498
+ const elapsedText = formatElapsedStatusValue(state, nowMs);
462
499
  const beepText = formatPiNotifyBeepStatus(config);
463
500
  const soundText = config["notify-sound"];
501
+ const pushoverText = formatPiNotifyPushoverStatus(config);
464
502
  return [
465
- formatStatusField(theme, "git", gitText),
466
503
  formatStatusField(theme, "base", baseText),
467
- formatStatusField(theme, "docs", config["docs-dir"]),
468
- formatStatusField(theme, "tests", config["tests-dir"]),
469
- formatStatusField(theme, "src", sourcePaths),
470
- formatStatusField(theme, "tools", String(activeTools.length)),
471
504
  formatRenderedStatusField(
472
505
  theme,
473
506
  "context",
474
507
  formatContextUsageBar(theme, state.contextUsage),
475
508
  ),
476
509
  formatStatusField(theme, "elapsed", elapsedText),
477
- formatStatusField(theme, "last", lastText),
478
510
  formatStatusField(theme, "beep", beepText),
479
511
  formatStatusField(theme, "sound", soundText),
512
+ formatStatusField(theme, "pushover", pushoverText),
480
513
  ].join(theme.separator);
481
514
  }
482
515
 
@@ -525,24 +558,21 @@ function syncPiUsereqStatusTicker(
525
558
 
526
559
  /**
527
560
  * @brief Creates an empty pi-usereq status controller.
528
- * @details Initializes the mutable status snapshot, stores the active-tools
529
- * provider used by render-time tool counting, and starts with no config, no
530
- * context, and no live ticker. Runtime is O(1). No external state is mutated.
531
- * @param[in] getActiveTools {() => readonly string[]} Provider for active tools.
561
+ * @details Initializes the mutable status snapshot, including empty prompt-request tracking, and starts with no config, no context, and no live ticker. Runtime is O(1). No external state is mutated.
532
562
  * @return {PiUsereqStatusController} New status controller.
533
563
  * @satisfies DES-010
534
564
  */
535
- export function createPiUsereqStatusController(
536
- getActiveTools: () => readonly string[],
537
- ): PiUsereqStatusController {
565
+ export function createPiUsereqStatusController(): PiUsereqStatusController {
538
566
  return {
539
- getActiveTools,
540
567
  config: undefined,
541
568
  latestContext: undefined,
542
569
  state: {
543
570
  contextUsage: undefined,
544
571
  runStartTimeMs: undefined,
545
572
  lastRunDurationMs: undefined,
573
+ totalRunDurationMs: undefined,
574
+ pendingPromptRequest: undefined,
575
+ activePromptRequest: undefined,
546
576
  },
547
577
  tickHandle: undefined,
548
578
  };
@@ -574,7 +604,7 @@ export function setPiUsereqStatusConfig(
574
604
  * @param[in,out] controller {PiUsereqStatusController} Mutable status controller.
575
605
  * @param[in] ctx {ExtensionContext} Active extension context.
576
606
  * @return {void} No return value.
577
- * @satisfies REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-135, REQ-136
607
+ * @satisfies REQ-120, REQ-121, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-135, REQ-136, REQ-148, REQ-159, REQ-170, REQ-171
578
608
  */
579
609
  export function renderPiUsereqStatus(
580
610
  controller: PiUsereqStatusController,
@@ -590,7 +620,6 @@ export function renderPiUsereqStatus(
590
620
  buildPiUsereqStatusText(
591
621
  ctx.cwd,
592
622
  controller.config,
593
- controller.getActiveTools(),
594
623
  theme,
595
624
  controller.state,
596
625
  Date.now(),
@@ -601,17 +630,17 @@ export function renderPiUsereqStatus(
601
630
  /**
602
631
  * @brief Updates mutable status state for one intercepted lifecycle hook.
603
632
  * @details Refreshes stored context usage on every hook, starts run timing on
604
- * `agent_start`, captures non-aborted run duration on `agent_end`, clears live
605
- * timing on shutdown, synchronizes the live ticker, and re-renders the status
606
- * bar when configuration is available. Runtime is O(n) in `agent_end` message
607
- * count and otherwise O(1). Side effects include in-memory state mutation,
608
- * interval scheduling, and footer-status updates.
633
+ * `agent_start`, promotes pending prompt-request metadata into the active run, captures non-aborted run duration on `agent_end`, accumulates successful runtime into `Σ`, clears live timing on shutdown, synchronizes
634
+ * the live ticker, and re-renders the status bar when configuration is
635
+ * available. Runtime is O(n) in `agent_end` message count and otherwise O(1).
636
+ * Side effects include in-memory state mutation, interval scheduling, and
637
+ * footer-status updates.
609
638
  * @param[in,out] controller {PiUsereqStatusController} Mutable status controller.
610
639
  * @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
611
640
  * @param[in] event {unknown} Hook payload forwarded from the wrapper.
612
641
  * @param[in] ctx {ExtensionContext} Active extension context.
613
642
  * @return {void} No return value.
614
- * @satisfies REQ-117, REQ-118, REQ-119, REQ-123, REQ-124, REQ-125
643
+ * @satisfies REQ-117, REQ-118, REQ-119, REQ-123, REQ-124, REQ-125, REQ-159, REQ-169
615
644
  */
616
645
  export function updateExtensionStatus(
617
646
  controller: PiUsereqStatusController,
@@ -625,18 +654,25 @@ export function updateExtensionStatus(
625
654
 
626
655
  if (hookName === "agent_start") {
627
656
  controller.state.runStartTimeMs = nowMs;
657
+ controller.state.activePromptRequest = controller.state.pendingPromptRequest;
658
+ controller.state.pendingPromptRequest = undefined;
628
659
  }
629
660
 
630
661
  if (hookName === "agent_end" && controller.state.runStartTimeMs !== undefined) {
631
662
  const durationMs = nowMs - controller.state.runStartTimeMs;
632
663
  if (!didAgentEndAbort((event as AgentEndEvent).messages ?? [])) {
633
664
  controller.state.lastRunDurationMs = durationMs;
665
+ controller.state.totalRunDurationMs = controller.state.totalRunDurationMs === undefined
666
+ ? durationMs
667
+ : controller.state.totalRunDurationMs + durationMs;
634
668
  }
635
669
  controller.state.runStartTimeMs = undefined;
636
670
  }
637
671
 
638
672
  if (hookName === "session_shutdown") {
639
673
  controller.state.runStartTimeMs = undefined;
674
+ controller.state.pendingPromptRequest = undefined;
675
+ controller.state.activePromptRequest = undefined;
640
676
  }
641
677
 
642
678
  syncPiUsereqStatusTicker(controller);
@@ -156,10 +156,11 @@ export interface FindToolRequestSection {
156
156
 
157
157
  /**
158
158
  * @brief Describes the summary section of the find payload.
159
- * @details Exposes aggregate file, match, line, and Doxygen counts as numeric fields plus one stable search-status discriminator. The interface is compile-time only and introduces no runtime cost.
159
+ * @details Exposes aggregate file, match, line, and Doxygen counts as numeric fields plus one stable search-status discriminator and the normalized validation error when request parsing fails. The interface is compile-time only and introduces no runtime cost.
160
160
  */
161
161
  export interface FindToolSummarySection {
162
162
  search_status: FindSearchStatus;
163
+ validation_error_message?: string;
163
164
  processable_file_count: number;
164
165
  matched_file_count: number;
165
166
  no_match_file_count: number;
@@ -172,22 +173,20 @@ export interface FindToolSummarySection {
172
173
 
173
174
  /**
174
175
  * @brief Describes the repository section of the find payload.
175
- * @details Stores the base path, configured source-directory scope, canonical file list, and supported-tag matrix needed to specialize later searches without rereading tool descriptions. The interface is compile-time only and introduces no runtime cost.
176
+ * @details Stores the base path, configured source-directory scope, and canonical file list used during search while omitting the static supported-tag matrix because that data belongs in tool registration metadata. The interface is compile-time only and introduces no runtime cost.
176
177
  */
177
178
  export interface FindToolRepositorySection {
178
179
  root_directory_path: string;
179
180
  source_directory_paths: string[];
180
181
  file_count: number;
181
182
  file_canonical_paths: string[];
182
- supported_tags_by_language: Record<string, string[]>;
183
183
  }
184
184
 
185
185
  /**
186
186
  * @brief Describes the full agent-oriented find payload.
187
- * @details Orders the top-level sections as request, summary, repository, and files so execution metadata can be appended deterministically by the tool wrapper. The interface is compile-time only and introduces no runtime cost.
187
+ * @details Exposes only aggregate search totals, repository scope, and per-file match records, omitting request echoes and static supported-tag matrices that already belong in registration metadata. The interface is compile-time only and introduces no runtime cost.
188
188
  */
189
189
  export interface FindToolPayload {
190
- request: FindToolRequestSection;
191
190
  summary: FindToolSummarySection;
192
191
  repository: FindToolRepositorySection;
193
192
  files: FindToolFileEntry[];
@@ -262,19 +261,6 @@ function buildLineRange(startLineNumber: number, endLineNumber: number): FindLin
262
261
  };
263
262
  }
264
263
 
265
- /**
266
- * @brief Returns the supported-tag matrix ordered for deterministic JSON emission.
267
- * @details Sorts languages alphabetically and tag arrays lexicographically so downstream agents can reuse the matrix without reparsing human prose. Runtime is O(l * t log t). No side effects occur.
268
- * @return {Record<string, string[]>} Supported tags keyed by canonical language identifier.
269
- */
270
- function buildSupportedTagsByLanguage(): Record<string, string[]> {
271
- return Object.fromEntries(
272
- Object.entries(LANGUAGE_TAGS)
273
- .sort(([left], [right]) => left.localeCompare(right))
274
- .map(([language, tagSet]) => [language, [...tagSet].sort()]),
275
- );
276
- }
277
-
278
264
  /**
279
265
  * @brief Resolves one stable symbol name from an analyzed element.
280
266
  * @details Prefers explicit analyzer name metadata, then falls back to the derived signature or the first source line so every matched construct retains a direct-access identifier. Runtime is O(1). No side effects occur.
@@ -717,9 +703,9 @@ function analyzeFindFile(
717
703
 
718
704
  /**
719
705
  * @brief Builds the full agent-oriented find payload.
720
- * @details Validates request parameters, analyzes requested files in caller order when the request is valid, preserves skipped and no-match outcomes in structured file entries, computes aggregate numeric totals, and emits a structured supported-tag matrix. Runtime is O(F log F + S + M). Side effects are limited to filesystem reads and optional stderr logging.
706
+ * @details Validates request parameters, analyzes requested files in caller order when the request is valid, preserves skipped and no-match outcomes in structured file entries, computes aggregate numeric totals, and omits request echoes plus the static supported-tag matrix already encoded in registration metadata. Runtime is O(F log F + S + M). Side effects are limited to filesystem reads and optional stderr logging.
721
707
  * @param[in] options {BuildFindToolPayloadOptions} Payload-construction options.
722
- * @return {FindToolPayload} Structured find payload ordered as request, summary, repository, and files.
708
+ * @return {FindToolPayload} Structured find payload ordered as summary, repository, and files.
723
709
  * @satisfies REQ-089, REQ-090, REQ-091, REQ-092, REQ-093, REQ-094, REQ-096, REQ-098
724
710
  */
725
711
  export function buildFindToolPayload(options: BuildFindToolPayloadOptions): FindToolPayload {
@@ -835,28 +821,20 @@ export function buildFindToolPayload(options: BuildFindToolPayloadOptions): Find
835
821
  const processableFiles = files.filter((file) => file.status !== "skipped");
836
822
  const repositoryFileCanonicalPaths = files.map((file) => file.canonical_path);
837
823
 
824
+ void toolName;
825
+ void scope;
826
+ void lineNumberMode;
827
+ void tagFilter;
828
+ void pattern;
829
+ void canonicalRequestedPaths;
838
830
  return {
839
- request: {
840
- tool_name: toolName,
841
- scope,
842
- base_dir_path: absoluteBaseDir,
843
- line_number_mode: lineNumberMode,
844
- tag_filter_text: tagFilter,
845
- tag_filter_values: tagValidation.tagValues,
846
- tag_filter_status: tagValidation.status,
847
- tag_filter_error_message: tagValidation.errorMessage,
848
- name_regex_text: pattern,
849
- regex_engine: "javascript-regexp-search",
850
- regex_status: regexValidation.status,
851
- regex_error_message: regexValidation.errorMessage,
852
- source_directory_count: sourceDirectoryPaths.length,
853
- source_directory_paths: sourceDirectoryPaths.map((sourceDirectoryPath) => canonicalizeFindPath(sourceDirectoryPath, absoluteBaseDir)),
854
- requested_file_count: requestedPaths.length,
855
- requested_input_paths: [...requestedPaths],
856
- requested_canonical_paths: canonicalRequestedPaths,
857
- },
858
831
  summary: {
859
832
  search_status: searchStatus,
833
+ validation_error_message: tagValidation.status === "invalid"
834
+ ? tagValidation.errorMessage
835
+ : regexValidation.status === "invalid"
836
+ ? regexValidation.errorMessage
837
+ : undefined,
860
838
  processable_file_count: processableFiles.length,
861
839
  matched_file_count: matchedFiles.length,
862
840
  no_match_file_count: files.filter((file) => file.status === "no_match").length,
@@ -874,7 +852,6 @@ export function buildFindToolPayload(options: BuildFindToolPayloadOptions): Find
874
852
  source_directory_paths: sourceDirectoryPaths.map((sourceDirectoryPath) => canonicalizeFindPath(sourceDirectoryPath, absoluteBaseDir)),
875
853
  file_count: repositoryFileCanonicalPaths.length,
876
854
  file_canonical_paths: repositoryFileCanonicalPaths,
877
- supported_tags_by_language: buildSupportedTagsByLanguage(),
878
855
  },
879
856
  files,
880
857
  };
@@ -889,11 +866,11 @@ export function buildFindToolPayload(options: BuildFindToolPayloadOptions): Find
889
866
  */
890
867
  export function buildFindToolExecutionStderr(payload: FindToolPayload): string {
891
868
  const diagnostics: string[] = [];
892
- if (payload.request.tag_filter_status === "invalid") {
893
- diagnostics.push(`error: tag_filter: ${payload.request.tag_filter_error_message ?? "invalid tag filter"}`);
869
+ if (payload.summary.search_status === "invalid_tag_filter") {
870
+ diagnostics.push(`error: tag_filter: ${payload.summary.validation_error_message ?? "invalid tag filter"}`);
894
871
  }
895
- if (payload.request.regex_status === "invalid") {
896
- diagnostics.push(`error: name_regex: ${payload.request.regex_error_message ?? "invalid regex"}`);
872
+ if (payload.summary.search_status === "invalid_regex") {
873
+ diagnostics.push(`error: name_regex: ${payload.summary.validation_error_message ?? "invalid regex"}`);
897
874
  }
898
875
  payload.files.forEach((file) => {
899
876
  if (file.status === "skipped") {