@evandu/super-pi 0.1.163 → 0.1.165

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/agents/super-pi-task/core.ts +1 -1
  2. package/dist/client/.super-pi-client-sources.json +4 -4
  3. package/dist/client/assets/{CodeMirrorEditor-DEkvKQts.js → CodeMirrorEditor-B3-4Qpd1.js} +2 -2
  4. package/dist/client/assets/{dist-CNrwgb43.js → dist-2Ax3GOhT.js} +1 -1
  5. package/dist/client/assets/{dist-DYahm0uS.js → dist-BD4w6y25.js} +1 -1
  6. package/dist/client/assets/{dist-D5nqZFe3.js → dist-BIqjsoet.js} +1 -1
  7. package/dist/client/assets/{dist-a40vvieq.js → dist-BWsed8P8.js} +1 -1
  8. package/dist/client/assets/{dist-D5doZ1C8.js → dist-Bh9jVMEA.js} +1 -1
  9. package/dist/client/assets/{dist-BtNbcR8w.js → dist-BhiW2HYK.js} +1 -1
  10. package/dist/client/assets/{dist-Dgkmmss0.js → dist-C2mhG_5m.js} +1 -1
  11. package/dist/client/assets/{dist-CbFClRtF.js → dist-CL9_Ff9R.js} +1 -1
  12. package/dist/client/assets/{dist-B-8UFI-6.js → dist-CN6rdrJA.js} +1 -1
  13. package/dist/client/assets/{dist-DwUj9On7.js → dist-Cn4Q7-eG.js} +1 -1
  14. package/dist/client/assets/{dist-GFhYndkm.js → dist-D45Y5G5v.js} +1 -1
  15. package/dist/client/assets/{dist-CVljZ_cT.js → dist-DF05sEDO.js} +1 -1
  16. package/dist/client/assets/{dist-DpVnCvxg.js → dist-DGOgW2hA.js} +1 -1
  17. package/dist/client/assets/{dist-C2Uv2gLb.js → dist-DQnuHIqA.js} +1 -1
  18. package/dist/client/assets/{dist-DC-1StQ4.js → dist-DRgOr5sP.js} +1 -1
  19. package/dist/client/assets/{dist-CSiW1PcF.js → dist-Di8I1Dsi.js} +1 -1
  20. package/dist/client/assets/{dist-C2mmiivD.js → dist-Do72hsvz.js} +1 -1
  21. package/dist/client/assets/{dist-OOixZci6.js → dist-DtNwDyK1.js} +1 -1
  22. package/dist/client/assets/{dist-FPDQhYJa.js → dist-QTqWnNGE.js} +1 -1
  23. package/dist/client/assets/{dist-DywKNYXP.js → dist-hUAwe2Kn.js} +1 -1
  24. package/dist/client/assets/{dist-CipOPceB.js → dist-rFG_iRW3.js} +1 -1
  25. package/dist/client/assets/{dist-ChSDUOFD.js → dist-wW-2h0X5.js} +1 -1
  26. package/dist/client/assets/{index-CLV7t20O.js → index-BUTxkA3D.js} +11 -11
  27. package/dist/client/index.html +1 -1
  28. package/dist/server/application/project-service.d.ts +95 -4
  29. package/dist/server/application/project-service.js +341 -72
  30. package/dist/server/application/run-service.d.ts +3 -0
  31. package/dist/server/application/run-service.js +27 -7
  32. package/dist/server/application/session-watchdog.d.ts +14 -2
  33. package/dist/server/application/session-watchdog.js +42 -2
  34. package/dist/server/http-server.js +7 -0
  35. package/dist/server/index.js +8 -1
  36. package/dist/server/tool-runner.js +11 -2
  37. package/dist/server/types.d.ts +4 -0
  38. package/package.json +3 -1
@@ -176,8 +176,15 @@ export class RunService {
176
176
  runCompletions = new Map();
177
177
  /** A selected interrupt owns the next free run slot, even while the old child exits. */
178
178
  interruptTargets = new Map();
179
+ shuttingDown = false;
180
+ /** Preserve incoming background results as waiting messages while the server drains its producers. */
181
+ beginShutdown() {
182
+ this.shuttingDown = true;
183
+ }
179
184
  /** Claims the run slot and arms its completion promise. Call sites release with `releaseRunSlot`. */
180
185
  claimRunSlot(conversationId, controller) {
186
+ if (this.shuttingDown)
187
+ throw new Error("服务正在关闭,请重启后再发送消息。");
181
188
  let resolve;
182
189
  const promise = new Promise((resolvePromise) => { resolve = resolvePromise; });
183
190
  this.runCompletions.set(conversationId, { promise, resolve });
@@ -322,7 +329,7 @@ export class RunService {
322
329
  //
323
330
  // BUSY: steer the result into the running turn so Pi drains it at the very next tool batch
324
331
  // boundary instead of waiting for the whole turn to complete.
325
- void this.dispatchBackgroundResult(task.conversationId, queued.id).catch(async (error) => {
332
+ void this.trackDetached(this.dispatchBackgroundResult(task.conversationId, queued.id)).catch(async (error) => {
326
333
  console.warn(`Super Pi: dispatch background exec result failed: ${error instanceof Error ? error.message : "unknown error"}`);
327
334
  await this.settleBackgroundStatus(task.conversationId);
328
335
  });
@@ -378,7 +385,7 @@ export class RunService {
378
385
  });
379
386
  if (!queued)
380
387
  return;
381
- void this.dispatchBackgroundResult(parentId, queued.id).catch((error) => {
388
+ void this.trackDetached(this.dispatchBackgroundResult(parentId, queued.id)).catch((error) => {
382
389
  console.warn(`Super Pi: dispatch child session result failed: ${error instanceof Error ? error.message : "unknown error"}`);
383
390
  });
384
391
  })());
@@ -392,6 +399,8 @@ export class RunService {
392
399
  * INTERRUPTING: quietly leaves the message waiting; the interrupted or next turn will pick it up.
393
400
  */
394
401
  async dispatchBackgroundResult(conversationId, messageId) {
402
+ if (this.shuttingDown)
403
+ return;
395
404
  if (this.interruptTargets.has(conversationId))
396
405
  return;
397
406
  if (!this.activeRuns.has(conversationId)) {
@@ -404,7 +413,7 @@ export class RunService {
404
413
  // so calling it from inside the serialized callback re-enters the lane and deadlocks: the
405
414
  // outer work waits for the inner one, and the inner one waits for the outer one.
406
415
  const startWaiting = await this.serializeConversation(conversationId, async () => {
407
- if (this.interruptTargets.has(conversationId))
416
+ if (this.shuttingDown || this.interruptTargets.has(conversationId))
408
417
  return false;
409
418
  // `processWaitingMessage` acquires this lane, so the fallback must run after serialization exits.
410
419
  if (!this.activeRuns.has(conversationId))
@@ -421,6 +430,11 @@ export class RunService {
421
430
  this.broadcaster.broadcastState();
422
431
  const claimed = await this.store.claimWaitingMessage(conversationId, messageId);
423
432
  this.broadcaster.broadcastState();
433
+ if (this.shuttingDown) {
434
+ await this.store.releaseWaitingMessage(conversationId, claimed.id);
435
+ this.broadcaster.broadcastState();
436
+ return false;
437
+ }
424
438
  try {
425
439
  await this.runtime.steer(conversationId, composePrompt(claimed), claimed.images);
426
440
  }
@@ -1788,14 +1802,15 @@ export class RunService {
1788
1802
  await this.awaitRunSlotReleased(conversation.id, false);
1789
1803
  this.assertHistoryAvailable(conversationId);
1790
1804
  const busy = this.activeRuns.has(conversation.id) || this.interruptTargets.has(conversation.id);
1791
- const reservedController = busy ? undefined : new AbortController();
1805
+ const queueOnly = busy || this.shuttingDown;
1806
+ const reservedController = queueOnly ? undefined : new AbortController();
1792
1807
  if (reservedController) {
1793
1808
  this.cancelledQuestionConversations.delete(conversation.id);
1794
1809
  this.claimRunSlot(conversation.id, reservedController);
1795
1810
  }
1796
1811
  let message;
1797
1812
  try {
1798
- message = await this.store.addAgentMessage(conversationId, trimmed, { ...(busy ? { delivery: "waiting" } : {}) });
1813
+ message = await this.store.addAgentMessage(conversationId, trimmed, { ...(queueOnly ? { delivery: "waiting" } : {}) });
1799
1814
  }
1800
1815
  catch (error) {
1801
1816
  if (reservedController)
@@ -1896,16 +1911,17 @@ export class RunService {
1896
1911
  }
1897
1912
  this.assertHistoryAvailable(conversationId);
1898
1913
  const busy = this.activeRuns.has(conversation.id) || this.interruptTargets.has(conversation.id);
1914
+ const queueOnly = busy || this.shuttingDown;
1899
1915
  // Reserve BEFORE the first persistence await. Revert/stop must see this admitted send even while
1900
1916
  // its message write is pending; otherwise a cut can start while this send goes on to launch Pi.
1901
- const reservedController = busy ? undefined : new AbortController();
1917
+ const reservedController = queueOnly ? undefined : new AbortController();
1902
1918
  if (reservedController) {
1903
1919
  this.cancelledQuestionConversations.delete(conversation.id);
1904
1920
  this.claimRunSlot(conversation.id, reservedController);
1905
1921
  }
1906
1922
  let message;
1907
1923
  try {
1908
- message = await this.store.addUserMessage(conversationId, trimmed, validImages, attachments, { clientId, ...(busy ? { delivery: "waiting" } : {}) });
1924
+ message = await this.store.addUserMessage(conversationId, trimmed, validImages, attachments, { clientId, ...(queueOnly ? { delivery: "waiting" } : {}) });
1909
1925
  }
1910
1926
  catch (error) {
1911
1927
  if (reservedController)
@@ -2140,6 +2156,8 @@ export class RunService {
2140
2156
  // steer/reorder/cancel wait until that turn ends, which is especially visible after a background
2141
2157
  // result auto-starts a long follow-up. The activeRuns slot protects the turn after this lane exits.
2142
2158
  const controller = await this.serializeConversation(conversationId, async () => {
2159
+ if (this.shuttingDown)
2160
+ return undefined;
2143
2161
  this.assertHistoryAvailable(conversationId);
2144
2162
  const reserved = this.interruptTargets.get(conversationId);
2145
2163
  if (reserved && reserved !== messageId)
@@ -2154,6 +2172,8 @@ export class RunService {
2154
2172
  this.claimRunSlot(conversationId, controller);
2155
2173
  return controller;
2156
2174
  });
2175
+ if (!controller)
2176
+ return;
2157
2177
  try {
2158
2178
  const { conversation, workspace } = this.turnContext(conversationId);
2159
2179
  const message = conversation.messages.find((item) => item.id === messageId);
@@ -83,6 +83,12 @@ export declare class SessionWatchdog {
83
83
  private readonly timers;
84
84
  /** One write chain per workspace, so two watchdogs in one workspace cannot interleave a write. */
85
85
  private readonly writes;
86
+ /** A shutdown is a fence: no timer may start another reminder after it is raised. */
87
+ private disposed;
88
+ /** Reminders already accepted by a timer before shutdown still own their state and disk writes. */
89
+ private readonly inFlight;
90
+ /** Every caller must observe the same drain, including callers that arrive while it is running. */
91
+ private disposal;
86
92
  constructor(host: WatchdogHost);
87
93
  /**
88
94
  * Declares (or re-declares) the watchdog for a child.
@@ -121,8 +127,14 @@ export declare class SessionWatchdog {
121
127
  * remaining time is measured from the last sign of life, not from the restart.
122
128
  */
123
129
  restore(): Promise<void>;
124
- /** Stops every timer. The process is going away; the state stays on disk. */
125
- dispose(): void;
130
+ /**
131
+ * Stops reminder production and drains work the timers had already accepted.
132
+ *
133
+ * `disposed` is set before the first await so a timer cannot sneak a second reminder into the
134
+ * shutdown window. A reminder that was already in `onTimeout` remains owned by this instance:
135
+ * its parent delivery and the state write must finish before its workspace can be removed.
136
+ */
137
+ dispose(): Promise<void>;
126
138
  /**
127
139
  * Arms the timer for one entry.
128
140
  *
@@ -19,6 +19,12 @@ export class SessionWatchdog {
19
19
  timers = new Map();
20
20
  /** One write chain per workspace, so two watchdogs in one workspace cannot interleave a write. */
21
21
  writes = new Map();
22
+ /** A shutdown is a fence: no timer may start another reminder after it is raised. */
23
+ disposed = false;
24
+ /** Reminders already accepted by a timer before shutdown still own their state and disk writes. */
25
+ inFlight = new Set();
26
+ /** Every caller must observe the same drain, including callers that arrive while it is running. */
27
+ disposal;
22
28
  constructor(host) {
23
29
  this.host = host;
24
30
  }
@@ -30,6 +36,8 @@ export class SessionWatchdog {
30
36
  * watchdog that quietly switched itself off would leave the parent believing it was still watched.
31
37
  */
32
38
  async arm(input) {
39
+ if (this.disposed)
40
+ return;
33
41
  const { childConversationId } = input;
34
42
  if (!childConversationId.trim())
35
43
  return;
@@ -64,6 +72,8 @@ export class SessionWatchdog {
64
72
  * the child is out of sight.
65
73
  */
66
74
  async noteSignal(childConversationId) {
75
+ if (this.disposed)
76
+ return;
67
77
  const entry = this.entries.get(childConversationId);
68
78
  if (!entry)
69
79
  return;
@@ -99,6 +109,8 @@ export class SessionWatchdog {
99
109
  * remaining time is measured from the last sign of life, not from the restart.
100
110
  */
101
111
  async restore() {
112
+ if (this.disposed)
113
+ return;
102
114
  for (const workspaceId of this.host.workspaceIds()) {
103
115
  const path = this.statePath(workspaceId);
104
116
  if (!path)
@@ -117,6 +129,8 @@ export class SessionWatchdog {
117
129
  if (!records || typeof records !== "object")
118
130
  continue;
119
131
  for (const [childConversationId, record] of Object.entries(records)) {
132
+ if (this.disposed)
133
+ return;
120
134
  if (!record || typeof record.timeoutSeconds !== "number" || !record.parentConversationId)
121
135
  continue;
122
136
  const entry = {
@@ -132,11 +146,27 @@ export class SessionWatchdog {
132
146
  }
133
147
  }
134
148
  }
135
- /** Stops every timer. The process is going away; the state stays on disk. */
149
+ /**
150
+ * Stops reminder production and drains work the timers had already accepted.
151
+ *
152
+ * `disposed` is set before the first await so a timer cannot sneak a second reminder into the
153
+ * shutdown window. A reminder that was already in `onTimeout` remains owned by this instance:
154
+ * its parent delivery and the state write must finish before its workspace can be removed.
155
+ */
136
156
  dispose() {
157
+ if (this.disposal)
158
+ return this.disposal;
159
+ this.disposed = true;
137
160
  for (const timer of this.timers.values())
138
161
  clearTimeout(timer);
139
162
  this.timers.clear();
163
+ this.disposal = (async () => {
164
+ await Promise.allSettled([...this.inFlight]);
165
+ // In-flight reminders can append state writes after the first drain starts, so take this
166
+ // snapshot only after they have settled.
167
+ await Promise.allSettled([...this.writes.values()]);
168
+ })();
169
+ return this.disposal;
140
170
  }
141
171
  /**
142
172
  * Arms the timer for one entry.
@@ -145,11 +175,17 @@ export class SessionWatchdog {
145
175
  * allowed to shut down with a watchdog in flight, and the state is on disk anyway.
146
176
  */
147
177
  schedule(entry) {
178
+ if (this.disposed || this.entries.get(entry.childConversationId) !== entry)
179
+ return;
148
180
  this.clearTimer(entry.childConversationId);
149
181
  const elapsed = Date.now() - entry.lastSignalAt;
150
182
  const remaining = Math.max(0, entry.timeoutSeconds * 1000 - elapsed);
151
183
  const timer = setTimeout(() => {
152
- void this.fire(entry.childConversationId);
184
+ if (this.disposed)
185
+ return;
186
+ const firing = this.fire(entry.childConversationId);
187
+ this.inFlight.add(firing);
188
+ void firing.then(() => this.inFlight.delete(firing), () => this.inFlight.delete(firing));
153
189
  }, remaining);
154
190
  timer.unref?.();
155
191
  this.timers.set(entry.childConversationId, timer);
@@ -174,6 +210,8 @@ export class SessionWatchdog {
174
210
  * cannot speak any more is noise, not information.
175
211
  */
176
212
  async fire(childConversationId) {
213
+ if (this.disposed)
214
+ return;
177
215
  const entry = this.entries.get(childConversationId);
178
216
  if (!entry)
179
217
  return;
@@ -195,6 +233,8 @@ export class SessionWatchdog {
195
233
  catch (error) {
196
234
  this.host.warn?.(`Super Pi: watchdog reminder for ${childConversationId} failed: ${error instanceof Error ? error.message : String(error)}`);
197
235
  }
236
+ // Shutdown must preserve a reminder that was already accepted, but it must not schedule its
237
+ // successor after that reminder has drained.
198
238
  entry.endedReminded = !childWorking;
199
239
  entry.lastSignalAt = Date.now();
200
240
  await this.persist(entry.workspaceId);
@@ -2099,6 +2099,13 @@ export function createHttpServer(options) {
2099
2099
  response.writeHead(403, { "content-type": "application/json" }).end(JSON.stringify({ error: verdict.message }));
2100
2100
  return;
2101
2101
  }
2102
+ if (context.projectService) {
2103
+ const check = context.projectService.canAskQuestion(body.conversationId);
2104
+ if (!check.ok) {
2105
+ response.writeHead(403, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ error: check.reason }));
2106
+ return;
2107
+ }
2108
+ }
2102
2109
  const result = await runService.askQuestion(body.conversationId);
2103
2110
  response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(result));
2104
2111
  }
@@ -108,7 +108,12 @@ const agentPromptDir = join(dataDir, "agent-prompts");
108
108
  const clientDir = join(rootDir, "dist", "client");
109
109
  const accessToken = process.env.SUPER_PI_ACCESS_TOKEN;
110
110
  const uploadsDir = join(dataDir, "uploads");
111
- await mkdir(uploadsDir, { recursive: true }).catch(() => { });
111
+ try {
112
+ await mkdir(uploadsDir, { recursive: true });
113
+ }
114
+ catch (error) {
115
+ logging.write("warn", `uploads directory unavailable: ${error instanceof Error ? error.message : String(error)}`);
116
+ }
112
117
  const taskEndpoint = `http://${internalHost}:${port}/api/subagent/dispatch`;
113
118
  const execEndpoint = `http://${internalHost}:${port}/api/exec`;
114
119
  const todoEndpoint = `http://${internalHost}:${port}/api/todos`;
@@ -339,6 +344,7 @@ async function shutdown(reason) {
339
344
  return shutdownPromise;
340
345
  shutdownPromise = (async () => {
341
346
  console.log(`Super Pi is shutting down (${reason}).`);
347
+ runService.beginShutdown();
342
348
  // Stop accepting new connections first. Existing clients stay connected until
343
349
  // their cancelled work has recorded its terminal state.
344
350
  const httpClosed = closeHttpServer();
@@ -354,6 +360,7 @@ async function shutdown(reason) {
354
360
  for (const c of store.snapshot().conversations) {
355
361
  extensionUiService.cancelPendingUi(c.id, "aborted");
356
362
  }
363
+ await sessionOrchestratorService.watchdog.dispose();
357
364
  await Promise.all([runService.waitForIdle(), workflows.waitForIdle()]);
358
365
  // THE LIVE SESSIONS GO NEXT, AND THEY HAVE TO GO EXPLICITLY. `runService.abortAll()` stops the
359
366
  // TURNS, and a stopped turn is not a stopped session: a session with nothing running on it is idle,
@@ -8,6 +8,15 @@ const MAX_TIMEOUT_MS = 10 * 60_000;
8
8
  /** The environment build gets its OWN budget: a cold `pip install` must not eat the tool's timeout. */
9
9
  const MAX_ENV_TIMEOUT_MS = 10 * 60_000;
10
10
  const isWindows = process.platform === "win32";
11
+ /** Keep OS architecture fallback data without exposing unrelated host configuration to Python. */
12
+ function pythonEnvironment() {
13
+ return {
14
+ PATH: process.env.PATH, SystemRoot: process.env.SystemRoot, TEMP: process.env.TEMP, TMP: process.env.TMP,
15
+ PROCESSOR_ARCHITECTURE: process.env.PROCESSOR_ARCHITECTURE,
16
+ PROCESSOR_ARCHITEW6432: process.env.PROCESSOR_ARCHITEW6432,
17
+ PYTHONIOENCODING: "utf-8", PYTHONNOUSERSITE: "1",
18
+ };
19
+ }
11
20
  /** Runs a registered Python entrypoint without a shell. It intentionally accepts no command string. */
12
21
  export class ToolRunner {
13
22
  dataDir;
@@ -154,7 +163,7 @@ export class ToolRunner {
154
163
  }
155
164
  launch(id, active, python, input) {
156
165
  // -I ignores user Python configuration and user site packages. No shell means params never become code.
157
- const child = spawn(python.path, ["-I", "-u", input.script], { cwd: input.cwd, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot, TEMP: process.env.TEMP, TMP: process.env.TMP, PYTHONIOENCODING: "utf-8", PYTHONNOUSERSITE: "1", SUPER_PI_TOOL_ID: input.toolId } });
166
+ const child = spawn(python.path, ["-I", "-u", input.script], { cwd: input.cwd, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"], env: { ...pythonEnvironment(), SUPER_PI_TOOL_ID: input.toolId } });
158
167
  active.child = child;
159
168
  // A PIPE CARRIES BYTES, AND A READ BOUNDARY CAN LAND INSIDE A CHARACTER. A Chinese character is
160
169
  // three bytes, so decoding one chunk at a time turns every character that straddles a boundary
@@ -210,7 +219,7 @@ export class ToolRunner {
210
219
  function runCapture(python, args, timeoutMs, onOutput) {
211
220
  return new Promise((resolve) => {
212
221
  let stdout = "", stderr = "", settled = false;
213
- const child = spawn(python, args, { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], env: { PATH: process.env.PATH, SystemRoot: process.env.SystemRoot, TEMP: process.env.TEMP, TMP: process.env.TMP, PYTHONIOENCODING: "utf-8", PYTHONNOUSERSITE: "1" } });
222
+ const child = spawn(python, args, { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], env: pythonEnvironment() });
214
223
  const timer = setTimeout(() => { if (settled)
215
224
  return; settled = true; try {
216
225
  child.kill();
@@ -949,6 +949,7 @@ export type ProjectTask = {
949
949
  priority: "low" | "medium" | "high" | "urgent";
950
950
  dueAt?: string;
951
951
  blockedReason?: string;
952
+ blockedReportedByConversationId?: string;
952
953
  output?: string;
953
954
  evidenceRefs?: string[];
954
955
  /** The acceptance of this task. `lightweight: true` means the executor accepted its own
@@ -989,6 +990,9 @@ export type ProjectEscalation = {
989
990
  decision?: string;
990
991
  createdAt: string;
991
992
  resolvedAt?: string;
993
+ audience?: "supervisor" | "boss";
994
+ targetConversationId?: string;
995
+ resolvedByConversationId?: string;
992
996
  };
993
997
  export type ProjectEvent = {
994
998
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.163",
3
+ "version": "0.1.165",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -85,6 +85,7 @@
85
85
  "scripts/session-orchestrator-tools.test.mjs",
86
86
  "scripts/project-service.test.mjs",
87
87
  "scripts/project-api-scope.test.mjs",
88
+ "scripts/project-report-chain.test.mjs",
88
89
  "scripts/session-tree-collapse.test.mjs",
89
90
  "scripts/archive-cascade.test.mjs",
90
91
  "scripts/workspace-cwd.test.mjs",
@@ -128,6 +129,7 @@
128
129
  "scripts/git-write-endpoint-guard.test.mjs",
129
130
  "scripts/question-stop.test.mjs",
130
131
  "scripts/background-result-body.test.mjs",
132
+ "scripts/browser-click.test.mjs",
131
133
  "scripts/question-request-size.test.mjs",
132
134
  "scripts/md-render.test.mjs",
133
135
  "scripts/revert-session-format.test.mjs",