pi-usereq 0.74.0 → 0.76.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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # PI-useReq/pi-usereq (0.74.0)
1
+ # PI-useReq/pi-usereq (0.76.0)
2
2
 
3
3
  <p align="center">
4
4
  <img src="https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python 3.11+">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-usereq",
3
- "version": "0.74.0",
3
+ "version": "0.76.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Ogekuri/PI-useReq.git"
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file
3
3
  * @brief Declares debug inventories, normalizers, and JSON log persistence helpers.
4
- * @details Centralizes debug-menu selector inventories, config-field normalization, workflow-status gating, and append-only JSON log writing for tool, prompt, and dedicated workflow debug events. Runtime is dominated by JSON serialization plus filesystem I/O during log writes. Side effects include directory creation and file overwrite when debug entries are appended.
4
+ * @details Centralizes debug-menu selector inventories, config-field normalization, workflow-status gating, append-only JSON log writing for tool, prompt, and dedicated workflow debug events, process-scoped capture of the exact `before_provider_request` provider payload for active prompt runs, and error-stage provider payload dumps gated by `after_provider_response` error statuses. Runtime is dominated by JSON serialization plus filesystem I/O during log writes. Side effects include directory creation and file overwrite when debug entries are appended.
5
5
  */
6
6
 
7
7
  import fs from "node:fs";
@@ -710,3 +710,119 @@ export function logDebugPromptContent(
710
710
  return false;
711
711
  }
712
712
  }
713
+
714
+ /**
715
+ * @brief Describes one process-scoped captured provider request payload bound to an active prompt run.
716
+ * @details Pairs the bundled prompt name with the JSON serialization of the exact `before_provider_request` payload so a later `after_provider_response` error status can dump the failing request verbatim without retaining live provider objects. The interface is compile-time only and introduces no runtime cost.
717
+ */
718
+ interface CapturedPromptErrorPayload {
719
+ promptName: PromptCommandName;
720
+ payloadText: string;
721
+ }
722
+
723
+ /**
724
+ * @brief Describes the process-scoped provider error payload capture state persisted across extension rebinds.
725
+ * @details Stores the latest captured prompt error payload on `globalThis` because pi rebinds extension modules for `/new`, `/resume`, `/fork`, and `/reload` between prompt dispatch and provider execution, while the hosting process persists across those operations, so the capture survives the forked execution-session switch. The interface is compile-time only and introduces no runtime cost.
726
+ */
727
+ interface ProcessScopedPromptErrorCaptureStore {
728
+ capturedPromptErrorPayload: CapturedPromptErrorPayload | undefined;
729
+ }
730
+
731
+ /**
732
+ * @brief Returns the process-scoped provider error payload capture store.
733
+ * @details Lazily initializes one `globalThis` record so session rebinds never discard an already captured failing provider request. Runtime is O(1). Side effect: initializes process-scoped state on first access.
734
+ * @return {ProcessScopedPromptErrorCaptureStore} Mutable process-scoped capture store.
735
+ */
736
+ function getProcessScopedPromptErrorCaptureStore(): ProcessScopedPromptErrorCaptureStore {
737
+ const globalScope = globalThis as typeof globalThis & { __piUsereqPromptErrorCaptureStore?: ProcessScopedPromptErrorCaptureStore };
738
+ if (!globalScope.__piUsereqPromptErrorCaptureStore) {
739
+ globalScope.__piUsereqPromptErrorCaptureStore = { capturedPromptErrorPayload: undefined };
740
+ }
741
+ return globalScope.__piUsereqPromptErrorCaptureStore;
742
+ }
743
+
744
+ /**
745
+ * @brief Builds one provider error payload debug filename for one bundled prompt.
746
+ * @details Combines the write-time `YYYYMMDDHHMMSSmmm` timestamp, the invokable `req-*` command name, and the failing provider response status into the `<timestamp>-<req-command>-error-<status>` shape so every saved error-stage prompt payload stays attributable to the originating `/req-*` prompt and its session error code. Runtime is O(1). No external state is mutated.
747
+ * @param[in] promptName {PromptCommandName} Bundled prompt name.
748
+ * @param[in] errorCode {number} Failing provider response status, such as `400`.
749
+ * @param[in] date {Date} Write-time timestamp. Defaults to the current wall-clock time.
750
+ * @return {string} Provider error payload filename in the `<timestamp>-<req-command>-error-<status>` shape.
751
+ * @satisfies REQ-423
752
+ */
753
+ export function formatPromptDebugErrorFileName(
754
+ promptName: PromptCommandName,
755
+ errorCode: number,
756
+ date = new Date(),
757
+ ): string {
758
+ return `${formatPromptDebugTimestamp(date)}-${getDebugPromptName(promptName)}-error-${errorCode}`;
759
+ }
760
+
761
+ /**
762
+ * @brief Captures one exact provider request payload for the active prompt run when prompt debug logging is enabled.
763
+ * @details Applies the identical global-debug, prompt-debug, and workflow-state gating used by dispatched prompt content files, serializes the exact `before_provider_request` payload as pretty-printed JSON, and stores it in the process-scoped capture store keyed by the bundled prompt name so the payload survives session replacement until an error response flushes it. Runtime is O(n) in serialized payload size when enabled and O(1) otherwise. Side effect: mutates the process-scoped capture store for enabled matching requests.
764
+ * @param[in] config {UseReqConfig} Effective project configuration.
765
+ * @param[in] workflowState {DebugWorkflowState} Current workflow state.
766
+ * @param[in] promptName {PromptCommandName} Active bundled prompt name.
767
+ * @param[in] payload {unknown} Exact `before_provider_request` provider request payload.
768
+ * @return {void} No return value.
769
+ * @satisfies REQ-416, REQ-421
770
+ */
771
+ export function capturePromptProviderRequestForDebug(
772
+ config: UseReqConfig,
773
+ workflowState: DebugWorkflowState,
774
+ promptName: PromptCommandName,
775
+ payload: unknown,
776
+ ): void {
777
+ if (!shouldLogDebugPromptContent(config, workflowState, promptName)) {
778
+ return;
779
+ }
780
+ let payloadText: string;
781
+ try {
782
+ payloadText = JSON.stringify(payload, null, 2) ?? "";
783
+ } catch {
784
+ return;
785
+ }
786
+ getProcessScopedPromptErrorCaptureStore().capturedPromptErrorPayload = { promptName, payloadText };
787
+ }
788
+
789
+ /**
790
+ * @brief Writes the captured provider request payload as one error-stage prompt debug file for the failing prompt.
791
+ * @details Consumes the process-scoped capture for the supplied prompt, discards it after every write attempt so stale payloads never reach later runs, applies the identical global-debug, prompt-debug, and workflow-state gating used by dispatched prompt content files, and writes the exact captured payload into one `<timestamp>-<req-command>-error-<status>` file beside the initial dispatched prompt file under the configured prompt log path. Runtime is dominated by one directory creation plus one file write when a matching capture exists and O(1) otherwise. Side effects include directory creation and file creation only for enabled matching failures.
792
+ * @param[in] projectBase {string} Absolute original project base path.
793
+ * @param[in] config {UseReqConfig} Effective project configuration.
794
+ * @param[in] workflowState {DebugWorkflowState} Current workflow state.
795
+ * @param[in] promptName {PromptCommandName} Active bundled prompt name.
796
+ * @param[in] errorCode {number} Failing provider response status, such as `400`.
797
+ * @return {boolean} `true` when the provider error payload file is written; otherwise `false`.
798
+ * @satisfies REQ-422, REQ-423, REQ-424, REQ-425
799
+ */
800
+ export function flushCapturedPromptErrorPayload(
801
+ projectBase: string,
802
+ config: UseReqConfig,
803
+ workflowState: DebugWorkflowState,
804
+ promptName: PromptCommandName,
805
+ errorCode: number,
806
+ ): boolean {
807
+ const captureStore = getProcessScopedPromptErrorCaptureStore();
808
+ const captured = captureStore.capturedPromptErrorPayload;
809
+ captureStore.capturedPromptErrorPayload = undefined;
810
+ if (!captured || captured.promptName !== promptName) {
811
+ return false;
812
+ }
813
+ if (!shouldLogDebugPromptContent(config, workflowState, promptName)) {
814
+ return false;
815
+ }
816
+ const logDirectory = resolveDebugPromptsLogPath(projectBase, config);
817
+ try {
818
+ fs.mkdirSync(logDirectory, { recursive: true });
819
+ fs.writeFileSync(
820
+ path.join(logDirectory, formatPromptDebugErrorFileName(promptName, errorCode)),
821
+ captured.payloadText,
822
+ "utf8",
823
+ );
824
+ return true;
825
+ } catch {
826
+ return false;
827
+ }
828
+ }
@@ -74,6 +74,7 @@ export const PI_USEREQ_STATUS_HOOK_NAMES = [
74
74
  "session_tree",
75
75
  "context",
76
76
  "before_provider_request",
77
+ "after_provider_response",
77
78
  "before_agent_start",
78
79
  "agent_start",
79
80
  "agent_end",
@@ -195,23 +195,6 @@ function buildContextFilesBlock(projectBase: string, config: UseReqConfig): stri
195
195
  return sections.join("\n\n");
196
196
  }
197
197
 
198
- /**
199
- * @brief Injects the context-files block in place of the dedicated trailing `%%CONTEXT_FILES%%` placeholder.
200
- * @details Replaces only the last occurrence of the `%%CONTEXT_FILES%%` token, which every bundled prompt places as the dedicated injection point at the end of its `## Context Files` section, so inline backticked references to the token inside prompt prose stay verbatim and no prompt content is overwritten by the injected block. Returns the input unchanged when the token is absent. Runs after every other prompt replacement so literal `%%...%%` tokens inside context-file content are never substituted. Time complexity O(n) in prompt length. No external state is mutated.
201
- * @param[in] text {string} Prompt markdown after every other replacement, block injection, and tool-reference rewrite.
202
- * @param[in] block {string} Rendered context-files markdown block, or the empty string when no enabled context file exists.
203
- * @return {string} Prompt markdown whose dedicated trailing placeholder is replaced by the supplied block while all earlier occurrences of the token remain untouched.
204
- * @satisfies REQ-329, REQ-331, REQ-332
205
- */
206
- function injectContextFilesBlock(text: string, block: string): string {
207
- const token = "%%CONTEXT_FILES%%";
208
- const lastIndex = text.lastIndexOf(token);
209
- if (lastIndex === -1) {
210
- return text;
211
- }
212
- return text.slice(0, lastIndex) + block + text.slice(lastIndex + token.length);
213
- }
214
-
215
198
  /**
216
199
  * @brief Rewrites bundled prompt tool references from legacy `req --...` syntax to internal tool names.
217
200
  * @details Applies deterministic global regex replacements so prompt text matches the extension-registered tool surface instead of the standalone CLI spelling. Time complexity O(p*r) where p is pattern count and r is prompt length.
@@ -388,7 +371,11 @@ export function renderPrompt(
388
371
  projectBase,
389
372
  );
390
373
  const adapted = adaptPromptForInternalTools(withPiDevConformance);
391
- return injectContextFilesBlock(adapted, buildContextFilesBlock(projectBase, config));
374
+ const contextFilesBlock = buildContextFilesBlock(projectBase, config);
375
+ if (contextFilesBlock) {
376
+ return adapted.split("%%CONTEXT_FILES%%").join(contextFilesBlock);
377
+ }
378
+ return adapted.split("%%CONTEXT_FILES%%").join("");
392
379
  }
393
380
 
394
381
  /**
package/src/index.ts CHANGED
@@ -133,6 +133,8 @@ import {
133
133
  DEFAULT_DEBUG_STATUS_CHANGES,
134
134
  DEFAULT_DEBUG_TOOL_COMMANDS_ENABLED,
135
135
  DEFAULT_DEBUG_WORKFLOW_EVENTS,
136
+ capturePromptProviderRequestForDebug,
137
+ flushCapturedPromptErrorPayload,
136
138
  logDebugPromptContent,
137
139
  logDebugPromptEvent,
138
140
  logDebugPromptWorkflowEvent,
@@ -1509,6 +1511,27 @@ async function handleExtensionStatusEvent(
1509
1511
  }
1510
1512
  : undefined;
1511
1513
  updateExtensionStatus(statusController, hookName, event, ctx);
1514
+ if (hookName === "before_provider_request" && activePromptRequest !== undefined && statusController.config) {
1515
+ const providerRequestEvent = event as { payload?: unknown };
1516
+ capturePromptProviderRequestForDebug(
1517
+ statusController.config,
1518
+ statusController.state.workflowState,
1519
+ activePromptRequest.promptName,
1520
+ providerRequestEvent.payload,
1521
+ );
1522
+ }
1523
+ if (hookName === "after_provider_response" && activePromptRequest !== undefined && statusController.config) {
1524
+ const providerResponseEvent = event as { status?: unknown };
1525
+ if (typeof providerResponseEvent.status === "number" && providerResponseEvent.status >= 400) {
1526
+ flushCapturedPromptErrorPayload(
1527
+ activePromptRequest.basePath,
1528
+ statusController.config,
1529
+ statusController.state.workflowState,
1530
+ activePromptRequest.promptName,
1531
+ providerResponseEvent.status,
1532
+ );
1533
+ }
1534
+ }
1512
1535
  if (hookName === "tool_result" && statusController.config) {
1513
1536
  const toolEvent = event as {
1514
1537
  toolName?: string;