@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
@@ -61,6 +61,16 @@ export interface RecommendedExtension {
61
61
  * start working.
62
62
  */
63
63
  autowired?: boolean;
64
+
65
+ /**
66
+ * Companion dashboard plugin id, if this extension is paired with one
67
+ * (e.g. `pi-memory-honcho` extension <-> `honcho` dashboard plugin).
68
+ * The recommended-extensions enricher carries this through alongside a
69
+ * computed `dashboardPluginInstalled: boolean` so the install browser
70
+ * can render a "+plugin: <id>" badge.
71
+ * See change: add-plugin-activation-ui (Layer 1.5).
72
+ */
73
+ dashboardPlugin?: string;
64
74
  }
65
75
 
66
76
  /** Enriched manifest entry returned by GET /api/packages/recommended. */
@@ -77,6 +87,12 @@ export interface EnrichedRecommendedExtension extends RecommendedExtension {
77
87
  activeInPi: boolean;
78
88
  /** True iff a newer version is available upstream. */
79
89
  updateAvailable: boolean;
90
+ /**
91
+ * True iff the entry declares a `dashboardPlugin` and the named plugin is
92
+ * present in the dashboard's plugin status store.
93
+ * See change: add-plugin-activation-ui.
94
+ */
95
+ dashboardPluginInstalled?: boolean;
80
96
  }
81
97
 
82
98
  export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
@@ -96,21 +112,24 @@ export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
96
112
  autowired: true,
97
113
  },
98
114
  {
99
- id: "tintinweb-pi-subagents",
100
- source: "npm:@tintinweb/pi-subagents",
101
- displayName: "@tintinweb/pi-subagents",
115
+ id: "pi-dashboard-subagents",
116
+ source: "https://github.com/BlackBeltTechnology/pi-dashboard-subagents.git",
117
+ displayName: "pi-dashboard-subagents",
102
118
  fallbackDescription:
103
- "Claude Code-style autonomous sub-agents for pi. Registers " +
104
- "the Agent tool and its companions. The dashboard has custom " +
105
- "card UI for it.",
106
- status: "strongly-suggested",
119
+ "Foreground in-memory subagents for pi with a streamed timeline " +
120
+ "(every tool call, reasoning step, and assistant text). Pairs with " +
121
+ "the dashboard's subagent-inspector plugin for inline-expand + popout " +
122
+ "card UI. Producer of the Agent tool; no background spawning.",
123
+ status: "optional",
107
124
  unlocks: [
108
125
  "Agent tool card UI",
109
- "Subagent activity badge",
110
- "get_subagent_result / steer_subagent renderers",
126
+ "Subagent inspector (inline expand + popout)",
127
+ "agent-md path display",
111
128
  ],
112
- toolsRegistered: ["Agent", "get_subagent_result", "steer_subagent"],
129
+ toolsRegistered: ["Agent"],
113
130
  autowired: true,
131
+ // Companion dashboard plugin id. See change: add-subagent-inspector.
132
+ dashboardPlugin: "subagents",
114
133
  },
115
134
  {
116
135
  id: "pi-flows",
@@ -182,6 +201,8 @@ export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
182
201
  ],
183
202
  toolsRegistered: ["honcho_search", "honcho_context", "honcho_profile"],
184
203
  autowired: true,
204
+ // Companion dashboard plugin id. See change: add-plugin-activation-ui.
205
+ dashboardPlugin: "honcho",
185
206
  },
186
207
  ];
187
208
 
@@ -198,6 +219,9 @@ export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
198
219
  */
199
220
  export const BUNDLED_EXTENSION_IDS: readonly string[] = [
200
221
  "pi-anthropic-messages",
222
+ // Foreground subagent producer. Git source + MIT license = bundle eligible.
223
+ // See change: add-subagent-inspector §13.
224
+ "pi-dashboard-subagents",
201
225
  // "pi-flows" is intentionally NOT bundled until the upstream repo declares
202
226
  // an SPDX-conformant license (`LICENSE` file or `package.json#license`).
203
227
  // The bundle-recommended-extensions.mjs license allowlist enforcement
@@ -1,26 +1,94 @@
1
1
  /**
2
2
  * Server identity verification via HTTP health check.
3
3
  * Replaces bare TCP port probes with identity-verified dashboard detection.
4
+ *
5
+ * Retry semantics (cherry-pick 2 of harvest-bootstrap-survivor-fixes):
6
+ * the pre-wizard probe in Electron's main process fires while a *previous*
7
+ * server instance may still be mid-bootstrap (jiti TypeScript transpile +
8
+ * cold-cache extraction can block the event loop for 5–15 s). The default
9
+ * 2 s timeout + 1 attempt produces false negatives in that window.
10
+ * Callers can opt into a bounded retry loop via `opts.retries` /
11
+ * `opts.timeoutMs` / `opts.retryDelayMs`. Defaults preserve legacy
12
+ * behaviour (single attempt, 2 s timeout) so existing call sites are
13
+ * unaffected.
4
14
  */
5
15
 
6
- const HEALTH_TIMEOUT = 2000;
16
+ const DEFAULT_TIMEOUT_MS = 2000;
17
+ const DEFAULT_RETRIES = 0;
18
+ const DEFAULT_RETRY_DELAY_MS = 500;
7
19
 
8
20
  export interface DashboardStatus {
9
21
  /** Whether the dashboard server is running on this port */
10
22
  running: boolean;
11
23
  /** PID of the running server (if detected) */
12
24
  pid?: number;
25
+ /** Server version from /api/health (when detected) */
26
+ version?: string;
13
27
  /** Port is occupied by a non-dashboard service */
14
28
  portConflict?: boolean;
15
29
  }
16
30
 
31
+ export interface DashboardCheckOpts {
32
+ /**
33
+ * Per-attempt fetch timeout. Default 2000 ms — preserves legacy single-shot behaviour.
34
+ * Bootstrap-aware callers should pass ~8000 ms to absorb event-loop hiccups
35
+ * during cold-cache install.
36
+ */
37
+ timeoutMs?: number;
38
+ /**
39
+ * Number of additional attempts after the first. Default 0 (no retries).
40
+ * On `AbortError` (timeout) or 5xx the loop sleeps `retryDelayMs` and
41
+ * retries. `portConflict: true` (HTTP 200 with foreign JSON shape)
42
+ * short-circuits — that's a deterministic conflict, not a transient
43
+ * fault, and retrying would mask a real port collision.
44
+ * ECONNREFUSED is *not* retried (no process to talk to).
45
+ */
46
+ retries?: number;
47
+ /** Sleep between retries. Default 500 ms. */
48
+ retryDelayMs?: number;
49
+ /**
50
+ * Test seam: replace `setTimeout`-based sleep. Receives the configured
51
+ * `retryDelayMs`. Must return a promise that resolves after the sleep.
52
+ */
53
+ _sleep?: (ms: number) => Promise<void>;
54
+ }
55
+
17
56
  /**
18
57
  * Check if a dashboard server is running on the given port by hitting GET /api/health.
19
58
  * Returns identity-verified status instead of just "port is open".
20
59
  */
21
- export async function isDashboardRunning(port: number, host = "localhost"): Promise<DashboardStatus> {
60
+ export async function isDashboardRunning(
61
+ port: number,
62
+ host: string = "localhost",
63
+ opts?: DashboardCheckOpts,
64
+ ): Promise<DashboardStatus> {
65
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
66
+ const retries = opts?.retries ?? DEFAULT_RETRIES;
67
+ const retryDelayMs = opts?.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
68
+ const sleep = opts?._sleep ?? ((ms: number) => new Promise((r) => setTimeout(r, ms)));
69
+
70
+ const attempts = retries + 1;
71
+ let lastResult: DashboardStatus = { running: false };
72
+
73
+ for (let i = 0; i < attempts; i++) {
74
+ const result = await probeOnce(port, host, timeoutMs);
75
+ // Success — return immediately.
76
+ if (result.running) return result;
77
+ // Deterministic conflict — short-circuit (retrying would mask it).
78
+ if (result.portConflict) return result;
79
+ lastResult = result;
80
+ if (i < attempts - 1) await sleep(retryDelayMs);
81
+ }
82
+ return lastResult;
83
+ }
84
+
85
+ async function probeOnce(
86
+ port: number,
87
+ host: string,
88
+ timeoutMs: number,
89
+ ): Promise<DashboardStatus> {
22
90
  const controller = new AbortController();
23
- const timer = setTimeout(() => controller.abort(), HEALTH_TIMEOUT);
91
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
24
92
 
25
93
  try {
26
94
  const res = await fetch(`http://${host}:${port}/api/health`, {
@@ -32,9 +100,10 @@ export async function isDashboardRunning(port: number, host = "localhost"): Prom
32
100
  return { running: false, portConflict: true };
33
101
  }
34
102
 
35
- const data = await res.json() as Record<string, unknown>;
103
+ const data = (await res.json()) as Record<string, unknown>;
36
104
  if (data && data.ok === true && typeof data.pid === "number") {
37
- return { running: true, pid: data.pid };
105
+ const version = typeof data.version === "string" ? data.version : undefined;
106
+ return { running: true, pid: data.pid, version };
38
107
  }
39
108
 
40
109
  // HTTP 200 but not our format — another service
@@ -136,6 +136,19 @@ export interface LaunchOpts {
136
136
  _now?: () => number;
137
137
  /** Override the sleep function used between polls. */
138
138
  _sleep?: (ms: number) => Promise<void>;
139
+ /**
140
+ * Called once when the spawned child exits (any exit — crash or graceful).
141
+ * Attached via `child.on("exit", …)` before the readiness loop so the
142
+ * handler fires even if the child exits during the health-wait window.
143
+ * No-op when omitted — existing callers are unaffected.
144
+ *
145
+ * Callers that need to distinguish crash from graceful shutdown should
146
+ * maintain their own flag (see `setGracefulShutdownInProgress` in
147
+ * `electron/server-lifecycle.ts`) and consult it inside the callback.
148
+ *
149
+ * See change: harvest-bootstrap-survivor-fixes (cherry-pick 6a).
150
+ */
151
+ onChildExit?: (code: number | null, signal: NodeJS.Signals | null) => void;
139
152
  }
140
153
 
141
154
  export interface LaunchResult {
@@ -242,6 +255,13 @@ export async function launchDashboardServer(opts: LaunchOpts): Promise<LaunchRes
242
255
 
243
256
  try { child.unref(); } catch { /* ignore */ }
244
257
 
258
+ // Attach caller's exit handler before the readiness loop so it fires
259
+ // even for exits that happen during the health-wait window.
260
+ // See change: harvest-bootstrap-survivor-fixes (cherry-pick 6a).
261
+ if (opts.onChildExit) {
262
+ child.once("exit", opts.onChildExit);
263
+ }
264
+
245
265
  if (!child.pid) {
246
266
  throw new EarlyExitError(child.exitCode ?? null, child.signalCode ?? null);
247
267
  }
@@ -9,7 +9,7 @@
9
9
  // Input sources take one of these forms:
10
10
  //
11
11
  // npm:<name>[@<version>]
12
- // e.g. "npm:pi-web-access", "npm:@tintinweb/pi-subagents@0.5.2"
12
+ // e.g. "npm:pi-web-access", "npm:@scope/example-pkg@0.5.2"
13
13
  //
14
14
  // git@<host>:<owner>/<repo>[.git]
15
15
  // e.g. "git@github.com:BlackBeltTechnology/pi-flows.git"
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Regression: `nodeScriptToArgv` MUST always prepend a Node interpreter
3
+ * on Windows + `.js` paths.
4
+ *
5
+ * Live repro on Windows 11: when the registry's `node` strategy chain
6
+ * failed (no managed runtime, no PATH hit), `nodeScriptToArgv`
7
+ * previously returned `[cli.js]` and `spawn(cli.js)` crashed with
8
+ * `EFTYPE`. The fix falls back to `process.execPath` — the dashboard
9
+ * server's own Node — which is by definition spawn-able.
10
+ *
11
+ * See change: fix-windows-standalone-spawn.
12
+ */
13
+ import os from "node:os";
14
+ import path from "node:path";
15
+ import { describe, expect, it } from "vitest";
16
+ import {
17
+ ToolRegistry,
18
+ registerDefaultTools,
19
+ OverridesStore,
20
+ } from "../index.js";
21
+
22
+ function freshRegistry(opts: {
23
+ platform: NodeJS.Platform;
24
+ exists?: (p: string) => boolean;
25
+ which?: (name: string) => string | null;
26
+ npmRootGlobal?: () => string;
27
+ overrides?: Record<string, string>;
28
+ }) {
29
+ const store = new OverridesStore({
30
+ filePath: path.join(os.tmpdir(), `node-script-toargv-${Math.random()}.json`),
31
+ warn: () => {},
32
+ });
33
+ for (const [k, v] of Object.entries(opts.overrides ?? {})) store.set(k, v);
34
+
35
+ const r = new ToolRegistry({
36
+ overrides: store,
37
+ platform: opts.platform,
38
+ });
39
+ registerDefaultTools(r, {
40
+ exists: opts.exists ?? (() => false),
41
+ which: opts.which ?? (() => null),
42
+ npmRootGlobal: opts.npmRootGlobal ?? (() => ""),
43
+ });
44
+ return r;
45
+ }
46
+
47
+ describe("nodeScriptToArgv — Windows fallback (Bug 3)", () => {
48
+ it("falls back to process.execPath when registry.resolve('node') returns ok:false", () => {
49
+ // Locate pi via an explicit override pointing at a fake cli.js, so
50
+ // the pi executor resolves successfully. The `node` chain has no
51
+ // sources (no managed runtime, no PATH hit) so it must fail —
52
+ // forcing nodeScriptToArgv into the process.execPath fallback.
53
+ const fakePiCli = "C:\\Users\\u\\.pi-dashboard\\node_modules\\@earendil-works\\pi-coding-agent\\dist\\cli.js";
54
+ const r = freshRegistry({
55
+ platform: "win32",
56
+ // Only the pi override path exists; node has no candidates.
57
+ exists: (p) => p === fakePiCli,
58
+ overrides: { pi: fakePiCli },
59
+ });
60
+
61
+ const nodeRes = r.resolve("node");
62
+ expect(nodeRes.ok).toBe(false);
63
+
64
+ const piExec = r.resolveExecutor("pi");
65
+ expect(piExec.ok).toBe(true);
66
+ expect(piExec.path).toBe(fakePiCli);
67
+ expect(piExec.argv).toEqual([process.execPath, fakePiCli]);
68
+ });
69
+
70
+ it("uses the registry node.path when registry.resolve('node') succeeds", () => {
71
+ const fakePiCli = "C:\\Users\\u\\.pi-dashboard\\node_modules\\@earendil-works\\pi-coding-agent\\dist\\cli.js";
72
+ const fakeNode = "C:\\Program Files\\nodejs\\node.exe";
73
+ const r = freshRegistry({
74
+ platform: "win32",
75
+ exists: (p) => p === fakePiCli || p === fakeNode,
76
+ overrides: { pi: fakePiCli, node: fakeNode },
77
+ });
78
+
79
+ const piExec = r.resolveExecutor("pi");
80
+ expect(piExec.ok).toBe(true);
81
+ expect(piExec.argv[0]).toBe(fakeNode);
82
+ expect(piExec.argv[1]).toBe(fakePiCli);
83
+ });
84
+ });
@@ -11,6 +11,7 @@
11
11
  import { existsSync } from "node:fs";
12
12
  import { createRequire } from "node:module";
13
13
  import path from "node:path";
14
+ import { fileURLToPath } from "node:url";
14
15
  import type { ToolDefinition, Source } from "./types.js";
15
16
  import type { ToolRegistry } from "./registry.js";
16
17
  import {
@@ -110,15 +111,64 @@ function bareImportPackageDirStrategy(
110
111
  return {
111
112
  name: "bare-import",
112
113
  run() {
113
- const pkgJson = resolveModule(`${pkgName}/package.json`, import.meta.url);
114
+ const pkgJson =
115
+ resolveModule(`${pkgName}/package.json`, import.meta.url)
116
+ ?? findPackageJsonByDirWalk(pkgName, import.meta.url, searchPaths, deps?.exists);
114
117
  if (!pkgJson) {
115
- return { ok: false, reason: `cannot resolve ${pkgName}/package.json` };
118
+ return { ok: false, reason: `cannot resolve ${pkgName} package directory` };
116
119
  }
117
120
  return { ok: true, path: path.dirname(pkgJson) };
118
121
  },
119
122
  };
120
123
  }
121
124
 
125
+ /**
126
+ * Helper: walks up from `fromUrl`'s directory looking for
127
+ * `node_modules/<pkgName>/package.json` directly on the filesystem.
128
+ *
129
+ * Exports-map-immune: required because both `@earendil-works/pi-coding-agent`
130
+ * and `@fission-ai/openspec` declare `exports` blocks that omit
131
+ * `./package.json`, so `createRequire(from).resolve("<pkg>/package.json")`
132
+ * returns `ERR_PACKAGE_PATH_NOT_EXPORTED` in modern Node. This walk is
133
+ * a deliberate end-run around the resolver — we already know the file
134
+ * we want and just need its absolute path.
135
+ *
136
+ * Honors the injected `exists` predicate so tests with mocked
137
+ * filesystems stay deterministic; falls back to `existsSync` when
138
+ * none is injected.
139
+ *
140
+ * See change: eliminate-electron-runtime-install (F9 follow-on).
141
+ */
142
+ function findPackageJsonByDirWalk(
143
+ pkgName: string,
144
+ fromUrl: string,
145
+ searchPaths?: readonly string[],
146
+ exists?: StrategyDeps["exists"],
147
+ ): string | null {
148
+ const check = exists ?? existsSync;
149
+ const candidates: string[] = [];
150
+ try {
151
+ candidates.push(path.dirname(fileURLToPath(fromUrl)));
152
+ } catch {
153
+ // fromUrl might not be a file: URL in synthetic test contexts.
154
+ }
155
+ for (const sp of searchPaths ?? []) candidates.push(sp);
156
+ for (const start of candidates) {
157
+ let dir = start;
158
+ // Bound the walk: stop at filesystem root or once dirname is
159
+ // unchanged. Defensive cap at 64 levels covers any plausible
160
+ // workspace nesting without an infinite-loop risk on broken paths.
161
+ for (let i = 0; i < 64; i += 1) {
162
+ const candidate = path.join(dir, "node_modules", pkgName, "package.json");
163
+ if (check(candidate)) return candidate;
164
+ const parent = path.dirname(dir);
165
+ if (parent === dir) break;
166
+ dir = parent;
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+
122
172
  /** Module def that returns the package directory (containing package.json). */
123
173
  function packageDirModuleDef(
124
174
  toolName: string,
@@ -168,11 +218,24 @@ function packageDirModuleDef(
168
218
  * registered with this `toArgv` so the spawn becomes
169
219
  * `node.exe <script.js>` (pure console-subsystem inherit, no new
170
220
  * window ever).
221
+ *
222
+ * Node resolution order on Windows (see change:
223
+ * fix-windows-standalone-spawn):
224
+ * 1. `registry.resolve("node")` when it returns ok with a non-null
225
+ * path (the strategy chain has already validated existence via
226
+ * its injected `exists` dep).
227
+ * 2. `process.execPath` — the dashboard server's own Node — as a
228
+ * guaranteed-working fallback. Live repro: Windows 11 standalone
229
+ * install where the registry chain failed to find node and the
230
+ * spawn argv became `[cli.js]` → `spawn EFTYPE`. Falling back to
231
+ * execPath keeps the spawn argv well-formed because the dashboard
232
+ * server is itself running on a compatible Node.
171
233
  */
172
234
  const nodeScriptToArgv: ToolDefinition["toArgv"] = (resolvedPath, { platform, registry }) => {
173
235
  if (platform === "win32" && /\.js$/i.test(resolvedPath)) {
174
236
  const node = registry.resolve("node");
175
237
  if (node.ok && node.path) return [node.path, resolvedPath];
238
+ return [process.execPath, resolvedPath];
176
239
  }
177
240
  return [resolvedPath];
178
241
  };
@@ -185,7 +248,17 @@ const nodeScriptToArgv: ToolDefinition["toArgv"] = (resolvedPath, { platform, re
185
248
  * `node.exe` to produce `[node.exe, cli.js]`. Falls back to `pi.cmd`
186
249
  * on PATH when the cli.js is nowhere to be found.
187
250
  *
188
- * On Unix, the chain finds `pi` on PATH; argv = [pi].
251
+ * On Unix, the chain first tries `bare-import` so a bundled
252
+ * `<server>/node_modules/@earendil-works/pi-coding-agent/dist/cli.js`
253
+ * wins over a system install. This is load-bearing for the Electron
254
+ * immutable-bundle architecture (see openspec change
255
+ * `eliminate-electron-runtime-install` finding F9). On a clean machine
256
+ * with no system `pi` and no managed `~/.pi-dashboard/node/bin/`,
257
+ * bare-import resolves the bundled cli.js (`#!/usr/bin/env node`
258
+ * shebang, executable) and `nodeScriptToArgv` returns `[cli.js]`
259
+ * directly. Without this strategy, the server falls into
260
+ * `bootstrapInstall(...)` and writes to `~/.pi-dashboard/` — the
261
+ * exact failure mode the immutable-bundle architecture eliminates.
189
262
  */
190
263
  function piExecutorDef(deps?: StrategyDeps): ToolDefinition {
191
264
  const piPkgAliases = ["@earendil-works/pi-coding-agent", "@mariozechner/pi-coding-agent"];
@@ -202,6 +275,7 @@ function piExecutorDef(deps?: StrategyDeps): ToolDefinition {
202
275
 
203
276
  const unixStrategies = [
204
277
  overrideStrategy("pi", deps),
278
+ ...piPkgAliases.map((pkg) => bareImportCliStrategy(pkg, cliEntry, deps)),
205
279
  managedBinStrategy("pi", deps),
206
280
  whereStrategy("pi", deps),
207
281
  ];
@@ -221,7 +295,10 @@ function piExecutorDef(deps?: StrategyDeps): ToolDefinition {
221
295
  *
222
296
  * On Windows: finds `@fission-ai/openspec/bin/openspec.js` via managed
223
297
  * → bare-import → npm-global. `toArgv` wraps with node.exe.
224
- * On Unix: finds `openspec` binary on PATH.
298
+ * On Unix: tries bare-import first (bundled
299
+ * `<server>/node_modules/@fission-ai/openspec/bin/openspec.js`), then
300
+ * managed-bin, then PATH. Symmetric with pi; same Electron
301
+ * immutable-bundle rationale (F9).
225
302
  */
226
303
  function openspecExecutorDef(deps?: StrategyDeps): ToolDefinition {
227
304
  const pkgName = "@fission-ai/openspec";
@@ -229,7 +306,7 @@ function openspecExecutorDef(deps?: StrategyDeps): ToolDefinition {
229
306
 
230
307
  const winStrategies = [
231
308
  overrideStrategy("openspec", deps),
232
- bareImportCliStrategy(pkgName, cliEntry),
309
+ bareImportCliStrategy(pkgName, cliEntry, deps),
233
310
  managedModuleStrategy(pkgName, cliEntry, deps),
234
311
  npmGlobalStrategy(pkgName, cliEntry, deps),
235
312
  managedBinStrategy("openspec", deps),
@@ -238,6 +315,7 @@ function openspecExecutorDef(deps?: StrategyDeps): ToolDefinition {
238
315
 
239
316
  const unixStrategies = [
240
317
  overrideStrategy("openspec", deps),
318
+ bareImportCliStrategy(pkgName, cliEntry, deps),
241
319
  managedBinStrategy("openspec", deps),
242
320
  whereStrategy("openspec", deps),
243
321
  ];
@@ -330,6 +408,10 @@ function bareImportCliStrategy(
330
408
  ) {
331
409
  // Default uses the real module resolver anchored to this file;
332
410
  // tests inject a fake via deps.resolveModule.
411
+ // Fallback to a filesystem walk because both pi-coding-agent and
412
+ // openspec declare exports maps that omit ./package.json (modern Node
413
+ // resolver returns ERR_PACKAGE_PATH_NOT_EXPORTED). See change:
414
+ // eliminate-electron-runtime-install (F9 follow-on).
333
415
  const resolveModule: NonNullable<StrategyDeps["resolveModule"]> =
334
416
  deps?.resolveModule
335
417
  ?? ((id, from) => {
@@ -342,9 +424,11 @@ function bareImportCliStrategy(
342
424
  return {
343
425
  name: "bare-import",
344
426
  run(): { ok: true; path: string } | { ok: false; reason: string } {
345
- const pkgJson = resolveModule(`${pkgName}/package.json`, import.meta.url);
427
+ const pkgJson =
428
+ resolveModule(`${pkgName}/package.json`, import.meta.url)
429
+ ?? findPackageJsonByDirWalk(pkgName, import.meta.url, undefined, deps?.exists);
346
430
  if (!pkgJson) {
347
- return { ok: false, reason: `cannot resolve ${pkgName}/package.json` };
431
+ return { ok: false, reason: `cannot locate ${pkgName} package directory` };
348
432
  }
349
433
  const entry = path.join(path.dirname(pkgJson), entryRelative);
350
434
  return { ok: true, path: entry };
@@ -89,14 +89,6 @@ export interface DashboardSession {
89
89
  firstMessage?: string;
90
90
  dataUnavailable?: boolean;
91
91
  resuming?: boolean;
92
- /** Active flow name (set during flow execution) */
93
- activeFlowName?: string;
94
- /** Number of completed agents in the active flow */
95
- flowAgentsDone?: number;
96
- /** Total number of agents in the active flow */
97
- flowAgentsTotal?: number;
98
- /** Flow execution status */
99
- flowStatus?: FlowStatus;
100
92
  /** Last known bridge entry count (for skip-wipe comparison on reconnect) */
101
93
  lastEntryCount?: number;
102
94
  /** OS process ID of the pi agent — used for force-kill escalation */
@@ -134,6 +126,14 @@ export interface DashboardSession {
134
126
  * See change: chat-markdown-local-images-and-math.
135
127
  */
136
128
  assets?: Record<string, { data: string; mimeType: string }>;
129
+ /**
130
+ * Mirror of pi's native steering + follow-up queues for this session.
131
+ * Populated from pi's `queue_update` event, forwarded by the bridge.
132
+ * `steering[]` typically empties every turn boundary (1-15 s); `followUp`
133
+ * is dashboard-enforced capacity 1 and drains on `agent_end`.
134
+ * See capability `mid-turn-prompt-queue`. See change: add-followup-edit-and-steer-cancel.
135
+ */
136
+ pendingQueues?: { steering: string[]; followUp: string[] };
137
137
  }
138
138
 
139
139
  // ── Extension UI System (Phase 1: management-modal slot) ───────────
@@ -316,6 +316,10 @@ export interface ImageContent {
316
316
  mimeType: string;
317
317
  }
318
318
 
319
+ // PendingPrompt removed in change: add-followup-edit-and-steer-cancel.
320
+ // Pi's native queues are now the single source of truth; `Session.pendingQueues`
321
+ // holds `string[]` arrays directly from pi's `queue_update` event.
322
+
319
323
  /** File entry from directory listing */
320
324
  export interface FileEntry {
321
325
  path: string;
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // Conditionally runs `patch-package` only when both (a) the patch-package
3
+ // module is resolvable and (b) a `patches/` directory exists in cwd.
4
+ //
5
+ // Why: this repo wires `patch-package` into the root `postinstall` so dev
6
+ // installs (and CI `npm ci`) replay patches in node_modules. End-users who
7
+ // run `npm install --omit=dev` against the published tarball have neither
8
+ // `patch-package` in node_modules nor a `patches/` dir (it isn't published).
9
+ // Without this guard the postinstall fails with exit 127 (`patch-package:
10
+ // not found`), breaking standalone installs and Docker smoke tests.
11
+ //
12
+ // See change: fix-electron-appimage-maker (regression of CI standalone-install-smoke).
13
+
14
+ "use strict";
15
+
16
+ const fs = require("node:fs");
17
+ const path = require("node:path");
18
+ const { spawnSync } = require("node:child_process");
19
+
20
+ const patchesDir = path.resolve(process.cwd(), "patches");
21
+ if (!fs.existsSync(patchesDir)) {
22
+ process.exit(0);
23
+ }
24
+
25
+ let patchPackageBin;
26
+ try {
27
+ // Resolve the patch-package binary path via its package.json's `bin` field
28
+ // without invoking npm; works on Windows where `which patch-package` is unreliable.
29
+ const pkgJsonPath = require.resolve("patch-package/package.json");
30
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
31
+ const binEntry = typeof pkgJson.bin === "string"
32
+ ? pkgJson.bin
33
+ : (pkgJson.bin && pkgJson.bin["patch-package"]);
34
+ if (!binEntry) {
35
+ process.exit(0);
36
+ }
37
+ patchPackageBin = path.resolve(path.dirname(pkgJsonPath), binEntry);
38
+ } catch {
39
+ // patch-package not installed (e.g. `npm install --omit=dev`). No-op.
40
+ process.exit(0);
41
+ }
42
+
43
+ const result = spawnSync(process.execPath, [patchPackageBin], { stdio: "inherit" });
44
+ process.exit(result.status ?? 1);