pi-usereq 0.83.0 → 0.85.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.83.0)
1
+ # PI-useReq/pi-usereq (0.85.0)
2
2
 
3
3
  <p align="center">
4
4
  <img src="https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python 3.11+">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-usereq",
3
- "version": "0.83.0",
3
+ "version": "0.85.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Ogekuri/PI-useReq.git"
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @file
3
+ * @brief Persists deferred UI notifications across session rebinding for later replay.
4
+ * @details Stores non-agentic command-output notifications whose stale-context-safe delivery failed on one `globalThis` queue because pi rebinds extension modules for `/new`, `/resume`, `/fork`, and `/reload` while the hosting process persists across those operations, so undeliverable payloads survive until a later `session_start` restores a live UI context. Runtime is O(1) per operation. Side effects are limited to process-scoped state mutation; no local or global configuration file is ever written.
5
+ */
6
+
7
+ /**
8
+ * @brief Enumerates the `session_start` reasons that trigger deferred-notification replay.
9
+ * @details Covers the session-restoration boundaries where pi rebuilt the TUI and invalidated prior UI contexts: `new`, `resume`, `reload`, and `fork`. The `startup` reason starts one fresh host process whose process-scoped queue is empty by construction, so it is excluded from replay. Lookup complexity is O(1).
10
+ */
11
+ const DEFERRED_NOTIFICATION_REPLAY_SESSION_START_REASONS = new Set([
12
+ "new",
13
+ "resume",
14
+ "reload",
15
+ "fork",
16
+ ]);
17
+
18
+ /**
19
+ * @brief Describes one deferred notification queued for later replay.
20
+ * @details Pairs the exact notification message text with its original severity so the replayed delivery preserves the deferred payload without reparsing or reconstruction. The interface is compile-time only and introduces no runtime cost.
21
+ */
22
+ export interface DeferredNotificationRecord {
23
+ message: string;
24
+ level: "info" | "error";
25
+ }
26
+
27
+ /**
28
+ * @brief Defines the global property used for deferred-notification replay persistence.
29
+ * @details The property lives on `globalThis` because pi tears down and reloads extension modules on session switches while the host process persists, so the queue survives `/new`, `/resume`, `/fork`, `/reload`, and `req-reset` session restoration. Access complexity is O(1).
30
+ */
31
+ const PI_USEREQ_NOTIFICATION_REPLAY_KEY = "__piUsereqDeferredNotificationReplay";
32
+
33
+ /**
34
+ * @brief Describes the process-scoped deferred-notification replay state persisted across extension rebinds.
35
+ * @details Stores the ordered queue of undelivered notification payloads so a later `session_start` on one restored UI context can redeliver exactly the evidence that stale contexts dropped. The interface is compile-time only and introduces no runtime cost.
36
+ * @satisfies REQ-439
37
+ */
38
+ interface ProcessScopedNotificationReplayStore {
39
+ queue: DeferredNotificationRecord[];
40
+ }
41
+
42
+ /**
43
+ * @brief Returns the process-scoped deferred-notification replay store.
44
+ * @details Lazily initializes one `globalThis` record holding an empty queue so session rebinds reuse the accumulated deferred payloads instead of resetting them. Runtime is O(1). Side effect: initializes process-scoped state on first access.
45
+ * @return {ProcessScopedNotificationReplayStore} Mutable process-scoped replay store.
46
+ * @satisfies REQ-439
47
+ */
48
+ function getProcessScopedNotificationReplayStore(): ProcessScopedNotificationReplayStore {
49
+ const globalState = globalThis as typeof globalThis & {
50
+ __piUsereqDeferredNotificationReplay?: ProcessScopedNotificationReplayStore;
51
+ };
52
+ globalState[PI_USEREQ_NOTIFICATION_REPLAY_KEY] ??= { queue: [] };
53
+ return globalState[PI_USEREQ_NOTIFICATION_REPLAY_KEY]!;
54
+ }
55
+
56
+ /**
57
+ * @brief Queues one undeliverable notification for later replay.
58
+ * @details Appends the exact message plus severity to the tail of the process-scoped replay queue so the payload survives extension rebinding and can be redelivered on the next eligible `session_start`. Already-delivered notifications are never enqueued by contract, so replay can never duplicate them. Runtime is O(1). Side effect: mutates process-scoped state.
59
+ * @param[in] message {string} Exact notification message text whose stale-context delivery failed.
60
+ * @param[in] level {"info" | "error"} Original notification severity preserved for the replayed delivery.
61
+ * @return {void} No return value.
62
+ * @satisfies REQ-439, REQ-442
63
+ */
64
+ export function enqueueDeferredNotification(message: string, level: "info" | "error"): void {
65
+ getProcessScopedNotificationReplayStore().queue.push({ message, level });
66
+ }
67
+
68
+ /**
69
+ * @brief Returns a snapshot of the queued deferred notifications.
70
+ * @details Copies the queued records into one detached array so callers can inspect pending replay payloads without mutating the store. Runtime is O(n) in queued-record count. No external state is mutated.
71
+ * @return {DeferredNotificationRecord[]} Detached copy of the queued records in enqueue order.
72
+ * @satisfies REQ-439
73
+ */
74
+ export function readDeferredNotifications(): DeferredNotificationRecord[] {
75
+ return [...getProcessScopedNotificationReplayStore().queue];
76
+ }
77
+
78
+ /**
79
+ * @brief Atomically removes and returns every queued deferred notification.
80
+ * @details Empties the process-scoped queue and returns the removed records in enqueue order so replay delivery owns each payload exactly once; payloads whose replay delivery fails stale again are re-enqueued by the deferred-delivery helper instead of remaining in this drain result. Runtime is O(n) in queued-record count. Side effect: clears process-scoped state.
81
+ * @return {DeferredNotificationRecord[]} Removed records in enqueue order.
82
+ * @satisfies REQ-440
83
+ */
84
+ export function drainDeferredNotifications(): DeferredNotificationRecord[] {
85
+ const store = getProcessScopedNotificationReplayStore();
86
+ const drained = store.queue;
87
+ store.queue = [];
88
+ return drained;
89
+ }
90
+
91
+ /**
92
+ * @brief Clears every queued deferred notification.
93
+ * @details Resets the process-scoped queue to empty so stale deferred payloads never reach later sessions after explicit cleanup. Runtime is O(1). Side effect: mutates process-scoped state.
94
+ * @return {void} No return value.
95
+ * @satisfies REQ-439
96
+ */
97
+ export function clearDeferredNotifications(): void {
98
+ getProcessScopedNotificationReplayStore().queue = [];
99
+ }
100
+
101
+ /**
102
+ * @brief Tests whether one `session_start` event must trigger deferred-notification replay.
103
+ * @details Reads the loosely-typed `reason` field from the session-start payload and accepts exactly the documented restoration reasons `new`, `resume`, `reload`, and `fork`, excluding `startup` because one fresh host process starts with an empty process-scoped queue. Missing or unknown reasons degrade to `false`. Runtime is O(1). No external state is mutated.
104
+ * @param[in] event {unknown} Session-start payload forwarded by the pi lifecycle wrapper.
105
+ * @return {boolean} `true` when deferred notifications must be replayed for this session-start reason.
106
+ * @satisfies REQ-440
107
+ */
108
+ export function shouldReplayDeferredNotificationsOnSessionStart(event: unknown): boolean {
109
+ const reason = (event as { reason?: unknown } | undefined)?.reason;
110
+ return typeof reason === "string"
111
+ && DEFERRED_NOTIFICATION_REPLAY_SESSION_START_REASONS.has(reason);
112
+ }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { spawnSync, type SpawnSyncReturns } from "node:child_process";
8
+ import fs from "node:fs";
8
9
  import path from "node:path";
9
10
  import type { UseReqConfig } from "./config.js";
10
11
  import { ReqError } from "./errors.js";
@@ -34,6 +35,22 @@ export interface ReqReferencesCommandPlan {
34
35
  commitMessage: string;
35
36
  }
36
37
 
38
+ /**
39
+ * @brief Describes the per-operation outcome facts of one completed `req-references` run.
40
+ * @details Reports whether the regenerated `REFERENCES.md` content differs from the pre-run file and whether the guarded commit created a commit or was skipped, so the extension command handler can emit notifications that conform to the operations actually performed regardless of the update and commit outcomes. The interface is compile-time only and introduces no runtime cost.
41
+ * @satisfies REQ-436
42
+ */
43
+ export interface ReqReferencesCommandExecutionResult {
44
+ /** Absolute path of the regenerated `REFERENCES.md` document. */
45
+ referencesPath: string;
46
+ /** `true` when the regenerated content differs from the pre-run file content or the file was absent. */
47
+ referencesChanged: boolean;
48
+ /** Fixed commit message that a created commit carries. */
49
+ commitMessage: string;
50
+ /** `true` when the guarded commit created a commit; `false` when it was skipped because content matched `HEAD`. */
51
+ commitCreated: boolean;
52
+ }
53
+
37
54
  /**
38
55
  * @brief Executes one synchronous subprocess and captures UTF-8 output.
39
56
  * @details Delegates to `spawnSync(...)`, preserves the supplied working directory, and returns the raw result so callers can interpret git exit status plus diagnostics deterministically. Runtime is dominated by external process execution. Side effects include subprocess creation.
@@ -135,17 +152,17 @@ function hasStagedChangesForPaths(gitRoot: string, targetPaths: string[]): boole
135
152
 
136
153
  /**
137
154
  * @brief Executes one guarded `git commit` invocation for the staged target paths.
138
- * @details Runs a staged-changes precheck against the cached index and returns without creating a commit when no staged difference exists for the target paths, preventing empty-commit failures such as `nothing to commit, working tree clean`. When a staged difference exists, delegates to `git commit -m <commitMessage>` and converts any non-zero result into a deterministic `ReqError`. Runtime is dominated by up to two git subprocesses. Side effects include subprocess creation and conditional commit creation.
155
+ * @details Runs a staged-changes precheck against the cached index and returns `false` without creating a commit when no staged difference exists for the target paths, preventing empty-commit failures such as `nothing to commit, working tree clean`. When a staged difference exists, delegates to `git commit -m <commitMessage>`, converts any non-zero result into a deterministic `ReqError`, and returns `true`. Runtime is dominated by up to two git subprocesses. Side effects include subprocess creation and conditional commit creation.
139
156
  * @param[in] gitRoot {string} Absolute git root path.
140
157
  * @param[in] targetPaths {string[]} Git-add target paths inspected by the staged-changes precheck.
141
158
  * @param[in] commitMessage {string} Commit message used when a staged difference exists.
142
- * @return {void} No return value.
159
+ * @return {boolean} `true` when the commit was created; `false` when it was skipped because the cached index held no staged difference for the target paths.
143
160
  * @throws {ReqError} Throws when staged-difference inspection or commit creation fails.
144
161
  * @satisfies REQ-357, REQ-358
145
162
  */
146
- function runGuardedGitCommit(gitRoot: string, targetPaths: string[], commitMessage: string): void {
163
+ function runGuardedGitCommit(gitRoot: string, targetPaths: string[], commitMessage: string): boolean {
147
164
  if (!hasStagedChangesForPaths(gitRoot, targetPaths)) {
148
- return;
165
+ return false;
149
166
  }
150
167
  const commitResult = runCapture(["git", "commit", "-m", commitMessage], gitRoot);
151
168
  if (commitResult.error || commitResult.status !== 0) {
@@ -155,6 +172,7 @@ function runGuardedGitCommit(gitRoot: string, targetPaths: string[], commitMessa
155
172
  || "unknown error";
156
173
  throw new ReqError(`ERROR: git commit failed: ${diagnostic}`, 1);
157
174
  }
175
+ return true;
158
176
  }
159
177
 
160
178
  /**
@@ -182,28 +200,50 @@ export function prepareReqReferencesCommandExecution(
182
200
  }
183
201
 
184
202
  /**
185
- * @brief Executes the specialized `req-references` direct-write workflow.
186
- * @details Regenerates `REFERENCES.md` through the same source-summary path used by the `references` tool, stages only the target file, creates the fixed-message commit through the guarded commit helper whenever a staged difference exists, and verifies that no residual git-status rows remain after ignored extension-owned debug artifacts are filtered out. Runtime is dominated by summary generation plus two to four git subprocesses. Side effects include documentation writes, index mutation, conditional commit creation, and subprocess creation.
203
+ * @brief Executes the specialized `req-references` direct-write workflow and reports per-operation outcome facts.
204
+ * @details Snapshots the pre-run `REFERENCES.md` content, regenerates the document through the same source-summary path used by the `references` tool, compares the regenerated content against the pre-run snapshot, stages only the target file, creates the fixed-message commit through the guarded commit helper whenever a staged difference exists, verifies that no residual git-status rows remain after ignored extension-owned debug artifacts are filtered out, and returns whether the references content changed and whether the commit was created or skipped so callers can emit outcome-conformant notifications. Runtime is dominated by summary generation plus two to four git subprocesses. Side effects include documentation writes, index mutation, conditional commit creation, and subprocess creation.
187
205
  * @param[in] plan {ReqReferencesCommandPlan} Prepared direct-write execution plan.
188
206
  * @param[in] config {UseReqConfig} Effective project configuration.
189
- * @return {void} No return value.
190
- * @throws {ReqError} Throws when reference generation, staging, guarded commit creation, or cleanliness verification fails.
191
- * @satisfies REQ-300, REQ-301, REQ-302, REQ-303, REQ-357, REQ-358
207
+ * @return {ReqReferencesCommandExecutionResult} Per-operation outcome facts including `referencesChanged` and `commitCreated`.
208
+ * @throws {ReqError} Throws when reference generation, post-run content readback, staging, guarded commit creation, or cleanliness verification fails.
209
+ * @satisfies REQ-300, REQ-301, REQ-302, REQ-303, REQ-357, REQ-358, REQ-436
192
210
  */
193
211
  export function executeReqReferencesCommandExecution(
194
212
  plan: ReqReferencesCommandPlan,
195
213
  config: UseReqConfig,
196
- ): void {
214
+ ): ReqReferencesCommandExecutionResult {
215
+ let previousReferencesContent: string | undefined;
216
+ try {
217
+ previousReferencesContent = fs.readFileSync(plan.referencesPath, "utf8");
218
+ } catch {
219
+ previousReferencesContent = undefined;
220
+ }
197
221
  runReferences(plan.basePath, config);
222
+ let regeneratedReferencesContent: string | undefined;
223
+ try {
224
+ regeneratedReferencesContent = fs.readFileSync(plan.referencesPath, "utf8");
225
+ } catch {
226
+ regeneratedReferencesContent = undefined;
227
+ }
228
+ if (regeneratedReferencesContent === undefined) {
229
+ throw new ReqError(`ERROR: Unable to read the regenerated REFERENCES.md document at ${plan.referencesPath}.`, 1);
230
+ }
231
+ const referencesChanged = regeneratedReferencesContent !== previousReferencesContent;
198
232
  const addTargetPath = getGitAddTargetPath(plan.gitPath, plan.referencesPath);
199
233
  const addResult = runCapture(["git", "add", "--", addTargetPath], plan.gitPath);
200
234
  if (addResult.error || addResult.status !== 0) {
201
235
  const diagnostic = addResult.stderr.trim() || addResult.error?.message || "unknown error";
202
236
  throw new ReqError(`ERROR: git add failed for ${addTargetPath}: ${diagnostic}`, 1);
203
237
  }
204
- runGuardedGitCommit(plan.gitPath, [addTargetPath], plan.commitMessage);
238
+ const commitCreated = runGuardedGitCommit(plan.gitPath, [addTargetPath], plan.commitMessage);
205
239
  const residualStatusLines = listResidualGitStatusLines(plan.basePath, plan.gitPath, config);
206
240
  if (residualStatusLines.length > 0) {
207
241
  throw new ReqError("ERROR: Git repository is not clean after req-references commit.", 1);
208
242
  }
243
+ return {
244
+ referencesPath: plan.referencesPath,
245
+ referencesChanged,
246
+ commitMessage: plan.commitMessage,
247
+ commitCreated,
248
+ };
209
249
  }
package/src/index.ts CHANGED
@@ -168,6 +168,11 @@ import {
168
168
  writePersistedPromptCommandRuntimeState,
169
169
  type PersistedPromptCommandRuntimeApi,
170
170
  } from "./core/prompt-command-state.js";
171
+ import {
172
+ drainDeferredNotifications,
173
+ enqueueDeferredNotification,
174
+ shouldReplayDeferredNotificationsOnSessionStart,
175
+ } from "./core/notification-replay.js";
171
176
  import { ensureBundledResourcesAccessible, readBundledPromptDescription } from "./core/resources.js";
172
177
  import { ReqError } from "./core/errors.js";
173
178
  import {
@@ -1199,13 +1204,13 @@ function resolveDebugProjectBase(cwd: string, statusController: PiUsereqStatusCo
1199
1204
  }
1200
1205
 
1201
1206
  /**
1202
- * @brief Delivers one best-effort UI notification without failing on stale replacement contexts.
1203
- * @details Attempts to use the supplied extension context for UI notification delivery and suppresses the documented stale-extension-context runtime error raised after session replacement, because prompt-orchestration closure can outlive the context that initiated the switch. Runtime is O(n) in message length. Side effects are limited to user notification delivery when the context is still active.
1207
+ * @brief Delivers one best-effort UI notification, deferring the payload when the context is stale.
1208
+ * @details Attempts to use the supplied extension context for UI notification delivery; when no context is available or pi raises the documented stale-extension-context error because the context was invalidated by session replacement or reload, the exact message plus severity is enqueued into the process-scoped deferred-notification replay store so one later eligible `session_start` can redeliver it on the restored context instead of losing the evidence. The payload is enqueued only on failed delivery, so already-delivered notifications are never replayed. Runtime is O(1). Side effects are limited to user notification delivery when the context is still active and process-scoped queue mutation when delivery fails.
1204
1209
  * @param[in] ctx {ExtensionContext | ExtensionCommandContext | undefined} Candidate UI context.
1205
1210
  * @param[in] message {string} Notification message.
1206
1211
  * @param[in] level {"info" | "error"} Notification severity.
1207
- * @return {boolean} `true` when the notification was delivered and `false` when the context was already stale.
1208
- * @satisfies REQ-280
1212
+ * @return {boolean} `true` when the notification was delivered and `false` when delivery failed and the payload was deferred for replay.
1213
+ * @satisfies REQ-280, REQ-439, REQ-442
1209
1214
  */
1210
1215
  function notifyContextSafely(
1211
1216
  ctx: (ExtensionContext | ExtensionCommandContext) | undefined,
@@ -1213,6 +1218,7 @@ function notifyContextSafely(
1213
1218
  level: "info" | "error",
1214
1219
  ): boolean {
1215
1220
  if (ctx === undefined) {
1221
+ enqueueDeferredNotification(message, level);
1216
1222
  return false;
1217
1223
  }
1218
1224
  try {
@@ -1220,6 +1226,7 @@ function notifyContextSafely(
1220
1226
  return true;
1221
1227
  } catch (error) {
1222
1228
  if (isStaleExtensionContextError(error)) {
1229
+ enqueueDeferredNotification(message, level);
1223
1230
  return false;
1224
1231
  }
1225
1232
  throw error;
@@ -1232,7 +1239,7 @@ function notifyContextSafely(
1232
1239
  * @param[in] statusController {PiUsereqStatusController} Mutable status controller supplying the latest normalized context-usage snapshot.
1233
1240
  * @param[in] ctx {(ExtensionContext | ExtensionCommandContext) | undefined} Candidate UI context for notification delivery.
1234
1241
  * @return {void} No return value.
1235
- * @satisfies REQ-360, REQ-361, REQ-362, REQ-363, REQ-364, REQ-365
1242
+ * @satisfies REQ-360, REQ-361, REQ-362, REQ-363, REQ-364, REQ-365, REQ-439, REQ-441
1236
1243
  */
1237
1244
  function notifyContextRetentionReminder(
1238
1245
  statusController: PiUsereqStatusController,
@@ -1494,7 +1501,7 @@ async function finalizeMatchedPromptSuccess(
1494
1501
  * @param[in] event {unknown} Hook payload forwarded by pi.
1495
1502
  * @param[in] ctx {ExtensionContext} Active extension context.
1496
1503
  * @return {Promise<void>} Promise resolved when hook processing completes.
1497
- * @satisfies REQ-117, REQ-118, REQ-119, REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-176, REQ-178, REQ-184, REQ-185, REQ-186, REQ-187, REQ-208, REQ-209, REQ-221, REQ-228, REQ-229, REQ-230, REQ-244, REQ-245, REQ-246, REQ-247, REQ-276, REQ-277, REQ-278, REQ-279, REQ-280, REQ-291, REQ-292, REQ-421, REQ-422, REQ-425
1504
+ * @satisfies REQ-117, REQ-118, REQ-119, REQ-131, REQ-132, REQ-133, REQ-166, REQ-167, REQ-168, REQ-169, REQ-172, REQ-176, REQ-178, REQ-184, REQ-185, REQ-186, REQ-187, REQ-208, REQ-209, REQ-221, REQ-228, REQ-229, REQ-230, REQ-244, REQ-245, REQ-246, REQ-247, REQ-276, REQ-277, REQ-278, REQ-279, REQ-280, REQ-291, REQ-292, REQ-421, REQ-422, REQ-425, REQ-440
1498
1505
  */
1499
1506
  async function handleExtensionStatusEvent(
1500
1507
  pi: ExtensionAPI,
@@ -1529,6 +1536,12 @@ async function handleExtensionStatusEvent(
1529
1536
  "warning",
1530
1537
  );
1531
1538
  }
1539
+ if (shouldReplayDeferredNotificationsOnSessionStart(event)) {
1540
+ const deferredNotifications = drainDeferredNotifications();
1541
+ for (const deferredNotification of deferredNotifications) {
1542
+ notifyContextSafely(ctx, deferredNotification.message, deferredNotification.level);
1543
+ }
1544
+ }
1532
1545
  }
1533
1546
  if (hookName === "before_agent_start") {
1534
1547
  const requestForActivation = activePromptRequest ?? pendingPromptRequest;
@@ -3409,7 +3422,7 @@ function resolveReqResetPromptRequest(
3409
3422
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3410
3423
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
3411
3424
  * @return {void} No return value.
3412
- * @satisfies REQ-304, REQ-305, REQ-306, REQ-307, REQ-308, REQ-309, REQ-310, REQ-311, REQ-312, REQ-313
3425
+ * @satisfies REQ-304, REQ-305, REQ-306, REQ-307, REQ-308, REQ-309, REQ-310, REQ-311, REQ-312, REQ-313, REQ-441
3413
3426
  */
3414
3427
  function registerReqResetCommand(
3415
3428
  pi: ExtensionAPI,
@@ -3480,11 +3493,11 @@ function registerReqResetCommand(
3480
3493
 
3481
3494
  /**
3482
3495
  * @brief Registers the specialized `req-references` slash command.
3483
- * @details Registers the non-agentic references-maintenance command that rejects non-`idle` invocations by transitioning workflow state to `error` before direct execution, otherwise reuses slash-command-owned git validation, transitions workflow state through `checking|running|idle`, regenerates `REFERENCES.md` directly from configured source directories, stages only the generated file, creates the fixed-message git commit, verifies repository cleanliness, and notifies pi without starting an LLM session or creating a worktree. Runtime is dominated by git subprocess execution plus source-summary generation. Side effects include command registration, status-controller mutation, filesystem writes, git index/history mutation, and user notifications.
3496
+ * @details Registers the non-agentic references-maintenance command that rejects non-`idle` invocations by transitioning workflow state to `error` before direct execution, otherwise reuses slash-command-owned git validation, transitions workflow state through `checking|running|idle`, regenerates `REFERENCES.md` directly from configured source directories, stages only the generated file, creates the fixed-message git commit, verifies repository cleanliness, and emits execution notifications that conform to the operations actually performed — one stating whether the references file was updated or already matched the generated content and one stating whether the fixed-message commit was created or skipped — regardless of the update and commit outcomes, without starting an LLM session or creating a worktree. Runtime is dominated by git subprocess execution plus source-summary generation. Side effects include command registration, status-controller mutation, filesystem writes, git index/history mutation, and user notifications.
3484
3497
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3485
3498
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
3486
3499
  * @return {void} No return value.
3487
- * @satisfies REQ-200, REQ-221, REQ-224, REQ-298, REQ-299, REQ-300, REQ-301, REQ-302, REQ-303
3500
+ * @satisfies REQ-200, REQ-221, REQ-224, REQ-298, REQ-299, REQ-300, REQ-301, REQ-302, REQ-303, REQ-437, REQ-438, REQ-441
3488
3501
  */
3489
3502
  function registerReqReferencesCommand(
3490
3503
  pi: ExtensionAPI,
@@ -3510,10 +3523,28 @@ function registerReqReferencesCommand(
3510
3523
  try {
3511
3524
  const executionPlan = prepareReqReferencesCommandExecution(projectBase, config);
3512
3525
  setPiUsereqWorkflowState(statusController, "running", ctx);
3513
- executeReqReferencesCommandExecution(executionPlan, config);
3526
+ const executionResult = executeReqReferencesCommandExecution(executionPlan, config);
3514
3527
  setPiUsereqWorkflowState(statusController, "idle", ctx);
3515
- ctx.ui.notify(
3516
- `SUCCESS: Updated ${formatRuntimePathForDisplay(executionPlan.referencesPath)} and committed changes.`,
3528
+ const referencesDisplayPath = formatRuntimePathForDisplay(executionResult.referencesPath);
3529
+ notifyContextSafely(
3530
+ ctx,
3531
+ executionResult.referencesChanged
3532
+ ? `req-references: Updated ${referencesDisplayPath}.`
3533
+ : `req-references: ${referencesDisplayPath} already matches the generated references content; no file update was needed.`,
3534
+ "info",
3535
+ );
3536
+ notifyContextSafely(
3537
+ ctx,
3538
+ executionResult.commitCreated
3539
+ ? `req-references: Created commit ${executionResult.commitMessage}`
3540
+ : "req-references: Skipped the git commit because the references content already matched HEAD.",
3541
+ "info",
3542
+ );
3543
+ notifyContextSafely(
3544
+ ctx,
3545
+ executionResult.referencesChanged && executionResult.commitCreated
3546
+ ? `SUCCESS: Updated ${referencesDisplayPath} and committed changes.`
3547
+ : `SUCCESS: req-references completed with a clean repository (${executionResult.referencesChanged ? "references updated" : "references unchanged"}; ${executionResult.commitCreated ? "commit created" : "commit skipped"}).`,
3517
3548
  "info",
3518
3549
  );
3519
3550
  notifyContextRetentionReminder(statusController, ctx);
@@ -3522,7 +3553,7 @@ function registerReqReferencesCommand(
3522
3553
  statusController.state.activePromptRequest = undefined;
3523
3554
  setPiUsereqWorkflowState(statusController, "error", ctx);
3524
3555
  const message = error instanceof Error ? error.message : String(error);
3525
- ctx.ui.notify(message, "error");
3556
+ notifyContextSafely(ctx, message, "error");
3526
3557
  throw error;
3527
3558
  }
3528
3559
  },