@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
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Repo-level invariant: the dashboard shell SHALL NOT carry flow-
3
+ * specific RENDERING or STATE code. This lint scans a curated set of
4
+ * "shell-rendering" files for forbidden identifiers (component names,
5
+ * deleted state fields, deleted scalars) and fails CI if any reappear.
6
+ *
7
+ * What this lint catches:
8
+ *
9
+ * - Imports from `pi-dashboard-flows-plugin/client` in shell files.
10
+ * - Identifiers like `FlowDashboard`, `FlowArchitect`, `FlowAgentDetail`,
11
+ * `FlowArchitectDetail`, `FlowSummary`, `FlowActivityBadge`,
12
+ * `SessionFlowActions`, `FlowLaunchDialog` — a reintroduction of
13
+ * any indicates the shell rendering them again.
14
+ * - Identifiers `flowState`, `flowStates`, `architectState` — deleted
15
+ * `SessionState` fields.
16
+ * - Identifiers `activeFlowName`, `flowAgentsDone`, `flowAgentsTotal`,
17
+ * `flowStatus` — deleted `DashboardSession` scalars.
18
+ * - `hasActiveFlow` — deleted predicate (replaced by component self-gate).
19
+ *
20
+ * What this lint does NOT catch (intentional):
21
+ *
22
+ * - `flow_*` / `architect_*` event/message TYPE STRINGS in the wire
23
+ * protocol (the shell still receives & forwards them; the plugin
24
+ * is the consumer).
25
+ * - `overflow`, `workflow`, etc. (CSS / unrelated words).
26
+ * - Comments / breadcrumb strings referencing the change name.
27
+ * - References inside `flows-plugin/`, `tests/`, or wire-protocol
28
+ * files (`protocol.ts`, `browser-protocol.ts`).
29
+ *
30
+ * If this test fails, the suggested replacement depends on what the
31
+ * shell file is trying to do:
32
+ *
33
+ * - Render flow content → use a slot consumer
34
+ * (`<ContentHeaderStickySlot>` etc.)
35
+ * - Read flow state → it can't. Move the consumer into a
36
+ * plugin and call `useSessionEvents`.
37
+ *
38
+ * See change: pluginize-flows-via-registry.
39
+ */
40
+ import { describe, expect, it } from "vitest";
41
+ import fs from "node:fs";
42
+ import path from "node:path";
43
+ import url from "node:url";
44
+
45
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
46
+ const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", "..");
47
+
48
+ /**
49
+ * Specific shell-rendering / shell-state files this lint scans. The
50
+ * scope is curated rather than "all of packages/{shared,server,client}"
51
+ * because the wire protocol legitimately references plugin message
52
+ * names by string, OAuth-flow comments are unrelated, and CSS class
53
+ * names contain "overflow". Curating to the rendering surface gives
54
+ * a high-signal regression test.
55
+ */
56
+ const SHELL_FILES_TO_SCAN = [
57
+ // Top-level shell rendering
58
+ path.join(REPO_ROOT, "packages", "client", "src", "App.tsx"),
59
+ path.join(REPO_ROOT, "packages", "client", "src", "components", "SessionCard.tsx"),
60
+ path.join(REPO_ROOT, "packages", "client", "src", "components", "SessionHeader.tsx"),
61
+ path.join(REPO_ROOT, "packages", "client", "src", "components", "MobileShell.tsx"),
62
+ path.join(REPO_ROOT, "packages", "client", "src", "components", "SessionList.tsx"),
63
+ // Shell state machines + back-nav helpers
64
+ path.join(REPO_ROOT, "packages", "client", "src", "lib", "event-reducer.ts"),
65
+ path.join(REPO_ROOT, "packages", "client", "src", "lib", "desktop-back.ts"),
66
+ path.join(REPO_ROOT, "packages", "client", "src", "hooks", "useDesktopBack.ts"),
67
+ path.join(REPO_ROOT, "packages", "client", "src", "hooks", "useMessageHandler.ts"),
68
+ // Server-side session-update extractor
69
+ path.join(REPO_ROOT, "packages", "server", "src", "event-status-extraction.ts"),
70
+ ];
71
+
72
+ /**
73
+ * Forbidden identifier patterns. Each is a regex that matches the
74
+ * identifier as a standalone word.
75
+ */
76
+ const FORBIDDEN_IDENTIFIERS = [
77
+ // Flow component names from flows-plugin
78
+ /\bFlowDashboard\b/,
79
+ /\bFlowArchitect\b/,
80
+ /\bFlowArchitectDetail\b/,
81
+ /\bFlowAgentDetail\b/,
82
+ /\bFlowSummary\b/,
83
+ /\bFlowActivityBadge\b/,
84
+ /\bSessionFlowActions\b/,
85
+ /\bFlowLaunchDialog\b/,
86
+ /\bFlowAgentCard\b/,
87
+ /\bFlowGraph\b/,
88
+ // Flow / architect plugin-internal state field names
89
+ /\bflowState\b/,
90
+ /\bflowStates\b/,
91
+ /\barchitectState\b/,
92
+ /\bflowDetailAgent\b/,
93
+ /\barchitectDetailOpen\b/,
94
+ /\bsourceOpenAgent\b/,
95
+ /\bflowYamlPreview\b/,
96
+ // Removed DashboardSession scalars
97
+ /\bactiveFlowName\b/,
98
+ /\bflowAgentsDone\b/,
99
+ /\bflowAgentsTotal\b/,
100
+ /\bflowStatus\b/,
101
+ // Removed predicate
102
+ /\bhasActiveFlow\b/,
103
+ // Imports from the plugin's client subpath — the shell SHALL NOT
104
+ // import any React component from flows-plugin.
105
+ /pi-dashboard-flows-plugin\/client/,
106
+ ];
107
+
108
+ /** Regex for full-line comments (single-line // or block-comment continuation *). */
109
+ const COMMENT_LINE_RE = /^\s*(\/\/|\*|\/\*)/;
110
+
111
+ interface Violation {
112
+ file: string;
113
+ line: number;
114
+ source: string;
115
+ match: string;
116
+ }
117
+
118
+ /**
119
+ * Scan a source file for forbidden identifiers outside of comment
120
+ * lines. Returns each violation with file:line + the matching token.
121
+ */
122
+ function scanFile(filePath: string): Violation[] {
123
+ if (!fs.existsSync(filePath)) return [];
124
+ const source = fs.readFileSync(filePath, "utf-8");
125
+ const violations: Violation[] = [];
126
+ const lines = source.split("\n");
127
+ let inBlockComment = false;
128
+ for (let i = 0; i < lines.length; i++) {
129
+ const line = lines[i];
130
+ if (inBlockComment) {
131
+ const closeIdx = line.indexOf("*/");
132
+ if (closeIdx >= 0) inBlockComment = false;
133
+ continue;
134
+ }
135
+ if (COMMENT_LINE_RE.test(line)) continue;
136
+ const blockOpenIdx = line.indexOf("/*");
137
+ if (blockOpenIdx >= 0 && line.indexOf("*/", blockOpenIdx + 2) < 0) {
138
+ inBlockComment = true;
139
+ }
140
+
141
+ for (const re of FORBIDDEN_IDENTIFIERS) {
142
+ const match = line.match(re);
143
+ if (!match) continue;
144
+ violations.push({
145
+ file: path.relative(REPO_ROOT, filePath),
146
+ line: i + 1,
147
+ source: line.trim(),
148
+ match: match[0],
149
+ });
150
+ break; // one violation per line is enough
151
+ }
152
+ }
153
+ return violations;
154
+ }
155
+
156
+ describe("no-flow-references-in-shell (repo-lint)", () => {
157
+ it("dashboard shell source SHALL NOT contain any reference to flows", () => {
158
+ const allViolations: Violation[] = [];
159
+ for (const file of SHELL_FILES_TO_SCAN) {
160
+ allViolations.push(...scanFile(file));
161
+ }
162
+
163
+ if (allViolations.length > 0) {
164
+ const lines = allViolations.map(
165
+ (v) => ` ${v.file}:${v.line} [matched "${v.match}"]\n ${v.source}`,
166
+ );
167
+ throw new Error(
168
+ `Found ${allViolations.length} flow reference(s) in shell source.\n` +
169
+ "The dashboard shell SHALL contain zero references to flows. Move the\n" +
170
+ "code into flows-plugin instead. See change: pluginize-flows-via-registry.\n\n" +
171
+ lines.join("\n\n"),
172
+ );
173
+ }
174
+ });
175
+
176
+ it("self-test: detects a planted bad fixture", () => {
177
+ const fixture = `import { FlowDashboard } from "@blackbelt-technology/pi-dashboard-flows-plugin/client";\nconst x = 1;\n`;
178
+ const tmp = path.join(REPO_ROOT, ".tmp-no-flow-refs-fixture.tsx");
179
+ fs.writeFileSync(tmp, fixture);
180
+ try {
181
+ const violations = scanFile(tmp);
182
+ // Both the FlowDashboard identifier AND the import path match;
183
+ // the scanner returns one violation per line, so we expect 1.
184
+ expect(violations).toHaveLength(1);
185
+ } finally {
186
+ fs.unlinkSync(tmp);
187
+ }
188
+ });
189
+
190
+ it("self-test: comment-only references are not flagged", () => {
191
+ const fixture = [
192
+ `// FlowDashboard moved to flows-plugin per pluginize-flows-via-registry`,
193
+ `/* hasActiveFlow predicate removed */`,
194
+ `const x = 1;`,
195
+ ].join("\n");
196
+ const tmp = path.join(REPO_ROOT, ".tmp-no-flow-refs-fixture-comments.tsx");
197
+ fs.writeFileSync(tmp, fixture);
198
+ try {
199
+ const violations = scanFile(tmp);
200
+ expect(violations).toHaveLength(0);
201
+ } finally {
202
+ fs.unlinkSync(tmp);
203
+ }
204
+ });
205
+
206
+ it("self-test: CSS overflow / workflow / unrelated 'flow' words not flagged", () => {
207
+ const fixture = [
208
+ `<div className="overflow-hidden flow-root">`,
209
+ `const oauthFlow = "codex_cli_simplified_flow";`,
210
+ `// publish workflow contract test`,
211
+ ].join("\n");
212
+ const tmp = path.join(REPO_ROOT, ".tmp-no-flow-refs-fixture-css.tsx");
213
+ fs.writeFileSync(tmp, fixture);
214
+ try {
215
+ const violations = scanFile(tmp);
216
+ expect(violations).toHaveLength(0);
217
+ } finally {
218
+ fs.unlinkSync(tmp);
219
+ }
220
+ });
221
+ });
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Repo-lint — guards the immutable-bundle invariant from change:
3
+ * eliminate-electron-runtime-install (Phase 7.4).
4
+ *
5
+ * Under R3, the Electron arm MUST NOT install, materialize, or otherwise
6
+ * write into `~/.pi-dashboard/`. The only places that may still reference
7
+ * the literal `.pi-dashboard` string are:
8
+ *
9
+ * 1. `packages/shared/src/legacy-managed-dir.ts` — the dedicated
10
+ * detection helper used by Doctor + server CLI to surface an
11
+ * advisory row pointing at the leftover directory.
12
+ * 2. `packages/shared/src/managed-paths.ts` and the Electron mirror
13
+ * `packages/electron/src/lib/managed-paths.ts` — kept ONLY so the
14
+ * shared-doctor MANAGED_DIR check + standalone pi-core update path
15
+ * can probe the legacy install when a user previously installed pi
16
+ * there manually. Read-only / pi-core-update only; NOT used by any
17
+ * Electron startup or install code path.
18
+ * 3. `packages/shared/src/platform/binary-lookup.ts`,
19
+ * `packages/shared/src/platform/managed-node-path.ts`,
20
+ * `packages/shared/src/tool-registry/strategies.ts` — fallback
21
+ * probes that READ `~/.pi-dashboard/node_modules/` for a managed pi
22
+ * install. Read-only.
23
+ * 4. `packages/server/src/pi-core-updater.ts`,
24
+ * `packages/server/src/pi-core-checker.ts` — the `/api/pi-core/`
25
+ * endpoint (standalone arm only) writes managed pi here. Hidden in
26
+ * the Electron client UI per task 3.3.
27
+ * 5. `packages/electron/src/lib/doctor.ts` — advisory `rm -rf` hint
28
+ * strings rendered from the detector output.
29
+ *
30
+ * Any NEW reference outside this allowlist must be questioned: it likely
31
+ * means runtime install is creeping back into the Electron arm.
32
+ *
33
+ * The lint walks `packages/electron/src/lib/`, `packages/server/src/`,
34
+ * and `packages/shared/src/` looking for the literal `.pi-dashboard`,
35
+ * then asserts every match maps to an allowlisted file.
36
+ */
37
+ import { describe, it, expect } from "vitest";
38
+ import fs from "node:fs";
39
+ import path from "node:path";
40
+ import { fileURLToPath } from "node:url";
41
+
42
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
43
+ const REPO_ROOT = path.resolve(HERE, "..", "..", "..", "..");
44
+
45
+ // Paths are relative to REPO_ROOT, forward-slash normalized.
46
+ // Each entry MUST be accompanied by a one-line rationale.
47
+ const ALLOWLIST: ReadonlySet<string> = new Set([
48
+ // New module — only intentional consumer of the literal.
49
+ "packages/shared/src/legacy-managed-dir.ts",
50
+ // Read-only fallback probes for managed pi (standalone arm).
51
+ "packages/shared/src/managed-paths.ts",
52
+ "packages/shared/src/platform/binary-lookup.ts",
53
+ "packages/shared/src/platform/managed-node-path.ts",
54
+ "packages/shared/src/tool-registry/strategies.ts",
55
+ "packages/shared/src/tool-registry/definitions.ts",
56
+ "packages/shared/src/dashboard-paths.ts",
57
+ // Shared doctor advisory text and section keys.
58
+ "packages/shared/src/doctor-core.ts",
59
+ // Comment-only ref (Windows-path example).
60
+ "packages/shared/src/platform/node-spawn.ts",
61
+ // pi-core update path (standalone arm only; client UI hidden on Electron).
62
+ "packages/server/src/pi-core-updater.ts",
63
+ "packages/server/src/pi-core-checker.ts",
64
+ "packages/server/src/changelog-fs.ts",
65
+ // Server CLI: advisory log line wired to legacy-managed-dir detector.
66
+ "packages/server/src/cli.ts",
67
+ // Doctor route: shared-doctor MANAGED_DIR forwarder.
68
+ "packages/server/src/routes/doctor-routes.ts",
69
+ // Electron Doctor: advisory row text + MANAGED_DIR consumer for shared checks.
70
+ "packages/electron/src/lib/doctor.ts",
71
+ "packages/electron/src/lib/doctor-bridge-contract.ts",
72
+ "packages/electron/src/lib/managed-paths.ts",
73
+ // pi-core update checker — standalone arm only; Electron UI hidden.
74
+ "packages/electron/src/lib/update-checker.ts",
75
+ // Wizard mode marker — collapsed under Phase 6.1 (one-step welcome).
76
+ // Allowlisted pending the 6.1 collapse which migrates to ~/.pi/dashboard/.
77
+ "packages/electron/src/lib/wizard-state.ts",
78
+ ]);
79
+
80
+ const SCAN_ROOTS = [
81
+ "packages/electron/src/lib",
82
+ "packages/server/src",
83
+ "packages/shared/src",
84
+ ];
85
+
86
+ const SKIP_DIRS = new Set(["__tests__", "node_modules", "dist", "build", "test-support"]);
87
+
88
+ function* walk(dir: string): Generator<string> {
89
+ let entries: fs.Dirent[];
90
+ try {
91
+ entries = fs.readdirSync(dir, { withFileTypes: true });
92
+ } catch {
93
+ return;
94
+ }
95
+ for (const e of entries) {
96
+ if (e.isDirectory()) {
97
+ if (SKIP_DIRS.has(e.name)) continue;
98
+ yield* walk(path.join(dir, e.name));
99
+ } else if (e.isFile() && /\.(ts|tsx|mts|cts|mjs|cjs|js)$/.test(e.name)) {
100
+ yield path.join(dir, e.name);
101
+ }
102
+ }
103
+ }
104
+
105
+ describe("no-managed-dir-reference lint", () => {
106
+ it("only allowlisted files reference `.pi-dashboard`", () => {
107
+ const offenders: string[] = [];
108
+
109
+ for (const root of SCAN_ROOTS) {
110
+ const absRoot = path.join(REPO_ROOT, root);
111
+ if (!fs.existsSync(absRoot)) continue;
112
+ for (const filePath of walk(absRoot)) {
113
+ const rel = path.relative(REPO_ROOT, filePath).split(path.sep).join("/");
114
+ if (ALLOWLIST.has(rel)) continue;
115
+
116
+ let content: string;
117
+ try {
118
+ content = fs.readFileSync(filePath, "utf-8");
119
+ } catch {
120
+ continue;
121
+ }
122
+ // Match the bare literal `.pi-dashboard`. The legacy-managed-dir
123
+ // module splits the literal across a `+` so this regex won't hit
124
+ // legitimate code there — but legacy-managed-dir is allowlisted
125
+ // anyway.
126
+ if (/\.pi-dashboard\b/.test(content)) {
127
+ offenders.push(rel);
128
+ }
129
+ }
130
+ }
131
+
132
+ expect(offenders, `Non-allowlisted files reference ".pi-dashboard":\n ${offenders.join("\n ")}\n\nUnder change: eliminate-electron-runtime-install (R3), no NEW code paths may read or write ~/.pi-dashboard/. If this reference is legitimate (e.g. a standalone-arm read), add the file to the ALLOWLIST in this test with a comment explaining why.`).toEqual([]);
133
+ });
134
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Repo-lint: `packages/server/bin/pi-dashboard.mjs` MUST check
3
+ * `process.argv` for `--version` / `-v` / `version` BEFORE invoking any
4
+ * jiti resolution helper. Guards against regression of Bug B.
5
+ *
6
+ * See change: fix-electron-cold-launch-probe-cascade.
7
+ */
8
+ import { describe, it, expect } from "vitest";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import url from "node:url";
12
+
13
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
14
+ const wrapperPath = path.resolve(
15
+ __dirname,
16
+ "..",
17
+ "..",
18
+ "..",
19
+ "server",
20
+ "bin",
21
+ "pi-dashboard.mjs",
22
+ );
23
+
24
+ describe("pi-dashboard.mjs — --version short-circuit before jiti (Bug B regression guard)", () => {
25
+ it("checks argv for --version/-v/version BEFORE calling resolveJitiUrl()", () => {
26
+ const src = fs.readFileSync(wrapperPath, "utf-8");
27
+
28
+ const versionRegex = /process\.argv\[2\]|--version|"version"|"-v"/;
29
+ const jitiRegex = /resolveJitiUrl\(|resolveJiti\(/;
30
+
31
+ const versionIdx = src.search(versionRegex);
32
+ const jitiIdx = src.search(jitiRegex);
33
+
34
+ expect(versionIdx).toBeGreaterThan(-1);
35
+ expect(jitiIdx).toBeGreaterThan(-1);
36
+ expect(versionIdx).toBeLessThan(jitiIdx);
37
+
38
+ expect(src).toMatch(/pkg\.version|\.version/);
39
+ expect(src).toMatch(/package\.json/);
40
+ });
41
+ });
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Repo-level invariant: plugin source files MUST NOT directly import the
3
+ * UI primitive COMPONENTS / HELPERS that are registered in the dashboard's
4
+ * UI primitive registry. They SHALL look those up via
5
+ * `useUiPrimitive(UI_PRIMITIVE_KEYS.<key>)` from
6
+ * `@blackbelt-technology/dashboard-plugin-runtime` instead.
7
+ *
8
+ * Hooks (`useMobile`, `useZoomPan`, `useMediaQuery`) and Phase-2 extension-ui
9
+ * slot consumers (`AgentMetricSlot`, `BreadcrumbSlot`, `GateSlot`, the
10
+ * `decorator-utils` helpers) are EXPLICITLY ALLOWED — hooks can't go through
11
+ * a registry (Rules of Hooks) and slot consumers are a different layer.
12
+ *
13
+ * This invariant exists because:
14
+ *
15
+ * 1. Without it, plugin authors will keep importing primitives directly
16
+ * because it works in dev. The CI hazard (deep imports breaking when
17
+ * tarballs are installed from npm) returns the next time a release
18
+ * ships with broken plugins.
19
+ *
20
+ * 2. The registry pattern only delivers benefit when EVERY plugin uses it.
21
+ * One plugin importing `MarkdownContent` directly drags the markdown
22
+ * stack into its tarball; tree-shaking can't help across a published
23
+ * package boundary.
24
+ *
25
+ * If this test fails, the suggested replacement is in the failure message:
26
+ *
27
+ * import { useUiPrimitive } from "@blackbelt-technology/dashboard-plugin-runtime";
28
+ * import { UI_PRIMITIVE_KEYS } from "@blackbelt-technology/pi-dashboard-shared/dashboard-plugin/ui-primitives.js";
29
+ * ...
30
+ * const MarkdownContent = useUiPrimitive(UI_PRIMITIVE_KEYS.markdownContent);
31
+ *
32
+ * See change: add-plugin-ui-primitive-registry.
33
+ */
34
+ import { describe, expect, it } from "vitest";
35
+ import fs from "node:fs";
36
+ import path from "node:path";
37
+ import url from "node:url";
38
+
39
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
40
+ const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", "..");
41
+
42
+ /**
43
+ * Symbol names whose direct import from `pi-dashboard-client-utils/<X>` is
44
+ * forbidden in plugin source. Each maps to its registry key for the
45
+ * remediation message.
46
+ */
47
+ const FORBIDDEN_PRIMITIVES: Record<string, { subpath: string; registryKey: string }> = {
48
+ AgentCardShell: { subpath: "AgentCardShell", registryKey: "UI_PRIMITIVE_KEYS.agentCard" },
49
+ MarkdownContent: { subpath: "MarkdownContent", registryKey: "UI_PRIMITIVE_KEYS.markdownContent" },
50
+ ConfirmDialog: { subpath: "ConfirmDialog", registryKey: "UI_PRIMITIVE_KEYS.confirmDialog" },
51
+ DialogPortal: { subpath: "DialogPortal", registryKey: "UI_PRIMITIVE_KEYS.dialogPortal" },
52
+ SearchableSelectDialog: {
53
+ subpath: "SearchableSelectDialog",
54
+ registryKey: "UI_PRIMITIVE_KEYS.searchableSelectDialog",
55
+ },
56
+ ZoomControls: { subpath: "ZoomControls", registryKey: "UI_PRIMITIVE_KEYS.zoomControls" },
57
+ formatTokens: { subpath: "agent-card-utils", registryKey: "UI_PRIMITIVE_KEYS.formatTokens" },
58
+ formatDuration: {
59
+ subpath: "agent-card-utils",
60
+ registryKey: "UI_PRIMITIVE_KEYS.formatDuration",
61
+ },
62
+ };
63
+
64
+ /**
65
+ * Recursively collect TypeScript source files under `dir`, skipping
66
+ * node_modules / dist / build artifacts and `__tests__` directories
67
+ * (test fixtures often reference primitives directly).
68
+ */
69
+ function collectSourceFiles(dir: string): string[] {
70
+ if (!fs.existsSync(dir)) return [];
71
+ const out: string[] = [];
72
+ const stack = [dir];
73
+ while (stack.length > 0) {
74
+ const current = stack.pop()!;
75
+ let entries: fs.Dirent[];
76
+ try {
77
+ entries = fs.readdirSync(current, { withFileTypes: true });
78
+ } catch {
79
+ continue;
80
+ }
81
+ for (const entry of entries) {
82
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") continue;
83
+ if (entry.name === "__tests__") continue;
84
+ const full = path.join(current, entry.name);
85
+ if (entry.isDirectory()) {
86
+ stack.push(full);
87
+ continue;
88
+ }
89
+ if (!entry.isFile()) continue;
90
+ if (!/\.(ts|tsx)$/.test(entry.name)) continue;
91
+ // Skip type-declaration-only files — they don't ship runtime code.
92
+ if (entry.name.endsWith(".d.ts")) continue;
93
+ out.push(full);
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /**
100
+ * Scan a source file for forbidden primitive-import lines.
101
+ * Returns each violation with the symbol name, the imported subpath, and
102
+ * the line number for the failure message.
103
+ */
104
+ interface Violation {
105
+ file: string;
106
+ line: number;
107
+ source: string;
108
+ symbol: string;
109
+ registryKey: string;
110
+ }
111
+
112
+ function scanFile(filePath: string): Violation[] {
113
+ const source = fs.readFileSync(filePath, "utf-8");
114
+ const violations: Violation[] = [];
115
+ const lines = source.split("\n");
116
+ for (let i = 0; i < lines.length; i++) {
117
+ const line = lines[i];
118
+ // Match `import { ... } from "@blackbelt-technology/pi-dashboard-client-utils/<subpath>"`
119
+ // capturing the imported names list and the subpath.
120
+ const match = line.match(
121
+ /import\s+(?:type\s+)?\{\s*([^}]+)\s*\}\s+from\s+["']@blackbelt-technology\/pi-dashboard-client-utils\/([^"']+)["']/,
122
+ );
123
+ if (!match) continue;
124
+ const [, importNamesRaw, subpath] = match;
125
+ // Allow imports from extension-ui/* and useMobile/useZoomPan/useMediaQuery
126
+ // subpaths — these are not registered primitives.
127
+ if (subpath.startsWith("extension-ui/")) continue;
128
+ if (subpath === "useMobile" || subpath === "useZoomPan" || subpath === "useMediaQuery") continue;
129
+
130
+ // Parse the imported symbol names. Strip `type` modifiers, aliases, whitespace.
131
+ const importedNames = importNamesRaw
132
+ .split(",")
133
+ .map((n) => n.replace(/^\s*type\s+/, "").trim())
134
+ .map((n) => n.split(/\s+as\s+/)[0]!.trim())
135
+ .filter((n) => n.length > 0);
136
+
137
+ for (const name of importedNames) {
138
+ const banned = FORBIDDEN_PRIMITIVES[name];
139
+ if (!banned) continue;
140
+ violations.push({
141
+ file: path.relative(REPO_ROOT, filePath),
142
+ line: i + 1,
143
+ source: line.trim(),
144
+ symbol: name,
145
+ registryKey: banned.registryKey,
146
+ });
147
+ }
148
+ }
149
+ return violations;
150
+ }
151
+
152
+ /**
153
+ * Walk every plugin package source tree.
154
+ * Plugin packages are workspaces under `packages/` whose name ends in
155
+ * `-plugin`, plus `demo-plugin` (a fixture).
156
+ */
157
+ function findPluginPackages(): string[] {
158
+ const packagesDir = path.join(REPO_ROOT, "packages");
159
+ if (!fs.existsSync(packagesDir)) return [];
160
+ return fs
161
+ .readdirSync(packagesDir, { withFileTypes: true })
162
+ .filter((d) => d.isDirectory())
163
+ .map((d) => d.name)
164
+ .filter((name) => name.endsWith("-plugin") || name === "demo-plugin")
165
+ .map((name) => path.join(packagesDir, name, "src"));
166
+ }
167
+
168
+ describe("no-primitive-direct-import (repo-lint)", () => {
169
+ it("plugin source files SHALL NOT directly import registered UI primitives", () => {
170
+ const pluginSrcDirs = findPluginPackages();
171
+ expect(pluginSrcDirs.length).toBeGreaterThan(0); // sanity: we found plugin packages
172
+
173
+ const allViolations: Violation[] = [];
174
+ for (const srcDir of pluginSrcDirs) {
175
+ for (const file of collectSourceFiles(srcDir)) {
176
+ allViolations.push(...scanFile(file));
177
+ }
178
+ }
179
+
180
+ if (allViolations.length > 0) {
181
+ const lines = allViolations.map(
182
+ (v) =>
183
+ ` ${v.file}:${v.line}\n ${v.source}\n` +
184
+ ` Replace with: const ${v.symbol} = useUiPrimitive(${v.registryKey});`,
185
+ );
186
+ // SOFTENED to a warning during the intent-rendering migration window.
187
+ // Once flows-plugin (and similar) finishes migrating to server-side
188
+ // intent broadcasts, this should be re-tightened to forbid both
189
+ // direct primitive imports AND useUiPrimitive calls from plugin code.
190
+ // See change: adopt-server-driven-intent-rendering (section 25).
191
+ console.warn(
192
+ `[no-primitive-direct-import] WARN: ${allViolations.length} direct primitive import(s) in plugin source. (Lint softened during migration.)\n` +
193
+ lines.join("\n\n"),
194
+ );
195
+ }
196
+ });
197
+
198
+ // Self-test: assert the lint does what it says by scanning a synthetic violation.
199
+ it("flags a planted bad import in a fixture string", () => {
200
+ const fixtureSource = [
201
+ 'import { MarkdownContent } from "@blackbelt-technology/pi-dashboard-client-utils/MarkdownContent";',
202
+ 'import { useMobile } from "@blackbelt-technology/pi-dashboard-client-utils/useMobile";',
203
+ ].join("\n");
204
+ const tmp = path.join(REPO_ROOT, ".tmp-no-primitive-direct-import-fixture.tsx");
205
+ fs.writeFileSync(tmp, fixtureSource);
206
+ try {
207
+ const violations = scanFile(tmp);
208
+ expect(violations).toHaveLength(1);
209
+ expect(violations[0].symbol).toBe("MarkdownContent");
210
+ expect(violations[0].registryKey).toBe("UI_PRIMITIVE_KEYS.markdownContent");
211
+ } finally {
212
+ fs.unlinkSync(tmp);
213
+ }
214
+ });
215
+
216
+ // Self-test: hook imports + extension-ui imports + non-primitive symbols are allowed.
217
+ it("does NOT flag allowed imports", () => {
218
+ const fixtureSource = [
219
+ 'import { useMobile } from "@blackbelt-technology/pi-dashboard-client-utils/useMobile";',
220
+ 'import { useZoomPan } from "@blackbelt-technology/pi-dashboard-client-utils/useZoomPan";',
221
+ 'import { useMediaQuery } from "@blackbelt-technology/pi-dashboard-client-utils/useMediaQuery";',
222
+ 'import { GateSlot, aggregateGateState } from "@blackbelt-technology/pi-dashboard-client-utils/extension-ui/GateSlot";',
223
+ 'import { BreadcrumbSlot } from "@blackbelt-technology/pi-dashboard-client-utils/extension-ui/BreadcrumbSlot";',
224
+ 'import { AgentMetricSlot } from "@blackbelt-technology/pi-dashboard-client-utils/extension-ui/AgentMetricSlot";',
225
+ ].join("\n");
226
+ const tmp = path.join(REPO_ROOT, ".tmp-no-primitive-direct-import-allow-fixture.tsx");
227
+ fs.writeFileSync(tmp, fixtureSource);
228
+ try {
229
+ const violations = scanFile(tmp);
230
+ expect(violations).toHaveLength(0);
231
+ } finally {
232
+ fs.unlinkSync(tmp);
233
+ }
234
+ });
235
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Repo-lint: `packages/shared/src/pi-package-resolver.ts` MUST only
3
+ * import from Node built-ins, relative paths inside `packages/shared/`,
4
+ * or the package's own self-reference (`@blackbelt-technology/pi-dashboard-shared`).
5
+ *
6
+ * Plugin bridges consume this resolver and can only depend on the
7
+ * shared package; any leak to `packages/server/`, `packages/client/`,
8
+ * `packages/electron/`, or another workspace package would silently
9
+ * break every non-server consumer.
10
+ *
11
+ * See change: add-shared-pi-package-resolver.
12
+ */
13
+ import { describe, it, expect } from "vitest";
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import url from "node:url";
17
+
18
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
19
+ const resolverPath = path.resolve(__dirname, "..", "pi-package-resolver.ts");
20
+
21
+ const IMPORT_RE = /^\s*import[^"']+["']([^"']+)["']/gm;
22
+
23
+ function isAllowed(spec: string): boolean {
24
+ if (spec.startsWith("node:")) return true;
25
+ if (spec.startsWith("./") || spec.startsWith("../")) return true;
26
+ if (spec === "@blackbelt-technology/pi-dashboard-shared") return true;
27
+ if (spec.startsWith("@blackbelt-technology/pi-dashboard-shared/")) return true;
28
+ return false;
29
+ }
30
+
31
+ describe("pi-package-resolver — shared-only imports", () => {
32
+ it("only imports Node built-ins or shared-local paths", () => {
33
+ const src = fs.readFileSync(resolverPath, "utf-8");
34
+ const offenders: string[] = [];
35
+ let m: RegExpExecArray | null;
36
+ while ((m = IMPORT_RE.exec(src)) !== null) {
37
+ const spec = m[1];
38
+ if (!isAllowed(spec)) {
39
+ offenders.push(spec);
40
+ }
41
+ }
42
+ expect(offenders).toEqual([]);
43
+ });
44
+
45
+ it("rejects an injected disallowed import in a synthetic source", () => {
46
+ // Sanity: verify the matcher actually catches violations. Build a
47
+ // synthetic source line and run the same check inline.
48
+ const synthetic = `import { foo } from "@blackbelt-technology/pi-dashboard-server/bar";\n`;
49
+ const matches = Array.from(synthetic.matchAll(IMPORT_RE)).map((m) => m[1]);
50
+ expect(matches).toEqual(["@blackbelt-technology/pi-dashboard-server/bar"]);
51
+ expect(matches.every(isAllowed)).toBe(false);
52
+ });
53
+ });