pi-usereq 0.82.0 → 0.83.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.82.0)
1
+ # PI-useReq/pi-usereq (0.83.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+">
@@ -335,7 +335,7 @@ The extension renders a status field in the pi status line showing: extension id
335
335
  pi -p "Rewrite this function" --append-system-prompt /tmp/PI-useReq/20261009072235123-req-implement
336
336
  ```
337
337
 
338
- - **Provider error logging** — when a dispatched `req-*` run receives a provider response with status >= 400, pi-usereq writes the exact failing request payload into `<timestamp>-<command>-error-<status>` (raw provider-request JSON), the provider error message text into `<timestamp>-<command>-error-<status>-response`, a byte-for-byte snapshot of the retained execution session into `<timestamp>-<command>-error-<status>-session.jsonl`, and replay metadata into `<timestamp>-<command>-error-<status>-replay.json`, all beside the run's `-request` and `-prompt` files. The `-replay.json` sidecar carries the provider, base URL, model id, thinking level, context window, max output tokens, the retained execution-session file, and ready-to-use replay commands; `replayCommands.piFork` targets the log-local `-session.jsonl` snapshot, which freezes the transcript exactly at the failing provider request (before any context edit or compaction). You can re-generate the failure for immediate pi CLI debugging:
338
+ - **Provider error logging** — when a dispatched `req-*` run receives a provider response with status >= 400, pi-usereq writes the exact failing request payload into `<timestamp>-<command>-error-<status>.json` (raw provider-request JSON), the provider error message text into `<timestamp>-<command>-error-<status>-response.txt`, a byte-for-byte snapshot of the retained execution session into `<timestamp>-<command>-error-<status>-session.jsonl`, and replay metadata into `<timestamp>-<command>-error-<status>-replay.json`, all beside the run's `-request.md` and `-prompt.md` files. The `-replay.json` sidecar carries the provider, base URL, model id, thinking level, context window, max output tokens, the retained execution-session file, and ready-to-use replay commands; `replayCommands.piFork` targets the log-local `-session.jsonl` snapshot, which freezes the transcript exactly at the failing provider request (before any context edit or compaction). You can re-generate the failure for immediate pi CLI debugging:
339
339
 
340
340
  ```bash
341
341
  # Re-send the exact failing provider request (the payload file is raw request JSON)
@@ -345,7 +345,7 @@ The extension renders a status field in the pi status line showing: extension id
345
345
  curl -sS https://openrouter.ai/api/v1/chat/completions \
346
346
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
347
347
  -H "Content-Type: application/json" \
348
- --data @/tmp/PI-useReq/20261012070707447-req-analyze-error-400
348
+ --data @/tmp/PI-useReq/20261012070707447-req-analyze-error-400.json
349
349
 
350
350
  # Replay the saved prompt through pi with the same model and thinking level
351
351
  pi --no-session --model '~deepseek/deepseek-flash-latest' --thinking high \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-usereq",
3
- "version": "0.82.0",
3
+ "version": "0.83.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Ogekuri/PI-useReq.git"
@@ -724,32 +724,32 @@ export function formatPromptDebugFileName(
724
724
 
725
725
  /**
726
726
  * @brief Builds one command-invocation summary debug filename for one bundled prompt.
727
- * @details Appends the `-request` suffix to the shared `<timestamp>-<req-command>` base so every saved command invocation summary stays attributable to the originating `/req-*` prompt. Runtime is O(1). No external state is mutated.
727
+ * @details Appends the `-request.md` suffix to the shared `<timestamp>-<req-command>` base so every saved command invocation summary stays attributable to the originating `/req-*` prompt, with the `.md` extension coherent with the persisted markdown summary content per REQ-435. Runtime is O(1). No external state is mutated.
728
728
  * @param[in] promptName {PromptCommandName} Bundled prompt name.
729
729
  * @param[in] date {Date} Write-time timestamp. Defaults to the current wall-clock time.
730
- * @return {string} Prompt debug filename in the `<timestamp>-<req-command>-request` shape.
731
- * @satisfies REQ-417, REQ-427
730
+ * @return {string} Prompt debug filename in the `<timestamp>-<req-command>-request.md` shape.
731
+ * @satisfies REQ-417, REQ-427, REQ-435
732
732
  */
733
733
  export function formatPromptDebugRequestFileName(
734
734
  promptName: PromptCommandName,
735
735
  date = new Date(),
736
736
  ): string {
737
- return `${formatPromptDebugFileName(promptName, date)}-request`;
737
+ return `${formatPromptDebugFileName(promptName, date)}-request.md`;
738
738
  }
739
739
 
740
740
  /**
741
741
  * @brief Builds one dispatched-prompt content debug filename for one bundled prompt.
742
- * @details Appends the `-prompt` suffix to the shared `<timestamp>-<req-command>` base so every saved initial LLM prompt stays attributable to the originating `/req-*` prompt. Runtime is O(1). No external state is mutated.
742
+ * @details Appends the `-prompt.md` suffix to the shared `<timestamp>-<req-command>` base so every saved initial LLM prompt stays attributable to the originating `/req-*` prompt, with the `.md` extension coherent with the persisted markdown prompt content per REQ-435. Runtime is O(1). No external state is mutated.
743
743
  * @param[in] promptName {PromptCommandName} Bundled prompt name.
744
744
  * @param[in] date {Date} Write-time timestamp. Defaults to the current wall-clock time.
745
- * @return {string} Prompt debug filename in the `<timestamp>-<req-command>-prompt` shape.
746
- * @satisfies REQ-417, REQ-418
745
+ * @return {string} Prompt debug filename in the `<timestamp>-<req-command>-prompt.md` shape.
746
+ * @satisfies REQ-417, REQ-418, REQ-435
747
747
  */
748
748
  export function formatPromptDebugPromptFileName(
749
749
  promptName: PromptCommandName,
750
750
  date = new Date(),
751
751
  ): string {
752
- return `${formatPromptDebugFileName(promptName, date)}-prompt`;
752
+ return `${formatPromptDebugFileName(promptName, date)}-prompt.md`;
753
753
  }
754
754
 
755
755
  /**
@@ -787,14 +787,14 @@ function writePromptDebugFile(
787
787
 
788
788
  /**
789
789
  * @brief Writes one command invocation summary debug file when prompt debug logging is enabled.
790
- * @details Persists the exact summary text displayed on screen into one `<timestamp>-<req-command>-request` file so the `/req-*` invocation headers survive beside the dispatched prompt. Runtime is dominated by one directory creation plus one file write when enabled and O(1) otherwise. Side effects include directory creation and file creation only for enabled matching entries.
790
+ * @details Persists the exact summary text displayed on screen into one `<timestamp>-<req-command>-request.md` file so the `/req-*` invocation headers survive beside the dispatched prompt. Runtime is dominated by one directory creation plus one file write when enabled and O(1) otherwise. Side effects include directory creation and file creation only for enabled matching entries.
791
791
  * @param[in] projectBase {string} Absolute original project base path.
792
792
  * @param[in] config {UseReqConfig} Effective project configuration.
793
793
  * @param[in] workflowState {DebugWorkflowState} Current workflow state; excluded from gating per REQ-416.
794
794
  * @param[in] promptName {PromptCommandName} Bundled prompt name.
795
795
  * @param[in] requestText {string} Exact command invocation summary text displayed on screen.
796
796
  * @return {boolean} `true` when the request file is written; otherwise `false`.
797
- * @satisfies REQ-416, REQ-417, REQ-427, REQ-428
797
+ * @satisfies REQ-416, REQ-417, REQ-427, REQ-428, REQ-435
798
798
  */
799
799
  export function logDebugPromptRequest(
800
800
  projectBase: string,
@@ -815,14 +815,14 @@ export function logDebugPromptRequest(
815
815
 
816
816
  /**
817
817
  * @brief Writes one dispatched prompt content file when prompt debug logging is enabled.
818
- * @details Persists the exact rendered prompt content delivered through `sendMessage` or `sendUserMessage` into one `<timestamp>-<req-command>-prompt` file so the send can be replayed from the pi CLI without pi-usereq installed. Runtime is dominated by one directory creation plus one file write when enabled and O(1) otherwise. Side effects include directory creation and file creation only for enabled matching entries.
818
+ * @details Persists the exact rendered prompt content delivered through `sendMessage` or `sendUserMessage` into one `<timestamp>-<req-command>-prompt.md` file so the send can be replayed from the pi CLI without pi-usereq installed. Runtime is dominated by one directory creation plus one file write when enabled and O(1) otherwise. Side effects include directory creation and file creation only for enabled matching entries.
819
819
  * @param[in] projectBase {string} Absolute original project base path.
820
820
  * @param[in] config {UseReqConfig} Effective project configuration.
821
821
  * @param[in] workflowState {DebugWorkflowState} Current workflow state; excluded from gating per REQ-416.
822
822
  * @param[in] promptName {PromptCommandName} Bundled prompt name.
823
823
  * @param[in] content {string} Exact rendered prompt content delivered through `sendMessage` or `sendUserMessage`.
824
824
  * @return {boolean} `true` when the prompt content file is written; otherwise `false`.
825
- * @satisfies REQ-416, REQ-417, REQ-418, REQ-428
825
+ * @satisfies REQ-416, REQ-417, REQ-418, REQ-428, REQ-435
826
826
  */
827
827
  export function logDebugPromptContent(
828
828
  projectBase: string,
@@ -878,7 +878,7 @@ interface CapturedPromptErrorPayload {
878
878
 
879
879
  /**
880
880
  * @brief Describes the location of one already flushed provider error payload file set.
881
- * @details Stores the prompt name, failing status, resolved log directory, and payload base filename of the most recent error-stage flush so a later finalized `message_end` error message can still append the `-response` file beside the already written payload when the provider fired `after_provider_response` before surfacing its error text. The interface is compile-time only and introduces no runtime cost.
881
+ * @details Stores the prompt name, failing status, resolved log directory, and shared extensionless error base filename of the most recent error-stage flush so a later finalized `message_end` error message can still append the `-response.txt` file beside the already written payload when the provider fired `after_provider_response` before surfacing its error text. The interface is compile-time only and introduces no runtime cost.
882
882
  * @satisfies REQ-429
883
883
  */
884
884
  interface FlushedPromptErrorRecord {
@@ -916,12 +916,12 @@ function getProcessScopedPromptErrorCaptureStore(): ProcessScopedPromptErrorCapt
916
916
  }
917
917
 
918
918
  /**
919
- * @brief Builds one provider error payload debug filename for one bundled prompt.
920
- * @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.
919
+ * @brief Builds the shared extensionless provider error base filename for one bundled prompt.
920
+ * @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>` base shape so every saved error-stage prompt artifact stays attributable to the originating `/req-*` prompt and its session error code. This extensionless base is the suffix-attachment anchor for the `-response.txt`, `-replay.json`, and `-session.jsonl` variants; the flushed raw provider-request JSON payload appends its content-coherent `.json` extension through `formatPromptDebugErrorPayloadFileName(...)`. Runtime is O(1). No external state is mutated.
921
921
  * @param[in] promptName {PromptCommandName} Bundled prompt name.
922
922
  * @param[in] errorCode {number} Failing provider response status, such as `400`.
923
923
  * @param[in] date {Date} Write-time timestamp. Defaults to the current wall-clock time.
924
- * @return {string} Provider error payload filename in the `<timestamp>-<req-command>-error-<status>` shape.
924
+ * @return {string} Extensionless provider error base filename in the `<timestamp>-<req-command>-error-<status>` shape.
925
925
  * @satisfies REQ-423
926
926
  */
927
927
  export function formatPromptDebugErrorFileName(
@@ -932,21 +932,38 @@ export function formatPromptDebugErrorFileName(
932
932
  return `${formatPromptDebugTimestamp(date)}-${getDebugPromptName(promptName)}-error-${errorCode}`;
933
933
  }
934
934
 
935
+ /**
936
+ * @brief Builds one provider error payload debug filename for one bundled prompt.
937
+ * @details Appends the `.json` extension to the shared extensionless `<timestamp>-<req-command>-error-<status>` base so the flushed raw provider-request JSON payload keeps a filename extension coherent with its JSON content per REQ-435 while staying attributable to the exact failing request. Runtime is O(1). No external state is mutated.
938
+ * @param[in] promptName {PromptCommandName} Bundled prompt name.
939
+ * @param[in] errorCode {number} Failing provider response status, such as `400`.
940
+ * @param[in] date {Date} Write-time timestamp shared with the sibling error artifacts. Defaults to the current wall-clock time.
941
+ * @return {string} Provider error payload filename in the `<timestamp>-<req-command>-error-<status>.json` shape.
942
+ * @satisfies REQ-423, REQ-431, REQ-435
943
+ */
944
+ export function formatPromptDebugErrorPayloadFileName(
945
+ promptName: PromptCommandName,
946
+ errorCode: number,
947
+ date = new Date(),
948
+ ): string {
949
+ return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}.json`;
950
+ }
951
+
935
952
  /**
936
953
  * @brief Builds one provider error response debug filename for one bundled prompt.
937
- * @details Appends the `-response` suffix to the shared `<timestamp>-<req-command>-error-<status>` shape so the persisted provider error text stays attributable to the exact failing request payload file written with the identical write-time timestamp. Runtime is O(1). No external state is mutated.
954
+ * @details Appends the `-response.txt` suffix to the shared extensionless `<timestamp>-<req-command>-error-<status>` shape so the persisted plain-text provider error message keeps a filename extension coherent with its text content per REQ-435 and stays attributable to the exact failing request payload file written with the identical write-time timestamp. Runtime is O(1). No external state is mutated.
938
955
  * @param[in] promptName {PromptCommandName} Bundled prompt name.
939
956
  * @param[in] errorCode {number} Failing provider response status, such as `400`.
940
957
  * @param[in] date {Date} Write-time timestamp shared with the payload file. Defaults to the current wall-clock time.
941
- * @return {string} Provider error response filename in the `<timestamp>-<req-command>-error-<status>-response` shape.
942
- * @satisfies REQ-423, REQ-429
958
+ * @return {string} Provider error response filename in the `<timestamp>-<req-command>-error-<status>-response.txt` shape.
959
+ * @satisfies REQ-423, REQ-429, REQ-435
943
960
  */
944
961
  export function formatPromptDebugErrorResponseFileName(
945
962
  promptName: PromptCommandName,
946
963
  errorCode: number,
947
964
  date = new Date(),
948
965
  ): string {
949
- return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}-response`;
966
+ return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}-response.txt`;
950
967
  }
951
968
 
952
969
  /**
@@ -1178,11 +1195,11 @@ function buildPromptErrorReplaySidecarText(
1178
1195
 
1179
1196
  /**
1180
1197
  * @brief Writes the captured provider request payload plus its response text and replay sidecar as error-stage prompt debug files.
1181
- * @details Consumes the process-scoped capture, discards it after every write attempt so stale payloads never reach later runs, rejects failing statuses below 400, and writes the exact captured payload into one `<timestamp>-<req-command>-error-<status>` file that stays raw provider-request JSON usable with `curl --data @`, the provider error text into one identically timestamped `-response` file when supplied, one identically timestamped byte-for-byte `-session.jsonl` copy of the retained execution session so `pi --fork` can resume the exact failing state from the prompt log directory alone, and the replay sidecar into one identically timestamped `-replay.json` file inside the captured log directory beside the initial dispatched prompt artifacts. Records the flushed location so a later finalized `message_end` error message can still append the `-response` file when `after_provider_response` fired first without error text. All write inputs come from the capture itself, so the flush requires no live workflow state, cached configuration, or active controller prompt request. Runtime is dominated by one directory creation plus at most four file writes when a capture exists and O(1) otherwise. Side effects include directory creation and file creation only when a capture exists and the status is an error status.
1198
+ * @details Consumes the process-scoped capture, discards it after every write attempt so stale payloads never reach later runs, rejects failing statuses below 400, and writes the exact captured payload into one `<timestamp>-<req-command>-error-<status>.json` file that stays raw provider-request JSON usable with `curl --data @`, the provider error text into one identically timestamped `-response.txt` file when supplied, one identically timestamped byte-for-byte `-session.jsonl` copy of the retained execution session so `pi --fork` can resume the exact failing state from the prompt log directory alone, and the replay sidecar into one identically timestamped `-replay.json` file inside the captured log directory beside the initial dispatched prompt artifacts. Records the flushed location so a later finalized `message_end` error message can still append the `-response.txt` file when `after_provider_response` fired first without error text. All write inputs come from the capture itself, so the flush requires no live workflow state, cached configuration, or active controller prompt request. Runtime is dominated by one directory creation plus at most four file writes when a capture exists and O(1) otherwise. Side effects include directory creation and file creation only when a capture exists and the status is an error status.
1182
1199
  * @param[in] errorCode {number} Failing provider response status, such as `400`.
1183
1200
  * @param[in] errorResponseText {string | undefined} Finalized provider error message text when already available; the `-response` file is written later when omitted.
1184
1201
  * @return {boolean} `true` when the provider error payload file set is written; otherwise `false`.
1185
- * @satisfies REQ-422, REQ-423, REQ-424, REQ-425, REQ-429, REQ-430, REQ-431, REQ-432, REQ-433
1202
+ * @satisfies REQ-422, REQ-423, REQ-424, REQ-425, REQ-429, REQ-430, REQ-431, REQ-432, REQ-433, REQ-435
1186
1203
  */
1187
1204
  export function flushCapturedPromptErrorPayload(errorCode: number, errorResponseText?: string): boolean {
1188
1205
  const captureStore = getProcessScopedPromptErrorCaptureStore();
@@ -1192,7 +1209,7 @@ export function flushCapturedPromptErrorPayload(errorCode: number, errorResponse
1192
1209
  return false;
1193
1210
  }
1194
1211
  const writeTime = new Date();
1195
- const payloadFileName = formatPromptDebugErrorFileName(captured.promptName, errorCode, writeTime);
1212
+ const payloadFileName = formatPromptDebugErrorPayloadFileName(captured.promptName, errorCode, writeTime);
1196
1213
  try {
1197
1214
  fs.mkdirSync(captured.logDirectory, { recursive: true });
1198
1215
  fs.writeFileSync(
@@ -1220,7 +1237,7 @@ export function flushCapturedPromptErrorPayload(errorCode: number, errorResponse
1220
1237
  promptName: captured.promptName,
1221
1238
  errorCode,
1222
1239
  logDirectory: captured.logDirectory,
1223
- baseFileName: payloadFileName,
1240
+ baseFileName: formatPromptDebugErrorFileName(captured.promptName, errorCode, writeTime),
1224
1241
  };
1225
1242
  return true;
1226
1243
  } catch {
@@ -1273,7 +1290,7 @@ function copyPromptErrorSessionSnapshotFile(
1273
1290
 
1274
1291
  /**
1275
1292
  * @brief Appends the provider error response text beside one already flushed error payload file.
1276
- * @details Providers that fire `after_provider_response` before surfacing their error text flush the payload without a `-response` file, so this helper writes the finalized `message_end` error message beside the recorded flush location when the failing status matches. Runtime is dominated by one directory creation plus one file write when a matching record exists and O(1) otherwise. Side effects include directory creation and file creation only for a matching record.
1293
+ * @details Providers that fire `after_provider_response` before surfacing their error text flush the payload without a `-response.txt` file, so this helper writes the finalized `message_end` error message beside the recorded flush location when the failing status matches. Runtime is dominated by one directory creation plus one file write when a matching record exists and O(1) otherwise. Side effects include directory creation and file creation only for a matching record.
1277
1294
  * @param[in] errorCode {number} Failing provider response status carried by the finalized error message.
1278
1295
  * @param[in] errorResponseText {string} Finalized provider error message text.
1279
1296
  * @return {boolean} `true` when the response file is written; otherwise `false`.
@@ -1293,7 +1310,7 @@ export function writeFlushedPromptProviderErrorResponse(
1293
1310
  try {
1294
1311
  writePromptProviderErrorResponseFile(
1295
1312
  record.logDirectory,
1296
- `${record.baseFileName}-response`,
1313
+ `${record.baseFileName}-response.txt`,
1297
1314
  errorResponseText,
1298
1315
  );
1299
1316
  return true;
@@ -325,7 +325,15 @@ function readReqResetSessionHeader(sessionFile: string): { cwd?: string; parentS
325
325
 
326
326
  /**
327
327
  * @brief Resolves the original base-path session file from the active session header.
328
- * @details Reads the current session file exposed by the supplied command context, accepts only a persisted `parentSession` link whose own header `cwd` remains inside the main base path, and returns it so `req-reset` can switch the pi CLI back to the original session-backed base path. Runtime is O(n) in session-header size plus bounded filesystem probes. No external state is mutated.
328
+ * @details Reads the current session file exposed by the supplied command context and accepts one
329
+ * persisted `parentSession` link as the reserved original base-path session under two conditions:
330
+ * when the linked file already exists on disk its own header `cwd` must remain inside the main base
331
+ * path, and when the linked file was never flushed to disk (pi persists session files lazily on the
332
+ * first entry append) the link itself proves the reserved original-session identity so the caller
333
+ * can materialize the base session before switching. Unpersisted links are accepted only when their
334
+ * parent directory already exists so recovery never writes session files into arbitrary locations.
335
+ * Runtime is O(n) in session-header size plus bounded filesystem probes. No external state is
336
+ * mutated.
329
337
  * @param[in] activeContext {ReqResetCommandContext | undefined} Session-bound command context whose current session file links the base-path session.
330
338
  * @param[in] basePath {string} Absolute main repository base path that must contain the parent session cwd.
331
339
  * @return {string | undefined} Original base-path session file or `undefined` when unavailable.
@@ -346,10 +354,15 @@ function resolveReqResetParentSessionFile(
346
354
  return undefined;
347
355
  }
348
356
  const header = readReqResetSessionHeader(path.resolve(currentSessionFile));
349
- if (header?.parentSession === undefined || !fs.existsSync(header.parentSession)) {
357
+ if (header?.parentSession === undefined) {
350
358
  return undefined;
351
359
  }
352
360
  const resolvedParentSession = path.resolve(header.parentSession);
361
+ if (!fs.existsSync(resolvedParentSession)) {
362
+ return fs.existsSync(path.dirname(resolvedParentSession))
363
+ ? resolvedParentSession
364
+ : undefined;
365
+ }
353
366
  const parentHeader = readReqResetSessionHeader(resolvedParentSession);
354
367
  if (parentHeader?.cwd === undefined || !fs.existsSync(parentHeader.cwd)) {
355
368
  return undefined;
@@ -359,6 +372,93 @@ function resolveReqResetParentSessionFile(
359
372
  : undefined;
360
373
  }
361
374
 
375
+ /**
376
+ * @brief Guarantees one switchable persisted base-path session file for `req-reset` re-anchoring.
377
+ * @details Leaves existing session files untouched and, when the resolved base session file was
378
+ * never flushed to disk (pi persists session files lazily on the first entry append), materializes
379
+ * one minimal pi JSONL session header whose `cwd` equals the main base path using the same header
380
+ * contract as pi `SessionManager.newSession(...)`/`forkFrom(...)` and the extension-owned execution
381
+ * session snapshots. Materialization is restricted to existing session directories so recovery
382
+ * never writes into arbitrary locations, and any write failure degrades to `false` so the caller
383
+ * surfaces the re-anchor failure instead of silently deleting the anchoring worktree.
384
+ * Runtime is O(p) in path length plus one bounded filesystem probe and at most one file write.
385
+ * Side effects include one conditional session-file creation.
386
+ * @param[in] sessionFile {string} Resolved base-path session file path.
387
+ * @param[in] basePath {string} Absolute main repository base path recorded in the materialized header.
388
+ * @return {boolean} `true` when a persisted session file exists or was materialized; `false` when the session directory is missing or the write fails.
389
+ */
390
+ function ensureReqResetBaseSessionFile(sessionFile: string, basePath: string): boolean {
391
+ const normalizedSessionFile = path.resolve(sessionFile);
392
+ if (fs.existsSync(normalizedSessionFile)) {
393
+ return true;
394
+ }
395
+ if (!fs.existsSync(path.dirname(normalizedSessionFile))) {
396
+ return false;
397
+ }
398
+ const sessionHeader = {
399
+ type: "session",
400
+ version: 3,
401
+ id: `req-reset-${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`,
402
+ timestamp: new Date().toISOString(),
403
+ cwd: path.resolve(basePath),
404
+ };
405
+ try {
406
+ fs.writeFileSync(normalizedSessionFile, `${JSON.stringify(sessionHeader)}\n`, "utf8");
407
+ } catch {
408
+ return false;
409
+ }
410
+ return fs.existsSync(normalizedSessionFile);
411
+ }
412
+
413
+ /**
414
+ * @brief Reads one live session-bound context session directory while tolerating stale contexts.
415
+ * @details Real pi command contexts resolve `sessionManager` through getter-only accessors whose
416
+ * reads throw the documented stale-extension-context error after session replacement, so the
417
+ * session-directory probe must degrade stale reads to `undefined`. Runtime is O(1). No external
418
+ * state is mutated.
419
+ * @param[in] activeContext {ReqResetCommandContext | undefined} Candidate session-bound context.
420
+ * @return {string | undefined} Live session directory when readable; `undefined` when absent or stale.
421
+ */
422
+ function readReqResetContextSessionDir(activeContext: ReqResetCommandContext | undefined): string | undefined {
423
+ try {
424
+ const sessionDir = typeof activeContext?.sessionManager?.getSessionDir === "function"
425
+ ? activeContext.sessionManager.getSessionDir()
426
+ : undefined;
427
+ return typeof sessionDir === "string" && sessionDir !== "" ? sessionDir : undefined;
428
+ } catch {
429
+ return undefined;
430
+ }
431
+ }
432
+
433
+ /**
434
+ * @brief Allocates one fresh persisted base-path session file for `req-reset` re-anchoring.
435
+ * @details Mirrors pi `/new` semantics for recovery situations without any resolvable original or
436
+ * recent base session: reserves one new session file path inside the live session directory
437
+ * through `SessionManager.create(...)` so the caller can materialize its header with the main base
438
+ * path and switch the pi CLI onto it before generated worktrees are removed. Allocation is
439
+ * restricted to existing session directories and any failure degrades to `undefined`. Runtime is
440
+ * O(p) in path length plus one bounded filesystem probe. No external state is mutated beyond the
441
+ * pi session-manager reservation.
442
+ * @param[in] basePath {string} Absolute main repository base path for the reserved session.
443
+ * @param[in] sessionDir {string | undefined} Live session directory reused for the reserved file.
444
+ * @return {string | undefined} Reserved session file path or `undefined` when allocation is impossible.
445
+ */
446
+ function allocateReqResetBaseSessionFile(
447
+ basePath: string,
448
+ sessionDir: string | undefined,
449
+ ): string | undefined {
450
+ if (typeof sessionDir !== "string" || sessionDir === "" || !fs.existsSync(sessionDir)) {
451
+ return undefined;
452
+ }
453
+ try {
454
+ const sessionManager = SessionManager.create(path.resolve(basePath), sessionDir);
455
+ const sessionFile = sessionManager.getSessionFile();
456
+ return typeof sessionFile === "string" && sessionFile !== "" ? path.resolve(sessionFile) : undefined;
457
+ } catch {
458
+ return undefined;
459
+ }
460
+ }
461
+
362
462
  /**
363
463
  * @brief Resolves the most recent persisted session file rooted at the main base path.
364
464
  * @details Uses the pi SDK default session discovery for the supplied cwd and returns the persisted session file only when it already exists on disk, so the pi CLI can resume the recent base-path session instead of being created a fresh one. Runtime is O(s) in session-directory listing cost. No external state is mutated.
@@ -408,20 +508,51 @@ function reqResetExecutionPathNeedsRedirect(
408
508
  });
409
509
  }
410
510
 
511
+ /**
512
+ * @brief Reads one live session-bound context cwd while tolerating stale contexts.
513
+ * @details Real pi command contexts expose `cwd` as a getter-only accessor whose read throws the
514
+ * documented stale-extension-context error after session replacement, so every `req-reset` probe
515
+ * must degrade stale reads to `undefined` instead of aborting the whole recovery. Runtime is O(1).
516
+ * No external state is mutated.
517
+ * @param[in] activeContext {ReqResetCommandContext | undefined} Candidate session-bound context.
518
+ * @return {string | undefined} Live context cwd when readable; `undefined` when absent or stale.
519
+ */
520
+ function readReqResetContextCwd(activeContext: ReqResetCommandContext | undefined): string | undefined {
521
+ try {
522
+ return typeof activeContext?.cwd === "string" && activeContext.cwd !== ""
523
+ ? activeContext.cwd
524
+ : undefined;
525
+ } catch {
526
+ return undefined;
527
+ }
528
+ }
529
+
411
530
  /**
412
531
  * @brief Returns the pi CLI to the main base path before generated worktrees are removed.
413
- * @details When the host process cwd or the supplied context cwd is still anchored inside a generated worktree targeted for deletion or points at a deleted path, best-effort switches the active pi session back to the original or recent base-path session, re-anchors `process.cwd()` and the context `cwd` mirror onto the main base path, and clears the runtime worktree path state so no post-cleanup surface keeps probing the removed worktree directory. Session switching failures are swallowed because the process and context surfaces remain authoritative for cleanup; session switching is skipped entirely when no `switchSession(...)` hook is available or no base-path session exists. Runtime is dominated by one optional session switch plus bounded filesystem probes. Side effects include active-session replacement, host-process cwd mutation, and optional context mirror mutation.
532
+ * @details When the host process cwd or the supplied context cwd is still anchored inside a
533
+ * generated worktree targeted for deletion or points at a deleted path, best-effort switches the
534
+ * active pi session back to the original or recent base-path session, guaranteeing the resolved
535
+ * base session file is persisted through `ensureReqResetBaseSessionFile(...)` before switching so
536
+ * lazily-flushed reserved sessions still re-anchor the pi CLI onto the main base path. Re-anchors
537
+ * `process.cwd()`, clears the runtime worktree path state so no post-cleanup surface keeps probing
538
+ * the removed worktree directory, and reports any session-switch failure through the returned
539
+ * re-anchor diagnostic so the caller can surface it and retain anchoring worktrees instead of
540
+ * silently orphaning the live session at a deleted directory. Context `cwd` mirrors are probed
541
+ * stale-safely because pi contexts expose getter-only accessors that `Reflect.set(...)` cannot
542
+ * mutate. Runtime is dominated by one optional session switch plus bounded filesystem probes.
543
+ * Side effects include active-session replacement, host-process cwd mutation, and one conditional
544
+ * base-session file materialization.
414
545
  * @param[in] basePath {string} Absolute main repository base path.
415
546
  * @param[in,out] activeContext {ReqResetCommandContext | undefined} Mutated session context mirror and potential replacement-session context.
416
547
  * @param[in] matchingWorktreeRoots {string[]} Absolute generated worktree roots targeted for deletion.
417
- * @return {Promise<ReqResetCommandContext | undefined>} Replacement-session context when the runtime provides one; otherwise the caller-supplied context.
418
- * @satisfies REQ-257, REQ-307
548
+ * @return {Promise<{ context: ReqResetCommandContext | undefined; reanchorErrorMessage?: string }>} Re-anchored context plus one optional re-anchor failure diagnostic.
549
+ * @satisfies REQ-257, REQ-307, REQ-308, REQ-313
419
550
  */
420
551
  async function redirectReqResetExecutionToMainBranch(
421
552
  basePath: string,
422
553
  activeContext: ReqResetCommandContext | undefined,
423
554
  matchingWorktreeRoots: string[],
424
- ): Promise<ReqResetCommandContext | undefined> {
555
+ ): Promise<{ context: ReqResetCommandContext | undefined; reanchorErrorMessage?: string }> {
425
556
  const normalizedBasePath = path.resolve(basePath);
426
557
  let processCwd: string | undefined;
427
558
  try {
@@ -429,7 +560,7 @@ async function redirectReqResetExecutionToMainBranch(
429
560
  } catch {
430
561
  processCwd = undefined;
431
562
  }
432
- const contextCwd = typeof activeContext?.cwd === "string" ? activeContext.cwd : undefined;
563
+ const contextCwd = readReqResetContextCwd(activeContext);
433
564
  const processNeedsRedirect = reqResetExecutionPathNeedsRedirect(
434
565
  processCwd,
435
566
  normalizedBasePath,
@@ -441,17 +572,26 @@ async function redirectReqResetExecutionToMainBranch(
441
572
  matchingWorktreeRoots,
442
573
  );
443
574
  if (!processNeedsRedirect && !contextNeedsRedirect) {
444
- return activeContext;
575
+ return { context: activeContext };
445
576
  }
577
+ let reanchorErrorMessage: string | undefined;
446
578
  if (typeof activeContext?.switchSession === "function") {
447
- const mainSessionFile = resolveReqResetParentSessionFile(activeContext, normalizedBasePath)
579
+ let mainSessionFile = resolveReqResetParentSessionFile(activeContext, normalizedBasePath)
448
580
  ?? resolveReqResetMainSessionFile(normalizedBasePath);
449
- if (mainSessionFile !== undefined) {
581
+ if (mainSessionFile === undefined) {
582
+ mainSessionFile = allocateReqResetBaseSessionFile(
583
+ normalizedBasePath,
584
+ readReqResetContextSessionDir(activeContext),
585
+ );
586
+ }
587
+ if (mainSessionFile !== undefined && ensureReqResetBaseSessionFile(mainSessionFile, normalizedBasePath)) {
450
588
  try {
451
589
  activeContext = await switchPromptCommandSession(mainSessionFile, activeContext);
452
- } catch {
453
- // Best-effort; host process and context surfaces below remain authoritative.
590
+ } catch (error) {
591
+ reanchorErrorMessage = `ERROR: req-reset could not switch the pi session back to ${normalizedBasePath}: ${error instanceof Error ? error.message : String(error)}`;
454
592
  }
593
+ } else {
594
+ reanchorErrorMessage = `ERROR: req-reset found no persisted base-path session to restore for ${normalizedBasePath}.`;
455
595
  }
456
596
  }
457
597
  try {
@@ -461,7 +601,7 @@ async function redirectReqResetExecutionToMainBranch(
461
601
  }
462
602
  if (activeContext !== undefined) {
463
603
  try {
464
- if (path.resolve(activeContext.cwd ?? "") !== normalizedBasePath) {
604
+ if (path.resolve(readReqResetContextCwd(activeContext) ?? "") !== normalizedBasePath) {
465
605
  Reflect.set(activeContext, "cwd", normalizedBasePath);
466
606
  }
467
607
  } catch {
@@ -470,7 +610,38 @@ async function redirectReqResetExecutionToMainBranch(
470
610
  }
471
611
  setRuntimeContextPath(normalizedBasePath);
472
612
  setRuntimeWorktreePathState({});
473
- return activeContext;
613
+ return { context: activeContext, reanchorErrorMessage };
614
+ }
615
+
616
+ /**
617
+ * @brief Lists matching worktree roots that still anchor the live session cwd.
618
+ * @details Used only after a failed session re-anchor so `req-reset` retains the worktree directory
619
+ * the live pi session is still checked out in, preventing the session cwd from being orphaned at a
620
+ * deleted directory (which renders `branch:unknown`). Stale or unreadable context cwds and paths
621
+ * that no longer exist yield an empty list because the session cannot be probed reliably. Runtime
622
+ * is O(r) in matched-worktree count plus one bounded filesystem probe. No external state is
623
+ * mutated.
624
+ * @param[in] activeContext {ReqResetCommandContext | undefined} Session-bound context whose live cwd may still anchor one matching worktree.
625
+ * @param[in] matchingWorktreeRoots {string[]} Absolute generated worktree roots targeted for deletion.
626
+ * @return {string[]} Absolute matching worktree roots that still contain the live session cwd.
627
+ */
628
+ function resolveReqResetAnchoredWorktreeRoots(
629
+ activeContext: ReqResetCommandContext | undefined,
630
+ matchingWorktreeRoots: string[],
631
+ ): string[] {
632
+ const contextCwd = readReqResetContextCwd(activeContext);
633
+ if (contextCwd === undefined) {
634
+ return [];
635
+ }
636
+ const normalizedContextCwd = path.resolve(contextCwd);
637
+ if (!fs.existsSync(normalizedContextCwd)) {
638
+ return [];
639
+ }
640
+ return matchingWorktreeRoots.filter((worktreeRootPath) => {
641
+ const normalizedRoot = path.resolve(worktreeRootPath);
642
+ return normalizedContextCwd === normalizedRoot
643
+ || normalizedContextCwd.startsWith(`${normalizedRoot}${path.sep}`);
644
+ });
474
645
  }
475
646
 
476
647
  /**
@@ -547,7 +718,7 @@ export function prepareReqResetCommandExecution(
547
718
 
548
719
  /**
549
720
  * @brief Executes the specialized `req-reset` recovery and cleanup workflow.
550
- * @details Preserves the execution-session transcript into the original session file when a worktree-backed prompt execution plan is still available, restores the original session-backed `base-path` through the shared prompt-command restoration helper, ensures the main repository HEAD is on the main branch so generated branch deletion cannot hit git's checked-out-worktree guard, enumerates every matching sibling worktree, returns the pi CLI execution surfaces to the main base path before any matching worktree directory is removed, force-removes every matching sibling worktree directory, force-removes every remaining matching local branch, restores deleted live cwd surfaces, and aggregates any failure diagnostics without rolling back successful cleanup steps. Runtime is dominated by session switching plus git subprocess execution. Side effects include session-file reads and writes, active-session replacement, host-process cwd mutation, branch switching, worktree deletion, branch deletion, and filesystem reads.
721
+ * @details Preserves the execution-session transcript into the original session file when a worktree-backed prompt execution plan is still available, restores the original session-backed `base-path` through the shared prompt-command restoration helper, ensures the main repository HEAD is on the main branch so generated branch deletion cannot hit git's checked-out-worktree guard, enumerates every matching sibling worktree, returns the pi CLI execution surfaces to the main base path (materializing the reserved base session when pi never flushed it) before any matching worktree directory is removed, force-removes every matching sibling worktree directory, force-removes every remaining matching local branch, retains the worktree plus branch that still anchor the live pi session whenever the session re-anchor fails so the session cwd is never orphaned at a deleted directory, restores deleted live cwd surfaces, and aggregates any failure diagnostics without rolling back successful cleanup steps. Runtime is dominated by session switching plus git subprocess execution. Side effects include session-file reads and writes, active-session replacement, host-process cwd mutation, branch switching, worktree deletion, branch deletion, and filesystem reads.
551
722
  * @param[in] plan {ReqResetCommandPlan} Prepared recovery and cleanup plan.
552
723
  * @param[in] ctx {ReqResetCommandContext | undefined} Optional session-bound command context.
553
724
  * @return {Promise<ReqResetCommandExecutionResult>} Recovery and cleanup outcome facts.
@@ -604,14 +775,30 @@ export async function executeReqResetCommandExecution(
604
775
  errorMessages.push(error instanceof Error ? error.message : String(error));
605
776
  }
606
777
 
607
- activeContext = await redirectReqResetExecutionToMainBranch(
778
+ const redirectOutcome = await redirectReqResetExecutionToMainBranch(
608
779
  plan.basePath,
609
780
  activeContext,
610
781
  matchingWorktreeRoots,
611
782
  );
783
+ activeContext = redirectOutcome.context;
784
+ if (redirectOutcome.reanchorErrorMessage !== undefined) {
785
+ errorMessages.push(redirectOutcome.reanchorErrorMessage);
786
+ }
787
+ const retainedWorktreeDirs = new Set(
788
+ redirectOutcome.reanchorErrorMessage !== undefined
789
+ ? resolveReqResetAnchoredWorktreeRoots(activeContext, matchingWorktreeRoots)
790
+ .map((worktreeRootPath) => path.basename(worktreeRootPath))
791
+ : [],
792
+ );
612
793
 
613
794
  for (const worktreeRootPath of matchingWorktreeRoots) {
614
795
  const worktreeDir = path.basename(worktreeRootPath);
796
+ if (retainedWorktreeDirs.has(worktreeDir)) {
797
+ errorMessages.push(
798
+ `ERROR: req-reset retained worktree ${worktreeDir} because the pi session could not be re-anchored to ${plan.basePath}.`,
799
+ );
800
+ continue;
801
+ }
615
802
  try {
616
803
  deletePromptWorktree(plan.basePath, worktreeDir, worktreeRootPath);
617
804
  removedWorktreeDirs.push(worktreeDir);
@@ -623,6 +810,12 @@ export async function executeReqResetCommandExecution(
623
810
  try {
624
811
  const matchingBranchNames = listReqResetMatchingBranchNames(plan.gitPath, plan.worktreeNamePattern);
625
812
  for (const branchName of matchingBranchNames) {
813
+ if (retainedWorktreeDirs.has(branchName)) {
814
+ errorMessages.push(
815
+ `ERROR: req-reset retained branch ${branchName} because its worktree still anchors the pi session.`,
816
+ );
817
+ continue;
818
+ }
626
819
  const deleteResult = runCapture(["git", "branch", "-D", branchName], plan.gitPath);
627
820
  if (deleteResult.error || deleteResult.status !== 0) {
628
821
  const diagnostic = deleteResult.stderr.trim()