@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.4

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 (212) hide show
  1. package/AGENTS.md +19 -30
  2. package/README.md +69 -1
  3. package/docs/architecture.md +89 -165
  4. package/package.json +11 -7
  5. package/packages/extension/package.json +2 -2
  6. package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
  7. package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
  8. package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
  9. package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
  10. package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
  11. package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
  12. package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
  13. package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
  14. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
  15. package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
  16. package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
  17. package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
  18. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
  19. package/packages/extension/src/bridge-default-model-gate.ts +32 -0
  20. package/packages/extension/src/bridge.ts +299 -20
  21. package/packages/extension/src/command-handler.ts +53 -7
  22. package/packages/extension/src/dashboard-default-adapter.ts +5 -0
  23. package/packages/extension/src/prompt-bus.ts +15 -0
  24. package/packages/extension/src/slash-dispatch.ts +30 -15
  25. package/packages/extension/src/source-detector.ts +13 -5
  26. package/packages/extension/src/usage-limit-orderer.ts +18 -1
  27. package/packages/server/bin/pi-dashboard.mjs +62 -14
  28. package/packages/server/package.json +9 -5
  29. package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
  30. package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
  31. package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
  32. package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
  33. package/packages/server/src/__tests__/cli-version.test.ts +151 -0
  34. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
  35. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
  36. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
  37. package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
  38. package/packages/server/src/__tests__/directory-service.test.ts +9 -0
  39. package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
  40. package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
  41. package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
  42. package/packages/server/src/__tests__/health-shape.test.ts +35 -12
  43. package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
  44. package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
  45. package/packages/server/src/__tests__/package-routes.test.ts +6 -2
  46. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
  47. package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
  48. package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
  49. package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
  50. package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
  51. package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
  52. package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
  53. package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
  54. package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
  55. package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
  56. package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
  57. package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
  58. package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
  59. package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
  60. package/packages/server/src/browser-gateway.ts +83 -5
  61. package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
  62. package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
  63. package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
  64. package/packages/server/src/changelog-parser.ts +1 -1
  65. package/packages/server/src/cli.ts +68 -250
  66. package/packages/server/src/event-status-extraction.ts +14 -62
  67. package/packages/server/src/event-wiring.ts +23 -10
  68. package/packages/server/src/memory-session-manager.ts +4 -0
  69. package/packages/server/src/pi-core-checker.ts +1 -1
  70. package/packages/server/src/pi-dev-version-check.ts +1 -1
  71. package/packages/server/src/pi-version-skew.ts +24 -46
  72. package/packages/server/src/plugin-intent-cache.ts +67 -0
  73. package/packages/server/src/preferences-store.ts +199 -13
  74. package/packages/server/src/recovery-server.ts +366 -0
  75. package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
  76. package/packages/server/src/routes/doctor-routes.ts +26 -21
  77. package/packages/server/src/routes/manifest-route.ts +162 -0
  78. package/packages/server/src/routes/openspec-routes.ts +4 -25
  79. package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
  80. package/packages/server/src/routes/pi-core-routes.ts +3 -23
  81. package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
  82. package/packages/server/src/routes/recommended-routes.ts +21 -0
  83. package/packages/server/src/routes/system-routes.ts +73 -11
  84. package/packages/server/src/server.ts +105 -307
  85. package/packages/server/src/session-api.ts +5 -63
  86. package/packages/shared/package.json +1 -1
  87. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
  88. package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
  89. package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
  90. package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
  91. package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
  92. package/packages/shared/src/__tests__/config.test.ts +40 -0
  93. package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
  94. package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
  95. package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
  96. package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
  97. package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
  98. package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
  99. package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
  100. package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
  101. package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
  102. package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
  103. package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
  104. package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
  105. package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
  106. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
  107. package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
  108. package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
  109. package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
  110. package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
  111. package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
  112. package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
  113. package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
  114. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
  115. package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
  116. package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
  117. package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
  118. package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
  119. package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
  120. package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
  121. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
  122. package/packages/shared/src/bridge-register.ts +35 -2
  123. package/packages/shared/src/browser-protocol.ts +176 -2
  124. package/packages/shared/src/config.ts +12 -0
  125. package/packages/shared/src/dashboard-paths.ts +69 -0
  126. package/packages/shared/src/dashboard-plugin/index.ts +2 -0
  127. package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
  128. package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
  129. package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
  130. package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
  131. package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
  132. package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
  133. package/packages/shared/src/dashboard-starter.ts +22 -0
  134. package/packages/shared/src/doctor-core.ts +49 -27
  135. package/packages/shared/src/launch-source-types.ts +9 -9
  136. package/packages/shared/src/legacy-managed-dir.ts +97 -0
  137. package/packages/shared/src/mdns-discovery.ts +4 -1
  138. package/packages/shared/src/pi-package-resolver.ts +388 -0
  139. package/packages/shared/src/platform/binary-lookup.ts +27 -3
  140. package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
  141. package/packages/shared/src/platform/exec.ts +22 -0
  142. package/packages/shared/src/platform/node-spawn.ts +42 -41
  143. package/packages/shared/src/plugin-bridge-register.ts +275 -18
  144. package/packages/shared/src/protocol.ts +94 -2
  145. package/packages/shared/src/recommended-extensions.ts +34 -10
  146. package/packages/shared/src/server-identity.ts +74 -5
  147. package/packages/shared/src/server-launcher.ts +20 -0
  148. package/packages/shared/src/source-matching.ts +1 -1
  149. package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
  150. package/packages/shared/src/tool-registry/definitions.ts +91 -7
  151. package/packages/shared/src/types.ts +12 -8
  152. package/scripts/maybe-patch-package.cjs +44 -0
  153. package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
  154. package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
  155. package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
  156. package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
  157. package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
  158. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
  159. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
  160. package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
  161. package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
  162. package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
  163. package/packages/server/src/bootstrap-install-from-list.ts +0 -232
  164. package/packages/server/src/bootstrap-queue.ts +0 -130
  165. package/packages/server/src/bootstrap-state.ts +0 -159
  166. package/packages/server/src/legacy-pi-cleanup.ts +0 -151
  167. package/packages/server/src/routes/bootstrap-routes.ts +0 -125
  168. package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
  169. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
  170. package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
  171. package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
  172. package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
  173. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
  174. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
  175. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
  176. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
  177. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
  178. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
  179. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
  180. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
  181. package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
  182. package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
  183. package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
  184. package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
  185. package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
  186. package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
  187. package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
  188. package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
  189. package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
  190. package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
  191. package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
  192. package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
  193. package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
  194. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
  195. package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
  196. package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
  197. package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
  198. package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
  199. package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
  200. package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
  201. package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
  202. package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
  203. package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
  204. package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
  205. package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
  206. package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
  207. package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
  208. package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
  209. package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
  210. package/packages/shared/src/bootstrap-install.ts +0 -406
  211. package/packages/shared/src/installable-list.ts +0 -152
  212. package/packages/shared/src/launch-source-flag.ts +0 -14
@@ -9,7 +9,7 @@ import type {
9
9
  ExtensionToServerMessage,
10
10
  } from "@blackbelt-technology/pi-dashboard-shared/protocol.js";
11
11
  import { killProcessByPgid } from "./process-scanner.js";
12
- import type { FileEntry, PiSessionInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
12
+ import type { FileEntry, ImageContent, PiSessionInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
13
13
  import { filterHiddenCommands } from "./bridge-context.js";
14
14
  import { expandPromptTemplateFromDisk } from "./prompt-expander.js";
15
15
  import { tryDispatchExtensionCommand } from "./slash-dispatch.js";
@@ -169,7 +169,25 @@ export function createCommandHandler(
169
169
  * events emitted by the dispatch path arrive before this turn returns.
170
170
  * See change: fix-extension-slash-commands-in-dashboard.
171
171
  */
172
- sessionPrompt?: (text: string) => void | Promise<void>;
172
+ sessionPrompt?: (text: string, delivery?: "steer" | "followUp") => void | Promise<void>;
173
+ /**
174
+ * Bridge-shadow-queue hooks: called AFTER pi accepts the user message,
175
+ * gated by `isStreaming()` captured BEFORE the send. The capture order
176
+ * matters — `pi.sendUserMessage` on an idle session synchronously
177
+ * triggers `agent_start`, which flips bridge state to streaming. Checking
178
+ * AFTER the send would mis-record idle sends as chip entries.
179
+ * Pi doesn't expose `queue_update` to extensions, so the bridge is the
180
+ * source of truth. See change: add-followup-edit-and-steer-cancel.
181
+ */
182
+ onSteerSent?: (text: string) => void;
183
+ onFollowupSent?: (text: string) => void;
184
+ /**
185
+ * Returns true iff the agent was streaming at the moment of the call.
186
+ * Used to capture pre-send streaming state before `pi.sendUserMessage`
187
+ * runs (which may flip the flag synchronously via agent_start).
188
+ * See change: add-followup-edit-and-steer-cancel.
189
+ */
190
+ isStreaming?: () => boolean;
173
191
  },
174
192
  ): CommandHandler {
175
193
  const getSessionId = typeof sessionIdOrGetter === "function" ? sessionIdOrGetter : () => sessionIdOrGetter;
@@ -304,7 +322,7 @@ export function createCommandHandler(
304
322
  // extension-command dispatch. Do NOT emit completed here — would
305
323
  // duplicate the dispatch path's terminal event.
306
324
  // See change: fix-extension-slash-commands-in-dashboard.
307
- await options.sessionPrompt(parsed.text);
325
+ await options.sessionPrompt(parsed.text, msg.delivery);
308
326
  } else {
309
327
  // Test / non-bridge callers: apply the extension-command dispatch
310
328
  // branch inline before falling through to sendUserMessage. Keeps
@@ -314,12 +332,17 @@ export function createCommandHandler(
314
332
  parsed.text,
315
333
  sessionId,
316
334
  options?.eventSink,
335
+ undefined, // connection — absent in non-bridge path
336
+ msg.delivery,
317
337
  );
318
338
  if (!handled) {
319
339
  // sendUserMessage exempt from gating: only typed single-line
320
340
  // slashes that are NOT extension commands reach this — i.e.
321
341
  // skills, prompt templates, unrecognized slashes.
322
- pi.sendUserMessage(parsed.text);
342
+ // Forward delivery so steering on slash fallback honors the
343
+ // dashboard's keyboard contract. See change: add-steering-message.
344
+ const deliverAs = msg.delivery ?? ("followUp" as const);
345
+ (pi.sendUserMessage as any)(parsed.text, { deliverAs });
323
346
  }
324
347
  }
325
348
  return undefined;
@@ -339,11 +362,27 @@ export function createCommandHandler(
339
362
  if (outgoing.startsWith("/")) {
340
363
  outgoing = expandPromptTemplateFromDisk(outgoing, process.cwd(), pi);
341
364
  }
342
- sendUserMessageWithImages(pi, outgoing, msg.images);
365
+ // Pi owns the steering + follow-up queues natively. The helper enforces
366
+ // capacity-1 on follow-up by clearing pi's slot before sending. After
367
+ // pi accepts, notify the bridge so it updates its shadow queue — but
368
+ // only if the agent was already streaming BEFORE the send. Pi flips
369
+ // idle→streaming synchronously on the first user message, so checking
370
+ // after sendUserMessage gives false positives.
371
+ // See change: add-followup-edit-and-steer-cancel.
372
+ const wasStreaming = options?.isStreaming?.() ?? false;
373
+ sendUserMessageWithImages(pi, outgoing, msg.images, msg.delivery);
374
+ if (wasStreaming) {
375
+ const da = msg.delivery ?? "followUp";
376
+ if (da === "steer") options?.onSteerSent?.(outgoing);
377
+ else options?.onFollowupSent?.(outgoing);
378
+ }
343
379
  return undefined;
344
380
  }
345
381
 
346
382
  case "abort":
383
+ // Pi owns both queues now. abort() asks pi to halt the current turn;
384
+ // pi's native drain logic handles any remaining queue entries naturally.
385
+ // See change: add-followup-edit-and-steer-cancel.
347
386
  if (options?.abort) {
348
387
  options.abort();
349
388
  }
@@ -502,13 +541,20 @@ export function createCommandHandler(
502
541
  }
503
542
 
504
543
  /** Send a user message with optional image validation.
505
- * Uses deliverAs: "followUp" so messages queue properly when the agent is streaming. */
544
+ * Uses deliverAs: "followUp" by default so messages queue properly when the agent is streaming.
545
+ * Pass deliverAs: "steer" for steering messages (delivered after current turn).
546
+ * See change: add-steering-message. */
506
547
  function sendUserMessageWithImages(
507
548
  pi: ExtensionAPI,
508
549
  text: string,
509
550
  images?: Array<{ type: string; data: string; mimeType: string }>,
551
+ delivery?: "steer" | "followUp",
510
552
  ): void {
511
- const sendOptions = { deliverAs: "followUp" as const };
553
+ const deliverAs = delivery ?? ("followUp" as const);
554
+ const sendOptions = { deliverAs };
555
+ // v2: follow-up is a multi-entry queue. Sends APPEND — the cap-1 invariant
556
+ // is gone. See change: add-followup-edit-and-steer-cancel design.md Decision 8.
557
+ // (The bridge's shadow queue records the append via onFollowupSent.)
512
558
  if (images && images.length > 0) {
513
559
  const validMimeTypes = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
514
560
  const validImages = images.filter((img) => {
@@ -10,6 +10,11 @@ import type { PromptAdapter, PromptRequest, PromptResponse, PromptClaim } from "
10
10
 
11
11
  export class DashboardDefaultAdapter implements PromptAdapter {
12
12
  readonly name = "dashboard-default";
13
+ /**
14
+ * Last-resort fallback. Any plugin adapter with default priority (1000)
15
+ * or lower beats this. See change: route-flow-asks-to-upper-slot.
16
+ */
17
+ readonly priority = 9999;
13
18
 
14
19
  onRequest(_prompt: PromptRequest): PromptClaim {
15
20
  return {
@@ -41,6 +41,14 @@ export interface PromptResponse {
41
41
 
42
42
  export interface PromptAdapter {
43
43
  name: string;
44
+ /**
45
+ * Routing priority. Lower numbers run first. Default 1000.
46
+ * `DashboardDefaultAdapter` uses 9999 so any plugin adapter with the
47
+ * default priority (1000) or lower beats it automatically.
48
+ *
49
+ * See change: route-flow-asks-to-upper-slot.
50
+ */
51
+ priority?: number;
44
52
  /**
45
53
  * Called when a new prompt arrives.
46
54
  * Return a PromptClaim to participate, or null/undefined to skip.
@@ -92,11 +100,18 @@ export class PromptBus {
92
100
  /**
93
101
  * Register an adapter. Returns an unsubscribe function.
94
102
  * Re-registering with the same name replaces the previous adapter.
103
+ *
104
+ * Adapters are kept sorted by `priority` (default 1000, lower first).
105
+ * `Array.prototype.sort` is stable in V8 ≥ ES2019 so equal priorities
106
+ * preserve insertion order. See change: route-flow-asks-to-upper-slot.
95
107
  */
96
108
  registerAdapter(adapter: PromptAdapter): () => void {
97
109
  // Replace existing adapter with same name
98
110
  this.adapters = this.adapters.filter(a => a.name !== adapter.name);
99
111
  this.adapters.push(adapter);
112
+ this.adapters.sort(
113
+ (a, b) => (a.priority ?? 1000) - (b.priority ?? 1000),
114
+ );
100
115
 
101
116
  return () => {
102
117
  this.adapters = this.adapters.filter(a => a !== adapter);
@@ -8,10 +8,14 @@
8
8
  * dashboard-spawned headless `pi --mode rpc` AND a `connection` is wired
9
9
  * → emit `dispatch_extension_command` to the server (server forwards to
10
10
  * the per-session RPC keeper UDS and emits the terminal command_feedback).
11
- * - Path D (stopgap, last resort): `pi.dispatchCommand` absent AND the bridge
12
- * is NOT headless (tmux / wt / unrecognized spawn shape) OR no `connection`
13
- * was supplied → emit `command_feedback {status:"error"}` with a pi-version
14
- * reminder.
11
+ * - Path D: `pi.dispatchCommand` absent AND the bridge is NOT headless
12
+ * (tmux / wt) OR no `connection` was supplied → emit
13
+ * `command_feedback {status:"error"}` with a hint to enable
14
+ * `useRpcKeeper: true` for headless sessions.
15
+ * Note: pi.sendUserMessage() hardcodes expandPromptTemplates: false, which
16
+ * skips _tryExecuteExtensionCommand; extension commands sent this way
17
+ * become regular LLM messages. This is a pi limitation — the bridge has
18
+ * no mechanism to dispatch extension commands outside the RPC path.
15
19
  *
16
20
  * If `text` is NOT an extension command, return `false` so the caller can
17
21
  * fall through to its existing template-expansion / sendUserMessage path.
@@ -21,7 +25,8 @@
21
25
  * Path C does NOT emit a terminal event — the server emits it.
22
26
  *
23
27
  * See change: fix-extension-slash-commands-in-dashboard,
24
- * add-rpc-stdin-dispatch-with-keeper-sidecar.
28
+ * add-rpc-stdin-dispatch-with-keeper-sidecar,
29
+ * fix-slash-dispatch-delivery.
25
30
  */
26
31
  import crypto from "node:crypto";
27
32
  import type { ExtensionToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/protocol.js";
@@ -38,9 +43,6 @@ export interface DispatchConnection {
38
43
  send(msg: ExtensionToServerMessage): void;
39
44
  }
40
45
 
41
- const PI_071_REQUIRED =
42
- "Extension slash commands cannot be dispatched from the dashboard yet — requires pi 0.71+ (`pi.dispatchCommand`). Invoke from the pi TUI, or use the extension's tools directly.";
43
-
44
46
  function emitFeedback(
45
47
  sink: FeedbackSink | undefined,
46
48
  sessionId: string,
@@ -64,8 +66,8 @@ function emitFeedback(
64
66
  * Try to dispatch a slash command as an extension command.
65
67
  *
66
68
  * @returns `true` if the helper handled the text (extension command detected;
67
- * dispatch attempted or stopgap emitted). The caller MUST NOT fall
68
- * through to template expansion or `sendUserMessage`.
69
+ * dispatch attempted or error feedback emitted). The caller MUST NOT
70
+ * fall through to template expansion or `sendUserMessage`.
69
71
  * @returns `false` if `text` is not an extension slash command. The caller
70
72
  * SHOULD continue with its existing fallback path.
71
73
  */
@@ -75,6 +77,7 @@ export async function tryDispatchExtensionCommand(
75
77
  sessionId: string,
76
78
  sink: FeedbackSink | undefined,
77
79
  connection?: DispatchConnection,
80
+ delivery?: "steer" | "followUp",
78
81
  ): Promise<boolean> {
79
82
  // Defensive: pi.getCommands() can throw on a stale ctx during dispose.
80
83
  let commands: Array<{ name: string; source?: string }> = [];
@@ -88,12 +91,13 @@ export async function tryDispatchExtensionCommand(
88
91
 
89
92
  if (!isExtensionSlashCommand(text, commands)) return false;
90
93
 
91
- emitFeedback(sink, sessionId, text, "started");
92
-
93
94
  // Path B (preferred when available): pi 0.71+ exposes dispatchCommand.
95
+ // Note: as of pi 0.74.1, dispatchCommand does NOT exist in the ExtensionAPI.
96
+ // This path is dead code until pi ships the API; preserved for future use.
94
97
  if (hasDispatchCommand(pi)) {
98
+ emitFeedback(sink, sessionId, text, "started");
95
99
  try {
96
- await (pi as any).dispatchCommand(text, { streamingBehavior: "followUp" });
100
+ await (pi as any).dispatchCommand(text, { streamingBehavior: delivery ?? "followUp" });
97
101
  emitFeedback(sink, sessionId, text, "completed");
98
102
  } catch (err: any) {
99
103
  const message = err instanceof Error ? err.message : String(err);
@@ -108,6 +112,7 @@ export async function tryDispatchExtensionCommand(
108
112
  // terminal event for this path — that would duplicate the reducer's
109
113
  // started→terminal upsert. See change: add-rpc-stdin-dispatch-with-keeper-sidecar.
110
114
  if (connection && isHeadlessRpcSession()) {
115
+ emitFeedback(sink, sessionId, text, "started");
111
116
  connection.send({
112
117
  type: "dispatch_extension_command",
113
118
  sessionId,
@@ -117,7 +122,17 @@ export async function tryDispatchExtensionCommand(
117
122
  return true;
118
123
  }
119
124
 
120
- // Path D (stopgap): no dispatchCommand and not headless (tmux / wt / unrecognized).
121
- emitFeedback(sink, sessionId, text, "error", PI_071_REQUIRED);
125
+ // Path D: No dispatchCommand, not headless (tmux / wt) or no connection.
126
+ // Extension commands can only be dispatched through the RPC keeper, which
127
+ // is available for headless sessions (`pi --mode rpc`). For tmux/wt sessions
128
+ // there is no injection channel — the command becomes a regular LLM message.
129
+ // To enable extension command dispatch for headless sessions:
130
+ // { "spawnStrategy": "headless", "useRpcKeeper": true }
131
+ // See change: fix-slash-dispatch-delivery.
132
+ const RPC_KEEPER_HINT =
133
+ "Extension slash commands cannot be dispatched from the dashboard for " +
134
+ "non-headless (tmux/wt) sessions. If you're using headless mode, add " +
135
+ '"useRpcKeeper": true to your dashboard config (~/.pi/dashboard/config.json).';
136
+ emitFeedback(sink, sessionId, text, "error", RPC_KEEPER_HINT);
122
137
  return true;
123
138
  }
@@ -11,16 +11,24 @@ import { readSessionMeta } from "@blackbelt-technology/pi-dashboard-shared/sessi
11
11
  * a .meta.json sidecar with source information.
12
12
  */
13
13
  export function detectSessionSource(hasUI?: boolean, sessionFile?: string): SessionSource {
14
- // Check for .meta.json sidecar written by the dashboard server
14
+ // A TUI is attached the session is a CLI/TUI session, not headless,
15
+ // regardless of any .meta.json sidecar (which can be stamped "dashboard"
16
+ // by event-wiring's pendingDashboardSpawns by-cwd matcher when an
17
+ // unrelated dashboard Spawn for the same cwd happened around the same time).
18
+ if (hasUI) {
19
+ if (process.env.ZED_TERM) return "tui"; // pi TUI inside Zed's terminal
20
+ if (process.env.TMUX) return "tmux";
21
+ return "tui";
22
+ }
23
+
24
+ // No TUI attached → headless. Check for .meta.json sidecar written by
25
+ // the dashboard server when it spawned this session.
15
26
  if (sessionFile) {
16
27
  const meta = readSessionMeta(sessionFile);
17
28
  if (meta?.source === "dashboard") return "dashboard";
18
29
  }
19
30
 
20
- if (process.env.ZED_TERM) {
21
- if (hasUI) return "tui";
22
- return "zed";
23
- }
31
+ if (process.env.ZED_TERM) return "zed";
24
32
  if (process.env.TMUX) return "tmux";
25
33
  return "tui";
26
34
  }
@@ -11,8 +11,25 @@
11
11
  * See change: fix-provider-retry-infinite-loop.
12
12
  */
13
13
 
14
+ /**
15
+ * Matches terminal billing/quota error categories observed in production
16
+ * across providers. The orderer uses this to surface a synthesized
17
+ * `auto_retry_end{success:false}` BEFORE the bridge forwards `agent_end`,
18
+ * so the dashboard's `retryState` clears before `lastError` is set.
19
+ *
20
+ * Coverage (verified via fixture tests):
21
+ * - Codex / Anthropic / generic: usage_limit_reached, usage_not_included,
22
+ * quota_exceeded, credit_balance, insufficient_quota, monthly limit,
23
+ * daily limit, hourly limit, "reset after Nh|Nm|Ns".
24
+ * - Gemini / Google: "monthly spending cap", "spending cap",
25
+ * RESOURCE_EXHAUSTED.
26
+ * - Generic catch-all for "exceeded ... (quota|cap|spending)" within
27
+ * ~40 chars (avoids a string with no terminal-meaning context).
28
+ *
29
+ * See change: fix-retry-banner-stuck-on-limit-exceeded.
30
+ */
14
31
  export const USAGE_LIMIT_PATTERN =
15
- /usage[_ ]limit[_ ]reached|usage_not_included|quota[_ ]exceeded|monthly limit|hourly limit|reset after \d+[hms]/i;
32
+ /usage[_ ]limit[_ ]reached|usage_not_included|insufficient_quota|credit[_ ]balance|quota[_ ]exceeded|resource[_ ]exhausted|monthly[_ ]limit|monthly[_ ]spending[_ ]cap|hourly[_ ]limit|daily[_ ]limit|spending[_ ]cap|exceeded[^"]{0,40}(quota|cap|spending)|reset after \d+[hms]/i;
16
33
 
17
34
  export interface SyntheticEventEnvelope {
18
35
  eventType: "auto_retry_end";
@@ -4,18 +4,26 @@
4
4
  *
5
5
  * The actual CLI is `../src/cli.ts`. This wrapper exists because a
6
6
  * `#!/usr/bin/env` shebang cannot interpolate a dynamic `--import`
7
- * loader path. The wrapper resolves jiti from pi's tree at runtime
8
- * and re-execs Node with `--import <jiti-url> cli.ts <args>`.
7
+ * loader path. The wrapper resolves jiti from `process.argv[1]`'s
8
+ * module graph at runtime and re-execs Node with
9
+ * `--import <jiti-url> cli.ts <args>`.
9
10
  *
10
- * No tsx fallback: if jiti cannot be resolved, the wrapper exits 1
11
- * with an install-hint pointing at pi. Mirrors the resolution shape
12
- * in `packages/shared/src/resolve-jiti.ts` (cannot import the .ts
13
- * module before a TS loader is registered, so the lookup is inlined).
11
+ * Since `@blackbelt-technology/pi-dashboard-server` declares `jiti` as
12
+ * a direct runtime dependency, `createRequire(argv[1]).resolve("jiti/...")`
13
+ * SHALL succeed in any well-formed npm install layout (flat, scoped,
14
+ * hoisted, pnpm). A miss therefore indicates a corrupted install, not
15
+ * a missing prerequisite. The error message reflects that.
14
16
  *
15
- * See change: replace-tsx-with-jiti.
17
+ * No tsx fallback: jiti is the sole supported TypeScript loader.
18
+ * Mirrors the resolution shape in
19
+ * `packages/shared/src/platform/binary-lookup.ts::ToolResolver.resolveJiti`
20
+ * (cannot import the .ts module before a TS loader is registered, so
21
+ * the lookup is inlined).
22
+ *
23
+ * See change: replace-tsx-with-jiti, enable-standalone-npm-install.
16
24
  */
17
25
  import { createRequire } from "node:module";
18
- import { realpathSync } from "node:fs";
26
+ import { realpathSync, readFileSync } from "node:fs";
19
27
  import { spawn } from "node:child_process";
20
28
  import { dirname, join, resolve } from "node:path";
21
29
  import { pathToFileURL, fileURLToPath } from "node:url";
@@ -23,7 +31,35 @@ import { pathToFileURL, fileURLToPath } from "node:url";
23
31
  const here = dirname(fileURLToPath(import.meta.url));
24
32
  const cliPath = resolve(here, "..", "src", "cli.ts");
25
33
 
26
- // Mirrors packages/shared/src/resolve-jiti.ts JITI_PACKAGES.
34
+ // Metadata short-circuit: --version / -v / version SHALL NOT require jiti.
35
+ // See change: fix-electron-cold-launch-probe-cascade (Bug B).
36
+ //
37
+ // Why: every metadata-only consumer (npmGlobal probe, doctor, the user
38
+ // asking "what's installed") was previously blocked when the wrapper
39
+ // couldn't resolve jiti — even when the install was perfectly capable
40
+ // of answering the query from its sibling package.json. Reading the
41
+ // version is a pure metadata operation; gating it on a TS loader was
42
+ // over-aggressive.
43
+ //
44
+ // Falls through (no exit) on read/parse error so the legacy install
45
+ // hint still surfaces for genuinely corrupt installs.
46
+ const metaArg = process.argv[2];
47
+ if (metaArg === "--version" || metaArg === "-v" || metaArg === "version") {
48
+ try {
49
+ const pkgPath = resolve(here, "..", "package.json");
50
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
51
+ if (pkg && typeof pkg.version === "string" && pkg.version.length > 0) {
52
+ process.stdout.write(pkg.version + "\n");
53
+ process.exit(0);
54
+ }
55
+ } catch {
56
+ /* fall through to jiti-resolve path so the legacy error still fires */
57
+ }
58
+ }
59
+
60
+ // Mirrors packages/shared/src/platform/binary-lookup.ts JITI_PACKAGES.
61
+ // Kept in sync by repo-lint: packages/shared/src/__tests__/jiti-packages-parity.test.ts.
62
+ // See change: enable-standalone-npm-install task 7.3.
27
63
  const JITI_PACKAGES = ["jiti", "@mariozechner/jiti"];
28
64
 
29
65
  /** Resolve pi's jiti register hook as a file:// URL. Returns null on miss. */
@@ -51,17 +87,29 @@ function resolveJitiUrl() {
51
87
 
52
88
  const loader = resolveJitiUrl();
53
89
  if (!loader) {
90
+ // jiti is a direct dep of @blackbelt-technology/pi-dashboard-server, so a
91
+ // miss here means the install is corrupted (deleted node_modules entry,
92
+ // partial extract, etc.). The legacy "install pi globally" hint is kept
93
+ // as a workaround for users who can't reinstall the dashboard cleanly.
54
94
  process.stderr.write(
55
- "pi-dashboard: cannot find jiti. " +
56
- "Install pi: 'npm install -g @earendil-works/pi-coding-agent'\n",
95
+ "pi-dashboard: cannot find jiti.\n" +
96
+ "This is unexpected: jiti ships as a direct dependency of pi-dashboard-server.\n" +
97
+ "Your install may be corrupted. Try:\n" +
98
+ " npm install -g @blackbelt-technology/pi-agent-dashboard\n" +
99
+ "Workaround: install pi globally (provides a fallback jiti):\n" +
100
+ " npm install -g @earendil-works/pi-coding-agent\n" +
101
+ "Please report at https://github.com/BlackBeltTechnology/pi-agent-dashboard/issues\n",
57
102
  );
58
103
  process.exit(1);
59
104
  }
60
105
 
61
106
  // Mirrors shouldUrlWrapEntry() in packages/shared/src/platform/node-spawn.ts:
62
- // jiti needs the entry URL-wrapped on Windows (Node rejects raw drive-letter
63
- // paths for --import). POSIX takes the raw path.
64
- const entry = process.platform === "win32" ? pathToFileURL(cliPath).href : cliPath;
107
+ // jiti misnormalises file:/// URL entries on Windows (verified live on
108
+ // Node 22.18.0 + jiti 2.7.0 in a standalone install — the entry gets
109
+ // re-prepended with cwd as if it were a relative specifier). Pass the
110
+ // RAW path on every platform; Node's drive-letter heuristic handles
111
+ // `C:\…` entries directly. See change: fix-windows-standalone-spawn.
112
+ const entry = cliPath;
65
113
 
66
114
  const child = spawn(
67
115
  process.execPath,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blackbelt-technology/pi-dashboard-server",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Dashboard server for monitoring and interacting with pi agent sessions",
5
5
  "type": "module",
6
6
  "repository": {
@@ -32,9 +32,10 @@
32
32
  "postinstall": "node scripts/fix-pty-permissions.cjs"
33
33
  },
34
34
  "dependencies": {
35
- "@blackbelt-technology/dashboard-plugin-runtime": "^0.5.2",
36
- "@blackbelt-technology/pi-dashboard-extension": "^0.5.2",
37
- "@blackbelt-technology/pi-dashboard-shared": "^0.5.2",
35
+ "@blackbelt-technology/dashboard-plugin-runtime": "^0.5.4",
36
+ "@blackbelt-technology/pi-dashboard-extension": "^0.5.4",
37
+ "@blackbelt-technology/pi-dashboard-shared": "^0.5.4",
38
+ "@earendil-works/pi-coding-agent": "^0.74.0",
38
39
  "@fastify/compress": "^8.3.1",
39
40
  "@fastify/cookie": "^11.0.2",
40
41
  "@fastify/cors": "^11.0.0",
@@ -42,12 +43,15 @@
42
43
  "@fastify/reply-from": "^12.6.1",
43
44
  "@fastify/static": "^8.0.0",
44
45
  "@fastify/websocket": "^11.0.0",
46
+ "@fission-ai/openspec": "^1.3.0",
45
47
  "bonjour-service": "^1.3.0",
46
48
  "diff": "^8.0.3",
47
49
  "fastify": "^5.0.0",
50
+ "jiti": "^2.7.0",
48
51
  "jsonwebtoken": "^9.0.3",
49
- "node-pty": "^1.1.0",
52
+ "node-pty": "1.2.0-beta.13",
50
53
  "proper-lockfile": "^4.1.2",
54
+ "tsx": "^4.21.0",
51
55
  "ws": "^8.18.0"
52
56
  },
53
57
  "devDependencies": {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Tests for browserGateway.registerHandler — the reverse channel that
3
+ * plugins use to receive Browser→Server custom message types.
4
+ *
5
+ * See change: adopt-server-driven-intent-rendering.
6
+ */
7
+ import { describe, it, expect, vi } from "vitest";
8
+ import { createBrowserGateway } from "../browser-gateway.js";
9
+
10
+ function makeMockDeps() {
11
+ // Minimal mock dependencies for createBrowserGateway. We only need the
12
+ // gateway's registerHandler + the message dispatch loop, not session
13
+ // management.
14
+ return {
15
+ sessionManager: {
16
+ listActive: () => [],
17
+ listAll: () => [],
18
+ getSession: () => undefined,
19
+ registerSession: () => {},
20
+ unregisterSession: () => {},
21
+ updateSession: () => {},
22
+ detachAll: () => {},
23
+ attachExtension: () => {},
24
+ detachExtension: () => {},
25
+ markEnded: () => {},
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ } as any,
28
+ eventStore: {
29
+ append: () => {},
30
+ getEvents: () => [],
31
+ getLatestEvent: () => undefined,
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ } as any,
34
+ piGateway: {
35
+ send: () => {},
36
+ sendToSession: () => {},
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ } as any,
39
+ };
40
+ }
41
+
42
+ describe("browserGateway.registerHandler", () => {
43
+ it("stores and looks up handlers by type", () => {
44
+ const deps = makeMockDeps();
45
+ const gateway = createBrowserGateway(deps.sessionManager, deps.eventStore, deps.piGateway);
46
+
47
+ const handler = vi.fn();
48
+ gateway.registerHandler("plugin_action", handler);
49
+
50
+ // We can't easily invoke the WS message loop without a real WebSocket
51
+ // connection, so we verify only that registration succeeds without
52
+ // throwing. End-to-end dispatch is verified in section 19 manual smoke.
53
+ expect(typeof gateway.registerHandler).toBe("function");
54
+ });
55
+
56
+ it("multiple handlers for different types can be registered", () => {
57
+ const deps = makeMockDeps();
58
+ const gateway = createBrowserGateway(deps.sessionManager, deps.eventStore, deps.piGateway);
59
+
60
+ const handlerA = vi.fn();
61
+ const handlerB = vi.fn();
62
+ gateway.registerHandler("plugin_action", handlerA);
63
+ gateway.registerHandler("plugin_other", handlerB);
64
+
65
+ // No throw on registration. (Last-write-wins for the same type
66
+ // is implicit Map semantics; not validated here.)
67
+ expect(true).toBe(true);
68
+ });
69
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Regression-prevention lint: forbid the `env: { ...process.env }` anti-pattern
3
+ * in `packages/server/src/cli.ts`.
4
+ *
5
+ * Why this is forbidden:
6
+ *
7
+ * `launchDashboardServer` (the shared spawn primitive) internally computes the
8
+ * spawn env as `ToolResolver.buildSpawnEnv(process.env)`, which augments PATH
9
+ * with managed-dir, bundled-node, and pi-bin prepends. Caller-supplied `env`
10
+ * is then overlaid on top with caller-wins semantics.
11
+ *
12
+ * Passing `env: { ...process.env }` re-supplies the raw, un-augmented PATH
13
+ * back over the augmented base, silently defeating the entire purpose of
14
+ * `buildSpawnEnv` — the spawned daemon then cannot find `pi` (or any other
15
+ * tool resolved via PATH augmentation) in environments where the launching
16
+ * shell's PATH lacks those prepends (e.g. `.desktop` launchers, systemd-user
17
+ * units, non-interactive logins that don't init nvm).
18
+ *
19
+ * See: openspec/changes/fix-cli-env-clobber/proposal.md
20
+ * See: dashboard-server capability spec — constraint C22 (env merge contract)
21
+ */
22
+
23
+ import { describe, it, expect } from "vitest";
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+ const CLI_SOURCE = path.resolve(__dirname, "..", "cli.ts");
31
+
32
+ describe("cli.ts env clobber lint", () => {
33
+ it("does not pass `env: { ...process.env }` to launchDashboardServer", () => {
34
+ const source = fs.readFileSync(CLI_SOURCE, "utf8");
35
+ const pattern = /env:\s*\{\s*\.\.\.process\.env\s*\}/;
36
+ const match = source.match(pattern);
37
+
38
+ expect(
39
+ match,
40
+ "packages/server/src/cli.ts must not contain `env: { ...process.env }`. " +
41
+ "This pattern clobbers the augmented PATH from ToolResolver.buildSpawnEnv. " +
42
+ "Omit `env` to let the shared primitive's resolver-merged env take effect. " +
43
+ "See openspec/changes/fix-cli-env-clobber/proposal.md.",
44
+ ).toBeNull();
45
+ });
46
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Repo-lint — guards Phase 3.0.a/3.0.b of change:
3
+ * eliminate-electron-runtime-install.
4
+ *
5
+ * Under R3, `packages/server/src/cli.ts::runForeground` MUST NOT reach into
6
+ * any of the deleted bootstrap modules. The tool-registry resolve is the
7
+ * sole point at which startup verifies pi is reachable; failure throws a
8
+ * hard error citing a corrupted node_modules/ tree (no degraded mode).
9
+ *
10
+ * This test asserts the cli.ts source text contains:
11
+ * 1. ZERO references to the deleted module names (`bootstrap-install`,
12
+ * `bootstrap-install-from-list`, `installable-list`, `bootstrap-state`,
13
+ * `bootstrap-queue`, `managed-workspace-materialize`,
14
+ * `defaultInstallableList`, `writeInstallableList`,
15
+ * `updateBootstrapCompatibility`, `BootstrapStateStore`).
16
+ * 2. The `[bootstrap] ready` log line, proving the tool-registry
17
+ * resolve path is in place.
18
+ * 3. The hard-throw branch citing a "corrupted node_modules" message.
19
+ *
20
+ * If you intentionally rename / move the resolve step, update this lint
21
+ * to match — but reaching back into any of the forbidden symbols means
22
+ * runtime install has crept back into the standalone arm.
23
+ */
24
+ import { describe, it, expect } from "vitest";
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+
29
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
30
+ const CLI_PATH = path.resolve(HERE, "..", "cli.ts");
31
+
32
+ const FORBIDDEN_SYMBOLS = [
33
+ "bootstrap-install",
34
+ "bootstrap-install-from-list",
35
+ "installable-list",
36
+ "bootstrap-state",
37
+ "bootstrap-queue",
38
+ "managed-workspace-materialize",
39
+ "defaultInstallableList",
40
+ "writeInstallableList",
41
+ "updateBootstrapCompatibility",
42
+ "BootstrapStateStore",
43
+ "bootstrapInstall",
44
+ "bootstrapInstallFromList",
45
+ "runDegradedModeBootstrap",
46
+ "maybeSeedDefaultInstallableList",
47
+ ];
48
+
49
+ describe("cli.ts has no bootstrap-install references (Phase 3.0.b)", () => {
50
+ it("does not reference any deleted bootstrap modules or symbols", () => {
51
+ const src = fs.readFileSync(CLI_PATH, "utf-8");
52
+ const offenders = FORBIDDEN_SYMBOLS.filter((sym) => src.includes(sym));
53
+ expect(
54
+ offenders,
55
+ `cli.ts must not reference deleted bootstrap symbols under R3:\n ${offenders.join(", ")}\n\nSee change: eliminate-electron-runtime-install (Phase 3.0).`,
56
+ ).toEqual([]);
57
+ });
58
+
59
+ it("contains the [bootstrap] ready log line proving tool-registry resolve is wired", () => {
60
+ const src = fs.readFileSync(CLI_PATH, "utf-8");
61
+ expect(src).toContain("[bootstrap] ready (pi resolved via");
62
+ });
63
+
64
+ it("throws hard on pi resolution failure (no degraded mode)", () => {
65
+ const src = fs.readFileSync(CLI_PATH, "utf-8");
66
+ // The hard-throw branch in runForeground cites "corrupted node_modules".
67
+ expect(src).toMatch(/corrupted node_modules/i);
68
+ });
69
+ });