pi-usereq 0.10.0 → 0.11.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file
3
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.
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 with restored-session transcript preservation, 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.
5
5
  */
6
6
 
7
7
  import fs from "node:fs";
@@ -421,6 +421,169 @@ function readPromptSessionFileCwd(sessionFile: string): string | undefined {
421
421
  return typeof headerCwd === "string" ? headerCwd : undefined;
422
422
  }
423
423
 
424
+ /**
425
+ * @brief Reads one persisted session file as ordered parsed JSONL records.
426
+ * @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.
427
+ * @param[in] sessionFile {string} Absolute session-file path.
428
+ * @return {Array<{ rawLine: string; parsed: Record<string, unknown> }>} Parsed non-empty JSONL lines in file order.
429
+ * @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.
430
+ */
431
+ function readPromptSessionJsonLines(
432
+ sessionFile: string,
433
+ ): Array<{ rawLine: string; parsed: Record<string, unknown> }> {
434
+ const normalizedSessionFile = path.resolve(sessionFile);
435
+ let raw: string;
436
+ try {
437
+ raw = fs.readFileSync(normalizedSessionFile, "utf8");
438
+ } catch (error) {
439
+ const errorMessage = error instanceof Error ? error.message : String(error);
440
+ throw new ReqError(
441
+ `ERROR: Unable to read session file ${normalizedSessionFile}: ${errorMessage}.`,
442
+ 1,
443
+ );
444
+ }
445
+ const parsedLines: Array<{ rawLine: string; parsed: Record<string, unknown> }> = [];
446
+ for (const [index, rawLine] of raw.split(/\r?\n/u).entries()) {
447
+ if (rawLine.trim() === "") {
448
+ continue;
449
+ }
450
+ let parsed: unknown;
451
+ try {
452
+ parsed = JSON.parse(rawLine);
453
+ } catch (error) {
454
+ const errorMessage = error instanceof Error ? error.message : String(error);
455
+ throw new ReqError(
456
+ `ERROR: Unable to parse session file ${normalizedSessionFile} line ${index + 1}: ${errorMessage}.`,
457
+ 1,
458
+ );
459
+ }
460
+ if (typeof parsed !== "object" || parsed === null) {
461
+ throw new ReqError(
462
+ `ERROR: Session file ${normalizedSessionFile} line ${index + 1} is not a JSON object.`,
463
+ 1,
464
+ );
465
+ }
466
+ parsedLines.push({ rawLine, parsed: parsed as Record<string, unknown> });
467
+ }
468
+ if (parsedLines.length === 0) {
469
+ throw new ReqError(`ERROR: Session file ${normalizedSessionFile} is empty.`, 1);
470
+ }
471
+ if (parsedLines[0]?.parsed.type !== "session") {
472
+ throw new ReqError(
473
+ `ERROR: Session file ${normalizedSessionFile} is missing a valid session header.`,
474
+ 1,
475
+ );
476
+ }
477
+ return parsedLines;
478
+ }
479
+
480
+ /**
481
+ * @brief Copies successful execution-session transcript records into the restored base session file.
482
+ * @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.
483
+ * @param[in] plan {PromptCommandExecutionPlan} Prompt execution plan whose original and execution session files must be synchronized.
484
+ * @return {void} No return value.
485
+ * @throws {ReqError} Throws when either session file is unreadable or when appended execution records are not persisted to the original session file.
486
+ * @satisfies REQ-208
487
+ */
488
+ function preservePromptCommandExecutionTranscript(plan: PromptCommandExecutionPlan): void {
489
+ const normalizedOriginalSessionFile = path.resolve(plan.originalSessionFile);
490
+ const normalizedExecutionSessionFile = path.resolve(plan.executionSessionFile);
491
+ if (normalizedOriginalSessionFile === normalizedExecutionSessionFile) {
492
+ return;
493
+ }
494
+ const executionLines = readPromptSessionJsonLines(normalizedExecutionSessionFile);
495
+ const executionEntryIds = executionLines
496
+ .slice(1)
497
+ .map((line) => line.parsed.id)
498
+ .filter((entryId): entryId is string => typeof entryId === "string" && entryId !== "");
499
+ if (!fs.existsSync(normalizedOriginalSessionFile)) {
500
+ const rewrittenHeader: Record<string, unknown> = {
501
+ ...executionLines[0]!.parsed,
502
+ cwd: plan.basePath,
503
+ };
504
+ if (
505
+ typeof rewrittenHeader.parentSession === "string"
506
+ && path.resolve(rewrittenHeader.parentSession) === normalizedOriginalSessionFile
507
+ ) {
508
+ delete rewrittenHeader.parentSession;
509
+ }
510
+ try {
511
+ fs.mkdirSync(path.dirname(normalizedOriginalSessionFile), { recursive: true });
512
+ const serializedLines = [
513
+ JSON.stringify(rewrittenHeader),
514
+ ...executionLines.slice(1).map((line) => line.rawLine),
515
+ ];
516
+ fs.writeFileSync(
517
+ normalizedOriginalSessionFile,
518
+ `${serializedLines.join("\n")}\n`,
519
+ "utf8",
520
+ );
521
+ } catch (error) {
522
+ const errorMessage = error instanceof Error ? error.message : String(error);
523
+ throw new ReqError(
524
+ `ERROR: Unable to materialize restored session transcript in ${normalizedOriginalSessionFile}: ${errorMessage}.`,
525
+ 1,
526
+ );
527
+ }
528
+ } else {
529
+ const originalLines = readPromptSessionJsonLines(normalizedOriginalSessionFile);
530
+ const existingEntryIds = new Set<string>();
531
+ for (const line of originalLines.slice(1)) {
532
+ const entryId = line.parsed.id;
533
+ if (typeof entryId === "string" && entryId !== "") {
534
+ existingEntryIds.add(entryId);
535
+ }
536
+ }
537
+ const missingExecutionLines = executionLines.slice(1).filter((line) => {
538
+ const entryId = line.parsed.id;
539
+ return typeof entryId === "string"
540
+ && entryId !== ""
541
+ && !existingEntryIds.has(entryId);
542
+ });
543
+ if (missingExecutionLines.length > 0) {
544
+ const originalRaw = fs.readFileSync(normalizedOriginalSessionFile, "utf8");
545
+ const leadingSeparator = originalRaw === "" || originalRaw.endsWith("\n") ? "" : "\n";
546
+ try {
547
+ fs.appendFileSync(
548
+ normalizedOriginalSessionFile,
549
+ `${leadingSeparator}${missingExecutionLines.map((line) => line.rawLine).join("\n")}\n`,
550
+ "utf8",
551
+ );
552
+ } catch (error) {
553
+ const errorMessage = error instanceof Error ? error.message : String(error);
554
+ throw new ReqError(
555
+ `ERROR: Unable to preserve execution transcript in ${normalizedOriginalSessionFile}: ${errorMessage}.`,
556
+ 1,
557
+ );
558
+ }
559
+ }
560
+ }
561
+ const synchronizedSessionCwd = readPromptSessionFileCwd(normalizedOriginalSessionFile);
562
+ if (
563
+ typeof synchronizedSessionCwd !== "string"
564
+ || path.resolve(synchronizedSessionCwd) !== path.resolve(plan.basePath)
565
+ ) {
566
+ throw new ReqError(
567
+ `ERROR: Restored session transcript expected cwd ${path.resolve(plan.basePath)} but observed ${synchronizedSessionCwd ?? "missing"}.`,
568
+ 1,
569
+ );
570
+ }
571
+ const synchronizedEntryIds = new Set<string>();
572
+ for (const line of readPromptSessionJsonLines(normalizedOriginalSessionFile).slice(1)) {
573
+ const entryId = line.parsed.id;
574
+ if (typeof entryId === "string" && entryId !== "") {
575
+ synchronizedEntryIds.add(entryId);
576
+ }
577
+ }
578
+ const missingSynchronizedIds = executionEntryIds.filter((entryId) => !synchronizedEntryIds.has(entryId));
579
+ if (missingSynchronizedIds.length > 0) {
580
+ throw new ReqError(
581
+ `ERROR: Unable to verify execution transcript preservation for ${normalizedOriginalSessionFile}: missing entries ${missingSynchronizedIds.join(", ")}.`,
582
+ 1,
583
+ );
584
+ }
585
+ }
586
+
424
587
  /**
425
588
  * @brief Verifies that the active session file and cwd surfaces match one expected prompt-orchestration target.
426
589
  * @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.
@@ -1528,7 +1691,7 @@ export async function abortPromptCommandExecution(
1528
1691
 
1529
1692
  /**
1530
1693
  * @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.
1694
+ * @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`, 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 session-file appends, active-session replacement, branch merges, worktree deletion, and optional debug-log writes.
1532
1695
  * @param[in] plan {PromptCommandExecutionPlan} Prompt execution plan.
1533
1696
  * @param[in] ctx {PromptCommandSessionContext | undefined} Optional prompt-command context.
1534
1697
  * @param[in] debugOptions {PromptCommandDebugOptions | undefined} Optional prompt debug logging context.
@@ -1550,6 +1713,9 @@ export async function finalizePromptCommandExecution(
1550
1713
  let verificationErrorMessage: string | undefined;
1551
1714
  try {
1552
1715
  verifyPromptCommandClosureArtifacts(plan);
1716
+ if (plan.worktreePath && plan.worktreeDir && plan.worktreeRootPath) {
1717
+ preservePromptCommandExecutionTranscript(plan);
1718
+ }
1553
1719
  } catch (error) {
1554
1720
  verificationErrorMessage = error instanceof Error ? error.message : String(error);
1555
1721
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @file
3
3
  * @brief Derives runtime-only repository facts.
4
- * @details Centralizes git-repository probing and repository-root resolution for extension status, tool execution, and CLI flows. Runtime is dominated by git subprocess execution plus path normalization. Side effects are limited to subprocess spawning.
4
+ * @details Centralizes git-repository probing, repository-root resolution, and active-branch lookup for extension status, tool execution, and CLI flows. Runtime is dominated by git subprocess execution plus path normalization. Side effects are limited to subprocess spawning.
5
5
  */
6
6
 
7
7
  import path from "node:path";
@@ -67,4 +67,24 @@ export function resolveRuntimeGitPath(executionPath: string): string | undefined
67
67
  return isSameOrAncestorPath(gitRoot, normalizedExecutionPath) ? gitRoot : undefined;
68
68
  }
69
69
 
70
+ /**
71
+ * @brief Resolves the active branch name for one runtime execution path.
72
+ * @details Resolves the enclosing git work tree from the supplied execution path, reads `git branch --show-current`, and falls back to `unknown` when the path is outside git or HEAD has no branch name. Runtime is dominated by git execution. Side effects include subprocess creation.
73
+ * @param[in] executionPath {string} Runtime execution path.
74
+ * @return {string} Active branch name or `unknown` when unavailable.
75
+ * @satisfies REQ-121, REQ-283
76
+ */
77
+ export function resolveRuntimeGitBranchName(executionPath: string): string {
78
+ const gitRoot = resolveRuntimeGitPath(executionPath);
79
+ if (!gitRoot) {
80
+ return "unknown";
81
+ }
82
+ const result = runGitCapture(["git", "branch", "--show-current"], gitRoot);
83
+ if (result.error || result.status !== 0) {
84
+ return "unknown";
85
+ }
86
+ const branchName = result.stdout.trim();
87
+ return branchName === "" ? "unknown" : branchName;
88
+ }
89
+
70
90
 
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * @brief Declares the extension version string.
9
9
  * @details The value is exported for external inspection and packaging metadata alignment. Access complexity is O(1).
10
10
  */
11
- export const VERSION = "0.10.0";
11
+ export const VERSION = "0.11.0";
12
12
 
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
@@ -279,20 +279,19 @@ function formatProjectConfigPathForMenu(cwd: string): string {
279
279
 
280
280
  /**
281
281
  * @brief Builds the standardized terminal rows appended to every configuration menu.
282
- * @details Returns the canonical `Reset defaults` row so all configuration menus and descendant selector menus share the same terminal ordering contract without rendering `Save and close`. Runtime is O(1). No external state is mutated.
283
- * @param[in] options {{ resetDefaultsDescription: string; resetDefaultsValue?: string | undefined }} Menu-specific terminal-row metadata.
282
+ * @details Returns the canonical value-less `Reset defaults` row so all configuration menus and descendant selector menus share the same terminal ordering contract without rendering `Save and close`. Runtime is O(1). No external state is mutated.
283
+ * @param[in] options {{ resetDefaultsDescription: string }} Menu-specific terminal-row metadata.
284
284
  * @return {PiUsereqSettingsMenuChoice[]} Ordered terminal menu rows.
285
285
  * @satisfies REQ-193
286
286
  */
287
287
  function buildTerminalSettingsMenuChoices(options: {
288
288
  resetDefaultsDescription: string;
289
- resetDefaultsValue?: string;
290
289
  }): PiUsereqSettingsMenuChoice[] {
291
290
  return [
292
291
  {
293
292
  id: "reset-defaults",
294
293
  label: "Reset defaults",
295
- value: options.resetDefaultsValue ?? "",
294
+ value: "",
296
295
  description: options.resetDefaultsDescription,
297
296
  },
298
297
  ];
@@ -1356,7 +1355,6 @@ async function selectDebugLogOnStatus(
1356
1355
  description: `Write matching debug entries only while workflow state is ${workflowState}.`,
1357
1356
  })),
1358
1357
  ...buildTerminalSettingsMenuChoices({
1359
- resetDefaultsValue: DEFAULT_DEBUG_LOG_ON_STATUS,
1360
1358
  resetDefaultsDescription: "Restore the documented default workflow-state filter.",
1361
1359
  }),
1362
1360
  ], { initialSelectedId: currentValue });
@@ -1444,7 +1442,6 @@ function buildDebugMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoice
1444
1442
  debugEnabled,
1445
1443
  )),
1446
1444
  ...buildTerminalSettingsMenuChoices({
1447
- resetDefaultsValue: DEFAULT_DEBUG_LOG_ON_STATUS,
1448
1445
  resetDefaultsDescription: "Restore the documented default Debug configuration.",
1449
1446
  }),
1450
1447
  ];
@@ -1794,7 +1791,7 @@ function buildPiNotifyEventLauncherChoice(
1794
1791
 
1795
1792
  /**
1796
1793
  * @brief Builds the shared settings-menu choices for one notification event submenu.
1797
- * @details Serializes completed/interrupted/failed rows with right-aligned `on|off` values, then appends `Reset defaults` and `Save and close` for submenu-scoped mutation control. Runtime is O(1). No external state is mutated.
1794
+ * @details Serializes completed/interrupted/failed rows with right-aligned `on|off` values, then appends a value-less `Reset defaults` row for submenu-scoped mutation control. Runtime is O(1). No external state is mutated.
1798
1795
  * @param[in] config {UseReqConfig} Effective project configuration.
1799
1796
  * @param[in] eventMenu {PiNotifyEventMenuDefinition} Notification-system event submenu contract.
1800
1797
  * @return {PiUsereqSettingsMenuChoice[]} Ordered event-submenu choice vector.
@@ -1983,7 +1980,7 @@ function buildPiNotifyPushoverRows(config: UseReqConfig): PiUsereqSettingsMenuCh
1983
1980
 
1984
1981
  /**
1985
1982
  * @brief Opens the shared settings-menu selector for Pushover priority.
1986
- * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the notification menus and appends subtree-local `Reset defaults` plus `Save and close` rows. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1983
+ * @details Reuses the pi-usereq settings-menu renderer so Pushover priority selection remains stylistically aligned with the notification menus and appends a value-less subtree-local `Reset defaults` row. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
1987
1984
  * @param[in] ctx {ExtensionCommandContext} Active command context.
1988
1985
  * @param[in] currentPriority {PiNotifyPushoverPriority} Persisted priority value.
1989
1986
  * @return {Promise<PiNotifyPushoverPriority | "reset-defaults" | undefined>} Selected priority, reset action, or `undefined` when cancelled.
@@ -2007,7 +2004,6 @@ async function selectPiNotifyPushoverPriority(
2007
2004
  description: "Send outbound Pushover messages with high priority `1`.",
2008
2005
  },
2009
2006
  ...buildTerminalSettingsMenuChoices({
2010
- resetDefaultsValue: formatPiNotifyPushoverPriority(getDefaultConfig("")["notify-pushover-priority"]),
2011
2007
  resetDefaultsDescription: "Restore the documented default Pushover priority.",
2012
2008
  }),
2013
2009
  ], { initialSelectedId: String(currentPriority) });
@@ -2088,7 +2084,7 @@ function buildPiNotifyMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuCho
2088
2084
 
2089
2085
  /**
2090
2086
  * @brief Opens the shared settings-menu selector for the active sound level.
2091
- * @details Reuses the pi-usereq settings-menu renderer so sound-level selection remains stylistically aligned with the notification menu and appends subtree-local `Reset defaults` plus `Save and close` rows. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
2087
+ * @details Reuses the pi-usereq settings-menu renderer so sound-level selection remains stylistically aligned with the notification menu and appends a value-less subtree-local `Reset defaults` row. Runtime depends on user interaction count. Side effects are limited to transient custom-UI rendering.
2092
2088
  * @param[in] ctx {ExtensionCommandContext} Active command context.
2093
2089
  * @param[in] currentLevel {PiNotifySoundLevel} Currently selected sound level.
2094
2090
  * @return {Promise<PiNotifySoundLevel | "reset-defaults" | undefined>} Selected sound level, reset action, or `undefined` when cancelled.
@@ -2124,7 +2120,6 @@ async function selectPiNotifySoundLevel(
2124
2120
  description: "Use the high-volume sound command when sound delivery is enabled for the current event.",
2125
2121
  },
2126
2122
  ...buildTerminalSettingsMenuChoices({
2127
- resetDefaultsValue: getDefaultConfig("")["notify-sound"],
2128
2123
  resetDefaultsDescription: "Restore the documented default sound level.",
2129
2124
  }),
2130
2125
  ], { initialSelectedId: currentLevel });
@@ -2923,7 +2918,6 @@ function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig):
2923
2918
  description: "Disable every configurable startup tool for future session starts.",
2924
2919
  },
2925
2920
  ...buildTerminalSettingsMenuChoices({
2926
- resetDefaultsValue: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
2927
2921
  resetDefaultsDescription: "Restore the documented default startup-tool selection.",
2928
2922
  }),
2929
2923
  ];
@@ -2931,7 +2925,7 @@ function buildPiUsereqToolsMenuChoices(pi: ExtensionAPI, config: UseReqConfig):
2931
2925
 
2932
2926
  /**
2933
2927
  * @brief Builds the shared settings-menu choices for per-tool startup toggles.
2934
- * @details Exposes every configurable startup tool as one row whose right-side value reports the current enabled state, preserves the documented custom/files/embedded/default-disabled ordering, and appends subtree-local `Reset defaults` plus `Save and close` rows. Runtime is O(t) in configurable-tool count. No external state is mutated.
2928
+ * @details Exposes every configurable startup tool as one row whose right-side value reports the current enabled state, preserves the documented custom/files/embedded/default-disabled ordering, and appends a value-less subtree-local `Reset defaults` row. Runtime is O(t) in configurable-tool count. No external state is mutated.
2935
2929
  * @param[in] pi {ExtensionAPI} Active extension API instance.
2936
2930
  * @param[in] config {UseReqConfig} Effective project configuration.
2937
2931
  * @return {PiUsereqSettingsMenuChoice[]} Ordered per-tool toggle choices.
@@ -2947,7 +2941,6 @@ function buildPiUsereqToolToggleChoices(pi: ExtensionAPI, config: UseReqConfig):
2947
2941
  description: tool.description ?? `Toggle startup activation for ${tool.name}.`,
2948
2942
  })),
2949
2943
  ...buildTerminalSettingsMenuChoices({
2950
- resetDefaultsValue: `${normalizeEnabledPiUsereqTools(undefined).length} defaults`,
2951
2944
  resetDefaultsDescription: "Restore the documented default startup-tool selection.",
2952
2945
  }),
2953
2946
  ];
@@ -3149,7 +3142,6 @@ function buildStaticCheckMenuChoices(config: UseReqConfig): PiUsereqSettingsMenu
3149
3142
  };
3150
3143
  }),
3151
3144
  ...buildTerminalSettingsMenuChoices({
3152
- resetDefaultsValue: formatStaticCheckLanguagesSummary(config),
3153
3145
  resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3154
3146
  }),
3155
3147
  ];
@@ -3175,7 +3167,6 @@ function buildSupportedStaticCheckLanguageChoices(config: UseReqConfig): PiUsere
3175
3167
  };
3176
3168
  }),
3177
3169
  ...buildTerminalSettingsMenuChoices({
3178
- resetDefaultsValue: formatStaticCheckLanguagesSummary(config),
3179
3170
  resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3180
3171
  }),
3181
3172
  ];
@@ -3201,7 +3192,6 @@ function buildConfiguredStaticCheckLanguageChoices(config: UseReqConfig): PiUser
3201
3192
  };
3202
3193
  }),
3203
3194
  ...buildTerminalSettingsMenuChoices({
3204
- resetDefaultsValue: formatStaticCheckLanguagesSummary(config),
3205
3195
  resetDefaultsDescription: "Restore the documented per-language static-check defaults.",
3206
3196
  }),
3207
3197
  ];
@@ -3471,7 +3461,6 @@ function buildSrcDirMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoic
3471
3461
  description: "Select one configured source directory to remove from the current configuration.",
3472
3462
  },
3473
3463
  ...buildTerminalSettingsMenuChoices({
3474
- resetDefaultsValue: `${DEFAULT_SRC_DIRS.join(", ")}`,
3475
3464
  resetDefaultsDescription: "Restore the documented default source-directory configuration.",
3476
3465
  }),
3477
3466
  ];
@@ -3479,7 +3468,7 @@ function buildSrcDirMenuChoices(config: UseReqConfig): PiUsereqSettingsMenuChoic
3479
3468
 
3480
3469
  /**
3481
3470
  * @brief Builds the shared settings-menu choices for removing one source-directory entry.
3482
- * @details Exposes every configured `src-dir` entry as one removable row and appends subtree-local `Reset defaults` plus `Save and close` rows. Runtime is O(s) in source-directory count. No external state is mutated.
3471
+ * @details Exposes every configured `src-dir` entry as one removable row and appends a value-less subtree-local `Reset defaults` row. Runtime is O(s) in source-directory count. No external state is mutated.
3483
3472
  * @param[in] config {UseReqConfig} Effective project configuration.
3484
3473
  * @return {PiUsereqSettingsMenuChoice[]} Ordered removable source-directory choices.
3485
3474
  * @satisfies REQ-006, REQ-151, REQ-152, REQ-153, REQ-154
@@ -3493,7 +3482,6 @@ function buildSrcDirRemovalChoices(config: UseReqConfig): PiUsereqSettingsMenuCh
3493
3482
  description: `Remove the source-directory entry ${entry} from the current configuration.`,
3494
3483
  })),
3495
3484
  ...buildTerminalSettingsMenuChoices({
3496
- resetDefaultsValue: `${DEFAULT_SRC_DIRS.join(", ")}`,
3497
3485
  resetDefaultsDescription: "Restore the documented default source-directory configuration.",
3498
3486
  }),
3499
3487
  ];
@@ -3764,7 +3752,7 @@ function registerConfigCommands(
3764
3752
  * optional debug-log writes, and timer scheduling.
3765
3753
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3766
3754
  * @return {void} No return value.
3767
- * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-067, REQ-068, REQ-109, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-131, REQ-132, REQ-133, REQ-134, REQ-137, REQ-148, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-174, REQ-179, REQ-180, REQ-184, REQ-188, REQ-190, REQ-191, REQ-192, REQ-193, REQ-194, REQ-195, REQ-196, REQ-197, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243, REQ-244, REQ-245, REQ-246, REQ-247
3755
+ * @satisfies DES-002, REQ-004, REQ-005, REQ-009, REQ-044, REQ-067, REQ-068, REQ-109, REQ-111, REQ-112, REQ-113, REQ-114, REQ-115, REQ-116, REQ-117, REQ-118, REQ-119, REQ-120, REQ-121, REQ-122, REQ-123, REQ-124, REQ-125, REQ-126, REQ-127, REQ-128, REQ-131, REQ-132, REQ-133, REQ-134, REQ-137, REQ-159, REQ-163, REQ-164, REQ-165, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-174, REQ-179, REQ-180, REQ-184, REQ-188, REQ-190, REQ-191, REQ-192, REQ-193, REQ-194, REQ-195, REQ-196, REQ-197, REQ-236, REQ-237, REQ-238, REQ-239, REQ-240, REQ-241, REQ-242, REQ-243, REQ-244, REQ-245, REQ-246, REQ-247
3768
3756
  */
3769
3757
  export default function piUsereqExtension(pi: ExtensionAPI): void {
3770
3758
  const statusController = createPiUsereqStatusController();
@@ -225,13 +225,19 @@ test("replaySessionStart captures active tools, statuses, and cwd semantics", as
225
225
  const report = await replaySessionStart(projectBase);
226
226
  const status = report.ui.statuses["pi-usereq"] ?? "";
227
227
 
228
+ const expectedBranch = spawnSync("git", ["branch", "--show-current"], {
229
+ cwd: projectBase,
230
+ encoding: "utf8",
231
+ }).stdout.trim() || "unknown";
232
+ const escapedBranch = expectedBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
233
+
228
234
  assert.deepEqual([...report.activeTools].sort(), ["files-tokens", "static-check"]);
229
235
  assert.equal(report.effectiveCtxCwd, projectBase);
230
236
  assert.equal(report.effectiveProcessCwd, projectBase);
231
- const normalizedBasePath = projectBase.split(path.sep).join("/");
232
237
  assert.match(status, /<accent>status:<\/accent><warning>idle<\/warning>/);
233
- assert.match(status, new RegExp(`<accent>current-path:</accent><warning>${normalizedBasePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}</warning>`));
234
- assert.match(status, /<accent>context:<\/accent><warning>▕_▏<\/warning>/);
238
+ assert.match(status, new RegExp(`<accent>branch:<\\/accent><warning>${escapedBranch}<\\/warning>`));
239
+ assert.doesNotMatch(status, /<accent>current-path:<\/accent>/);
240
+ assert.match(status, /<accent>context:<\/accent>▕_▏/);
235
241
  assert.match(status, /<accent>elapsed:<\/accent><warning>⏱︎ --:-- ⚑ --:-- ⌛︎--:--<\/warning>/);
236
242
  assert.match(status, /<accent>sound:<\/accent><warning>none<\/warning>/);
237
243
  assert.doesNotMatch(status, /<accent>beep:<\/accent>/);