pi-usereq 0.10.0 → 0.12.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 (52) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +6 -6
  3. package/package.json +1 -1
  4. package/pi-usereq/docs/REFERENCES.md +861 -704
  5. package/pi-usereq/docs/REQUIREMENTS.md +152 -95
  6. package/pi-usereq/docs/WORKFLOW.md +228 -65
  7. package/scripts/lib/extension-debug-harness.ts +2 -2
  8. package/scripts/tool-args-to-params.ts +2 -2
  9. package/src/cli.ts +12 -12
  10. package/src/core/debug-runtime.ts +2 -2
  11. package/src/core/extension-status.ts +98 -36
  12. package/src/core/pi-notify.ts +5 -5
  13. package/src/core/pi-usereq-tools.ts +4 -2
  14. package/src/core/prompt-command-catalog.ts +4 -5
  15. package/src/core/prompt-command-runtime.ts +347 -42
  16. package/src/core/prompts.ts +0 -2
  17. package/src/core/req-references-command.ts +175 -0
  18. package/src/core/req-reset-command.ts +323 -0
  19. package/src/core/resources.ts +6 -23
  20. package/src/core/runtime-project-paths.ts +21 -1
  21. package/src/core/settings-menu.ts +85 -28
  22. package/src/core/tool-runner.ts +26 -6
  23. package/src/index.ts +530 -104
  24. package/tests/attended-results-scenarios.ts +5 -5
  25. package/tests/cli-command-option-parity.test.ts +25 -25
  26. package/tests/debug-extension-harness.test.ts +8 -2
  27. package/tests/extension-registration.test.ts +1109 -76
  28. package/tests/oracle-project.test.ts +4 -4
  29. package/tests/oracle-standalone.test.ts +5 -5
  30. package/src/core/reference-payload.ts +0 -752
  31. package/src/resources/prompts/references.md +0 -64
  32. /package/tests/fixtures_attended_results/project/{references.json → summarize.json} +0 -0
  33. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_c.c.json +0 -0
  34. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_cpp.cpp.json +0 -0
  35. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_csharp.cs.json +0 -0
  36. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_elixir.ex.json +0 -0
  37. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_go.go.json +0 -0
  38. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_haskell.hs.json +0 -0
  39. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_java.java.json +0 -0
  40. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_javascript.js.json +0 -0
  41. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_kotlin.kt.json +0 -0
  42. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_lua.lua.json +0 -0
  43. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_perl.pl.json +0 -0
  44. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_php.php.json +0 -0
  45. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_python.py.json +0 -0
  46. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_ruby.rb.json +0 -0
  47. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_rust.rs.json +0 -0
  48. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_scala.scala.json +0 -0
  49. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_shell.sh.json +0 -0
  50. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_swift.swift.json +0 -0
  51. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_typescript.ts.json +0 -0
  52. /package/tests/fixtures_attended_results/standalone/{files-references → files-summarize}/fixture_zig.zig.json +0 -0
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file
3
- * @brief Implements prompt-command preflight and worktree orchestration.
4
- * @details Centralizes `req-<prompt>` repository validation, prompt-specific required-document checks, slash-command-owned worktree naming and lifecycle handling, session-backed cwd switching plus verification, persisted replacement-session context reuse for non-command lifecycle handlers, matched-success fast-forward merge finalization, and command-side abort cleanup. Runtime is dominated by git subprocess execution plus bounded filesystem and session-file metadata checks. Side effects include active-session replacement, worktree creation and deletion, branch merges, and filesystem reads and writes.
3
+ * @brief Implements bundled prompt-command preflight and worktree orchestration.
4
+ * @details Centralizes prompt-template-backed `req-<prompt>` repository validation, prompt-specific required-document checks, slash-command-owned worktree naming and lifecycle handling, reusable transcript-preservation plus session-restoration helpers, persisted replacement-session context reuse for non-command lifecycle handlers, matched-success stash-assisted fast-forward merge finalization, and command-side abort cleanup. Runtime is dominated by git subprocess execution plus bounded filesystem and session-file metadata checks. Side effects include active-session replacement, worktree creation and deletion, branch merges, stash-stack mutation, and filesystem reads and writes.
5
5
  */
6
6
 
7
7
  import fs from "node:fs";
@@ -19,10 +19,7 @@ import {
19
19
  logDebugPromptWorkflowEvent,
20
20
  type DebugWorkflowState,
21
21
  } from "./debug-runtime.js";
22
- import {
23
- PROMPT_COMMAND_NAMES,
24
- type PromptCommandName,
25
- } from "./prompt-command-catalog.js";
22
+ import { type PromptCommandName } from "./prompt-command-catalog.js";
26
23
  import {
27
24
  isSameOrAncestorPath,
28
25
  normalizeRelativeDirContract,
@@ -421,6 +418,169 @@ function readPromptSessionFileCwd(sessionFile: string): string | undefined {
421
418
  return typeof headerCwd === "string" ? headerCwd : undefined;
422
419
  }
423
420
 
421
+ /**
422
+ * @brief Reads one persisted session file as ordered parsed JSONL records.
423
+ * @details Loads the raw session file, preserves every non-empty serialized line verbatim, parses each line as one JSON object, and rejects unreadable or structurally invalid files so prompt-closure helpers can replay exact execution-session transcript records into the restored base session without reserialization drift. Runtime is O(n) in session-file size. No external state is mutated.
424
+ * @param[in] sessionFile {string} Absolute session-file path.
425
+ * @return {Array<{ rawLine: string; parsed: Record<string, unknown> }>} Parsed non-empty JSONL lines in file order.
426
+ * @throws {ReqError} Throws when the file cannot be read, when it contains no JSONL records, when any record is not a JSON object, or when the header record is missing.
427
+ */
428
+ function readPromptSessionJsonLines(
429
+ sessionFile: string,
430
+ ): Array<{ rawLine: string; parsed: Record<string, unknown> }> {
431
+ const normalizedSessionFile = path.resolve(sessionFile);
432
+ let raw: string;
433
+ try {
434
+ raw = fs.readFileSync(normalizedSessionFile, "utf8");
435
+ } catch (error) {
436
+ const errorMessage = error instanceof Error ? error.message : String(error);
437
+ throw new ReqError(
438
+ `ERROR: Unable to read session file ${normalizedSessionFile}: ${errorMessage}.`,
439
+ 1,
440
+ );
441
+ }
442
+ const parsedLines: Array<{ rawLine: string; parsed: Record<string, unknown> }> = [];
443
+ for (const [index, rawLine] of raw.split(/\r?\n/u).entries()) {
444
+ if (rawLine.trim() === "") {
445
+ continue;
446
+ }
447
+ let parsed: unknown;
448
+ try {
449
+ parsed = JSON.parse(rawLine);
450
+ } catch (error) {
451
+ const errorMessage = error instanceof Error ? error.message : String(error);
452
+ throw new ReqError(
453
+ `ERROR: Unable to parse session file ${normalizedSessionFile} line ${index + 1}: ${errorMessage}.`,
454
+ 1,
455
+ );
456
+ }
457
+ if (typeof parsed !== "object" || parsed === null) {
458
+ throw new ReqError(
459
+ `ERROR: Session file ${normalizedSessionFile} line ${index + 1} is not a JSON object.`,
460
+ 1,
461
+ );
462
+ }
463
+ parsedLines.push({ rawLine, parsed: parsed as Record<string, unknown> });
464
+ }
465
+ if (parsedLines.length === 0) {
466
+ throw new ReqError(`ERROR: Session file ${normalizedSessionFile} is empty.`, 1);
467
+ }
468
+ if (parsedLines[0]?.parsed.type !== "session") {
469
+ throw new ReqError(
470
+ `ERROR: Session file ${normalizedSessionFile} is missing a valid session header.`,
471
+ 1,
472
+ );
473
+ }
474
+ return parsedLines;
475
+ }
476
+
477
+ /**
478
+ * @brief Copies successful execution-session transcript records into the restored base session file.
479
+ * @details Reads the execution session JSONL file, preserves the original base-session header when it already exists, materializes a restored base-session header when the reserved original session file is still pending persistence, appends any execution-session records missing from the original session in original execution order, and re-reads the restored file to verify both `base-path` cwd and copied entry identifiers. Runtime is O(n) in combined session-file size. Side effects include session-file creation or append operations for the restored base session.
480
+ * @param[in] plan {PromptCommandExecutionPlan} Prompt execution plan whose original and execution session files must be synchronized.
481
+ * @return {void} No return value.
482
+ * @throws {ReqError} Throws when either session file is unreadable or when appended execution records are not persisted to the original session file.
483
+ * @satisfies REQ-208, REQ-307
484
+ */
485
+ export function preservePromptCommandExecutionTranscript(plan: PromptCommandExecutionPlan): void {
486
+ const normalizedOriginalSessionFile = path.resolve(plan.originalSessionFile);
487
+ const normalizedExecutionSessionFile = path.resolve(plan.executionSessionFile);
488
+ if (normalizedOriginalSessionFile === normalizedExecutionSessionFile) {
489
+ return;
490
+ }
491
+ const executionLines = readPromptSessionJsonLines(normalizedExecutionSessionFile);
492
+ const executionEntryIds = executionLines
493
+ .slice(1)
494
+ .map((line) => line.parsed.id)
495
+ .filter((entryId): entryId is string => typeof entryId === "string" && entryId !== "");
496
+ if (!fs.existsSync(normalizedOriginalSessionFile)) {
497
+ const rewrittenHeader: Record<string, unknown> = {
498
+ ...executionLines[0]!.parsed,
499
+ cwd: plan.basePath,
500
+ };
501
+ if (
502
+ typeof rewrittenHeader.parentSession === "string"
503
+ && path.resolve(rewrittenHeader.parentSession) === normalizedOriginalSessionFile
504
+ ) {
505
+ delete rewrittenHeader.parentSession;
506
+ }
507
+ try {
508
+ fs.mkdirSync(path.dirname(normalizedOriginalSessionFile), { recursive: true });
509
+ const serializedLines = [
510
+ JSON.stringify(rewrittenHeader),
511
+ ...executionLines.slice(1).map((line) => line.rawLine),
512
+ ];
513
+ fs.writeFileSync(
514
+ normalizedOriginalSessionFile,
515
+ `${serializedLines.join("\n")}\n`,
516
+ "utf8",
517
+ );
518
+ } catch (error) {
519
+ const errorMessage = error instanceof Error ? error.message : String(error);
520
+ throw new ReqError(
521
+ `ERROR: Unable to materialize restored session transcript in ${normalizedOriginalSessionFile}: ${errorMessage}.`,
522
+ 1,
523
+ );
524
+ }
525
+ } else {
526
+ const originalLines = readPromptSessionJsonLines(normalizedOriginalSessionFile);
527
+ const existingEntryIds = new Set<string>();
528
+ for (const line of originalLines.slice(1)) {
529
+ const entryId = line.parsed.id;
530
+ if (typeof entryId === "string" && entryId !== "") {
531
+ existingEntryIds.add(entryId);
532
+ }
533
+ }
534
+ const missingExecutionLines = executionLines.slice(1).filter((line) => {
535
+ const entryId = line.parsed.id;
536
+ return typeof entryId === "string"
537
+ && entryId !== ""
538
+ && !existingEntryIds.has(entryId);
539
+ });
540
+ if (missingExecutionLines.length > 0) {
541
+ const originalRaw = fs.readFileSync(normalizedOriginalSessionFile, "utf8");
542
+ const leadingSeparator = originalRaw === "" || originalRaw.endsWith("\n") ? "" : "\n";
543
+ try {
544
+ fs.appendFileSync(
545
+ normalizedOriginalSessionFile,
546
+ `${leadingSeparator}${missingExecutionLines.map((line) => line.rawLine).join("\n")}\n`,
547
+ "utf8",
548
+ );
549
+ } catch (error) {
550
+ const errorMessage = error instanceof Error ? error.message : String(error);
551
+ throw new ReqError(
552
+ `ERROR: Unable to preserve execution transcript in ${normalizedOriginalSessionFile}: ${errorMessage}.`,
553
+ 1,
554
+ );
555
+ }
556
+ }
557
+ }
558
+ const synchronizedSessionCwd = readPromptSessionFileCwd(normalizedOriginalSessionFile);
559
+ if (
560
+ typeof synchronizedSessionCwd !== "string"
561
+ || path.resolve(synchronizedSessionCwd) !== path.resolve(plan.basePath)
562
+ ) {
563
+ throw new ReqError(
564
+ `ERROR: Restored session transcript expected cwd ${path.resolve(plan.basePath)} but observed ${synchronizedSessionCwd ?? "missing"}.`,
565
+ 1,
566
+ );
567
+ }
568
+ const synchronizedEntryIds = new Set<string>();
569
+ for (const line of readPromptSessionJsonLines(normalizedOriginalSessionFile).slice(1)) {
570
+ const entryId = line.parsed.id;
571
+ if (typeof entryId === "string" && entryId !== "") {
572
+ synchronizedEntryIds.add(entryId);
573
+ }
574
+ }
575
+ const missingSynchronizedIds = executionEntryIds.filter((entryId) => !synchronizedEntryIds.has(entryId));
576
+ if (missingSynchronizedIds.length > 0) {
577
+ throw new ReqError(
578
+ `ERROR: Unable to verify execution transcript preservation for ${normalizedOriginalSessionFile}: missing entries ${missingSynchronizedIds.join(", ")}.`,
579
+ 1,
580
+ );
581
+ }
582
+ }
583
+
424
584
  /**
425
585
  * @brief Verifies that the active session file and cwd surfaces match one expected prompt-orchestration target.
426
586
  * @details Re-reads the persisted session-file header when present plus the host `process.cwd()` and throws on the first mismatch so prompt commands abort before prompt dispatch or prompt-end handling whenever session switching leaves execution attached to the wrong cwd. A missing persisted session file is treated as a non-fatal lazy-persistence state because pi's `SessionManager` writes session files on first assistant flush rather than eagerly during `ctx.switchSession(sessionPath)`; when the file is absent, pi aligns its internal session cwd to the live `process.cwd()`, so verifying `process.cwd()` alone is authoritative in that state. Reads of `ctx.cwd`, `ctx.sessionManager.getCwd()`, and `ctx.sessionManager.getSessionFile()` are advisory only because the pi `ctx.switchSession(sessionPath)` SDK contract does not mutate the handler-scoped `ctx` object, so those probes stay bound to the pre-switch session and a divergent value alone never triggers abort; they only surface a mismatch when they disagree with both the persisted header cwd and the live `process.cwd()`. Runtime is O(p) in aggregate path length plus one session-file header read. No external state is mutated.
@@ -666,9 +826,6 @@ const PROMPT_REQUIRED_DOCS: Record<PromptCommandName, readonly PromptRequiredDoc
666
826
  { fileName: "WORKFLOW.md", promptCommand: "/req-workflow" },
667
827
  { fileName: "REFERENCES.md", promptCommand: "/req-references" },
668
828
  ],
669
- references: [
670
- { fileName: "REQUIREMENTS.md", promptCommand: "/req-write" },
671
- ],
672
829
  renumber: [
673
830
  { fileName: "REQUIREMENTS.md", promptCommand: "/req-write" },
674
831
  { fileName: "WORKFLOW.md", promptCommand: "/req-workflow" },
@@ -692,6 +849,166 @@ function runCapture(command: string[], cwd: string): ReturnType<typeof spawnSync
692
849
  });
693
850
  }
694
851
 
852
+ /**
853
+ * @brief Lists tracked `base-path` status rows that require stash-assisted merge handling.
854
+ * @details Executes `git status --porcelain`, retains only tracked rows whose index or worktree slot reports a change, and excludes untracked or ignored rows because the required `git stash` command does not preserve them. Runtime is dominated by one git subprocess plus O(n) parsing in status-line count. Side effects include process spawning.
855
+ * @param[in] basePath {string} Restored project base path.
856
+ * @return {string[]} Tracked status rows requiring stash-assisted merge handling.
857
+ * @throws {ReqError} Throws when git status cannot be read from `basePath`.
858
+ * @satisfies REQ-291
859
+ */
860
+ function listPromptTrackedBasePathChanges(basePath: string): string[] {
861
+ const statusResult = runCapture(["git", "status", "--porcelain"], basePath);
862
+ if (statusResult.error || statusResult.status !== 0) {
863
+ throw new ReqError("ERROR: Unable to inspect base-path changes before merge.", 1);
864
+ }
865
+ return statusResult.stdout
866
+ .split(/\r?\n/)
867
+ .map((line) => line.trimEnd())
868
+ .filter((line) => line !== "")
869
+ .filter((line) => {
870
+ const statusCode = line.slice(0, 2);
871
+ if (statusCode === "??" || statusCode === "!!") {
872
+ return false;
873
+ }
874
+ const indexStatus = statusCode[0] ?? " ";
875
+ const worktreeStatus = statusCode[1] ?? " ";
876
+ return indexStatus !== " " || worktreeStatus !== " ";
877
+ });
878
+ }
879
+
880
+ /**
881
+ * @brief Executes the successful-closure merge sequence from restored `base-path`.
882
+ * @details Detects tracked staged or unstaged `base-path` changes, wraps the existing fast-forward merge in `git stash` and `git stash pop` when required, preserves the direct merge path when no tracked changes exist, emits a warning-only result after successful local-change restoration, and writes one merge-finalization debug entry when enabled. Runtime is dominated by up to four git subprocesses plus O(n) status parsing. Side effects include stash-stack mutation, branch merge attempts, and optional debug-log writes.
883
+ * @param[in] plan {PromptCommandExecutionPlan} Prompt execution plan whose branch should be merged.
884
+ * @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
885
+ * @return {{ mergeAttempted: boolean; mergeSucceeded: boolean; errorMessage?: string; warningMessage?: string }} Merge-attempt facts plus optional warning text.
886
+ * @satisfies REQ-208, REQ-245, REQ-291, REQ-292
887
+ */
888
+ function finalizePromptCommandMerge(
889
+ plan: PromptCommandExecutionPlan,
890
+ debugOptions?: PromptCommandDebugOptions,
891
+ ): {
892
+ mergeAttempted: boolean;
893
+ mergeSucceeded: boolean;
894
+ errorMessage?: string;
895
+ warningMessage?: string;
896
+ } {
897
+ const worktreeDir = plan.worktreeDir ?? plan.branchName;
898
+ const mergeLogInput = {
899
+ worktree_dir: worktreeDir,
900
+ branch_name: plan.branchName,
901
+ };
902
+ let trackedStatusLines: string[];
903
+ try {
904
+ trackedStatusLines = listPromptTrackedBasePathChanges(plan.basePath);
905
+ } catch (error) {
906
+ const errorMessage = error instanceof Error ? error.message : String(error);
907
+ if (debugOptions) {
908
+ logDebugPromptEvent(
909
+ plan.basePath,
910
+ debugOptions.config,
911
+ debugOptions.workflowState,
912
+ plan.promptName,
913
+ "merge",
914
+ mergeLogInput,
915
+ {
916
+ success: false,
917
+ merge_attempted: false,
918
+ error: errorMessage,
919
+ },
920
+ true,
921
+ );
922
+ }
923
+ return {
924
+ mergeAttempted: false,
925
+ mergeSucceeded: false,
926
+ errorMessage,
927
+ };
928
+ }
929
+ const usedStash = trackedStatusLines.length > 0;
930
+ let stashStatus: number | null | undefined;
931
+ if (usedStash) {
932
+ const stashResult = runCapture(["git", "stash"], plan.basePath);
933
+ stashStatus = stashResult.status;
934
+ if (stashResult.error || stashResult.status !== 0) {
935
+ const errorMessage = `ERROR: Unable to stash base-path changes before merge for worktree ${worktreeDir}.`;
936
+ if (debugOptions) {
937
+ logDebugPromptEvent(
938
+ plan.basePath,
939
+ debugOptions.config,
940
+ debugOptions.workflowState,
941
+ plan.promptName,
942
+ "merge",
943
+ {
944
+ ...mergeLogInput,
945
+ used_stash: true,
946
+ tracked_change_count: trackedStatusLines.length,
947
+ },
948
+ {
949
+ success: false,
950
+ merge_attempted: false,
951
+ error: errorMessage,
952
+ stash_status: stashStatus,
953
+ },
954
+ true,
955
+ );
956
+ }
957
+ return {
958
+ mergeAttempted: false,
959
+ mergeSucceeded: false,
960
+ errorMessage,
961
+ };
962
+ }
963
+ }
964
+ const mergeResult = runCapture(
965
+ ["git", "merge", "--ff-only", plan.branchName],
966
+ plan.basePath,
967
+ );
968
+ const mergeSucceeded = !mergeResult.error && mergeResult.status === 0;
969
+ const errorMessage = mergeSucceeded
970
+ ? undefined
971
+ : `ERROR: Fast-forward merge failed for worktree ${worktreeDir}.`;
972
+ let stashPopStatus: number | null | undefined;
973
+ let warningMessage: string | undefined;
974
+ if (usedStash) {
975
+ const stashPopResult = runCapture(["git", "stash", "pop"], plan.basePath);
976
+ stashPopStatus = stashPopResult.status;
977
+ if (mergeSucceeded) {
978
+ warningMessage = "WARNING: Restored base-path changes after merge; base-path is not clean.";
979
+ }
980
+ }
981
+ if (debugOptions) {
982
+ logDebugPromptEvent(
983
+ plan.basePath,
984
+ debugOptions.config,
985
+ debugOptions.workflowState,
986
+ plan.promptName,
987
+ "merge",
988
+ {
989
+ ...mergeLogInput,
990
+ used_stash: usedStash,
991
+ tracked_change_count: trackedStatusLines.length,
992
+ },
993
+ {
994
+ success: mergeSucceeded,
995
+ merge_attempted: true,
996
+ error: errorMessage,
997
+ warning: warningMessage,
998
+ stash_status: stashStatus,
999
+ stash_pop_status: stashPopStatus,
1000
+ },
1001
+ !mergeSucceeded,
1002
+ );
1003
+ }
1004
+ return {
1005
+ mergeAttempted: true,
1006
+ mergeSucceeded,
1007
+ errorMessage,
1008
+ warningMessage,
1009
+ };
1010
+ }
1011
+
695
1012
  /**
696
1013
  * @brief Stores or clears the prompt-command post-create test hook.
697
1014
  * @details Enables deterministic simulation of post-create worktree verification failures without altering production control flow. Runtime is O(1). Side effect: mutates module-local test state.
@@ -787,15 +1104,15 @@ function throwPromptGitStatusError(): never {
787
1104
  }
788
1105
 
789
1106
  /**
790
- * @brief Runs prompt-command-owned git validation and returns the runtime git root.
791
- * @details Validates work-tree membership, porcelain cleanliness, and symbolic or detached `HEAD` presence without invoking extension custom-tool executors. Runtime is dominated by git subprocess execution. Side effects include process spawning.
1107
+ * @brief Runs slash-command-owned git validation and returns the runtime git root.
1108
+ * @details Validates work-tree membership, porcelain cleanliness, and symbolic or detached `HEAD` presence for bundled prompt commands and `req-references` without invoking extension custom-tool executors. Runtime is dominated by git subprocess execution. Side effects include process spawning.
792
1109
  * @param[in] projectBase {string} Absolute current project base.
793
1110
  * @param[in] config {UseReqConfig | undefined} Optional effective project configuration used to ignore extension-owned debug-log artifacts.
794
1111
  * @return {string} Absolute runtime git root.
795
1112
  * @throws {ReqError} Throws the canonical prompt-command git-preflight error on any validation failure.
796
1113
  * @satisfies REQ-200, REQ-220
797
1114
  */
798
- function validatePromptGitState(projectBase: string, config?: UseReqConfig): string {
1115
+ export function validatePromptGitState(projectBase: string, config?: UseReqConfig): string {
799
1116
  const gitPath = resolveRuntimeGitPath(projectBase);
800
1117
  if (!gitPath) {
801
1118
  throwPromptGitStatusError();
@@ -1117,9 +1434,9 @@ function createPromptWorktree(
1117
1434
  * @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
1118
1435
  * @return {void} No return value.
1119
1436
  * @throws {ReqError} Throws when cleanup cannot remove the worktree and branch fully.
1120
- * @satisfies REQ-208, REQ-220, REQ-245
1437
+ * @satisfies REQ-208, REQ-220, REQ-245, REQ-309
1121
1438
  */
1122
- function deletePromptWorktree(
1439
+ export function deletePromptWorktree(
1123
1440
  basePath: string,
1124
1441
  worktreeDir: string,
1125
1442
  worktreeRootPath: string,
@@ -1528,12 +1845,12 @@ export async function abortPromptCommandExecution(
1528
1845
 
1529
1846
  /**
1530
1847
  * @brief Finalizes one matched successful worktree-backed prompt execution.
1531
- * @details Re-verifies persisted execution-session metadata plus worktree artifacts, restores the original session-backed `base-path`, fast-forward merges the successful worktree branch from `base-path`, deletes the worktree after merge success, and preserves the restored base session across closure failures. Closure intentionally treats `base-path` restoration as authoritative even when pi CLI has already started end-of-session session replacement or other housekeeping that moved the live runtime away from `worktree-path`. Runtime is dominated by session switching plus git subprocess execution. Side effects include active-session replacement, branch merges, worktree deletion, and optional debug-log writes.
1848
+ * @details Re-verifies persisted execution-session metadata plus worktree artifacts, copies any execution-session transcript records missing from the original session file, restores the original session-backed `base-path`, executes the stash-assisted fast-forward merge sequence from `base-path`, deletes the worktree after merge success, and preserves the restored base session across closure failures. Closure intentionally treats `base-path` restoration as authoritative even when pi CLI has already started end-of-session session replacement or other housekeeping that moved the live runtime away from `worktree-path`. Runtime is dominated by session switching plus git subprocess execution. Side effects include session-file appends, active-session replacement, branch merges, stash-stack mutation, worktree deletion, and optional debug-log writes.
1532
1849
  * @param[in] plan {PromptCommandExecutionPlan} Prompt execution plan.
1533
1850
  * @param[in] ctx {PromptCommandSessionContext | undefined} Optional prompt-command context.
1534
1851
  * @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
1535
- * @return {Promise<{ mergeAttempted: boolean; mergeSucceeded: boolean; cleanupSucceeded: boolean; errorMessage?: string; activeContext?: PromptCommandSessionContext }>} Finalization facts plus the last valid active prompt-command context.
1536
- * @satisfies REQ-208, REQ-209, REQ-220, REQ-245, REQ-282
1852
+ * @return {Promise<{ mergeAttempted: boolean; mergeSucceeded: boolean; cleanupSucceeded: boolean; errorMessage?: string; warningMessage?: string; activeContext?: PromptCommandSessionContext }>} Finalization facts plus the last valid active prompt-command context.
1853
+ * @satisfies REQ-208, REQ-209, REQ-220, REQ-245, REQ-282, REQ-291, REQ-292
1537
1854
  */
1538
1855
  export async function finalizePromptCommandExecution(
1539
1856
  plan: PromptCommandExecutionPlan,
@@ -1544,12 +1861,16 @@ export async function finalizePromptCommandExecution(
1544
1861
  mergeSucceeded: boolean;
1545
1862
  cleanupSucceeded: boolean;
1546
1863
  errorMessage?: string;
1864
+ warningMessage?: string;
1547
1865
  activeContext?: PromptCommandSessionContext;
1548
1866
  }> {
1549
1867
  let activeContext = ctx;
1550
1868
  let verificationErrorMessage: string | undefined;
1551
1869
  try {
1552
1870
  verifyPromptCommandClosureArtifacts(plan);
1871
+ if (plan.worktreePath && plan.worktreeDir && plan.worktreeRootPath) {
1872
+ preservePromptCommandExecutionTranscript(plan);
1873
+ }
1553
1874
  } catch (error) {
1554
1875
  verificationErrorMessage = error instanceof Error ? error.message : String(error);
1555
1876
  }
@@ -1583,32 +1904,14 @@ export async function finalizePromptCommandExecution(
1583
1904
  activeContext,
1584
1905
  };
1585
1906
  }
1586
- const mergeResult = runCapture(
1587
- ["git", "merge", "--ff-only", plan.branchName],
1588
- plan.basePath,
1589
- );
1590
- const mergeSucceeded = !mergeResult.error && mergeResult.status === 0;
1591
- const errorMessage = mergeSucceeded
1592
- ? undefined
1593
- : `ERROR: Fast-forward merge failed for worktree ${plan.worktreeDir}.`;
1594
- if (debugOptions) {
1595
- logDebugPromptEvent(
1596
- plan.basePath,
1597
- debugOptions.config,
1598
- debugOptions.workflowState,
1599
- plan.promptName,
1600
- "merge",
1601
- { worktree_dir: plan.worktreeDir, branch_name: plan.branchName },
1602
- { success: mergeSucceeded, error: errorMessage },
1603
- !mergeSucceeded,
1604
- );
1605
- }
1606
- if (!mergeSucceeded) {
1907
+ const mergeFinalization = finalizePromptCommandMerge(plan, debugOptions);
1908
+ if (!mergeFinalization.mergeSucceeded) {
1607
1909
  return {
1608
- mergeAttempted: true,
1910
+ mergeAttempted: mergeFinalization.mergeAttempted,
1609
1911
  mergeSucceeded: false,
1610
1912
  cleanupSucceeded: true,
1611
- errorMessage,
1913
+ errorMessage: mergeFinalization.errorMessage,
1914
+ warningMessage: mergeFinalization.warningMessage,
1612
1915
  activeContext,
1613
1916
  };
1614
1917
  }
@@ -1622,17 +1925,19 @@ export async function finalizePromptCommandExecution(
1622
1925
  );
1623
1926
  } catch {
1624
1927
  return {
1625
- mergeAttempted: true,
1928
+ mergeAttempted: mergeFinalization.mergeAttempted,
1626
1929
  mergeSucceeded: true,
1627
1930
  cleanupSucceeded: false,
1628
1931
  errorMessage: `ERROR: Unable to remove worktree or branch ${plan.worktreeDir}.`,
1932
+ warningMessage: mergeFinalization.warningMessage,
1629
1933
  activeContext,
1630
1934
  };
1631
1935
  }
1632
1936
  return {
1633
- mergeAttempted: true,
1937
+ mergeAttempted: mergeFinalization.mergeAttempted,
1634
1938
  mergeSucceeded: true,
1635
1939
  cleanupSucceeded: true,
1940
+ warningMessage: mergeFinalization.warningMessage,
1636
1941
  activeContext,
1637
1942
  };
1638
1943
  }
@@ -22,14 +22,12 @@ import { readBundledInstruction, readBundledPrompt } from "./resources.js";
22
22
  const TOOL_REFERENCE_REPLACEMENTS: Array<[RegExp, string]> = [
23
23
  [/`req --find`/g, "`search` tool"],
24
24
  [/`req --files-find`/g, "`files-search` tool"],
25
- [/`req --references`/g, "`references` tool"],
26
25
  [/`req --compress`/g, "`compress` tool"],
27
26
  [/`req --tokens`/g, "`tokens` tool"],
28
27
  [/`req --static-check`/g, "`static-check` tool"],
29
28
  [/`req --files-static-check`/g, "`files-static-check` tool"],
30
29
  [/\breq --find\b/g, "search tool"],
31
30
  [/\breq --files-find\b/g, "files-search tool"],
32
- [/\breq --references\b/g, "references tool"],
33
31
  [/\breq --compress\b/g, "compress tool"],
34
32
  [/\breq --tokens\b/g, "tokens tool"],
35
33
  [/\breq --static-check\b/g, "static-check tool"],