pi-usereq 0.84.0 → 0.86.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.84.0)
1
+ # PI-useReq/pi-usereq (0.86.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.84.0",
3
+ "version": "0.86.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Ogekuri/PI-useReq.git"
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @file
3
+ * @brief Persists durable non-agentic command execution reports as custom session entries.
4
+ * @details Defines the stable custom-entry type, machine-readable report payload, builders for `req-references` outcome lines, and the pi guarded helpers that append the report entry and register its transcript renderer. Custom entries are stored in the session file, excluded from LLM context, rendered live through `entry_appended`, and re-rendered after any session restore, so transient UI-notification collapsing or chat rebuilds never lose the executed-commands outcome report. Runtime is O(n) in report-line count. Side effects are limited to session-entry persistence and renderer registration through the pi extension API.
5
+ */
6
+
7
+ import type {
8
+ CustomEntry,
9
+ EntryRenderOptions,
10
+ EntryRenderer,
11
+ ExtensionAPI,
12
+ Theme,
13
+ } from "@earendil-works/pi-coding-agent";
14
+ import { Text } from "@earendil-works/pi-tui";
15
+ import type { Component } from "@earendil-works/pi-tui";
16
+ import { formatRuntimePathForDisplay } from "./path-context.js";
17
+ import type { ReqReferencesCommandExecutionResult } from "./req-references-command.js";
18
+
19
+ /**
20
+ * @brief Declares the stable custom-entry type used for durable command execution reports.
21
+ * @details Identifies `pi-usereq` report entries inside the persisted session file so interactive mode renders them through the registered entry renderer after reload, resume, fork, and session switches. Lookup complexity is O(1).
22
+ */
23
+ export const COMMAND_REPORT_ENTRY_TYPE = "pi-usereq.command-report";
24
+
25
+ /**
26
+ * @brief Describes one durable command execution report entry payload.
27
+ * @details Stores the originating slash-command name, the report severity, the terminal outcome, and the ordered per-operation outcome lines rendered in the transcript. The interface is compile-time only and introduces no runtime cost.
28
+ */
29
+ export interface CommandReportEntryData {
30
+ command: string;
31
+ level: "info" | "error";
32
+ outcome: "success" | "error";
33
+ lines: string[];
34
+ }
35
+
36
+ /**
37
+ * @brief Describes the loosely-typed pi runtime surface required for durable report entries.
38
+ * @details Narrows the extension API to the optional `registerEntryRenderer(...)` and `appendEntry(...)` methods consumed by the guarded report helpers so legacy hosts and offline harnesses without custom-entry support degrade silently instead of throwing. The interface is compile-time only and introduces no runtime cost.
39
+ */
40
+ interface CommandReportEntryRegistrar {
41
+ registerEntryRenderer?: (
42
+ customType: string,
43
+ renderer: EntryRenderer<CommandReportEntryData>,
44
+ ) => void;
45
+ appendEntry?: (customType: string, data?: CommandReportEntryData) => void;
46
+ }
47
+
48
+ /**
49
+ * @brief Validates one loosely-typed payload as a command report entry payload.
50
+ * @details Accepts only payloads with a non-empty command string, the documented `info|error` level, the documented `success|error` outcome, and a string-only lines array so malformed session data never reaches the transcript renderer. Runtime is O(n) in line count. No external state is mutated.
51
+ * @param[in] value {unknown} Candidate persisted payload.
52
+ * @return {boolean} `true` when the payload satisfies the report entry contract.
53
+ */
54
+ function isCommandReportEntryData(value: unknown): value is CommandReportEntryData {
55
+ if (typeof value !== "object" || value === null) {
56
+ return false;
57
+ }
58
+ const candidate = value as Partial<CommandReportEntryData>;
59
+ return typeof candidate.command === "string"
60
+ && candidate.command.trim() !== ""
61
+ && (candidate.level === "info" || candidate.level === "error")
62
+ && (candidate.outcome === "success" || candidate.outcome === "error")
63
+ && Array.isArray(candidate.lines)
64
+ && candidate.lines.every((line) => typeof line === "string");
65
+ }
66
+
67
+ /**
68
+ * @brief Builds the ordered per-operation outcome lines for one completed `req-references` run.
69
+ * @details Emits one machine-readable line per executed operation — terminal outcome, references-file regeneration result, guarded git-commit result, and post-run repository cleanliness — so the durable report states exactly which commands ran and with which outcome. Runtime is O(n) in path length. No external state is mutated.
70
+ * @param[in] executionResult {ReqReferencesCommandExecutionResult} Per-operation outcome facts of the completed run.
71
+ * @return {string[]} Ordered report lines starting with the terminal outcome.
72
+ * @satisfies REQ-436, REQ-437, REQ-438
73
+ */
74
+ export function buildReqReferencesCommandReportLines(
75
+ executionResult: ReqReferencesCommandExecutionResult,
76
+ ): string[] {
77
+ const referencesDisplayPath = formatRuntimePathForDisplay(executionResult.referencesPath);
78
+ return [
79
+ `outcome: success`,
80
+ `references file ${referencesDisplayPath}: ${
81
+ executionResult.referencesChanged ? "updated" : "already matched the generated content"
82
+ }`,
83
+ `git commit: ${
84
+ executionResult.commitCreated
85
+ ? `created "${executionResult.commitMessage}"`
86
+ : "skipped (references content matched HEAD)"
87
+ }`,
88
+ "repository: clean after run",
89
+ ];
90
+ }
91
+
92
+ /**
93
+ * @brief Builds the durable report entry payload for one completed `req-references` run.
94
+ * @details Pairs the invokable command name with the per-operation outcome lines and the `success` outcome. Runtime is O(n) in report-line count. No external state is mutated.
95
+ * @param[in] executionResult {ReqReferencesCommandExecutionResult} Per-operation outcome facts of the completed run.
96
+ * @return {CommandReportEntryData} Report entry payload for session persistence.
97
+ * @satisfies REQ-436, REQ-437, REQ-438
98
+ */
99
+ export function buildReqReferencesCommandReportEntryData(
100
+ executionResult: ReqReferencesCommandExecutionResult,
101
+ ): CommandReportEntryData {
102
+ return {
103
+ command: "req-references",
104
+ level: "info",
105
+ outcome: "success",
106
+ lines: buildReqReferencesCommandReportLines(executionResult),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * @brief Builds the durable report entry payload for one failed non-agentic command run.
112
+ * @details Records the invokable command name, the `error` outcome, and the surfaced failure diagnostic so failed runs keep their outcome evidence in the transcript. Runtime is O(n) in message length. No external state is mutated.
113
+ * @param[in] command {string} Invokable slash-command name that failed.
114
+ * @param[in] errorMessage {string} Surfaced failure diagnostic.
115
+ * @return {CommandReportEntryData} Report entry payload for session persistence.
116
+ */
117
+ export function buildCommandReportErrorEntryData(
118
+ command: string,
119
+ errorMessage: string,
120
+ ): CommandReportEntryData {
121
+ return {
122
+ command,
123
+ level: "error",
124
+ outcome: "error",
125
+ lines: [`outcome: error`, `error: ${errorMessage}`],
126
+ };
127
+ }
128
+
129
+ /**
130
+ * @brief Appends one durable command report entry to the active session when the host supports custom entries.
131
+ * @details Probes the optional `pi.appendEntry(...)` surface before use so legacy hosts and offline harnesses without custom-entry support skip persistence silently; supported hosts persist the payload as a `custom` session entry that survives session restore while remaining excluded from LLM context. Runtime is O(1) plus delegated session-entry persistence. Side effects include session-file entry persistence on supported hosts.
132
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
133
+ * @param[in] data {CommandReportEntryData} Report entry payload to persist.
134
+ * @return {boolean} `true` when the entry was appended; `false` when the host lacks the custom-entry surface.
135
+ */
136
+ export function appendCommandReportEntry(pi: ExtensionAPI, data: CommandReportEntryData): boolean {
137
+ const registrar = pi as ExtensionAPI & CommandReportEntryRegistrar;
138
+ if (typeof registrar.appendEntry !== "function") {
139
+ return false;
140
+ }
141
+ registrar.appendEntry(COMMAND_REPORT_ENTRY_TYPE, data);
142
+ return true;
143
+ }
144
+
145
+ /**
146
+ * @brief Renders one durable command report entry as a transcript component.
147
+ * @details Maps the report payload into a themed multi-line `Text` — header colored `error` for failed runs and `accent` otherwise, body lines colored `error` for failed runs and `muted` otherwise — and returns `undefined` for malformed payloads so the host transcript skips undisplayable entries. Runtime is O(n) in aggregate report-line length. No external state is mutated.
148
+ * @param[in] entry {CustomEntry<CommandReportEntryData>} Persisted custom report entry.
149
+ * @param[in] _options {EntryRenderOptions} Host render options; retained for renderer-signature compatibility.
150
+ * @param[in] theme {Theme} Active pi theme used for color tokens.
151
+ * @return {Component | undefined} Transcript component, or `undefined` when the payload is malformed.
152
+ */
153
+ function renderCommandReportEntry(
154
+ entry: CustomEntry<CommandReportEntryData>,
155
+ _options: EntryRenderOptions,
156
+ theme: Theme,
157
+ ): Component | undefined {
158
+ if (!isCommandReportEntryData(entry.data)) {
159
+ return undefined;
160
+ }
161
+ const data = entry.data;
162
+ const isError = data.level === "error";
163
+ const headerColor = isError ? "error" : "accent";
164
+ const bodyColor = isError ? "error" : "muted";
165
+ const header = theme.fg(headerColor, `pi-usereq ${data.command}: execution report`);
166
+ const body = data.lines.map((line) => theme.fg(bodyColor, line)).join("\n");
167
+ return new Text(`${header}\n${body}`, 1, 0);
168
+ }
169
+
170
+ /**
171
+ * @brief Registers the durable command report transcript renderer when the host supports entry rendering.
172
+ * @details Probes the optional `pi.registerEntryRenderer(...)` surface before use so legacy hosts and offline harnesses without custom-entry rendering skip registration silently; supported hosts bind the renderer for `pi-usereq.command-report` entries so appended reports render in the live transcript and re-render after every session restore. Runtime is O(1). Side effects include entry-renderer registration on supported hosts.
173
+ * @param[in] pi {ExtensionAPI} Active extension API instance.
174
+ * @return {void} No return value.
175
+ */
176
+ export function registerCommandReportEntryRenderer(pi: ExtensionAPI): void {
177
+ const registrar = pi as ExtensionAPI & CommandReportEntryRegistrar;
178
+ if (typeof registrar.registerEntryRenderer !== "function") {
179
+ return;
180
+ }
181
+ registrar.registerEntryRenderer(COMMAND_REPORT_ENTRY_TYPE, renderCommandReportEntry);
182
+ }
@@ -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
+ }
package/src/index.ts CHANGED
@@ -168,7 +168,18 @@ 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";
177
+ import {
178
+ appendCommandReportEntry,
179
+ buildCommandReportErrorEntryData,
180
+ buildReqReferencesCommandReportEntryData,
181
+ registerCommandReportEntryRenderer,
182
+ } from "./core/command-report-entry.js";
172
183
  import { ReqError } from "./core/errors.js";
173
184
  import {
174
185
  PI_USEREQ_STATUS_HOOK_NAMES,
@@ -1199,13 +1210,13 @@ function resolveDebugProjectBase(cwd: string, statusController: PiUsereqStatusCo
1199
1210
  }
1200
1211
 
1201
1212
  /**
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.
1213
+ * @brief Delivers one best-effort UI notification, deferring the payload when the context is stale.
1214
+ * @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
1215
  * @param[in] ctx {ExtensionContext | ExtensionCommandContext | undefined} Candidate UI context.
1205
1216
  * @param[in] message {string} Notification message.
1206
1217
  * @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
1218
+ * @return {boolean} `true` when the notification was delivered and `false` when delivery failed and the payload was deferred for replay.
1219
+ * @satisfies REQ-280, REQ-439, REQ-442
1209
1220
  */
1210
1221
  function notifyContextSafely(
1211
1222
  ctx: (ExtensionContext | ExtensionCommandContext) | undefined,
@@ -1213,6 +1224,7 @@ function notifyContextSafely(
1213
1224
  level: "info" | "error",
1214
1225
  ): boolean {
1215
1226
  if (ctx === undefined) {
1227
+ enqueueDeferredNotification(message, level);
1216
1228
  return false;
1217
1229
  }
1218
1230
  try {
@@ -1220,6 +1232,7 @@ function notifyContextSafely(
1220
1232
  return true;
1221
1233
  } catch (error) {
1222
1234
  if (isStaleExtensionContextError(error)) {
1235
+ enqueueDeferredNotification(message, level);
1223
1236
  return false;
1224
1237
  }
1225
1238
  throw error;
@@ -1232,7 +1245,7 @@ function notifyContextSafely(
1232
1245
  * @param[in] statusController {PiUsereqStatusController} Mutable status controller supplying the latest normalized context-usage snapshot.
1233
1246
  * @param[in] ctx {(ExtensionContext | ExtensionCommandContext) | undefined} Candidate UI context for notification delivery.
1234
1247
  * @return {void} No return value.
1235
- * @satisfies REQ-360, REQ-361, REQ-362, REQ-363, REQ-364, REQ-365
1248
+ * @satisfies REQ-360, REQ-361, REQ-362, REQ-363, REQ-364, REQ-365, REQ-439, REQ-441
1236
1249
  */
1237
1250
  function notifyContextRetentionReminder(
1238
1251
  statusController: PiUsereqStatusController,
@@ -1494,7 +1507,7 @@ async function finalizeMatchedPromptSuccess(
1494
1507
  * @param[in] event {unknown} Hook payload forwarded by pi.
1495
1508
  * @param[in] ctx {ExtensionContext} Active extension context.
1496
1509
  * @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
1510
+ * @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
1511
  */
1499
1512
  async function handleExtensionStatusEvent(
1500
1513
  pi: ExtensionAPI,
@@ -1529,6 +1542,12 @@ async function handleExtensionStatusEvent(
1529
1542
  "warning",
1530
1543
  );
1531
1544
  }
1545
+ if (shouldReplayDeferredNotificationsOnSessionStart(event)) {
1546
+ const deferredNotifications = drainDeferredNotifications();
1547
+ for (const deferredNotification of deferredNotifications) {
1548
+ notifyContextSafely(ctx, deferredNotification.message, deferredNotification.level);
1549
+ }
1550
+ }
1532
1551
  }
1533
1552
  if (hookName === "before_agent_start") {
1534
1553
  const requestForActivation = activePromptRequest ?? pendingPromptRequest;
@@ -3409,7 +3428,7 @@ function resolveReqResetPromptRequest(
3409
3428
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3410
3429
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
3411
3430
  * @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
3431
+ * @satisfies REQ-304, REQ-305, REQ-306, REQ-307, REQ-308, REQ-309, REQ-310, REQ-311, REQ-312, REQ-313, REQ-441
3413
3432
  */
3414
3433
  function registerReqResetCommand(
3415
3434
  pi: ExtensionAPI,
@@ -3480,11 +3499,11 @@ function registerReqResetCommand(
3480
3499
 
3481
3500
  /**
3482
3501
  * @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 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.
3502
+ * @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, appends one durable `pi-usereq.command-report` session entry recording the executed operations with their outcomes so the report renders in the transcript and survives session restore, 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, session-entry persistence, and user notifications.
3484
3503
  * @param[in] pi {ExtensionAPI} Active extension API instance.
3485
3504
  * @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
3486
3505
  * @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, REQ-437, REQ-438
3506
+ * @satisfies REQ-200, REQ-221, REQ-224, REQ-298, REQ-299, REQ-300, REQ-301, REQ-302, REQ-303, REQ-436, REQ-437, REQ-438, REQ-441
3488
3507
  */
3489
3508
  function registerReqReferencesCommand(
3490
3509
  pi: ExtensionAPI,
@@ -3513,19 +3532,23 @@ function registerReqReferencesCommand(
3513
3532
  const executionResult = executeReqReferencesCommandExecution(executionPlan, config);
3514
3533
  setPiUsereqWorkflowState(statusController, "idle", ctx);
3515
3534
  const referencesDisplayPath = formatRuntimePathForDisplay(executionResult.referencesPath);
3516
- ctx.ui.notify(
3535
+ appendCommandReportEntry(pi, buildReqReferencesCommandReportEntryData(executionResult));
3536
+ notifyContextSafely(
3537
+ ctx,
3517
3538
  executionResult.referencesChanged
3518
3539
  ? `req-references: Updated ${referencesDisplayPath}.`
3519
3540
  : `req-references: ${referencesDisplayPath} already matches the generated references content; no file update was needed.`,
3520
3541
  "info",
3521
3542
  );
3522
- ctx.ui.notify(
3543
+ notifyContextSafely(
3544
+ ctx,
3523
3545
  executionResult.commitCreated
3524
3546
  ? `req-references: Created commit ${executionResult.commitMessage}`
3525
3547
  : "req-references: Skipped the git commit because the references content already matched HEAD.",
3526
3548
  "info",
3527
3549
  );
3528
- ctx.ui.notify(
3550
+ notifyContextSafely(
3551
+ ctx,
3529
3552
  executionResult.referencesChanged && executionResult.commitCreated
3530
3553
  ? `SUCCESS: Updated ${referencesDisplayPath} and committed changes.`
3531
3554
  : `SUCCESS: req-references completed with a clean repository (${executionResult.referencesChanged ? "references updated" : "references unchanged"}; ${executionResult.commitCreated ? "commit created" : "commit skipped"}).`,
@@ -3537,7 +3560,8 @@ function registerReqReferencesCommand(
3537
3560
  statusController.state.activePromptRequest = undefined;
3538
3561
  setPiUsereqWorkflowState(statusController, "error", ctx);
3539
3562
  const message = error instanceof Error ? error.message : String(error);
3540
- ctx.ui.notify(message, "error");
3563
+ appendCommandReportEntry(pi, buildCommandReportErrorEntryData("req-references", message));
3564
+ notifyContextSafely(ctx, message, "error");
3541
3565
  throw error;
3542
3566
  }
3543
3567
  },
@@ -5345,13 +5369,14 @@ function registerConfigCommands(
5345
5369
 
5346
5370
  /**
5347
5371
  * @brief Registers the complete pi-usereq extension.
5348
- * @details Persists the live extension api into process-scoped storage on every bind so model re-application after session replacement binds to the api of the currently active session runner instead of an invalidated captured api, then validates installation-owned bundled resources, schedules a best-effort idle-time shared-tokenizer plus canonical-context-file measurement pre-warm so the first configuration-menu render never pays the one-time `js-tiktoken` module load or first content encodes synchronously, registers the specialized `req-reset` and `req-references` commands plus bundled prompt-backed commands and agent tools, conditionally registers config-gated debug tool wrapper commands when the current project enables them, registers configuration commands, registers the configurable notification-sound shortcut when the runtime supports shortcuts, and installs shared wrappers for all supported pi lifecycle hooks so status telemetry, context usage, prompt timing, cumulative runtime, prompt-specific Pushover metadata, tool-result debug logging, and prompt-orchestration effects remain synchronized with runtime events. Runtime is O(h) in hook count during registration. Side effects include filesystem reads, command/tool/shortcut registration, UI updates, active-tool changes, process-scoped extension-api persistence, optional debug-log writes, and timer scheduling.
5372
+ * @details Persists the live extension api into process-scoped storage on every bind so model re-application after session replacement binds to the api of the currently active session runner instead of an invalidated captured api, registers the durable `pi-usereq.command-report` transcript entry renderer, then validates installation-owned bundled resources, schedules a best-effort idle-time shared-tokenizer plus canonical-context-file measurement pre-warm so the first configuration-menu render never pays the one-time `js-tiktoken` module load or first content encodes synchronously, registers the specialized `req-reset` and `req-references` commands plus bundled prompt-backed commands and agent tools, conditionally registers config-gated debug tool wrapper commands when the current project enables them, registers configuration commands, registers the configurable notification-sound shortcut when the runtime supports shortcuts, and installs shared wrappers for all supported pi lifecycle hooks so status telemetry, context usage, prompt timing, cumulative runtime, prompt-specific Pushover metadata, tool-result debug logging, and prompt-orchestration effects remain synchronized with runtime events. Runtime is O(h) in hook count during registration. Side effects include filesystem reads, command/tool/shortcut registration, UI updates, active-tool changes, process-scoped extension-api persistence, optional debug-log writes, and timer scheduling.
5349
5373
  * @param[in] pi {ExtensionAPI} Active extension API instance.
5350
5374
  * @return {void} No return value.
5351
5375
  * @satisfies DES-002, DES-015, 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, REQ-298, REQ-299, REQ-300, REQ-301, REQ-302, REQ-303, REQ-304, REQ-305, REQ-306, REQ-312, REQ-313, REQ-323, REQ-324, REQ-325, REQ-326, REQ-327
5352
5376
  */
5353
5377
  export default function piUsereqExtension(pi: ExtensionAPI): void {
5354
5378
  writePersistedPromptCommandRuntimeApi(pi as unknown as PersistedPromptCommandRuntimeApi);
5379
+ registerCommandReportEntryRenderer(pi);
5355
5380
  const statusController = createPiUsereqStatusController();
5356
5381
  ensureBundledResourcesAccessible();
5357
5382
  try {