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

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 (201) hide show
  1. package/AGENTS.md +26 -5
  2. package/README.md +49 -7
  3. package/docs/architecture.md +129 -1
  4. package/package.json +15 -15
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
  13. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  14. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  15. package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
  16. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  17. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  18. package/packages/extension/src/ask-user-tool.ts +1 -1
  19. package/packages/extension/src/bridge-context.ts +68 -4
  20. package/packages/extension/src/bridge.ts +79 -11
  21. package/packages/extension/src/command-handler.ts +95 -15
  22. package/packages/extension/src/flow-event-wiring.ts +1 -1
  23. package/packages/extension/src/multiselect-list.ts +1 -1
  24. package/packages/extension/src/pi-env.d.ts +16 -9
  25. package/packages/extension/src/prompt-expander.ts +74 -63
  26. package/packages/extension/src/provider-register.ts +16 -9
  27. package/packages/extension/src/retry-tracker.ts +123 -0
  28. package/packages/extension/src/server-launcher.ts +31 -70
  29. package/packages/extension/src/session-sync.ts +10 -1
  30. package/packages/extension/src/slash-dispatch.ts +123 -0
  31. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  32. package/packages/server/bin/pi-dashboard.mjs +84 -0
  33. package/packages/server/package.json +8 -7
  34. package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
  35. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  36. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  37. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  38. package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
  39. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
  40. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  41. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  42. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  43. package/packages/server/src/__tests__/directory-service.test.ts +2 -2
  44. package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
  45. package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
  46. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
  47. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  48. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  49. package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
  50. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  51. package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
  52. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
  53. package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
  54. package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
  55. package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
  56. package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
  57. package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
  58. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
  59. package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
  60. package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
  61. package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
  62. package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
  63. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  64. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  65. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  66. package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
  67. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  68. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  69. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  70. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
  71. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  72. package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
  73. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  74. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  75. package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
  76. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  77. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
  78. package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
  79. package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
  80. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  81. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
  82. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  83. package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
  84. package/packages/server/src/auth-plugin.ts +3 -0
  85. package/packages/server/src/bootstrap-state.ts +10 -0
  86. package/packages/server/src/browser-gateway.ts +27 -10
  87. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  88. package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
  89. package/packages/server/src/changelog-fs.ts +167 -0
  90. package/packages/server/src/changelog-parser.ts +321 -0
  91. package/packages/server/src/changelog-remote.ts +134 -0
  92. package/packages/server/src/cli.ts +62 -82
  93. package/packages/server/src/config-api.ts +14 -2
  94. package/packages/server/src/directory-service.ts +106 -4
  95. package/packages/server/src/event-wiring.ts +90 -6
  96. package/packages/server/src/headless-pid-registry.ts +344 -37
  97. package/packages/server/src/legacy-pi-cleanup.ts +151 -0
  98. package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
  99. package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
  100. package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
  101. package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
  102. package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
  103. package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
  104. package/packages/server/src/model-proxy/api-key-store.ts +87 -0
  105. package/packages/server/src/model-proxy/auth-gate.ts +116 -0
  106. package/packages/server/src/model-proxy/concurrency.ts +76 -0
  107. package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
  108. package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
  109. package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
  110. package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
  111. package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
  112. package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
  113. package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
  114. package/packages/server/src/model-proxy/convert/index.ts +8 -0
  115. package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
  116. package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
  117. package/packages/server/src/model-proxy/convert/types.ts +70 -0
  118. package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
  119. package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
  120. package/packages/server/src/model-proxy/internal-registry.ts +157 -0
  121. package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
  122. package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
  123. package/packages/server/src/model-proxy/request-log.ts +53 -0
  124. package/packages/server/src/model-proxy/streamer.ts +59 -0
  125. package/packages/server/src/openspec-group-store.ts +490 -0
  126. package/packages/server/src/pending-client-correlations.ts +73 -0
  127. package/packages/server/src/pending-fork-registry.ts +24 -12
  128. package/packages/server/src/pi-core-checker.ts +77 -17
  129. package/packages/server/src/pi-core-updater.ts +16 -6
  130. package/packages/server/src/pi-dev-version-check.ts +145 -0
  131. package/packages/server/src/pi-gateway.ts +4 -0
  132. package/packages/server/src/pi-version-skew.ts +12 -4
  133. package/packages/server/src/process-manager.ts +182 -11
  134. package/packages/server/src/provider-auth-storage.ts +29 -47
  135. package/packages/server/src/provider-catalogue-cache.ts +24 -18
  136. package/packages/server/src/restart-helper.ts +17 -16
  137. package/packages/server/src/routes/bootstrap-routes.ts +37 -0
  138. package/packages/server/src/routes/jj-routes.ts +3 -0
  139. package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
  140. package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
  141. package/packages/server/src/routes/model-proxy-routes.ts +330 -0
  142. package/packages/server/src/routes/openspec-group-routes.ts +231 -0
  143. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  144. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  145. package/packages/server/src/routes/provider-auth-routes.ts +8 -1
  146. package/packages/server/src/routes/provider-routes.ts +28 -5
  147. package/packages/server/src/routes/system-routes.ts +44 -2
  148. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
  149. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
  150. package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
  151. package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
  152. package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
  153. package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
  154. package/packages/server/src/server.ts +254 -60
  155. package/packages/server/src/session-api.ts +63 -4
  156. package/packages/server/src/session-discovery.ts +1 -1
  157. package/packages/server/src/session-file-reader.ts +1 -1
  158. package/packages/server/src/spawn-register-watchdog.ts +62 -7
  159. package/packages/server/src/spawn-token.ts +20 -0
  160. package/packages/server/src/tunnel-watchdog.ts +230 -0
  161. package/packages/server/src/tunnel.ts +5 -1
  162. package/packages/shared/package.json +1 -1
  163. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
  164. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
  165. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  166. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  167. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  168. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  169. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  170. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  172. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  173. package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
  174. package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
  175. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
  176. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  177. package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
  178. package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
  179. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
  180. package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
  181. package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
  182. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  183. package/packages/shared/src/bootstrap-install.ts +1 -1
  184. package/packages/shared/src/browser-protocol.ts +70 -0
  185. package/packages/shared/src/changelog-types.ts +111 -0
  186. package/packages/shared/src/config.ts +172 -2
  187. package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
  188. package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
  189. package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
  190. package/packages/shared/src/platform/binary-lookup.ts +204 -0
  191. package/packages/shared/src/platform/node-spawn.ts +71 -26
  192. package/packages/shared/src/protocol.ts +27 -1
  193. package/packages/shared/src/recommended-extensions.ts +18 -0
  194. package/packages/shared/src/rest-api.ts +219 -1
  195. package/packages/shared/src/server-launcher.ts +277 -0
  196. package/packages/shared/src/skill-block-parser.ts +1 -1
  197. package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
  198. package/packages/shared/src/tool-registry/definitions.ts +15 -3
  199. package/packages/shared/src/types.ts +62 -0
  200. package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
  201. package/packages/shared/src/resolve-jiti.ts +0 -102
@@ -1,22 +1,28 @@
1
1
  /**
2
- * Pin Defect 2's jiti version contract for `shouldUrlWrapEntry()`.
2
+ * Pin the jiti version contract for `shouldUrlWrapEntry()`.
3
3
  *
4
4
  * The Windows-non-tsx arm in `platform/node-spawn.ts::shouldUrlWrapEntry`
5
- * assumes the jiti loader is from `@mariozechner/pi-coding-agent@0.70.x`
6
- * (jiti 2.x with the file:// URL handling fix). Newer pi versions ship
7
- * a different jiti that breaks this contract.
5
+ * relies on jiti's file:/// URL handling. This was VERIFIED on:
6
+ * - `@mariozechner/pi-coding-agent@0.70.x` shipping `jiti@2.x` (original baseline)
7
+ * - `@earendil-works/pi-coding-agent@0.74.x` shipping `jiti@^2.7.0` (current baseline)
8
+ * It was BROKEN on `pi-coding-agent@0.71.x` shipping `jiti@2.6.5`, which
9
+ * misnormalised triple-slash file:/// URLs on Windows. Keep that data
10
+ * point in the contract so a contributor recognises the regression
11
+ * pattern if it recurs.
8
12
  *
9
13
  * This test ensures:
10
14
  * 1. The offline-cacache pin in `packages/electron/offline-packages.json`
11
- * stays at `0.70.x` (the supported range). A bump elsewhere fires
12
- * this test and forces the contributor to either:
15
+ * stays on a supported pi version under one of the two supported
16
+ * forks. A bump outside the verified set fires this test and
17
+ * forces the contributor to either:
13
18
  * - re-verify the contract on Windows
14
19
  * - add a per-jiti-version branch
15
20
  * - switch the bundled loader to tsx
16
21
  * 2. The `shouldUrlWrapEntry` header comment documents the contract
17
22
  * so future contributors discover the constraint at the call site.
18
23
  *
19
- * See change: fix-electron-windows-installer-and-server-bootstrap (Defect 2).
24
+ * See changes: fix-electron-windows-installer-and-server-bootstrap (Defect 2),
25
+ * migrate-pi-fork-to-earendil (E.6).
20
26
  */
21
27
  import { describe, it, expect } from "vitest";
22
28
  import fs from "node:fs";
@@ -40,34 +46,54 @@ const NODE_SPAWN_PATH = path.join(
40
46
  "node-spawn.ts",
41
47
  );
42
48
 
49
+ /** Versions verified against the Windows file:/// jiti contract. */
50
+ const VERIFIED_PI_PINS: ReadonlyArray<{ name: string; versionPrefix: string }> = [
51
+ { name: "@earendil-works/pi-coding-agent", versionPrefix: "0.74." },
52
+ { name: "@mariozechner/pi-coding-agent", versionPrefix: "0.70." },
53
+ ];
54
+
43
55
  describe("jiti version contract for shouldUrlWrapEntry", () => {
44
- it("offline-packages.json pins @mariozechner/pi-coding-agent at a 0.70.x version", () => {
56
+ it("offline-packages.json pins pi-coding-agent at a verified version under a supported fork", () => {
45
57
  const raw = fs.readFileSync(OFFLINE_PACKAGES_PATH, "utf8");
46
58
  const manifest = JSON.parse(raw) as {
47
59
  packages: { name: string; version: string }[];
48
60
  };
49
- const piEntry = manifest.packages.find(
50
- (p) => p.name === "@mariozechner/pi-coding-agent",
61
+
62
+ const supportedNames = VERIFIED_PI_PINS.map((p) => p.name);
63
+ const piEntry = manifest.packages.find((p) =>
64
+ supportedNames.includes(p.name),
51
65
  );
52
66
  if (!piEntry) {
53
67
  throw new Error(
54
- "@mariozechner/pi-coding-agent not found in offline-packages.json. " +
55
- "The offline cacache must include pi-coding-agent. " +
56
- "See change: fix-electron-windows-installer-and-server-bootstrap (Defect 2).",
68
+ `No supported pi-coding-agent fork found in offline-packages.json. ` +
69
+ `Expected one of: ${supportedNames.join(", ")}. ` +
70
+ `The offline cacache must include pi-coding-agent. ` +
71
+ `See changes: fix-electron-windows-installer-and-server-bootstrap (Defect 2), ` +
72
+ `migrate-pi-fork-to-earendil (E.6).`,
57
73
  );
58
74
  }
59
- if (!piEntry.version.startsWith("0.70.")) {
75
+
76
+ const verifiedPin = VERIFIED_PI_PINS.find(
77
+ (p) => p.name === piEntry.name && piEntry.version.startsWith(p.versionPrefix),
78
+ );
79
+ if (!verifiedPin) {
80
+ const allowedRanges = VERIFIED_PI_PINS
81
+ .map((p) => `${p.name}@${p.versionPrefix}x`)
82
+ .join(", ");
60
83
  throw new Error(
61
- `pi-coding-agent pinned at ${piEntry.version}, but ` +
62
- `shouldUrlWrapEntry()'s Windows-non-tsx arm only supports 0.70.x. ` +
84
+ `pi-coding-agent pinned at ${piEntry.name}@${piEntry.version}, but ` +
85
+ `shouldUrlWrapEntry()'s Windows-non-tsx arm only supports verified pins: ` +
86
+ `${allowedRanges}. ` +
63
87
  `Newer jiti versions (e.g. 2.6.5 in pi 0.71.x) misnormalize ` +
64
88
  `file:/// URL entries on Windows. Either re-verify the contract, ` +
65
89
  `add a per-jiti-version branch in shouldUrlWrapEntry(), or switch ` +
66
- `the bundled loader to tsx. See change: ` +
67
- `fix-electron-windows-installer-and-server-bootstrap (Defect 2).`,
90
+ `the bundled loader to tsx. See changes: ` +
91
+ `fix-electron-windows-installer-and-server-bootstrap (Defect 2), ` +
92
+ `migrate-pi-fork-to-earendil (E.6).`,
68
93
  );
69
94
  }
70
- expect(piEntry.version).toMatch(/^0\.70\./);
95
+
96
+ expect(piEntry.version.startsWith(verifiedPin.versionPrefix)).toBe(true);
71
97
  });
72
98
 
73
99
  it("node-spawn.ts source contains the documented JITI VERSION CONTRACT block", () => {
@@ -75,7 +101,17 @@ describe("jiti version contract for shouldUrlWrapEntry", () => {
75
101
 
76
102
  // Contract block markers
77
103
  expect(source).toContain("JITI VERSION CONTRACT");
78
- expect(source).toContain("0.70.x");
104
+ // Documented baseline references (at least one of the verified version markers).
105
+ const hasBaselineMarker =
106
+ source.includes("0.74.") || source.includes("0.70.x");
107
+ if (!hasBaselineMarker) {
108
+ throw new Error(
109
+ "shouldUrlWrapEntry() docstring is missing the verified-baseline marker. " +
110
+ "It must mention at least one of: '0.74.' (current earendil baseline) " +
111
+ "or '0.70.x' (legacy mariozechner baseline). See change: " +
112
+ "migrate-pi-fork-to-earendil (E.7).",
113
+ );
114
+ }
79
115
 
80
116
  // Version drift markers (at least one of these identifies the broken jiti)
81
117
  const hasVersionDriftMarker =
@@ -180,6 +180,57 @@ describe("spawnNodeScript", () => {
180
180
  });
181
181
  });
182
182
 
183
+ describe("buildNodeImportArgvParts", () => {
184
+ // Pure helper shared by spawnNodeScript and restart-helper.ts so the
185
+ // `--import` argv shape lives in exactly one place.
186
+ it("POSIX + jiti: entry passed RAW (jiti rejects file:// URL entries)", async () => {
187
+ const { buildNodeImportArgvParts } = await import("../platform/node-spawn.js");
188
+ const parts = buildNodeImportArgvParts({
189
+ loader: "/usr/lib/jiti/lib/jiti-register.mjs",
190
+ entry: "/srv/cli.ts",
191
+ args: ["start", "--port", "8000"],
192
+ platform: "linux",
193
+ });
194
+ expect(parts[0]).toBe("--import");
195
+ expect(parts[1]).toMatch(/^file:\/\//);
196
+ expect(parts[2]).toBe("/srv/cli.ts"); // RAW
197
+ expect(parts.slice(3)).toEqual(["start", "--port", "8000"]);
198
+ });
199
+
200
+ it("Windows + jiti: entry URL-wrapped", async () => {
201
+ const { buildNodeImportArgvParts } = await import("../platform/node-spawn.js");
202
+ const parts = buildNodeImportArgvParts({
203
+ loader: "B:\\Dev\\jiti\\lib\\jiti-register.mjs",
204
+ entry: "B:\\srv\\cli.ts",
205
+ args: ["start"],
206
+ platform: "win32",
207
+ });
208
+ expect(parts[1]).toBe("file:///B:/Dev/jiti/lib/jiti-register.mjs");
209
+ expect(parts[2]).toBe("file:///B:/srv/cli.ts");
210
+ });
211
+
212
+ it("tsx loader: entry RAW on any platform", async () => {
213
+ const { buildNodeImportArgvParts } = await import("../platform/node-spawn.js");
214
+ const parts = buildNodeImportArgvParts({
215
+ loader: "/x/tsx/dist/esm/index.mjs",
216
+ entry: "C:\\srv\\cli.ts",
217
+ args: [],
218
+ platform: "win32",
219
+ });
220
+ expect(parts[2]).toBe("C:\\srv\\cli.ts"); // RAW (tsx rejects file:// entries)
221
+ });
222
+
223
+ it("omits args when none supplied", async () => {
224
+ const { buildNodeImportArgvParts } = await import("../platform/node-spawn.js");
225
+ const parts = buildNodeImportArgvParts({
226
+ loader: "/x/jiti/lib/jiti-register.mjs",
227
+ entry: "/srv/cli.ts",
228
+ platform: "linux",
229
+ });
230
+ expect(parts).toEqual(["--import", `file://${"/x/jiti/lib/jiti-register.mjs"}`, "/srv/cli.ts"]);
231
+ });
232
+ });
233
+
183
234
  describe("shouldUrlWrapEntry", () => {
184
235
  it("returns false for tsx loader on any platform", () => {
185
236
  const tsxLoader = "file:///home/u/node_modules/tsx/dist/esm/index.mjs";
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Type-level tests for OpenSpec change-grouping shared types.
3
+ *
4
+ * Asserts:
5
+ * - `OpenSpecGroup` shape compiles.
6
+ * - `OpenSpecChange.groupId?: string | null` is optional.
7
+ * - `OpenSpecGroupsFile` shape compiles.
8
+ * - `OPENSPEC_GROUPS_SCHEMA_VERSION` is the literal `1`.
9
+ * - `BrowserOpenSpecGroupsUpdateMessage` is a member of `ServerToBrowserMessage`
10
+ * (otherwise esbuild would dead-code-eliminate the consumer switch arm).
11
+ * - REST request/response shapes for the five `/api/openspec/groups*` routes compile.
12
+ *
13
+ * See change: add-openspec-change-grouping (tasks 1.1–1.7).
14
+ */
15
+ import { describe, it, expect } from "vitest";
16
+ import type {
17
+ OpenSpecGroup,
18
+ OpenSpecChange,
19
+ OpenSpecGroupsFile,
20
+ } from "../types.js";
21
+ import { OPENSPEC_GROUPS_SCHEMA_VERSION } from "../types.js";
22
+ import type {
23
+ ServerToBrowserMessage,
24
+ BrowserOpenSpecGroupsUpdateMessage,
25
+ } from "../browser-protocol.js";
26
+ import type {
27
+ GetOpenSpecGroupsResponse,
28
+ CreateOpenSpecGroupRequest,
29
+ CreateOpenSpecGroupResponse,
30
+ UpdateOpenSpecGroupRequest,
31
+ UpdateOpenSpecGroupResponse,
32
+ DeleteOpenSpecGroupResponse,
33
+ SetOpenSpecGroupAssignmentRequest,
34
+ SetOpenSpecGroupAssignmentResponse,
35
+ } from "../rest-api.js";
36
+
37
+ // Type-level assertion: if the type does NOT extend the union, this will fail to compile.
38
+ type AssertExtends<T, U> = T extends U ? true : never;
39
+
40
+ // 1.5 — broadcast variant lives in the union.
41
+ type _GroupsUpdateInBrowserUnion = AssertExtends<
42
+ BrowserOpenSpecGroupsUpdateMessage,
43
+ ServerToBrowserMessage
44
+ >;
45
+
46
+ // 1.1 — group shape.
47
+ type _GroupShape = AssertExtends<
48
+ { id: string; name: string; color?: string; order: number },
49
+ OpenSpecGroup
50
+ >;
51
+
52
+ // 1.2 — `groupId?` is optional on `OpenSpecChange`.
53
+ type _ChangeGroupIdOptional = AssertExtends<
54
+ { name: string; status: "in-progress"; completedTasks: 0; totalTasks: 0; artifacts: [] },
55
+ OpenSpecChange
56
+ >;
57
+ // And it accepts a string when present.
58
+ type _ChangeWithGroupId = AssertExtends<
59
+ {
60
+ name: string;
61
+ status: "in-progress";
62
+ completedTasks: 0;
63
+ totalTasks: 0;
64
+ artifacts: [];
65
+ groupId: "ui";
66
+ },
67
+ OpenSpecChange
68
+ >;
69
+
70
+ // 1.4 — file shape.
71
+ type _FileShape = AssertExtends<
72
+ {
73
+ schemaVersion: 1;
74
+ groups: OpenSpecGroup[];
75
+ assignments: Record<string, string>;
76
+ },
77
+ OpenSpecGroupsFile
78
+ >;
79
+
80
+ // 1.6 — REST shapes (compile-time only).
81
+ const _getResp: GetOpenSpecGroupsResponse = {
82
+ success: true,
83
+ data: { schemaVersion: 1, groups: [], assignments: {} },
84
+ };
85
+ const _createReq: CreateOpenSpecGroupRequest = { name: "UI", color: "#3b82f6" };
86
+ const _createResp: CreateOpenSpecGroupResponse = {
87
+ success: true,
88
+ data: { id: "ui", name: "UI", color: "#3b82f6", order: 0 },
89
+ };
90
+ const _updateReq: UpdateOpenSpecGroupRequest = { name: "Frontend" };
91
+ const _updateResp: UpdateOpenSpecGroupResponse = {
92
+ success: true,
93
+ data: { id: "ui", name: "Frontend", order: 0 },
94
+ };
95
+ const _deleteResp: DeleteOpenSpecGroupResponse = { success: true };
96
+ const _putReq: SetOpenSpecGroupAssignmentRequest = { changeName: "add-foo", groupId: "ui" };
97
+ const _putReqNull: SetOpenSpecGroupAssignmentRequest = { changeName: "add-foo", groupId: null };
98
+ const _putResp: SetOpenSpecGroupAssignmentResponse = { success: true };
99
+
100
+ // Suppress unused-locals for compile-time-only declarations.
101
+ void _getResp;
102
+ void _createReq;
103
+ void _createResp;
104
+ void _updateReq;
105
+ void _updateResp;
106
+ void _deleteResp;
107
+ void _putReq;
108
+ void _putReqNull;
109
+ void _putResp;
110
+
111
+ describe("OpenSpec change-grouping shared types", () => {
112
+ it("OPENSPEC_GROUPS_SCHEMA_VERSION is the literal 1", () => {
113
+ expect(OPENSPEC_GROUPS_SCHEMA_VERSION).toBe(1);
114
+ });
115
+
116
+ it("openspec_groups_update is reachable in a runtime switch over ServerToBrowserMessage", () => {
117
+ // Runtime check that the discriminant survives type-narrowing — mirrors
118
+ // the prompt-message regression guard in browser-protocol-types.test.ts.
119
+ const sample: ServerToBrowserMessage = {
120
+ type: "openspec_groups_update",
121
+ cwd: "/tmp/foo",
122
+ groups: [],
123
+ assignments: {},
124
+ };
125
+ let hit = false;
126
+ switch (sample.type) {
127
+ case "openspec_groups_update":
128
+ hit = true;
129
+ break;
130
+ default:
131
+ break;
132
+ }
133
+ expect(hit).toBe(true);
134
+ });
135
+ });
@@ -128,6 +128,102 @@ describe("publish.yml — electron job dependency-graph contract", () => {
128
128
  // single source of truth is the `prepare` job's computed `is_prerelease`
129
129
  // output. See change: eliminate-bash-on-windows-runners (D6).
130
130
 
131
+ // ── Lockfile-regen contract ──────────────────────────────────────────────
132
+ // The `prepare` job MUST regenerate package-lock.json with the bumped
133
+ // versions (between sync-versions.js and the git commit) so consumers'
134
+ // `npm ci` doesn't fall back to stale registry tarballs via strict
135
+ // prerelease semver. See change: fix-release-lockfile-drift.
136
+
137
+ /**
138
+ * Parse the `steps:` block of a single job into an array of `{ run }`
139
+ * entries. We only care about the `run:` field for this contract; the
140
+ * step delimiter is any ` - ` line (6-space indent + dash + space).
141
+ * Multi-line `run: |` blocks fold into a single `run` string.
142
+ */
143
+ function parseJobSteps(jobBlock: string): Array<{ run: string }> {
144
+ const lines = jobBlock.split("\n");
145
+ const steps: Array<{ run: string }> = [];
146
+ let i = 0;
147
+ // Find the ` steps:` line.
148
+ while (i < lines.length && !/^ steps:\s*$/.test(lines[i])) i++;
149
+ i++;
150
+ let current: { run: string } | null = null;
151
+ let inRunBlock = false;
152
+ let runBlockIndent = 0;
153
+ while (i < lines.length) {
154
+ const line = lines[i];
155
+ // New step delimiter: ` - ` at 6-space indent.
156
+ if (/^ - /.test(line)) {
157
+ if (current) steps.push(current);
158
+ current = { run: "" };
159
+ inRunBlock = false;
160
+ // Inline `- run: foo` form.
161
+ const inlineRun = line.match(/^ -\s+run:\s+(.*)$/);
162
+ if (inlineRun) current.run = inlineRun[1];
163
+ i++;
164
+ continue;
165
+ }
166
+ if (current) {
167
+ // Block scalar ` run: |`.
168
+ const blockStart = line.match(/^ run:\s*\|?\s*$/);
169
+ const inlineKey = line.match(/^ run:\s+(.+)$/);
170
+ if (blockStart) {
171
+ inRunBlock = true;
172
+ runBlockIndent = 10; // body lines start at ≥ 10-space indent
173
+ i++;
174
+ continue;
175
+ }
176
+ if (inlineKey) {
177
+ current.run += (current.run ? "\n" : "") + inlineKey[1];
178
+ i++;
179
+ continue;
180
+ }
181
+ if (inRunBlock) {
182
+ // Body line of a `run: |` block. Stop when we hit a less-indented
183
+ // line (next key at 8-space indent, or the next step at 6-space).
184
+ if (line.length === 0) {
185
+ current.run += "\n";
186
+ i++;
187
+ continue;
188
+ }
189
+ const indent = line.length - line.trimStart().length;
190
+ if (indent < runBlockIndent) {
191
+ inRunBlock = false;
192
+ continue; // re-process this line as a key
193
+ }
194
+ current.run += (current.run ? "\n" : "") + line.slice(runBlockIndent);
195
+ i++;
196
+ continue;
197
+ }
198
+ }
199
+ i++;
200
+ }
201
+ if (current) steps.push(current);
202
+ return steps;
203
+ }
204
+
205
+ describe("publish.yml — prepare job lockfile-regen contract", () => {
206
+ const yaml = fs.readFileSync(WORKFLOW_PATH, "utf8");
207
+ const prepareBlock = extractJobBlock(yaml, "prepare");
208
+ const prepareSteps = parseJobSteps(prepareBlock);
209
+
210
+ it("prepare job regenerates lockfile after version bump (fix-release-lockfile-drift)", () => {
211
+ const syncIdx = prepareSteps.findIndex((s) => /sync-versions\.js/.test(s.run || ""));
212
+ const regenIdx = prepareSteps.findIndex((s) =>
213
+ /npm install --package-lock-only/.test(s.run || ""),
214
+ );
215
+ const commitIdx = prepareSteps.findIndex((s) =>
216
+ /git commit -m "chore\(release\)/.test(s.run || ""),
217
+ );
218
+ expect(syncIdx, "sync-versions.js step missing").toBeGreaterThanOrEqual(0);
219
+ expect(
220
+ regenIdx,
221
+ "lockfile regen step missing — see change fix-release-lockfile-drift",
222
+ ).toBeGreaterThan(syncIdx);
223
+ expect(commitIdx, "git commit step missing").toBeGreaterThan(regenIdx);
224
+ });
225
+ });
226
+
131
227
  describe("publish.yml — prerelease safety contract", () => {
132
228
  const yaml = fs.readFileSync(WORKFLOW_PATH, "utf8");
133
229
  const prepareBlock = extractJobBlock(yaml, "prepare");
@@ -8,13 +8,14 @@ import {
8
8
  } from "../recommended-extensions.js";
9
9
 
10
10
  describe("RECOMMENDED_EXTENSIONS manifest", () => {
11
- it("contains exactly the five expected entries", () => {
11
+ it("contains exactly the six expected entries", () => {
12
12
  const ids = RECOMMENDED_EXTENSIONS.map((e) => e.id).sort();
13
13
  expect(ids).toEqual(
14
14
  [
15
15
  "pi-anthropic-messages",
16
16
  "pi-agent-browser",
17
17
  "pi-flows",
18
+ "pi-memory-honcho",
18
19
  "pi-web-access",
19
20
  "tintinweb-pi-subagents",
20
21
  ].sort(),
@@ -62,7 +63,12 @@ describe("RECOMMENDED_EXTENSIONS manifest", () => {
62
63
  it("npm-sourced entries use the npm: prefix", () => {
63
64
  const npmEntries = RECOMMENDED_EXTENSIONS.filter((e) => e.source.startsWith("npm:"));
64
65
  expect(npmEntries.map((e) => e.id).sort()).toEqual(
65
- ["pi-agent-browser", "pi-web-access", "tintinweb-pi-subagents"].sort(),
66
+ [
67
+ "pi-agent-browser",
68
+ "pi-memory-honcho",
69
+ "pi-web-access",
70
+ "tintinweb-pi-subagents",
71
+ ].sort(),
66
72
  );
67
73
  });
68
74
 
@@ -105,7 +111,9 @@ describe("getRecommendedByStatus", () => {
105
111
 
106
112
  it("filters by optional", () => {
107
113
  const optional = getRecommendedByStatus("optional");
108
- expect(optional.map((e) => e.id)).toEqual(["pi-agent-browser"]);
114
+ expect(optional.map((e) => e.id).sort()).toEqual(
115
+ ["pi-agent-browser", "pi-memory-honcho"].sort(),
116
+ );
109
117
  });
110
118
  });
111
119