@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
@@ -15,7 +15,16 @@
15
15
  * --dev Development mode (skip static files)
16
16
  * --no-tunnel Disable zrok tunnel
17
17
  */
18
- import { createServer, type ServerConfig } from "./server.js";
18
+ // `createServer` is imported dynamically inside `runForeground()` so a
19
+ // top-level module-resolution failure (missing `fastify` etc.) can be
20
+ // caught and degraded into the recovery HTTP server instead of crashing
21
+ // the process. The type-only import here is fully erased at runtime.
22
+ import type { createServer as _CreateServerType, ServerConfig } from "./server.js";
23
+ import {
24
+ startRecoveryServer,
25
+ isModuleNotFoundError,
26
+ parseModuleNotFoundError,
27
+ } from "./recovery-server.js";
19
28
  import { loadConfig, ensureConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
20
29
  import {
21
30
  launchDashboardServer,
@@ -47,48 +56,13 @@ import { discoverDashboard } from "@blackbelt-technology/pi-dashboard-shared/mdn
47
56
 
48
57
  import { assertNodeVersionSupported } from "./node-guard.js";
49
58
  import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
50
- import { bootstrapInstall } from "@blackbelt-technology/pi-dashboard-shared/bootstrap-install.js";
51
59
  import {
52
60
  findBundledExtension,
53
61
  registerBridgeExtension,
54
62
  } from "@blackbelt-technology/pi-dashboard-shared/bridge-register.js";
55
- import type { DashboardServer } from "./server.js";
56
- import { updateBootstrapCompatibility } from "./pi-version-skew.js";
57
- import type { BootstrapStateStore } from "./bootstrap-state.js";
58
63
  import { parseDashboardStarter } from "@blackbelt-technology/pi-dashboard-shared/dashboard-starter.js";
59
- import { bootstrapInstallFromList } from "./bootstrap-install-from-list.js";
60
-
61
- /**
62
- * Emit a stderr warning at CLI startup when the resolved pi version is
63
- * below `piCompatibility.minimum` (blocking) or below `.recommended`
64
- * (advisory). Reads from the already-populated `bootstrapState` so no
65
- * additional I/O happens here. See change: warn-pi-version-skew-in-cli.
66
- */
67
- function logCompatibilityWarning(store: BootstrapStateStore): void {
68
- const s = store.get();
69
- const c = s.compatibility;
70
- if (!c || !c.current) return;
71
- // Below minimum: `updateBootstrapCompatibility` sets `error.message`.
72
- // We treat the presence of a blocking error + upgradeRecommended as the
73
- // below-minimum signal; `upgradeRecommended` alone means below-recommended.
74
- if (s.error?.message && c.upgradeRecommended) {
75
- console.error(
76
- `[bootstrap] ⚠ pi ${c.current} is below the required minimum ${c.minimum}.`,
77
- );
78
- console.error(
79
- `[bootstrap] All pi-dependent features (sessions, resources, openspec) will return 503.`,
80
- );
81
- console.error(`[bootstrap] Run: pi-dashboard upgrade-pi`);
82
- return;
83
- }
84
- if (c.upgradeRecommended) {
85
- console.warn(
86
- `[bootstrap] pi ${c.current} is below the recommended ${c.recommended} — consider running \`pi-dashboard upgrade-pi\``,
87
- );
88
- }
89
- }
90
64
 
91
- const SUBCOMMANDS = ["start", "stop", "restart", "status", "upgrade-pi"] as const;
65
+ const SUBCOMMANDS = ["start", "stop", "restart", "status"] as const;
92
66
  type Subcommand = (typeof SUBCOMMANDS)[number];
93
67
 
94
68
  export interface ParsedArgs {
@@ -157,184 +131,83 @@ export function buildConfig(flags: Partial<ServerConfig>): ServerConfig {
157
131
  }
158
132
 
159
133
  /**
160
- * Run the server in the foreground (original behavior).
134
+ * Run the server in the foreground.
161
135
  *
162
- * After the server starts listening, the degraded-mode bootstrap kicks
163
- * off: if `pi` is not resolvable via the ToolRegistry, the server flips
164
- * `bootstrapState` to "installing" and begins a background
165
- * `bootstrapInstall`. Session-spawn and other pi-dependent endpoints
166
- * queue or 503 during this window (see change tasks §5).
167
- *
168
- * See change: unified-bootstrap-install.
136
+ * Pi/openspec/tsx ship as regular npm deps of this package, so the
137
+ * ToolRegistry resolve of "pi" at startup either succeeds (regular
138
+ * path) or signals a corrupted install (hard error). See change:
139
+ * eliminate-electron-runtime-install.
169
140
  */
170
141
  async function runForeground(config: ServerConfig): Promise<void> {
171
142
  assertNodeVersionSupported();
172
- const server = await createServer(config);
173
-
174
- // Stamp the bootstrap state with who started this server process.
175
- // parseDashboardStarter defaults to "Standalone" when DASHBOARD_STARTER is unset.
176
- const starter = parseDashboardStarter(process.env);
177
- server.bootstrapState.set({ starter });
178
- console.log(`[bootstrap] starter=${starter}`);
179
-
180
- let shuttingDown = false;
181
- const shutdown = async () => {
182
- if (shuttingDown) {
183
- console.log("Force exit.");
184
- process.exit(1);
185
- }
186
- shuttingDown = true;
187
- console.log("\nShutting down...");
188
- await server.stop();
189
- process.exit(0);
190
- };
191
-
192
- process.on("SIGINT", shutdown);
193
- process.on("SIGTERM", shutdown);
194
143
 
195
- // Reconcile installable.json before binding the port.
196
- // Required-package failures throw and prevent server start.
197
- // Optional failures are logged and continue.
198
- // File-absent is a no-op (Bridge/Standalone starters don't seed installable.json).
199
- // See change: simplify-electron-bootstrap-derived-state.
144
+ // Dynamic-import boundary for the main server module. If a top-level
145
+ // dependency (fastify, toad-cache, readable-stream, …) is missing, the
146
+ // import throws ERR_MODULE_NOT_FOUND here — caught and degraded to the
147
+ // recovery HTTP server bound to the same port.
148
+ let createServer: typeof _CreateServerType;
200
149
  try {
201
- await bootstrapInstallFromList(server.bootstrapState);
150
+ ({ createServer } = await import("./server.js"));
202
151
  } catch (err) {
203
- const message = err instanceof Error ? err.message : String(err);
204
- console.error(`[bootstrap] installable reconcile failed (required package): ${message}`);
205
- process.exit(1);
152
+ if (isModuleNotFoundError(err)) {
153
+ await startRecoveryServer({
154
+ port: config.port,
155
+ error: err as Error,
156
+ missingModule: parseModuleNotFoundError(err),
157
+ });
158
+ // startRecoveryServer never returns — its HTTP server keeps the
159
+ // event loop alive until the user clicks Retry (which respawns and
160
+ // process.exits) or the process is killed externally.
161
+ return new Promise<void>(() => { /* unreachable */ });
162
+ }
163
+ throw err;
206
164
  }
207
165
 
208
- await server.start();
209
-
210
- // Kick off the degraded-mode first-run bootstrap if pi is unresolvable.
211
- // Runs async — server is already listening, so UI + non-pi endpoints
212
- // remain fully operational during the ~30s install window.
213
- // TODO(single-dashboard-per-home): when home-lock wiring lands, wrap
214
- // this inside the acquired lock to serialize concurrent first-run
215
- // installs from multiple dashboard invocations on the same HOME.
216
- runDegradedModeBootstrap(server).catch((err) => {
217
- console.error("[bootstrap] unexpected failure in bootstrap orchestrator:", err);
218
- });
219
- }
166
+ const server = await createServer(config);
220
167
 
221
- /**
222
- * Orchestrate the first-run bootstrap flow.
223
- *
224
- * - If pi is already resolvable leave `bootstrapState` at the default
225
- * "ready" and return immediately.
226
- * - Otherwise flip to "installing", run `bootstrapInstall`, then:
227
- * • on success, rescan the registry, attempt bridge registration
228
- * (failures are non-fatal and land in `bridgeRegistrationError`),
229
- * flip to "ready".
230
- * • on failure, flip to "failed" with the error.
231
- *
232
- * Structured log lines at each transition aid diagnosis in daemon-mode
233
- * (stdout goes to ~/.pi/dashboard/server.log).
234
- */
235
- async function runDegradedModeBootstrap(server: DashboardServer): Promise<void> {
236
- const registry = getDefaultRegistry();
237
- const initial = registry.resolve("pi");
238
-
239
- if (initial.ok) {
240
- // Default state is "ready" — no change needed. Log once for clarity.
241
- console.log(`[bootstrap] ready (pi resolved via ${initial.source})`);
242
- // Populate version-skew compatibility info even when no install was
243
- // needed — the UI banner renders upgradeRecommended hints.
244
- try {
245
- const serverPkg = path.resolve(
246
- path.dirname(fileURLToPath(import.meta.url)),
247
- "..",
248
- "package.json",
168
+ // Tool-registry resolve confirms pi is reachable from the bundled
169
+ // node_modules/ under change: eliminate-electron-runtime-install,
170
+ // pi/openspec/tsx ship as regular deps so the registry must resolve
171
+ // at startup. A miss here means the install tree is corrupted.
172
+ {
173
+ const registry = getDefaultRegistry();
174
+ const res = registry.resolve("pi");
175
+ if (res.ok) {
176
+ console.log(`[bootstrap] ready (pi resolved via ${res.source})`);
177
+ } else {
178
+ const tried = res.tried?.map((t: any) => t.strategy).join(", ") ?? "(no strategies)";
179
+ throw new Error(
180
+ `[bootstrap] pi is not resolvable from the dashboard install. ` +
181
+ `This indicates a corrupted node_modules/ tree. Tried: ${tried}. ` +
182
+ `Reinstall the dashboard (npm i -g @blackbelt-technology/pi-agent-dashboard) ` +
183
+ `or reinstall the Electron app.`,
249
184
  );
250
- updateBootstrapCompatibility(server.bootstrapState, serverPkg);
251
- logCompatibilityWarning(server.bootstrapState);
252
- } catch (err) {
253
- console.warn("[bootstrap] version-skew check failed (non-fatal):", err);
254
185
  }
255
- return;
256
186
  }
257
187
 
258
- const installPackages = ["@earendil-works/pi-coding-agent", "@fission-ai/openspec"];
259
- server.bootstrapState.setLastInstallPackages(installPackages);
260
- console.log("[bootstrap] installing (pi unresolved, running background install)");
261
- server.bootstrapState.set({
262
- status: "installing",
263
- progress: { step: "pi", output: "starting install…" },
264
- error: undefined,
265
- });
266
-
188
+ // One-time advisory: legacy `~/.pi-dashboard/` directory left behind
189
+ // from pre-R3 versions. Nothing reads or writes it now — surface a
190
+ // single log line so the user knows it's safe to delete. Doctor UI
191
+ // shows the same advisory more visibly.
267
192
  try {
268
- const res = await bootstrapInstall({
269
- packages: installPackages,
270
- progress: (p) => {
271
- server.bootstrapState.set({
272
- progress: { step: p.step, output: p.output },
273
- });
274
- },
275
- });
276
-
277
- if (!res.ok) {
278
- console.error(`[bootstrap] failed: ${res.error}`);
279
- server.bootstrapState.set({
280
- status: "failed",
281
- error: { message: res.error },
282
- progress: undefined,
283
- });
284
- return;
285
- }
286
-
287
- // Post-install registry rescan + openspec/pi-resources force-refresh
288
- // are now centralized in server.ts's bootstrapState.subscribe hook,
289
- // which fires on every installing → ready transition (this caller +
290
- // triggerUpgradePi + triggerRetry).
291
- // See change: fix-openspec-buttons-after-bootstrap-install.
292
-
293
- // Attempt bridge registration. Failures are non-fatal per spec §10.3.
294
- let bridgeErr: string | undefined;
295
- try {
296
- const extPath = findBundledExtension(process.cwd());
297
- if (extPath) {
298
- registerBridgeExtension(extPath);
299
- } else {
300
- bridgeErr = "bundled extension not found after install";
301
- }
302
- } catch (err) {
303
- bridgeErr = err instanceof Error ? err.message : String(err);
304
- }
305
-
306
- server.bootstrapState.set({
307
- status: "ready",
308
- progress: undefined,
309
- error: undefined,
310
- bridgeRegistrationError: bridgeErr,
311
- });
312
- // Populate compatibility info after a successful install.
313
- try {
314
- const serverPkg = path.resolve(
315
- path.dirname(fileURLToPath(import.meta.url)),
316
- "..",
317
- "package.json",
193
+ const { detectLegacyManagedDir } = await import(
194
+ "@blackbelt-technology/pi-dashboard-shared/legacy-managed-dir.js"
195
+ );
196
+ const legacy = detectLegacyManagedDir();
197
+ if (legacy.present) {
198
+ console.log(
199
+ `[legacy] legacy install directory detected at ${legacy.path} ` +
200
+ `(${legacy.pkgCount} packages, ~${legacy.sizeMb} MB). No longer used — safe to delete.`,
318
201
  );
319
- updateBootstrapCompatibility(server.bootstrapState, serverPkg);
320
- logCompatibilityWarning(server.bootstrapState);
321
- } catch (err) {
322
- console.warn("[bootstrap] version-skew check failed (non-fatal):", err);
323
202
  }
324
- console.log(
325
- `[bootstrap] ready (installed ${res.installed.join(", ")}${bridgeErr ? `; bridge warning: ${bridgeErr}` : ""})`,
326
- );
327
- } catch (err) {
328
- const message = err instanceof Error ? err.message : String(err);
329
- console.error(`[bootstrap] failed: ${message}`);
330
- server.bootstrapState.set({
331
- status: "failed",
332
- error: { message },
333
- progress: undefined,
334
- });
203
+ } catch {
204
+ /* advisory only never block startup */
335
205
  }
206
+
207
+ await server.start();
336
208
  }
337
209
 
210
+
338
211
  /**
339
212
  * Start the server as a detached background daemon.
340
213
  */
@@ -376,7 +249,6 @@ async function cmdStart(config: ServerConfig): Promise<void> {
376
249
  starter: "Standalone",
377
250
  healthTimeoutMs: 30_000,
378
251
  port: config.port,
379
- env: { ...process.env },
380
252
  });
381
253
  const reportedPid = result.reportedPid ?? readPid() ?? result.childPid;
382
254
  console.log(`Dashboard server started (pid ${reportedPid}) at http://localhost:${config.port}`);
@@ -523,57 +395,6 @@ async function cmdRestartImpl(
523
395
  /**
524
396
  * Show server status.
525
397
  */
526
- /**
527
- * `pi-dashboard upgrade-pi` — upgrade pi-coding-agent via bootstrap.
528
- *
529
- * If a dashboard is currently running, POST to /api/bootstrap/upgrade-pi
530
- * (so the running server owns the install, broadcasts state, and reloads
531
- * connected sessions). Otherwise run `bootstrapInstall` directly with a
532
- * streaming progress formatter and exit when done.
533
- *
534
- * See change: unified-bootstrap-install §8.
535
- */
536
- async function cmdUpgradePi(config: ServerConfig): Promise<void> {
537
- const status = await isDashboardRunning(config.port);
538
- if (status.running) {
539
- console.log(
540
- `[upgrade-pi] dashboard running at http://localhost:${config.port}, delegating to server`,
541
- );
542
- try {
543
- const res = await fetch(`http://localhost:${config.port}/api/bootstrap/upgrade-pi`, {
544
- method: "POST",
545
- });
546
- if (!res.ok) {
547
- const body = await res.text();
548
- console.error(`[upgrade-pi] server rejected upgrade: HTTP ${res.status} ${body}`);
549
- process.exit(1);
550
- }
551
- const body = (await res.json()) as { ticketId?: string };
552
- console.log(`[upgrade-pi] queued (ticketId=${body.ticketId ?? "?"})`);
553
- console.log("[upgrade-pi] progress is streamed to open dashboard tabs; CLI exits now.");
554
- return;
555
- } catch (err) {
556
- console.error("[upgrade-pi] failed to reach server:", err);
557
- process.exit(1);
558
- }
559
- }
560
-
561
- console.log("[upgrade-pi] no dashboard running — installing directly");
562
- const res = await bootstrapInstall({
563
- packages: ["@earendil-works/pi-coding-agent"],
564
- progress: (p) => {
565
- const line = p.output
566
- ? `[upgrade-pi] ${p.step} ${p.status}: ${p.output}`
567
- : `[upgrade-pi] ${p.step} ${p.status}`;
568
- console.log(line);
569
- },
570
- });
571
- if (!res.ok) {
572
- console.error(`[upgrade-pi] failed: ${res.error}`);
573
- process.exit(1);
574
- }
575
- console.log(`[upgrade-pi] ✓ installed ${res.installed.join(", ")}`);
576
- }
577
398
 
578
399
  async function cmdStatus(port: number): Promise<void> {
579
400
  // 1. Try mDNS discovery first
@@ -665,9 +486,6 @@ async function main() {
665
486
  case "status":
666
487
  await cmdStatus(config.port);
667
488
  break;
668
- case "upgrade-pi":
669
- await cmdUpgradePi(config);
670
- break;
671
489
  default:
672
490
  // No subcommand — run in foreground (backward compatible)
673
491
  await runForeground(config);
@@ -2,16 +2,12 @@
2
2
  * Extract session status/tool updates from forwarded events.
3
3
  * Returns partial DashboardSession updates, or null if the event is not relevant.
4
4
  */
5
- import type { DashboardEvent, DashboardSession, FlowStatus, SessionStatus } from "@blackbelt-technology/pi-dashboard-shared/types.js";
5
+ import type { DashboardEvent, DashboardSession, SessionStatus } from "@blackbelt-technology/pi-dashboard-shared/types.js";
6
6
 
7
7
  // Use null (not undefined) for fields that must be cleared — undefined is
8
8
  // dropped during JSON serialisation so the browser would keep the stale value.
9
9
  type SessionUpdates = Partial<Pick<DashboardSession, "status" | "model" | "thinkingLevel">> & {
10
10
  currentTool?: string | null;
11
- activeFlowName?: string | null;
12
- flowAgentsDone?: number;
13
- flowAgentsTotal?: number;
14
- flowStatus?: FlowStatus | null;
15
11
  };
16
12
 
17
13
  /**
@@ -83,54 +79,10 @@ export function extractSessionUpdates(event: DashboardEvent): SessionUpdates | n
83
79
  return null;
84
80
  }
85
81
 
86
- // ── Flow events ──
87
- case "flow_started": {
88
- const d = event.data;
89
- const steps = d.steps as Array<{ stepType: string }> | undefined;
90
- const agentCount = steps?.filter(s => s.stepType === "agent").length ?? 0;
91
- return {
92
- activeFlowName: (d.flowName as string) ?? null,
93
- flowAgentsTotal: agentCount,
94
- flowAgentsDone: 0,
95
- flowStatus: "running" as FlowStatus,
96
- };
97
- }
98
-
99
- case "flow_agent_complete":
100
- // Increment is handled by the caller — we return a marker
101
- return { flowAgentsDone: -1 }; // sentinel: caller must increment
102
-
103
- case "flow_complete": {
104
- const result = event.data;
105
- const status = (result.status as string) ?? "success";
106
- return {
107
- flowStatus: status as FlowStatus,
108
- };
109
- }
110
-
111
- // ── Architect events ──
112
- case "architect_started": {
113
- const mode = (event.data.mode as string) || "new";
114
- return {
115
- activeFlowName: mode === "edit" ? "Editing flow..." : "Designing flow...",
116
- flowStatus: "running" as FlowStatus,
117
- };
118
- }
119
-
120
- case "flow_summary_dismissed": {
121
- return {
122
- activeFlowName: null,
123
- flowStatus: null,
124
- };
125
- }
126
-
127
- case "architect_complete":
128
- case "architect_cancelled": {
129
- return {
130
- activeFlowName: null,
131
- flowStatus: null,
132
- };
133
- }
82
+ // Flow / architect events are NOT extracted here. Per change
83
+ // pluginize-flows-via-registry, flows-plugin owns its own state
84
+ // derivation in the browser via useSessionEvents + plugin-internal
85
+ // contexts. The dashboard server has zero flow knowledge.
134
86
 
135
87
  default:
136
88
  return null;
@@ -164,16 +116,16 @@ const ACTIVITY_EVENT_TYPES: ReadonlySet<string> = new Set([
164
116
  "agent_end",
165
117
  // Bash command output
166
118
  "bash_output",
167
- // Flow lifecycle / agent steps
168
- "flow_started",
169
- "flow_complete",
170
- "flow_agent_started",
171
- "flow_agent_complete",
172
- // Architect (flow design) lifecycle
173
- "architect_started",
174
- "architect_complete",
175
- "architect_cancelled",
176
119
  ]);
120
+ // Note: flow / architect events used to live in this allowlist but the
121
+ // classification of "is this user-visible activity?" is plugin business.
122
+ // The plugin marks activity via its own session-state-derived signal
123
+ // (e.g. lastActivityAt stamping based on flowState changes). For now,
124
+ // the simpler tool/agent/message events are sufficient to keep
125
+ // `lastActivityAt` accurate; if a flow that does no other tool calls
126
+ // fails to bump activity, the user can re-add the events here behind a
127
+ // generic predicate like `isPluginActivityEvent` exposed by plugin-runtime.
128
+ // See change: pluginize-flows-via-registry.
177
129
 
178
130
  export function isActivityEvent(eventType: string): boolean {
179
131
  return ACTIVITY_EVENT_TYPES.has(eventType);
@@ -138,16 +138,15 @@ export function wireEvents(deps: EventWiringDeps): void {
138
138
 
139
139
  piGateway.onEvent = (sessionId, msg) => {
140
140
  if (msg.type === "event_forward") {
141
+ // Legacy queue_state event no longer emitted (bridge removed PromptQueue).
142
+ // See change: add-followup-edit-and-steer-cancel.
143
+ if (msg.event.eventType === "queue_state") return;
141
144
  // When canSkipWipe was true, the event store already has all events —
142
145
  // don't insert replayed events again (would cause exponential duplication)
143
146
  if (replayingSessions.has(sessionId) && skipReplayInsert.has(sessionId)) {
144
147
  // Still process status updates so session state stays accurate
145
148
  const updates = extractSessionUpdates(msg.event);
146
149
  if (updates) {
147
- if (updates.flowAgentsDone === -1) {
148
- const session = sessionManager.get(sessionId);
149
- updates.flowAgentsDone = (session?.flowAgentsDone ?? 0) + 1;
150
- }
151
150
  sessionManager.update(sessionId, updates as Partial<DashboardSession>);
152
151
  }
153
152
  // Skip insert + broadcast — events are already in store
@@ -173,10 +172,6 @@ export function wireEvents(deps: EventWiringDeps): void {
173
172
 
174
173
  const updates = extractSessionUpdates(msg.event);
175
174
  if (updates) {
176
- if (updates.flowAgentsDone === -1) {
177
- const session = sessionManager.get(sessionId);
178
- updates.flowAgentsDone = (session?.flowAgentsDone ?? 0) + 1;
179
- }
180
175
  sessionManager.update(sessionId, updates as Partial<DashboardSession>);
181
176
  // During replay, accumulate in sessionManager but don't broadcast
182
177
  // to avoid rapid status flickers on the session card
@@ -573,11 +568,29 @@ export function wireEvents(deps: EventWiringDeps): void {
573
568
  text: pendingResume.text,
574
569
  images: pendingResume.images,
575
570
  });
576
- sessionManager.update(sessionId, { resuming: false });
577
- browserGateway.broadcastSessionUpdated(sessionId, { resuming: false });
571
+ // Clear `resuming` on the OLD session that triggered the auto-resume,
572
+ // not the new session that just registered. The new session never had
573
+ // `resuming: true`; clearing it there was a no-op and left the old
574
+ // session permanently stuck. The 30s onTimeout was also cancelled by
575
+ // `consume()`, so without this fix the old session stays frozen forever.
576
+ sessionManager.update(pendingResume.oldSessionId, { resuming: false });
577
+ browserGateway.broadcastSessionUpdated(pendingResume.oldSessionId, { resuming: false });
578
578
  }
579
579
  }
580
580
 
581
+ // Pi's queue mirror (steer + follow-up) forwarded from the bridge.
582
+ // Caches `pendingQueues` on the session and broadcasts to subscribed browsers.
583
+ // See change: add-followup-edit-and-steer-cancel.
584
+ if (msg.type === "queue_update") {
585
+ const steering = Array.isArray(msg.steering) ? msg.steering : [];
586
+ const followUp = Array.isArray(msg.followUp) ? msg.followUp : [];
587
+ const update = { pendingQueues: { steering, followUp } } as Partial<DashboardSession>;
588
+ sessionManager.update(sessionId, update);
589
+ if (!replayingSessions.has(sessionId)) {
590
+ browserGateway.broadcastSessionUpdated(sessionId, update);
591
+ }
592
+ return;
593
+ }
581
594
  if (msg.type === "first_message_update") {
582
595
  sessionManager.update(sessionId, { firstMessage: msg.firstMessage });
583
596
  browserGateway.broadcastSessionUpdated(sessionId, { firstMessage: msg.firstMessage });
@@ -106,6 +106,10 @@ export function createMemorySessionManager(): SessionManager {
106
106
  firstMessage: params.firstMessage ?? existing?.firstMessage,
107
107
  dataUnavailable: false,
108
108
  pid: params.pid,
109
+ // Pi-native queue mirror: reset to empty on register / re-register;
110
+ // a fresh `queue_update` from the bridge populates it.
111
+ // See change: add-followup-edit-and-steer-cancel.
112
+ pendingQueues: { steering: [], followUp: [] },
109
113
  };
110
114
  sessions.set(params.id, session);
111
115
  mgr.onChange?.(params.id, {
@@ -92,7 +92,7 @@ export function _resetDynamicPiAliases(): void {
92
92
  * tooling if and only if its name is in `CORE_PACKAGE_NAMES` OR a
93
93
  * pi.dev-declared alias. The previous `pi-*` name-prefix heuristic was
94
94
  * removed because it caused recommended-extension packages (e.g.
95
- * `pi-agent-browser`, `@tintinweb/pi-subagents`) to appear in BOTH the
95
+ * `pi-agent-browser`, `pi-dashboard-subagents`) to appear in BOTH the
96
96
  * Core ecosystem panel and the Installed Packages panel. Recommended
97
97
  * extensions are now surfaced exclusively through
98
98
  * `/api/packages/installed`. See change: consolidate-packages-settings-ui.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * pi.dev version-check client. Mirrors the implementation pi itself
3
- * uses for self-update checks (see `@mariozechner/pi-coding-agent/dist/
3
+ * uses for self-update checks (see `@earendil-works/pi-coding-agent/dist/
4
4
  * utils/version-check.js`). Returns `{ version, packageName? }` so the
5
5
  * dashboard can:
6
6
  * 1. Detect the genuinely-newest pi without npm-registry lag.
@@ -11,7 +11,6 @@
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
13
  import { createRequire } from "node:module";
14
- import type { BootstrapCompatibility, BootstrapStateStore } from "./bootstrap-state.js";
15
14
  import { getDefaultRegistry, type ToolRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
16
15
 
17
16
  /**
@@ -62,6 +61,30 @@ export function isAbove(version: string, threshold: string): boolean {
62
61
  return compareVersions(version, thresholdClean) > 0;
63
62
  }
64
63
 
64
+ /**
65
+ * Pi version compatibility snapshot.
66
+ *
67
+ * Previously declared in `./bootstrap-state.js`; moved here under change:
68
+ * eliminate-electron-runtime-install (task 3.6) once the bootstrap-state
69
+ * store was removed. The shape stays stable so consumers (CLI version
70
+ * skew log, future UI version-skew banner for standalone arm) keep
71
+ * compiling.
72
+ */
73
+ export interface BootstrapCompatibility {
74
+ /** Minimum pi version supported by this dashboard server. */
75
+ minimum: string;
76
+ /** Recommended pi version; below = soft warning, above = OK. */
77
+ recommended: string;
78
+ /** Maximum supported pi version, or `null` for unbounded. */
79
+ maximum: string | null;
80
+ /** Currently-resolved pi version (or `undefined` if pi unresolvable). */
81
+ current?: string;
82
+ /** Set when `current < recommended`. */
83
+ upgradeRecommended?: boolean;
84
+ /** Set when `current > maximum`. */
85
+ upgradeDashboard?: boolean;
86
+ }
87
+
65
88
  /**
66
89
  * Read the server's declared compatibility range from its own package.json.
67
90
  * Falls back to the hard-coded defaults when the field is missing or
@@ -168,48 +191,3 @@ export function computeCompatibility(
168
191
  return out;
169
192
  }
170
193
 
171
- interface CacheEntry {
172
- value: BootstrapCompatibility;
173
- /** Milliseconds epoch when this entry should be discarded. */
174
- expiresAt: number;
175
- }
176
-
177
- let cached: CacheEntry | undefined;
178
- const CACHE_TTL_MS = 60_000;
179
-
180
- /**
181
- * Convenience wrapper: read range + current version, compute result,
182
- * cache for 60 s. `store` is called with a structured compatibility
183
- * update and (when minimum is violated) a blocking `error` message.
184
- */
185
- export function updateBootstrapCompatibility(
186
- store: BootstrapStateStore,
187
- serverPkgJsonPath: string,
188
- registry: ToolRegistry = getDefaultRegistry(),
189
- now: () => number = Date.now,
190
- ): BootstrapCompatibility {
191
- const t = now();
192
- if (cached && t < cached.expiresAt) {
193
- store.set({ compatibility: cached.value });
194
- return cached.value;
195
- }
196
- const range = readPiCompatibility(serverPkgJsonPath);
197
- const current = readCurrentPiVersion(registry);
198
- const computed = computeCompatibility(range, current);
199
- cached = { value: computed, expiresAt: t + CACHE_TTL_MS };
200
- store.set({ compatibility: computed });
201
- // Minimum-violated → block pi-dependent ops by setting `error`.
202
- if (current && isBelow(current, range.minimum)) {
203
- store.set({
204
- error: {
205
- message: `pi version ${current} is below minimum ${range.minimum}. Please run \`pi-dashboard upgrade-pi\`.`,
206
- },
207
- });
208
- }
209
- return computed;
210
- }
211
-
212
- /** Test helper: clear the 60-second cache between runs. */
213
- export function _resetVersionSkewCache(): void {
214
- cached = undefined;
215
- }