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 +1 -1
- package/package.json +1 -1
- package/src/core/notification-replay.ts +112 -0
- package/src/core/req-references-command.ts +51 -11
- package/src/index.ts +44 -13
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -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
|
|
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 {
|
|
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):
|
|
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
|
|
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 {
|
|
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
|
-
):
|
|
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
|
|
1203
|
-
* @details Attempts to use the supplied extension context for UI notification delivery
|
|
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
|
|
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
|
|
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
|
-
|
|
3516
|
-
|
|
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
|
|
3556
|
+
notifyContextSafely(ctx, message, "error");
|
|
3526
3557
|
throw error;
|
|
3527
3558
|
}
|
|
3528
3559
|
},
|