pi-cursor-sdk 0.1.32 → 0.1.33

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.33 - 2026-06-04
6
+
7
+ ### Fixed
8
+
9
+ - Prevent connect-node-only Cursor SDK network resets such as `ConnectError: [aborted] read ECONNRESET` from escaping as process-level uncaught exceptions during active Cursor turns, while keeping provenance-free generic ConnectRPC errors unsuppressed (#121).
10
+ - Suppress expected Cursor SDK abort `ConnectError` / `AbortError` shapes during abandoned live-run cancellation so idle-resume and interrupt cleanup paths keep pi alive for later prompts (#120).
11
+
5
12
  ## 0.1.32 - 2026-06-02
6
13
 
7
14
  ### Added
package/README.md CHANGED
@@ -2,7 +2,27 @@
2
2
 
3
3
  A pi provider extension that lets pi use Cursor models through the local `@cursor/sdk` agent runtime.
4
4
 
5
- Use this extension if you want Cursor's model catalog inside pi while keeping pi's native model picker, thinking controls where the SDK exposes them, session restore, context display, and default footer UX.
5
+ Use this extension if you primarily use Cursor models inside pi and want Cursor's local SDK agent loop preserved while pi adds native model selection, auth, thinking/context controls, session behavior, replay UI, and optional pi tool bridging.
6
+
7
+ ## Why use this instead of an OpenAI-compatible Cursor endpoint?
8
+
9
+ Use `pi-cursor-sdk` when you primarily want to use Cursor models **inside pi**.
10
+
11
+ This extension runs Cursor models through the local `@cursor/sdk` agent runtime and keeps Cursor's agent loop intact. pi integrates around that loop: model discovery, model selection, context-window variants, thinking controls where Cursor exposes them, fast/slow aliases, Cursor mode, session handling, native replay cards, and the optional pi tool bridge.
12
+
13
+ OpenAI-compatible Cursor proxies are useful when you want a generic `/v1/chat/completions` or `/v1/responses` endpoint for many clients such as curl, the OpenAI SDK, OpenCode, or other tools. That compatibility comes from translating Cursor behavior into OpenAI-shaped requests, responses, and tool calls.
14
+
15
+ For pi users, that translation is usually the wrong abstraction. `pi-cursor-sdk` is pi-specific on purpose: it lets Cursor remain Cursor while making it feel native in pi.
16
+
17
+ | If you want... | Prefer |
18
+ | --- | --- |
19
+ | First-class Cursor usage inside pi | `pi-cursor-sdk` |
20
+ | Cursor's local SDK agent loop preserved, not replaced by an OpenAI-shaped adapter | `pi-cursor-sdk` |
21
+ | pi model picker, `/login`, `/model`, sessions, context display, footer/status UX | `pi-cursor-sdk` |
22
+ | Cursor SDK local-agent tools, settings, MCP, and native replay surfaced in pi | `pi-cursor-sdk` |
23
+ | pi extension tools exposed to Cursor through a local MCP bridge | `pi-cursor-sdk` |
24
+ | A generic OpenAI-compatible localhost `/v1` API for non-pi clients | An OpenAI-compatible Cursor proxy |
25
+ | One Cursor-ish endpoint shared across several unrelated tools | An OpenAI-compatible Cursor proxy |
6
26
 
7
27
  ## Quick start
8
28
 
@@ -1,6 +1,6 @@
1
1
  # Cursor Testing Lessons
2
2
 
3
- > **Platform Smoke (new):** The required cross-platform release gate is `npm run smoke:platform:doctor && npm run smoke:platform:all`. See [docs/platform-smoke.md](./platform-smoke.md). For portable lessons other pi extension projects can adapt without sharing repo-specific state, see [Crabbox Platform Testing Lessons](./crabbox-platform-testing-lessons.md). The live smoke checklist remains useful for inner-loop development but is not the release gate.
3
+ > **Platform Smoke (new):** The required cross-platform release gate is `npm run smoke:platform:doctor && npm run smoke:platform:all`. See [docs/platform-smoke.md](./platform-smoke.md). For portable lessons other pi extension projects can adapt without sharing repo-specific state, see the generic Crabbox platform testing guide at `/Users/mitchfultz/Projects/crabbox/docs/pi-extension-platform-testing.md`. The live smoke checklist remains useful for inner-loop development but is not the release gate.
4
4
 
5
5
  ## Purpose
6
6
 
@@ -6,6 +6,8 @@ Branch introduced by: `feat/crabbox-platform-smoke`
6
6
 
7
7
  Oracle review incorporated: this gate resolves the packed-install workspace conflict, Cursor budget contradiction, Windows shell drift, artifact-on-failure gap, render-location ambiguity, provider-debug ambiguity, and registry-classification gap called out during review.
8
8
 
9
+ Crabbox best-practice baseline applied from `~/Projects/crabbox`: Crabbox owns lease, sync, run, evidence transport, and cleanup; this repo owns target policy, package setup, scenario meaning, assertions, artifacts, auth forwarding, redaction, and release criteria.
10
+
9
11
  ## Decision
10
12
 
11
13
  Crabbox is the required platform smoke runner for `pi-cursor-sdk` releases that touch Cursor provider/runtime behavior.
@@ -53,7 +55,7 @@ Current baseline:
53
55
 
54
56
  ```text
55
57
  install: brew install openclaw/tap/crabbox
56
- version: 0.24.0 or newer
58
+ version: 0.26.0 or newer
57
59
  binary: Homebrew `crabbox` on PATH (`/opt/homebrew/bin/crabbox` on Apple Silicon Homebrew installs)
58
60
  ```
59
61
 
@@ -75,6 +77,8 @@ scenario + target capability + artifact contract
75
77
 
76
78
  not a one-off shell script.
77
79
 
80
+ Crabbox is deliberately kept as the transport/lifecycle layer. It must not be treated as proof that the pi extension behavior passed; every suite still fails or passes from project-owned assertions and artifact manifests.
81
+
78
82
  High-level flow:
79
83
 
80
84
  ```text
@@ -145,19 +149,21 @@ scripts/platform-smoke/artifacts.mjs
145
149
  scripts/platform-smoke/card-detect.mjs
146
150
  scripts/platform-smoke/crabbox-runner.mjs
147
151
  scripts/platform-smoke/doctor.mjs
152
+ scripts/platform-smoke/jsonl-text.mjs
148
153
  scripts/platform-smoke/live-suite-runner.mjs
149
154
  scripts/platform-smoke/platform-build-windows.ps1
150
155
  scripts/platform-smoke/pty-capture.mjs
151
156
  scripts/platform-smoke/render-ansi.mjs
152
157
  scripts/platform-smoke/scenarios.mjs
153
158
  scripts/platform-smoke/targets.mjs
159
+ scripts/platform-smoke/visual-evidence.mjs
154
160
  ```
155
161
 
156
162
  Package scripts:
157
163
 
158
164
  ```json
159
165
  {
160
- "check:platform-smoke": "node --check <platform smoke scripts> && vitest run test/smoke-tooling.test.ts",
166
+ "check:platform-smoke": "node --check platform-smoke.config.mjs && node --check <platform smoke scripts> && vitest run test/smoke-tooling.test.ts",
161
167
  "smoke:platform": "node scripts/platform-smoke.mjs",
162
168
  "smoke:platform:doctor": "node scripts/platform-smoke.mjs doctor",
163
169
  "smoke:platform:macos": "node scripts/platform-smoke.mjs run --target macos",
@@ -167,7 +173,7 @@ Package scripts:
167
173
  }
168
174
  ```
169
175
 
170
- Add `.artifacts/`, `.crabbox/`, and `.platform-smoke-runs/` to `.gitignore`.
176
+ Add `.artifacts/`, `.crabbox/`, `.debug/`, and `.platform-smoke-runs/` to `.gitignore`.
171
177
 
172
178
  ## Configuration source
173
179
 
@@ -189,18 +195,25 @@ export default {
189
195
  ],
190
196
  requiredCrabbox: {
191
197
  install: "Homebrew package or PLATFORM_SMOKE_CRABBOX override",
192
- minVersion: "0.24.0",
198
+ minVersion: "0.26.0",
193
199
  },
194
200
  ubuntuContainerImage: "cimg/node:24.16",
195
201
  nodeValidationMajor: 24,
202
+ windowsParallels: {
203
+ sourceVm: "pi-extension-windows-template",
204
+ snapshot: "crabbox-ready",
205
+ workRoot: "C:\\crabbox\\pi-cursor-sdk",
206
+ },
196
207
  };
197
208
  ```
198
209
 
199
210
  `ubuntuContainerImage` defaults the local-container Ubuntu target to an Ubuntu 24.04 Node 24 image with a current glibc baseline for native test dependencies; Crabbox still bootstraps SSH/Git/rsync/curl as needed. `nodeValidationMajor: 24` is the release-smoke validation baseline. It does not change the package engine by itself. A separate compatibility lane can test Node 22.19 later; this required gate validates Node 24 on every target.
200
211
 
212
+ `windowsParallels` records this repo's default shared Windows template contract. Environment overrides may point at a temporary candidate template during infrastructure work, but release runs should use the shared `pi-extension-windows-template` / `crabbox-ready` baseline unless this document is updated.
213
+
201
214
  ## Required local environment
202
215
 
203
- The doctor fails if any required value is missing.
216
+ The config owns reusable defaults. Environment variables are local-machine knobs and one-off overrides, not a second source of truth. The doctor fails if required auth or target readiness is missing.
204
217
 
205
218
  ```bash
206
219
  # Optional override; by default the gate uses Homebrew `crabbox` from PATH.
@@ -211,11 +224,13 @@ PLATFORM_SMOKE_MAC_USER="$USER"
211
224
  PLATFORM_SMOKE_MAC_WORK_ROOT="/Users/$USER/crabbox/pi-cursor-sdk"
212
225
  PLATFORM_SMOKE_UBUNTU_IMAGE="cimg/node:24.16"
213
226
 
214
- PLATFORM_SMOKE_WINDOWS_VM="Windows 11"
227
+ # Optional Parallels overrides; defaults come from platform-smoke.config.mjs.
228
+ PLATFORM_SMOKE_WINDOWS_VM="pi-extension-windows-template"
215
229
  PLATFORM_SMOKE_WINDOWS_SNAPSHOT="crabbox-ready"
216
230
  PLATFORM_SMOKE_WINDOWS_USER="<windows-ssh-user>"
217
231
  PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT="C:\\crabbox\\pi-cursor-sdk"
218
232
 
233
+ # Required for live suites; doctor fails before spending Cursor tokens if absent.
219
234
  CURSOR_API_KEY="..."
220
235
  ```
221
236
 
@@ -278,7 +293,13 @@ Required:
278
293
 
279
294
  ### Windows template VM
280
295
 
281
- The user's daily Windows VM is not the long-term test target. A dedicated template VM and snapshot are required.
296
+ The user's daily Windows VM is not the long-term test target. Use the shared pi-extension Parallels template unless this project documents a replacement with equal evidence:
297
+
298
+ ```text
299
+ source VM: pi-extension-windows-template
300
+ snapshot: crabbox-ready
301
+ work root: C:\\crabbox\\pi-cursor-sdk
302
+ ```
282
303
 
283
304
  Template requirements:
284
305
 
@@ -293,8 +314,9 @@ Template requirements:
293
314
  - `node-pty` self-test passes in native Windows.
294
315
  - Source VM is powered off.
295
316
  - Snapshot named `crabbox-ready` exists.
317
+ - The template contains reusable platform tools only; no repo checkout, `.pi` state, Cursor API key, browser auth, smoke artifacts, or temp files.
296
318
 
297
- Crabbox Parallels creates linked clones from the powered-off snapshot. The source template VM is never used directly for smoke runs.
319
+ Crabbox Parallels creates linked clones from the powered-off snapshot. The source template VM is never used directly for smoke runs. If a run has to install a missing global tool or browser on every Windows clone, treat that as template drift and refresh the shared template instead of making the per-run fallback normal.
298
320
 
299
321
  ### Windows native
300
322
 
@@ -313,13 +335,13 @@ tar --version
313
335
 
314
336
  Doctor checks:
315
337
 
316
- 1. Required env vars exist.
338
+ 1. Required auth is present and optional target overrides resolve against config defaults.
317
339
  2. Homebrew `crabbox` is available on PATH, or `PLATFORM_SMOKE_CRABBOX` points at an executable override.
318
340
  3. Crabbox build matches the configured baseline.
319
341
  4. Crabbox provider registry includes `local-container`, `ssh`, and `parallels`.
320
- 5. `crabbox doctor --provider local-container --json` passes.
342
+ 5. `crabbox doctor --provider local-container --target linux --json` passes.
321
343
  6. Docker runtime is active.
322
- 7. macOS SSH localhost probe passes and sees Node, npm, Git, rsync, and tar.
344
+ 7. Crabbox macOS static SSH doctor with `--doctor-probe-ssh` passes, and the localhost SSH probe sees Node, npm, Git, rsync, and tar.
323
345
  8. `prlctl` exists.
324
346
  9. Windows source VM exists.
325
347
  10. Windows source snapshot exists.
@@ -358,7 +380,7 @@ Per target, `platform-build` must:
358
380
 
359
381
  1. Record `node --version` and assert the target Node major is at least `nodeValidationMajor`.
360
382
  2. Run `npm ci` in `extensionSourceRoot`.
361
- 3. Run `npm run check:platform-smoke` on the target so smoke harness syntax and invariant tests fail before live Cursor calls, with only the target-local release-tag guard bypassed because Crabbox worktrees may not have release tags.
383
+ 3. Run `npm run check:platform-smoke` on the target so config syntax, smoke harness syntax, invalid target/suite guards, and invariant tests fail before live Cursor calls.
362
384
  4. Run `npm test` on the target with the same target-local release-tag guard bypass.
363
385
  5. Run `npm run typecheck`.
364
386
  6. Run `npm pack`.
@@ -379,7 +401,7 @@ Purpose:
379
401
  - fail before spending Cursor tokens;
380
402
  - produce the packed extension used by later suites.
381
403
 
382
- The host `smoke:platform:all` entrypoint enforces doctor first and then the release-version reuse guard before running targets, using local git tags and `package.json`. Required artifacts include `node-version.txt`, `npm-version.txt`, stdout/stderr for `npm ci`, `npm run check:platform-smoke`, `npm test`, `npm run typecheck`, `npm pack`, packed npm install, `pi install`, and `pi list`, plus `packed-tarball.txt`, `summary.json`, `artifact-manifest.json`, `assertions.json`, and `failures.md` on failed assertions.
404
+ The host `smoke:platform:all` entrypoint enforces doctor first before running targets. Required artifacts include `node-version.txt`, `npm-version.txt`, stdout/stderr for `npm ci`, `npm run check:platform-smoke`, `npm test`, `npm run typecheck`, `npm pack`, packed npm install, `pi install`, and `pi list`, plus `packed-tarball.txt`, `summary.json`, `artifact-manifest.json`, `assertions.json`, and `failures.md` on failed assertions.
383
405
 
384
406
  ### `cursor-native-visual-matrix`
385
407
 
@@ -596,7 +618,7 @@ cursor-abort-cleanup: 1
596
618
 
597
619
  Maximum per target: `3` Cursor invocations.
598
620
 
599
- Maximum full gate: `12` Cursor invocations.
621
+ Maximum full gate: `9` Cursor invocations.
600
622
 
601
623
  The merge gate is `npm run smoke:platform:all`; that script runs doctor first and then the matrix to preserve this budget. No suite adds a new Cursor invocation without updating this plan and `platform-smoke.config.mjs`.
602
624
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cursor-sdk",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "pi provider extension backed by @cursor/sdk local agents",
5
5
  "author": "Mitch Fultz (https://github.com/fitchmultz)",
6
6
  "license": "MIT",
@@ -69,7 +69,6 @@
69
69
  "docs/cursor-tool-surfaces.md",
70
70
  "docs/cursor-live-smoke-checklist.md",
71
71
  "docs/cursor-testing-lessons.md",
72
- "docs/crabbox-platform-testing-lessons.md",
73
72
  "docs/cursor-dogfood-checklist.md",
74
73
  "docs/cursor-native-tool-replay.md",
75
74
  "docs/cursor-native-tool-visual-audit.md",
@@ -98,7 +97,7 @@
98
97
  "debug:sdk-events": "node scripts/debug-sdk-events.mjs",
99
98
  "debug:provider-events": "node scripts/debug-provider-events.mjs",
100
99
  "debug:mcp-coldstart": "node scripts/probe-mcp-coldstart.mjs",
101
- "check:platform-smoke": "node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/assertions.mjs && node --check scripts/platform-smoke/artifacts.mjs && node --check scripts/platform-smoke/card-detect.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/jsonl-text.mjs && node --check scripts/platform-smoke/live-suite-runner.mjs && node --check scripts/platform-smoke/pty-capture.mjs && node --check scripts/platform-smoke/render-ansi.mjs && node --check scripts/platform-smoke/scenarios.mjs && node --check scripts/platform-smoke/targets.mjs && node --check scripts/platform-smoke/visual-evidence.mjs && vitest run test/smoke-tooling.test.ts",
100
+ "check:platform-smoke": "node --check platform-smoke.config.mjs && node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/assertions.mjs && node --check scripts/platform-smoke/artifacts.mjs && node --check scripts/platform-smoke/card-detect.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/jsonl-text.mjs && node --check scripts/platform-smoke/live-suite-runner.mjs && node --check scripts/platform-smoke/pty-capture.mjs && node --check scripts/platform-smoke/render-ansi.mjs && node --check scripts/platform-smoke/scenarios.mjs && node --check scripts/platform-smoke/targets.mjs && node --check scripts/platform-smoke/visual-evidence.mjs && vitest run test/smoke-tooling.test.ts",
102
101
  "smoke:platform": "node scripts/platform-smoke.mjs",
103
102
  "smoke:platform:doctor": "node scripts/platform-smoke.mjs doctor",
104
103
  "smoke:platform:macos": "node scripts/platform-smoke.mjs run --target macos",
@@ -14,8 +14,13 @@ export default {
14
14
  ],
15
15
  requiredCrabbox: {
16
16
  install: "Homebrew package or PLATFORM_SMOKE_CRABBOX override",
17
- minVersion: "0.24.0",
17
+ minVersion: "0.26.0",
18
18
  },
19
19
  ubuntuContainerImage: "cimg/node:24.16",
20
20
  nodeValidationMajor: 24,
21
+ windowsParallels: {
22
+ sourceVm: "pi-extension-windows-template",
23
+ snapshot: "crabbox-ready",
24
+ workRoot: "C:\\crabbox\\pi-cursor-sdk",
25
+ },
21
26
  };
@@ -98,10 +98,11 @@ export function buildTargetBaseArgs(targetName, config = {}) {
98
98
  ];
99
99
  }
100
100
  case "windows-native": {
101
- const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || "pi-extension-windows-template";
102
- const snap = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || "crabbox-ready";
103
- const user = env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
104
- const workRoot = env("PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || "C:\\crabbox\\pi-cursor-sdk";
101
+ const windows = config.windowsParallels ?? {};
102
+ const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || windows.sourceVm || "pi-extension-windows-template";
103
+ const snap = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || windows.snapshot || "crabbox-ready";
104
+ const user = env("PLATFORM_SMOKE_WINDOWS_USER") || windows.user || env("USER");
105
+ const workRoot = env("PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || windows.workRoot || "C:\\crabbox\\pi-cursor-sdk";
105
106
  return [
106
107
  "--provider", "parallels",
107
108
  "--target", "windows",
@@ -55,11 +55,22 @@ function parseLeaseId(output) {
55
55
  ?? null;
56
56
  }
57
57
 
58
- function windowsCrabboxBaseArgs() {
59
- const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || "pi-extension-windows-template";
60
- const snap = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || "crabbox-ready";
61
- const user = env("PLATFORM_SMOKE_WINDOWS_USER") || env("USER");
62
- const workRoot = env("PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || "C:\\crabbox\\pi-cursor-sdk";
58
+ function windowsParallelsDefaults(config = {}) {
59
+ const windows = config?.windowsParallels ?? {};
60
+ return {
61
+ vm: windows.sourceVm || "pi-extension-windows-template",
62
+ snapshot: windows.snapshot || "crabbox-ready",
63
+ user: windows.user || env("USER"),
64
+ workRoot: windows.workRoot || "C:\\crabbox\\pi-cursor-sdk",
65
+ };
66
+ }
67
+
68
+ function windowsCrabboxBaseArgs(config = {}) {
69
+ const defaults = windowsParallelsDefaults(config);
70
+ const vm = env("PLATFORM_SMOKE_WINDOWS_VM") || defaults.vm;
71
+ const snap = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || defaults.snapshot;
72
+ const user = env("PLATFORM_SMOKE_WINDOWS_USER") || defaults.user;
73
+ const workRoot = env("PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT") || defaults.workRoot;
63
74
  return [
64
75
  "--provider", "parallels",
65
76
  "--target", "windows",
@@ -91,9 +102,9 @@ function crabbox(cbox, args, timeout = 300_000) {
91
102
  }
92
103
  }
93
104
 
94
- function disposableWindowsSshProbe(cbox) {
105
+ function disposableWindowsSshProbe(cbox, config = {}) {
95
106
  const slug = "pi-cursor-sdk-doctor-windows";
96
- const baseArgs = windowsCrabboxBaseArgs();
107
+ const baseArgs = windowsCrabboxBaseArgs(config);
97
108
  const warm = crabbox(cbox, ["warmup", ...baseArgs, "--slug", slug, "--keep", "--reclaim"], 300_000);
98
109
  const leaseId = parseLeaseId(warm.stdout) ?? parseLeaseId(warm.stderr) ?? slug;
99
110
  try {
@@ -131,10 +142,6 @@ function runChecks(config) {
131
142
  console.log("\n── Environment variables ──");
132
143
  const requiredVars = [
133
144
  "CURSOR_API_KEY",
134
- "PLATFORM_SMOKE_WINDOWS_VM",
135
- "PLATFORM_SMOKE_WINDOWS_SNAPSHOT",
136
- "PLATFORM_SMOKE_WINDOWS_USER",
137
- "PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT",
138
145
  ];
139
146
  const optionalVars = [
140
147
  "PLATFORM_SMOKE_CRABBOX",
@@ -142,15 +149,27 @@ function runChecks(config) {
142
149
  "PLATFORM_SMOKE_MAC_USER",
143
150
  "PLATFORM_SMOKE_MAC_WORK_ROOT",
144
151
  "PLATFORM_SMOKE_UBUNTU_IMAGE",
152
+ "PLATFORM_SMOKE_WINDOWS_VM",
153
+ "PLATFORM_SMOKE_WINDOWS_SNAPSHOT",
154
+ "PLATFORM_SMOKE_WINDOWS_USER",
155
+ "PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT",
145
156
  ];
146
157
  for (const name of requiredVars) {
147
158
  const v = env(name);
148
159
  v ? ok(`${name} = ${name === "CURSOR_API_KEY" ? "(present, redacted)" : (v.length > 50 ? v.slice(0, 50) + "..." : v)}`)
149
160
  : fail(`${name} missing`);
150
161
  }
162
+ const windowsDefaults = windowsParallelsDefaults(config);
163
+ const optionalDefaults = {
164
+ PLATFORM_SMOKE_WINDOWS_VM: windowsDefaults.vm,
165
+ PLATFORM_SMOKE_WINDOWS_SNAPSHOT: windowsDefaults.snapshot,
166
+ PLATFORM_SMOKE_WINDOWS_USER: windowsDefaults.user,
167
+ PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT: windowsDefaults.workRoot,
168
+ };
151
169
  for (const name of optionalVars) {
152
170
  const v = env(name);
153
- ok(`${name} = ${v || "(default)"}`);
171
+ const fallback = optionalDefaults[name] ? `(default: ${optionalDefaults[name]})` : "(default)";
172
+ ok(`${name} = ${v || fallback}`);
154
173
  }
155
174
 
156
175
  // ── Phase 2: Crabbox binary ──
@@ -205,7 +224,7 @@ function runChecks(config) {
205
224
  fail("crabbox providers failed");
206
225
  }
207
226
  const ubuntuImage = env("PLATFORM_SMOKE_UBUNTU_IMAGE") || config?.ubuntuContainerImage || "cimg/node:24.16";
208
- const lcDoc = silent(cbox, ["doctor", "--provider", "local-container", "--local-container-image", ubuntuImage, "--json"]);
227
+ const lcDoc = silent(cbox, ["doctor", "--provider", "local-container", "--target", "linux", "--local-container-image", ubuntuImage, "--json"]);
209
228
  if (lcDoc) {
210
229
  try {
211
230
  const d = JSON.parse(lcDoc);
@@ -223,7 +242,7 @@ function runChecks(config) {
223
242
  "doctor", "--provider", "ssh", "--target", "macos",
224
243
  "--static-host", sshHost, "--static-user", sshUser,
225
244
  "--static-port", "22", "--static-work-root", sshRoot,
226
- "--json",
245
+ "--doctor-probe-ssh", "--json",
227
246
  ]);
228
247
  if (sshDoc) {
229
248
  try {
@@ -265,7 +284,7 @@ function runChecks(config) {
265
284
  fail("prlctl not found");
266
285
  } else {
267
286
  ok("prlctl found");
268
- const vmName = env("PLATFORM_SMOKE_WINDOWS_VM") || "pi-extension-windows-template";
287
+ const vmName = env("PLATFORM_SMOKE_WINDOWS_VM") || windowsParallelsDefaults(config).vm;
269
288
  const list = shell("prlctl list -a --no-header 2>/dev/null");
270
289
  if (list) {
271
290
  const vms = list.split("\n").filter(Boolean);
@@ -279,7 +298,7 @@ function runChecks(config) {
279
298
  fail(`VM "${vmName}" state: ${status} — source VM must be stopped for linked clones`);
280
299
  }
281
300
 
282
- const snapName = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || "crabbox-ready";
301
+ const snapName = env("PLATFORM_SMOKE_WINDOWS_SNAPSHOT") || windowsParallelsDefaults(config).snapshot;
283
302
  const snapsJson = shell(`prlctl snapshot-list "${vmName}" -j 2>/dev/null`);
284
303
  let snapshotFound = false;
285
304
  let snapshotPowerOff = false;
@@ -323,7 +342,7 @@ function runChecks(config) {
323
342
  } else {
324
343
  ok(`template "${vmName}" has no IP; verifying Windows SSH/tools through a disposable Crabbox clone`);
325
344
  if (cbox && snapshotFound && snapshotPowerOff) {
326
- const probe = disposableWindowsSshProbe(cbox);
345
+ const probe = disposableWindowsSshProbe(cbox, config);
327
346
  probe.ok ? ok(`disposable Windows clone SSH/tool probe OK: ${probe.message}`) : fail(probe.message);
328
347
  } else {
329
348
  fail(`Windows SSH probe could not run because "${vmName}" has no IP and no verified snapshot was available`);
@@ -62,19 +62,15 @@ Write-SectionFile "NPM_CI_STDOUT" $NpmCiOut
62
62
  Write-SectionFile "NPM_CI_STDERR" $NpmCiErr
63
63
 
64
64
  Write-Output "=== check:platform-smoke ==="
65
- $env:PI_CURSOR_SKIP_RELEASE_VERSION_GUARD = "1"
66
65
  & npm.cmd run check:platform-smoke 1> $CheckPlatformSmokeOut 2> $CheckPlatformSmokeErr
67
66
  $CHECK_PLATFORM_SMOKE_EXIT = Exit-CodeFromLastCommand
68
- Remove-Item Env:\PI_CURSOR_SKIP_RELEASE_VERSION_GUARD -ErrorAction SilentlyContinue
69
67
  Write-Output "PLATFORM_CHECK_PLATFORM_SMOKE_EXIT=$CHECK_PLATFORM_SMOKE_EXIT"
70
68
  Write-SectionFile "CHECK_PLATFORM_SMOKE_STDOUT" $CheckPlatformSmokeOut
71
69
  Write-SectionFile "CHECK_PLATFORM_SMOKE_STDERR" $CheckPlatformSmokeErr
72
70
 
73
71
  Write-Output "=== npm test ==="
74
- $env:PI_CURSOR_SKIP_RELEASE_VERSION_GUARD = "1"
75
72
  & npm.cmd test 1> $NpmTestOut 2> $NpmTestErr
76
73
  $TEST_EXIT = Exit-CodeFromLastCommand
77
- Remove-Item Env:\PI_CURSOR_SKIP_RELEASE_VERSION_GUARD -ErrorAction SilentlyContinue
78
74
  Write-Output "PLATFORM_NPM_TEST_EXIT=$TEST_EXIT"
79
75
  Write-SectionFile "NPM_TEST_STDOUT" $NpmTestOut
80
76
  Write-SectionFile "NPM_TEST_STDERR" $NpmTestErr
@@ -422,14 +422,14 @@ export function buildPlatformBuildCommand(targetName, packageName = "pi-cursor-s
422
422
  lines.push(...posixSection("NPM_CI_STDERR", 'cat "$PACK_DIR/npm-ci.stderr.txt" 2>/dev/null || true'));
423
423
  lines.push("");
424
424
  lines.push('echo "=== check:platform-smoke ==="');
425
- lines.push('PI_CURSOR_SKIP_RELEASE_VERSION_GUARD=1 npm run check:platform-smoke >"$PACK_DIR/check-platform-smoke.stdout.txt" 2>"$PACK_DIR/check-platform-smoke.stderr.txt"');
425
+ lines.push('npm run check:platform-smoke >"$PACK_DIR/check-platform-smoke.stdout.txt" 2>"$PACK_DIR/check-platform-smoke.stderr.txt"');
426
426
  lines.push("CHECK_PLATFORM_SMOKE_EXIT=$?");
427
427
  lines.push('echo "PLATFORM_CHECK_PLATFORM_SMOKE_EXIT=$CHECK_PLATFORM_SMOKE_EXIT"');
428
428
  lines.push(...posixSection("CHECK_PLATFORM_SMOKE_STDOUT", 'cat "$PACK_DIR/check-platform-smoke.stdout.txt" 2>/dev/null || true'));
429
429
  lines.push(...posixSection("CHECK_PLATFORM_SMOKE_STDERR", 'cat "$PACK_DIR/check-platform-smoke.stderr.txt" 2>/dev/null || true'));
430
430
  lines.push("");
431
431
  lines.push('echo "=== npm test ==="');
432
- lines.push('PI_CURSOR_SKIP_RELEASE_VERSION_GUARD=1 npm test >"$PACK_DIR/npm-test.stdout.txt" 2>"$PACK_DIR/npm-test.stderr.txt"');
432
+ lines.push('npm test >"$PACK_DIR/npm-test.stdout.txt" 2>"$PACK_DIR/npm-test.stderr.txt"');
433
433
  lines.push("TEST_EXIT=$?");
434
434
  lines.push('echo "PLATFORM_NPM_TEST_EXIT=$TEST_EXIT"');
435
435
  lines.push(...posixSection("NPM_TEST_STDOUT", 'cat "$PACK_DIR/npm-test.stdout.txt" 2>/dev/null || true'));
@@ -3,8 +3,7 @@
3
3
  import { createRequire } from "node:module";
4
4
  import { resolve, dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { accessSync, constants, readFileSync } from "node:fs";
7
- import { spawnSync } from "node:child_process";
6
+ import { accessSync, constants } from "node:fs";
8
7
 
9
8
  // ── helpers ────────────────────────────────────────────────────────────────
10
9
  const __filename = fileURLToPath(import.meta.url);
@@ -47,11 +46,11 @@ Environment:
47
46
  PLATFORM_SMOKE_MAC_HOST macOS SSH host (default: localhost)
48
47
  PLATFORM_SMOKE_MAC_USER macOS SSH user (default: \$USER)
49
48
  PLATFORM_SMOKE_MAC_WORK_ROOT macOS work root
50
- PLATFORM_SMOKE_WINDOWS_VM Parallels source VM name
51
- PLATFORM_SMOKE_WINDOWS_SNAPSHOT Snapshot name
52
- PLATFORM_SMOKE_WINDOWS_USER Windows SSH user
53
- PLATFORM_SMOKE_UBUNTU_IMAGE Ubuntu container image
54
- PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT Windows native work root
49
+ PLATFORM_SMOKE_UBUNTU_IMAGE Ubuntu container image
50
+ PLATFORM_SMOKE_WINDOWS_VM Parallels source VM override (default from config)
51
+ PLATFORM_SMOKE_WINDOWS_SNAPSHOT Snapshot override (default from config)
52
+ PLATFORM_SMOKE_WINDOWS_USER Windows SSH user override (default: \$USER)
53
+ PLATFORM_SMOKE_WINDOWS_NATIVE_WORK_ROOT Windows native work root override (default from config)
55
54
  `);
56
55
  }
57
56
 
@@ -90,17 +89,17 @@ function parseArgs(argv) {
90
89
  return args;
91
90
  }
92
91
 
93
- function assertHostReleaseVersionGuard() {
94
- const packageJson = JSON.parse(readFileSync(resolve(repoRoot, "package.json"), "utf8"));
95
- const result = spawnSync("git", ["tag", "--list", "v[0-9]*.[0-9]*.[0-9]*", "--sort=-v:refname"], {
96
- cwd: repoRoot,
97
- encoding: "utf8",
98
- });
99
- if (result.status !== 0) throw new Error(`failed to inspect release tags: ${result.stderr || result.error?.message || "unknown git error"}`);
100
- const latestTag = result.stdout.split(/\r?\n/).find((tag) => tag.length > 0);
101
- if (!latestTag) throw new Error("no local release tags found; cannot enforce package version reuse guard");
102
- const latestVersion = latestTag.replace(/^v/, "");
103
- if (packageJson.version === latestVersion) throw new Error(`package version ${packageJson.version} reuses latest release tag ${latestTag}`);
92
+ function validateSelections(targets, suites) {
93
+ const allowedTargets = new Set(config.requiredTargets ?? []);
94
+ const allowedSuites = new Set(config.requiredSuites ?? []);
95
+ const badTargets = targets.filter((target) => !allowedTargets.has(target));
96
+ const badSuites = suites.filter((suite) => !allowedSuites.has(suite));
97
+ if (badTargets.length > 0) {
98
+ throw new Error(`unknown target(s): ${badTargets.join(", ")}; allowed: ${[...allowedTargets].join(", ")}`);
99
+ }
100
+ if (badSuites.length > 0) {
101
+ throw new Error(`unknown suite(s): ${badSuites.join(", ")}; allowed: ${[...allowedSuites].join(", ")}`);
102
+ }
104
103
  }
105
104
 
106
105
  // ── commands ───────────────────────────────────────────────────────────────
@@ -158,7 +157,6 @@ async function main() {
158
157
  }
159
158
 
160
159
  if (args.command === "run") {
161
- assertHostReleaseVersionGuard();
162
160
  const targets = args.target
163
161
  ? args.target.split(",").map((s) => s.trim()).filter(Boolean)
164
162
  : config.requiredTargets;
@@ -167,6 +165,13 @@ async function main() {
167
165
  ? [args.suite]
168
166
  : config.requiredSuites;
169
167
 
168
+ try {
169
+ validateSelections(targets, suites);
170
+ } catch (err) {
171
+ console.error(err.message);
172
+ process.exit(2);
173
+ }
174
+
170
175
  const targetRuns = targets.map(async (targetName) => {
171
176
  console.log(`\n=== Target: ${targetName} ===`);
172
177
  const result = args.suite
@@ -11,6 +11,7 @@ import type { CursorNativeToolDisplayItem } from "./cursor-native-tool-display.j
11
11
  import type { CursorPiBridgeToolRequest, CursorPiToolBridgeRun } from "./cursor-pi-tool-bridge.js";
12
12
  import { getCursorSessionScopeKey } from "./cursor-session-scope.js";
13
13
  import type { CursorSdkEventDebugRecorder } from "./cursor-sdk-event-debug.js";
14
+ import { installCursorSdkProcessErrorGuard } from "./cursor-sdk-process-error-guard.js";
14
15
 
15
16
  export class CursorLiveRunAbortError extends Error {
16
17
  constructor() {
@@ -118,6 +119,17 @@ interface LeaseWaiter {
118
119
  onAbort?: () => void;
119
120
  }
120
121
 
122
+ async function cancelCursorLiveSdkRun(run: CursorLiveRun): Promise<void> {
123
+ if (!run.sdkRun) return;
124
+ const guard = installCursorSdkProcessErrorGuard();
125
+ guard.suppressAbortErrors();
126
+ try {
127
+ await run.sdkRun.cancel();
128
+ } finally {
129
+ guard.dispose();
130
+ }
131
+ }
132
+
121
133
  interface CursorLiveRunPrivateState {
122
134
  waiters: Set<ProgressWaiter>;
123
135
  idleDisposeTimer?: ReturnType<typeof setTimeout>;
@@ -474,7 +486,7 @@ export function createCursorLiveRunCoordinator(deps: CursorLiveRunCoordinatorDep
474
486
  if (abandoned) {
475
487
  if (!run.done) {
476
488
  try {
477
- await run.sdkRun?.cancel();
489
+ await cancelCursorLiveSdkRun(run);
478
490
  } catch {
479
491
  // cancellation failure should not block session-agent abandonment
480
492
  }
@@ -60,13 +60,15 @@ function getCursorConnectSource(error: unknown, record: Record<string, unknown>
60
60
  const type = getErrorStringField(asRecord(detail), "type");
61
61
  return typeof type === "string" && type.startsWith("aiserver.");
62
62
  });
63
- return hasCursorBackendDetails ? "cursor-backend-details" : "generic-connect";
63
+ if (hasCursorBackendDetails) return "cursor-backend-details";
64
+ return stack.includes("@connectrpc/connect-node") ? "connect-node-stack" : "generic-connect";
64
65
  }
65
66
 
66
67
  export type CursorConnectErrorSource =
67
68
  | "cursor-sdk-stack"
68
69
  | "cursor-extension-connect-stack"
69
70
  | "cursor-backend-details"
71
+ | "connect-node-stack"
70
72
  | "generic-connect";
71
73
 
72
74
  export type CursorConnectErrorClassification =
@@ -13,7 +13,7 @@ type GenericProcessEmit = (event: string | symbol, ...args: unknown[]) => boolea
13
13
 
14
14
  // The local Cursor SDK can surface some ConnectRPC failures as process-level
15
15
  // uncaught exceptions/unhandled rejections even when run.wait()/run.cancel() is awaited.
16
- // Keep suppression scoped to active Cursor provider turns and tightly matched SDK shapes.
16
+ // Keep suppression scoped to active Cursor provider turns and tightly matched ConnectRPC shapes.
17
17
  const activeProviderTurns = new Set<CursorSdkProcessErrorGuardToken>();
18
18
  let originalProcessEmit: GenericProcessEmit | undefined;
19
19
  let captureCallbackInstalled = false;
@@ -35,7 +35,9 @@ function shouldSuppressProcessError(event: string | symbol, args: readonly unkno
35
35
  const classification = classifyCursorConnectError(error);
36
36
  if (!classification) return false;
37
37
  if (classification.kind === "abort") return hasActiveAbortSuppression();
38
- return activeProviderTurns.size > 0 && isCursorProvenance(classification.source);
38
+ if (activeProviderTurns.size === 0) return false;
39
+ if (classification.kind === "network") return isCursorProvenance(classification.source) || classification.source === "connect-node-stack";
40
+ return isCursorProvenance(classification.source);
39
41
  }
40
42
 
41
43
  function installProcessEmitPatch(): void {
@@ -1,508 +0,0 @@
1
- # Crabbox Local Platform Testing Guide for pi Extensions
2
-
3
- ## Purpose
4
-
5
- This is the reusable field guide for adding **local Crabbox platform testing** to pi extension repositories.
6
-
7
- Current scope:
8
-
9
- - pi extension packages only;
10
- - local maintainer machine is macOS;
11
- - required target matrix is macOS, Ubuntu Linux, and native Windows;
12
- - Windows runs through the local Parallels template `pi-extension-windows-template` and snapshot `crabbox-ready` unless a project documents a different source of truth.
13
-
14
- This guide is generic on purpose. Do not copy another project's model IDs, package names, API keys, VM clones, artifact folders, prompts, or release decisions. Copy the architecture and conventions, then make the target project own its config, docs, assertions, and release gate.
15
-
16
- Official references:
17
-
18
- - [Crabbox docs](https://crabbox.sh/)
19
- - [openclaw/crabbox](https://github.com/openclaw/crabbox)
20
- - Crabbox source docs worth reading before changing a harness: `docs/commands/run.md`, `docs/commands/warmup.md`, `docs/commands/stop.md`, `docs/features/doctor.md`, `docs/features/sync.md`, `docs/features/env-forwarding.md`, `docs/providers/ssh.md`, `docs/providers/local-container.md`, and `docs/providers/parallels.md`.
21
-
22
- Local reference implementations reviewed for this guide:
23
-
24
- - `~/Projects/AI/pi-cursor-sdk` — full provider/runtime gate with live model, visual TUI, JSONL, bridge, usage/cache, and abort-cleanup assertions.
25
- - `~/Projects/AI/pi-oracle` — package/build platform gate plus project-specific real smoke, with a project-specific env prefix.
26
- - `~/Projects/AI/pi-codex-goal` — compact reusable harness with platform build plus real pi runtime smoke on all targets.
27
-
28
- ## Standard local matrix
29
-
30
- | Harness target | Crabbox provider | Local purpose | Shell contract | Default work root |
31
- | --- | --- | --- | --- | --- |
32
- | `macos` | `ssh` static localhost | Current host macOS | POSIX shell over SSH | `/Users/$USER/crabbox/<project>` |
33
- | `ubuntu` | `local-container` | Linux smoke without cloud | POSIX shell in a Docker-compatible container | provider default `/work/crabbox` |
34
- | `windows-native` | `parallels` | Real native Windows behavior | PowerShell/OpenSSH, `--windows-mode normal` | `C:\crabbox\<project>` |
35
-
36
- Use this matrix by default for pi extensions. If a project does not need all three targets, that project's docs must say which target is non-required and why. A missing required target is a blocked local setup, not a skipped pass.
37
-
38
- ## What Crabbox owns vs. what the project owns
39
-
40
- Crabbox owns the lease/sync/run loop:
41
-
42
- 1. lease or claim a target;
43
- 2. sync tracked plus nonignored local files;
44
- 3. run a command remotely;
45
- 4. stream output;
46
- 5. expose timing, logs, failure bundles, and cleanup commands;
47
- 6. stop or expire the lease.
48
-
49
- The project owns the test contract:
50
-
51
- - which targets and suites are required;
52
- - target setup and runtime versions;
53
- - package install semantics;
54
- - pi commands and prompts;
55
- - assertions over stdout, JSONL, visual evidence, artifacts, cleanup, and redaction;
56
- - docs and release criteria.
57
-
58
- Do not treat Crabbox as a runtime installer. If a target needs Node, npm, Git, `tar`, `rsync`, `zstd`, `ffmpeg`, a browser renderer, or another reusable tool, put that setup in the target image/template or in a documented project setup step.
59
-
60
- ## Recommended repository shape
61
-
62
- Use names that fit the project, but keep this shape unless the project already has a better source of truth.
63
-
64
- ```text
65
- platform-smoke.config.mjs
66
- scripts/platform-smoke.mjs
67
- scripts/platform-smoke/doctor.mjs
68
- scripts/platform-smoke/crabbox-runner.mjs
69
- scripts/platform-smoke/targets.mjs
70
- scripts/platform-smoke/artifacts.mjs
71
- scripts/platform-smoke/platform-build-windows.ps1
72
- scripts/platform-smoke/<runtime-suite>.mjs # optional real pi/model smoke
73
- scripts/platform-smoke/pty-capture.mjs # optional TUI/PTY suites
74
- scripts/platform-smoke/render-ansi.mjs # optional host-side visual renderer
75
- docs/platform-smoke.md # project source of truth
76
- ```
77
-
78
- Gitignored local state:
79
-
80
- ```text
81
- .artifacts/
82
- .crabbox/
83
- .debug/
84
- .platform-smoke-runs/
85
- ```
86
-
87
- Package scripts:
88
-
89
- ```json
90
- {
91
- "scripts": {
92
- "check:platform-smoke": "node --check scripts/platform-smoke.mjs && node --check scripts/platform-smoke/doctor.mjs && node --check scripts/platform-smoke/crabbox-runner.mjs && node --check scripts/platform-smoke/targets.mjs",
93
- "smoke:platform": "node scripts/platform-smoke.mjs",
94
- "smoke:platform:doctor": "node scripts/platform-smoke.mjs doctor",
95
- "smoke:platform:macos": "node scripts/platform-smoke.mjs run --target macos",
96
- "smoke:platform:ubuntu": "node scripts/platform-smoke.mjs run --target ubuntu",
97
- "smoke:platform:windows-native": "node scripts/platform-smoke.mjs run --target windows-native",
98
- "smoke:platform:all": "node scripts/platform-smoke.mjs run --target macos,ubuntu,windows-native"
99
- }
100
- }
101
- ```
102
-
103
- Add tests for cheap harness invariants: syntax, help text, target/suite validation, package-file inclusion or exclusion, packed-install command rendering, artifact-manifest failure behavior, cleanup-failure behavior, path traversal rejection, and secret redaction.
104
-
105
- ## Host Crabbox install
106
-
107
- Install the Crabbox CLI on the macOS host and keep the harness explicit about the binary it uses:
108
-
109
- ```sh
110
- brew install openclaw/tap/crabbox
111
- crabbox --version
112
- crabbox providers
113
- ```
114
-
115
- If Homebrew already has the OpenClaw tap configured, `brew install crabbox` may resolve to the same formula. Use `PLATFORM_SMOKE_CRABBOX=/path/to/crabbox` only when testing a non-default binary or a locally built Crabbox.
116
-
117
- ## Configuration conventions
118
-
119
- Keep project-specific defaults in `platform-smoke.config.mjs`:
120
-
121
- ```js
122
- export default {
123
- packageName: "pi-example-extension",
124
- artifactRoot: ".artifacts/platform-smoke",
125
- requiredTargets: ["macos", "ubuntu", "windows-native"],
126
- requiredSuites: ["platform-build"],
127
- requiredCrabbox: { minVersion: "0.24.0" },
128
- ubuntuContainerImage: "cimg/node:24.16",
129
- nodeValidationMajor: 24,
130
- };
131
- ```
132
-
133
- Use the config as the harness source of truth. Crabbox itself resolves config as `flags > env > repo .crabbox.yaml/crabbox.yaml > user config > defaults`; local smoke harnesses should still pass critical provider/work-root flags explicitly so the gate does not depend on hidden user config.
134
-
135
- Environment conventions:
136
-
137
- - Prefer defaults derived from `config.packageName` for project-specific work roots and slugs.
138
- - Do not export project-specific work-root overrides globally.
139
- - Use `PLATFORM_SMOKE_*` for reusable harness knobs when scripts are shared across projects.
140
- - Use a project-specific prefix such as `PI_ORACLE_SMOKE_*` only when a repo needs to coexist with another harness or already has established project-specific environment names.
141
- - Keep auth variable names in config, not auth values.
142
-
143
- Useful standard variables:
144
-
145
- ```text
146
- PLATFORM_SMOKE_CRABBOX=/opt/homebrew/bin/crabbox
147
- PLATFORM_SMOKE_MAC_HOST=localhost
148
- PLATFORM_SMOKE_MAC_USER=$USER
149
- PLATFORM_SMOKE_MAC_WORK_ROOT=/Users/$USER/crabbox/<project>
150
- PLATFORM_SMOKE_UBUNTU_IMAGE=cimg/node:24.16
151
- PLATFORM_SMOKE_WINDOWS_VM=pi-extension-windows-template
152
- PLATFORM_SMOKE_WINDOWS_SNAPSHOT=crabbox-ready
153
- PLATFORM_SMOKE_WINDOWS_USER=<windows-ssh-user>
154
- PLATFORM_SMOKE_WINDOWS_WORK_ROOT=C:\crabbox\<project>
155
- ```
156
-
157
- Pin Crabbox deliberately. Exact pins are best for release-critical harnesses whose parsing depends on CLI output. Minimum version checks are fine for simpler gates. Current local baseline is Crabbox `0.24.0` or newer from the Homebrew package.
158
-
159
- ## Target setup best practices
160
-
161
- ### macOS: static SSH to the current host
162
-
163
- Use the `ssh` provider for current macOS. It is a static provider: Crabbox does not create or clean up the host.
164
-
165
- Required setup:
166
-
167
- - macOS Remote Login enabled.
168
- - Noninteractive SSH works: `ssh -o BatchMode=yes $USER@localhost 'whoami'`.
169
- - Target user has a writable work root such as `/Users/$USER/crabbox/<project>`.
170
- - `node`, `npm`, `git`, `rsync`, `tar`, and project-specific native tools are on the remote SSH path.
171
-
172
- Base args:
173
-
174
- ```text
175
- --provider ssh
176
- --target macos
177
- --static-host localhost
178
- --static-user $USER
179
- --static-port 22
180
- --static-work-root /Users/$USER/crabbox/<project>
181
- ```
182
-
183
- Notes:
184
-
185
- - Static `stop` removes Crabbox's local claim only; it does not clean the Mac.
186
- - Use `--reclaim` intentionally when multiple repos reuse localhost and a previous claim blocks the run.
187
- - Because this is the real host, avoid tests that mutate global user state unless the project explicitly owns cleanup.
188
-
189
- ### Ubuntu: local-container provider
190
-
191
- Use `local-container` for the Linux target. It runs through Docker Desktop, OrbStack, Colima, or another Docker-compatible runtime on the local machine. There is no broker or cloud dependency.
192
-
193
- Required setup:
194
-
195
- - `docker info` passes.
196
- - The chosen image supports the project's Node/npm baseline or can bootstrap quickly.
197
- - Default image for current pi extension smokes: `cimg/node:24.16`.
198
-
199
- Base args:
200
-
201
- ```text
202
- --provider local-container
203
- --target linux
204
- --local-container-image cimg/node:24.16
205
- ```
206
-
207
- Notes:
208
-
209
- - Use a prebuilt image when first-start package bootstrapping becomes a bottleneck.
210
- - Do not mount the host Docker socket unless the suite actually needs nested Docker; that grants the container access to the host daemon.
211
- - Treat container cache volumes as local mutable state. Name them per project and clean obsolete keys manually.
212
-
213
- ### Windows: Parallels native Windows template
214
-
215
- Use the `parallels` provider for native Windows. The default reusable template is:
216
-
217
- ```text
218
- source VM: pi-extension-windows-template
219
- snapshot: crabbox-ready
220
- mode: windows normal
221
- ```
222
-
223
- Base args:
224
-
225
- ```text
226
- --provider parallels
227
- --target windows
228
- --windows-mode normal
229
- --parallels-source pi-extension-windows-template
230
- --parallels-source-snapshot crabbox-ready
231
- --parallels-user <windows-ssh-user>
232
- --parallels-work-root C:\crabbox\<project>
233
- ```
234
-
235
- Template requirements:
236
-
237
- - Parallels Tools installed.
238
- - A stable SSH user.
239
- - OpenSSH Server enabled and reachable on port `22`.
240
- - PowerShell available.
241
- - Git for Windows installed.
242
- - `tar` available for archive sync.
243
- - Node/npm at the project validation baseline.
244
- - Writable `C:\crabbox` work root.
245
- - The source VM is not used as a normal work machine.
246
- - `crabbox-ready` is a known-good power-off snapshot. Linked clones depend on that snapshot, so do not delete or replace it casually.
247
-
248
- PowerShell rules:
249
-
250
- - Use a checked-in `.ps1` script for long Windows suites.
251
- - Run with `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\scripts\platform-smoke\platform-build-windows.ps1`.
252
- - Avoid one giant quoted `--shell` string for Windows unless it is a small probe.
253
- - If installing Git or tools changes PATH, restart `sshd` or validate in a fresh SSH session.
254
-
255
- ### Windows template image policy
256
-
257
- Agents should reuse `pi-extension-windows-template` instead of creating one-off Windows VMs for each project.
258
-
259
- Add a tool to the template when all are true:
260
-
261
- - more than one pi extension is likely to need it;
262
- - installing it every run is slow, flaky, or network-dependent;
263
- - it is safe to have globally on Windows test machines;
264
- - it has no project secrets, local user auth, or repo-specific config.
265
-
266
- Keep project-specific tools in repo scripts when they are truly one-off.
267
-
268
- Template update runbook:
269
-
270
- 1. Prefer updating `pi-extension-windows-template` over adding per-project/per-run installers when a tool is reusable across pi extensions.
271
- 2. Boot the source VM, not a Crabbox clone.
272
- 3. Install or update the globally useful tool.
273
- 4. Verify from a fresh SSH session: `node --version`, `npm --version`, `git --version`, `tar --version`, and the new tool's `--version` or equivalent.
274
- 5. Remove caches, downloads, auth files, local checkouts, `.pi` state, `.artifacts`, `.debug`, and secrets.
275
- 6. Shut down the VM cleanly.
276
- 7. Create a new known-good power-off snapshot. Prefer a dated snapshot for trial adoption; promote it to `crabbox-ready` only after at least one project passes the Windows smoke.
277
- 8. Update project docs/config if the snapshot name changes.
278
- 9. Stop or clean stale clones after the template update so future runs do not reuse pre-update state.
279
-
280
- Never bake API keys, browser sessions, user project checkouts, generated artifacts, or repo-specific `.env` files into the template.
281
-
282
- ## Doctor is mandatory
283
-
284
- `npm run smoke:platform:doctor` should fail before any expensive, token-spending, or long-running suite starts. The release entrypoint should enforce this, either by making `smoke:platform:all` run doctor first or by making the canonical release command run `smoke:platform:doctor && smoke:platform:all`.
285
-
286
- Doctor should check:
287
-
288
- - Crabbox binary path and version/minimum version.
289
- - `crabbox providers` includes `ssh`, `local-container`, and `parallels`.
290
- - `crabbox doctor --provider local-container --json` passes for the configured image.
291
- - `crabbox doctor --provider ssh --target macos --json` passes or reports a concrete host setup failure.
292
- - Docker is running for Ubuntu.
293
- - macOS SSH probe reaches the host and sees Node/npm/Git.
294
- - `prlctl` exists.
295
- - The Windows source VM and snapshot exist.
296
- - The Windows snapshot is forkable/power-off; if the template has no live IP because it is stopped, a disposable Crabbox clone probe is acceptable.
297
- - Host `node`, `npm`, `git`, `tar`, and any host-side renderer tools exist.
298
- - Required auth variables for live suites are present, reported as redacted presence only.
299
- - Artifact root is writable.
300
- - Repo status is visible.
301
- - Forbidden files such as `.env`, `.env.*`, local package tarballs, `.artifacts`, `.crabbox`, and `.debug` are not in the package or source archive.
302
-
303
- Do not downgrade a missing required target to a warning. A release gate with missing Windows, Docker, SSH, auth, or Crabbox setup is blocked.
304
-
305
- ## Lease and run strategy
306
-
307
- Use target sessions, not one fresh lease per suite.
308
-
309
- Recommended shape:
310
-
311
- ```text
312
- for each target in parallel:
313
- warmup once with slug <project>-<target>
314
- run suites serially on that lease
315
- stop lease in finally
316
- ```
317
-
318
- Targets can run concurrently when the host can handle Docker, localhost SSH, and Parallels together. Suites should stay serial within a target unless the project has proven its ports, sessions, workspaces, and artifacts are isolated.
319
-
320
- Use stable slugs:
321
-
322
- ```text
323
- <project>-macos
324
- <project>-ubuntu
325
- <project>-windows-native
326
- ```
327
-
328
- Sync rules:
329
-
330
- - Start with `crabbox sync-plan` when first onboarding a repo or when sync is unexpectedly large.
331
- - Use `.gitignore`, `.crabboxignore`, or Crabbox `sync.exclude` for generated state.
332
- - Use `--fresh-sync` when a target workspace may be stale or a previous suite mutated the checkout.
333
- - Use `--no-sync` only after a deliberate shared prep step on the same lease.
334
- - If a private/local repo cannot use remote Git seeding reliably, set `CRABBOX_SYNC_GIT_SEED=false` in the harness and document why.
335
- - Do not use `--force-sync-large` unless the large transfer is understood and intentional.
336
-
337
- Always record:
338
-
339
- ```text
340
- crabbox.stdout.txt
341
- crabbox.stderr.txt
342
- crabbox.timing.json
343
- crabbox.stop.stdout.txt
344
- crabbox.stop.stderr.txt
345
- crabbox.stop.exit-code.txt
346
- ```
347
-
348
- A `stop` failure is a test result. Preserve the original suite result and add a failing `lease-cleanup` result or mark the owning suite failed.
349
-
350
- ## pi extension release contract
351
-
352
- For pi extensions, the baseline `platform-build` suite should prove package installation, not only source-tree execution.
353
-
354
- On every required target:
355
-
356
- 1. Check Node major version against `nodeValidationMajor`.
357
- 2. Run `npm ci`.
358
- 3. Run the repo's local verification command, usually `npm run verify` or the repo-specific equivalent.
359
- 4. Run `npm pack`.
360
- 5. Create a fresh target-local pi project/workspace.
361
- 6. Run `npm install --no-save <packed tarball>`.
362
- 7. Run `pi install -l ./node_modules/<package>`.
363
- 8. Run `pi list`.
364
- 9. Assert the installed package came from the packed install path.
365
- 10. Assert the release proof did not use `pi -e .` or `pi --extension .`.
366
-
367
- `pi -e .` is inner-loop debug only. It is not release proof because it bypasses package contents, `files`, install layout, and publish-time mistakes.
368
-
369
- Add a real pi runtime suite when the extension's user contract depends on runtime behavior that unit tests cannot prove. Keep it deterministic:
370
-
371
- - install the packed package into a clean project;
372
- - use a fixed model/provider unless the project config overrides it;
373
- - forward only named auth env vars;
374
- - write session JSONL and target-local result files;
375
- - assert final assistant text, tool calls/results, extension state, and persisted files structurally, not by broad substring scans.
376
-
377
- Add visual/TUI suites only when the extension has user-facing terminal UI. The portable visual contract is:
378
-
379
- ```text
380
- target captures PTY/ConPTY ANSI
381
- host renders ANSI through one xterm/Playwright renderer
382
- host writes HTML + PNG evidence
383
- assert rendered output, not prompt text
384
- ```
385
-
386
- Do not make tmux the cross-platform visual source of truth when native Windows is required. Use PTY on POSIX targets and ConPTY on Windows.
387
-
388
- ## Artifact contract
389
-
390
- Every suite should write a self-contained directory:
391
-
392
- ```text
393
- .artifacts/platform-smoke/<run-id>/<target>/<suite>/
394
- summary.json
395
- artifact-manifest.json
396
- target.json
397
- suite.json
398
- command.txt
399
- exit-code.txt
400
- crabbox.stdout.txt
401
- crabbox.stderr.txt
402
- crabbox.timing.json
403
- assertions.json
404
- failures.md # only when assertions fail
405
- ```
406
-
407
- Add suite-specific evidence, for example:
408
-
409
- ```text
410
- node-version.txt
411
- npm-ci.stdout.txt
412
- npm-ci.stderr.txt
413
- npm-test.stdout.txt
414
- npm-test.stderr.txt
415
- packed-tarball.txt
416
- packed-node-install.stdout.txt
417
- packed-node-install.stderr.txt
418
- pi-install.stdout.txt
419
- pi-install.stderr.txt
420
- pi-list.stdout.txt
421
- pi-list.stderr.txt
422
- session.jsonl
423
- terminal.ansi
424
- terminal.html
425
- terminal.png
426
- redaction-scan.json
427
- ```
428
-
429
- Pass/fail invariant:
430
-
431
- ```text
432
- summary.ok === assertions.ok
433
- artifact-manifest.missing.length === 0 for any passing suite
434
- missing required artifact => assertion failure + summary.ok=false
435
- ```
436
-
437
- Do not rely on Crabbox `--artifact-glob` for this matrix. Crabbox's SSH artifact collector is useful on Linux, but native Windows and macOS targets reject that collector. A portable harness should write host-side artifact files from captured stdout/stderr, explicit target output markers, session paths, or a safe target-produced bundle whose paths are validated before unpacking.
438
-
439
- ## Secrets and environment forwarding
440
-
441
- Crabbox intentionally does not forward your whole environment to the remote target. By default it forwards only narrow built-ins such as `CI` and `NODE_OPTIONS`. Live pi suites must opt in to exactly the variables they need.
442
-
443
- Local forwarding of secrets is acceptable for these maintainer-owned smoke gates when the suite needs real provider/model auth. The hard line is persistence and sharing: secrets must never be committed, baked into templates, written to artifacts, printed in docs/PRs, or posted in chat.
444
-
445
- Best practices:
446
-
447
- - Keep auth values out of docs, configs, shell commands, artifact names, and template images.
448
- - Store auth variable names in config, e.g. `defaultAuthEnv: ["ZAI_API_KEY"]`.
449
- - Forward only named auth variables with `--allow-env NAME` or `--env-from-profile <file> --allow-env NAME`.
450
- - Do not pass API keys as command-line arguments.
451
- - Preserve normal local process environment such as `PATH`, `HOME`, and tool configuration, but do not dump the full environment into artifacts.
452
- - Redact stdout, stderr, JSONL, HTML, ANSI, debug files, and failure bundles before committing, publishing, posting, or sharing them.
453
- - Fail if a redaction scan finds API keys, bearer tokens, cookies, auth headers, or raw `.env` contents in persisted artifacts.
454
-
455
- Docs may say `CURSOR_API_KEY=(present, redacted)` or `ZAI_API_KEY=(present, redacted)`. They must never include values.
456
-
457
- ## Make false green states impossible
458
-
459
- The main guardrails:
460
-
461
- - `doctor` is required before `all`.
462
- - Required targets do not skip green.
463
- - Release proof uses packed install, not `pi -e .`.
464
- - A suite cannot pass with missing required artifacts.
465
- - Cleanup failures fail the target result.
466
- - Visual assertions inspect rendered output, not only prompt text.
467
- - JSONL assertions inspect specific message fields, not all-file substrings.
468
- - Auth is forwarded to targets by explicit allowlist only.
469
- - Secrets can be used locally, but artifacts/docs/comments never expose them.
470
- - Target-specific assumptions live in `docs/platform-smoke.md`, not in chat.
471
-
472
- ## Adoption procedure for a new pi extension
473
-
474
- 1. Identify the package name and pi install path.
475
- 2. Define required targets: default to `macos`, `ubuntu`, and `windows-native`.
476
- 3. Define required suites: always start with `platform-build`; add runtime or visual suites only for real user contracts.
477
- 4. Add `platform-smoke.config.mjs` with package name, targets, suites, Crabbox version, Ubuntu image, and Node baseline.
478
- 5. Add `scripts/platform-smoke.mjs` with `doctor`, per-target, and `all` commands.
479
- 6. Add a thin `crabbox-runner.mjs` that owns target base args, warmup, run, timeout, env allowlist, and stop.
480
- 7. Add target command builders in `targets.mjs`; keep POSIX and PowerShell paths explicit.
481
- 8. Add `platform-build-windows.ps1` for the Windows suite body.
482
- 9. Add `artifacts.mjs` and make missing artifacts fail.
483
- 10. Add `doctor.mjs`; all required local prerequisites fail hard.
484
- 11. Add cheap tests for harness syntax, help, target selection, packed-install command rendering, manifest failure, cleanup failure, and package inclusion/exclusion.
485
- 12. Add `docs/platform-smoke.md` as the project-specific source of truth.
486
- 13. Add a short pointer in `AGENTS.md` and README if the platform gate is release-blocking.
487
- 14. Run `npm run check:platform-smoke`, then `npm run smoke:platform:doctor`, then a single target, then `npm run smoke:platform:all`.
488
-
489
- ## Project adoption checklist
490
-
491
- Before declaring a project integrated, answer these in that project's docs:
492
-
493
- 1. What package/install path must release prove?
494
- 2. Which OS targets are release-blocking?
495
- 3. What exact Crabbox version or minimum version is supported?
496
- 4. Which Ubuntu image is used?
497
- 5. Which Parallels template and snapshot are used?
498
- 6. What target tools are expected globally, especially on Windows?
499
- 7. What suite proves packed pi install?
500
- 8. What suite, if any, proves real pi runtime behavior?
501
- 9. What visual evidence, if any, is required?
502
- 10. What auth env names are allowed to cross into targets?
503
- 11. What artifacts must exist for a pass?
504
- 12. What redaction scans run before sharing evidence?
505
- 13. How are lease cleanup failures surfaced?
506
- 14. Which docs, package scripts, and tests are the source of truth?
507
-
508
- The standard is not "copy every file from pi-cursor-sdk." The standard is: define the platform failure modes that matter for the extension, then make the local Crabbox gate produce durable evidence for them on macOS, Ubuntu, and native Windows without sharing state between repositories.