pi-cursor-sdk 0.1.60 → 0.1.61

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 (56) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +123 -9
  3. package/docs/cursor-model-ux-spec.md +19 -11
  4. package/docs/cursor-native-tool-replay.md +4 -2
  5. package/docs/cursor-tool-surfaces.md +7 -1
  6. package/docs/platform-smoke.md +51 -20
  7. package/package.json +13 -2
  8. package/platform-smoke.config.mjs +1 -0
  9. package/scripts/cloud-runtime-smoke.d.mts +40 -1
  10. package/scripts/cloud-runtime-smoke.mjs +444 -325
  11. package/scripts/lib/cloud-smoke-artifacts.d.mts +16 -0
  12. package/scripts/lib/cloud-smoke-artifacts.mjs +94 -0
  13. package/scripts/lib/cloud-smoke-cleanup-evidence.d.mts +209 -0
  14. package/scripts/lib/cloud-smoke-cleanup-evidence.mjs +585 -0
  15. package/scripts/lib/cloud-smoke-github.d.mts +78 -0
  16. package/scripts/lib/cloud-smoke-github.mjs +331 -0
  17. package/scripts/lib/cloud-smoke-pi-runner.d.mts +42 -0
  18. package/scripts/lib/cloud-smoke-pi-runner.mjs +214 -0
  19. package/scripts/lib/cloud-smoke-shutdown.d.mts +67 -0
  20. package/scripts/lib/cloud-smoke-shutdown.mjs +133 -0
  21. package/scripts/lib/local-resume-smoke-harness.mjs +2 -0
  22. package/scripts/local-resume-cleanup-smoke.mjs +11 -2
  23. package/scripts/platform-smoke/card-detect.mjs +1 -0
  24. package/scripts/platform-smoke/scenarios.d.mts +22 -0
  25. package/scripts/platform-smoke/scenarios.mjs +17 -0
  26. package/scripts/platform-smoke/targets.mjs +1 -0
  27. package/src/cursor-agent-message-web-tools.ts +14 -6
  28. package/src/cursor-cloud-local-state.ts +536 -0
  29. package/src/cursor-cloud-options.ts +46 -70
  30. package/src/cursor-config.ts +160 -14
  31. package/src/cursor-http1.ts +65 -0
  32. package/src/cursor-live-run-coordinator.ts +8 -1
  33. package/src/cursor-pi-tool-bridge-abort.ts +10 -3
  34. package/src/cursor-pi-tool-bridge-constants.ts +6 -0
  35. package/src/cursor-pi-tool-bridge-env.ts +9 -0
  36. package/src/cursor-pi-tool-bridge-run.ts +27 -5
  37. package/src/cursor-pi-tool-bridge.ts +11 -2
  38. package/src/cursor-provider-errors.ts +72 -2
  39. package/src/cursor-provider-live-run-drain.ts +2 -1
  40. package/src/cursor-provider-run-finalizer.ts +8 -2
  41. package/src/cursor-provider-run-outcome.ts +7 -1
  42. package/src/cursor-provider-turn-finalize.ts +23 -4
  43. package/src/cursor-provider-turn-prepare.ts +17 -3
  44. package/src/cursor-provider-turn-runner.ts +5 -0
  45. package/src/cursor-provider-turn-send.ts +1 -1
  46. package/src/cursor-question-tool.ts +8 -0
  47. package/src/cursor-ripgrep-path.ts +32 -0
  48. package/src/cursor-runtime-state.ts +119 -100
  49. package/src/cursor-sdk-process-error-guard.ts +19 -0
  50. package/src/cursor-session-agent-cleanup.ts +66 -15
  51. package/src/cursor-session-agent-lifecycle.ts +9 -4
  52. package/src/cursor-session-agent-resume.ts +50 -7
  53. package/src/cursor-session-agent.ts +106 -67
  54. package/src/cursor-session-scope.ts +22 -2
  55. package/src/cursor-session-store.ts +155 -0
  56. package/src/cursor-state.ts +162 -21
@@ -1,22 +1,58 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
2
+ import {
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ renameSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from "node:fs";
4
9
  import { tmpdir } from "node:os";
5
10
  import { dirname, join, resolve } from "node:path";
6
11
  import { fileURLToPath, pathToFileURL } from "node:url";
7
- import {
8
- CHILD_PROCESS_TREE_SPAWN_OPTIONS,
9
- terminateChild,
10
- } from "./lib/cursor-child-process.mjs";
12
+ import { terminateChild } from "./lib/cursor-child-process.mjs";
11
13
  import { buildCursorSmokeEnv } from "./lib/cursor-smoke-env.mjs";
12
14
  import {
13
- CLOUD_AGENT_ID_PATTERN,
14
- CLOUD_LIFECYCLE_ENTRY_TYPE,
15
- CLOUD_LIFECYCLE_JOURNAL_PREFIX,
16
- } from "../shared/cursor-cloud-lifecycle-constants.mjs";
15
+ checkpointCloudSmokeShutdown,
16
+ createCloudSmokeShutdownController,
17
+ installCloudSmokeSignalHandlers,
18
+ } from "./lib/cloud-smoke-shutdown.mjs";
19
+ import {
20
+ assertAgentDeleted,
21
+ assertCloudSmokeEvidenceSafe,
22
+ buildCloudSmokeEvidenceProvenance,
23
+ cleanupCloudAgent,
24
+ coordinateCloudSmokeReleaseGate,
25
+ listCloudSmokePackageSourcePaths,
26
+ projectCloudSmokeMatrixEvidence,
27
+ validateCloudSmokeMatrixEvidence,
28
+ } from "./lib/cloud-smoke-cleanup-evidence.mjs";
29
+ import {
30
+ assertOwnedThrowawayRepositoryHandle,
31
+ authenticatedGitArgs,
32
+ cloudSmokeRepositoryDescription,
33
+ createThrowawayRepository,
34
+ deleteThrowawayRepository,
35
+ normalizeCloudSmokeGitHubRepo,
36
+ runTimedCommand,
37
+ validatePrUrl,
38
+ } from "./lib/cloud-smoke-github.mjs";
39
+ import { CLOUD_AGENT_ID_PATTERN } from "../shared/cursor-cloud-lifecycle-constants.mjs";
40
+ import {
41
+ cloudAgentIdsFromLifecycleArtifacts,
42
+ cloudAgentIdsFromMetadata,
43
+ cloudLifecycleRecords,
44
+ readCloudRunReport,
45
+ readCloudSmokeMetadata,
46
+ readLatestCloudSmokeMetadata,
47
+ } from "./lib/cloud-smoke-artifacts.mjs";
48
+ import { createCloudSmokePiRunner } from "./lib/cloud-smoke-pi-runner.mjs";
17
49
  import { scrubSensitiveText } from "../shared/cursor-sensitive-text.mjs";
18
50
 
19
51
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
52
+ const EVIDENCE_PATH = join(root, "docs", "evidence", "cursor-cloud-smoke-matrix-latest.json");
53
+ const MODEL = "cursor/composer-2-5";
54
+ const CLOUD_RUN_ID_PATTERN = /^run-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
55
+ const cloudSmokeShutdown = createCloudSmokeShutdownController((child) => terminateChild(child, { graceMs: 15_000 }));
20
56
  const argv = process.argv.slice(2);
21
57
  const args = new Set(argv);
22
58
  const CLOUD_ENV_NAMES = [
@@ -27,6 +63,8 @@ const CLOUD_ENV_NAMES = [
27
63
  "PI_CURSOR_CLOUD_REPO",
28
64
  "PI_CURSOR_CLOUD_BRANCH",
29
65
  "PI_CURSOR_CLOUD_DIRECT_PUSH",
66
+ "PI_CURSOR_CLOUD_AUTO_CREATE_PR",
67
+ "PI_CURSOR_CLOUD_SKIP_REVIEWER_REQUEST",
30
68
  "PI_CURSOR_CLOUD_ENV",
31
69
  "PI_CURSOR_CLOUD_ENV_FROM_FILES",
32
70
  "PI_CURSOR_CLOUD_ENV_TYPE",
@@ -45,27 +83,28 @@ class SmokeFailure extends Error {
45
83
  }
46
84
 
47
85
  function printHelp() {
48
- console.log(`Opt-in live Cursor cloud smoke for pi-cursor-sdk.
86
+ console.log(`Required live Cursor cloud release smoke for pi-cursor-sdk.
49
87
 
50
88
  Usage:
51
89
  npm run smoke:cloud
52
90
  npm run smoke:cloud:context
53
91
  node scripts/cloud-runtime-smoke.mjs [--context-matrix]
54
92
 
93
+ The no-flag release gate creates and deletes one private throwaway GitHub repository and runs named lanes for cancel, explicit repo/startingRef branch reporting, direct push, missing branch failure, lifecycle delete, and account-conditional artifacts/raw usage. It requires current gh auth with private-repo create/push/delete access and Cursor Cloud access to that repository.
94
+
55
95
  Environment:
56
- CURSOR_API_KEY Required for the cloud run and archival cleanup.
57
- CURSOR_CLOUD_SMOKE_MODEL Cursor model id (default: cursor/composer-2-5).
58
- CURSOR_CLOUD_SMOKE_TIMEOUT_MS Timeout in ms (default: 300000).
59
- CURSOR_CLOUD_SMOKE_ENV_TYPE Optional Cursor-managed env type: cloud, pool, or machine.
60
- CURSOR_CLOUD_SMOKE_ENV_NAME Optional Cursor-managed env name, used only with type.
61
- CURSOR_CLOUD_SMOKE_KEEP_ARTIFACTS Keep temp artifacts when set to 1.
96
+ CURSOR_API_KEY Required for cloud runs and verified agent cleanup.
97
+ CURSOR_CLOUD_SMOKE_TIMEOUT_MS Timeout in ms per lane (default: 300000).
98
+ CURSOR_CLOUD_SMOKE_ENV_TYPE Optional context-matrix Cursor env type: cloud, pool, or machine.
99
+ CURSOR_CLOUD_SMOKE_ENV_NAME Optional context-matrix env name, used only with type.
100
+ CURSOR_CLOUD_SMOKE_KEEP_ARTIFACTS Keep temp artifacts after success when set to 1.
62
101
 
63
102
  Options:
64
- --context-matrix Run sessionful fresh-vs-bootstrap context handoff proof.
103
+ --context-matrix Optional sessionful fresh-vs-bootstrap context handoff proof.
65
104
 
66
105
  Exit codes:
67
- 0 cloud run passed and artifact contract matched
68
- 1 missing entitlement/auth, run failure, assertion failure, or cleanup failure
106
+ 0 every selected lane and destructive throwaway cleanup proof passed
107
+ 1 missing prerequisite, run/assertion failure, or cleanup verification failure
69
108
  2 invalid command-line usage`);
70
109
  }
71
110
 
@@ -91,16 +130,12 @@ function fail(message, details = "") {
91
130
  function reportFailure(error) {
92
131
  console.error(scrubSmokeText(`[cloud-smoke] FAIL: ${error instanceof Error ? error.message : String(error)}`));
93
132
  if (error instanceof SmokeFailure && error.details) console.error(scrubSmokeText(error.details));
94
- }
95
-
96
- function findPiBin() {
97
- const local = join(root, "node_modules", ".bin", process.platform === "win32" ? "pi.cmd" : "pi");
98
- return existsSync(local) ? local : process.platform === "win32" ? "pi.cmd" : "pi";
133
+ else if (error?.details) console.error(scrubSmokeText(error.details));
99
134
  }
100
135
 
101
136
  function optionalSmokeValue(name) {
102
137
  const value = process.env[name]?.trim();
103
- return value ? value : undefined;
138
+ return value || undefined;
104
139
  }
105
140
 
106
141
  export function buildCloudSmokeEnv(artifactDir, options = {}) {
@@ -108,14 +143,17 @@ export function buildCloudSmokeEnv(artifactDir, options = {}) {
108
143
  mkdirSync(agentDir, { recursive: true });
109
144
  const env = buildCursorSmokeEnv({ settingSources: "none", eventDebugDir: artifactDir });
110
145
  for (const name of CLOUD_ENV_NAMES) delete env[name];
111
- const smokeEnvType = optionalSmokeValue("CURSOR_CLOUD_SMOKE_ENV_TYPE");
112
- const smokeEnvName = optionalSmokeValue("CURSOR_CLOUD_SMOKE_ENV_NAME");
146
+ const smokeEnvType = options.repoUrl ? undefined : optionalSmokeValue("CURSOR_CLOUD_SMOKE_ENV_TYPE");
147
+ const smokeEnvName = options.repoUrl ? undefined : optionalSmokeValue("CURSOR_CLOUD_SMOKE_ENV_NAME");
113
148
  Object.assign(env, {
114
149
  PI_CURSOR_RUNTIME: "cloud",
115
150
  PI_CURSOR_CLOUD_ACK: "1",
116
151
  PI_CURSOR_CLOUD_ALLOW_LOCAL_STATE: "1",
117
152
  PI_CODING_AGENT_DIR: agentDir,
118
153
  PI_CURSOR_CLOUD_CONTEXT: options.contextHandoff ?? "fresh",
154
+ ...(options.repoUrl ? { PI_CURSOR_CLOUD_REPO: options.repoUrl } : {}),
155
+ ...(options.startingRef ? { PI_CURSOR_CLOUD_BRANCH: options.startingRef } : {}),
156
+ ...(options.directPush ? { PI_CURSOR_CLOUD_DIRECT_PUSH: "1" } : {}),
119
157
  ...(smokeEnvType ? { PI_CURSOR_CLOUD_ENV_TYPE: smokeEnvType } : {}),
120
158
  ...(smokeEnvType && smokeEnvName ? { PI_CURSOR_CLOUD_ENV_NAME: smokeEnvName } : {}),
121
159
  });
@@ -129,369 +167,450 @@ export function buildCloudSmokeWorkspace(artifactDir) {
129
167
  return workspaceDir;
130
168
  }
131
169
 
132
- function runPi({ artifactDir, timeoutMs }) {
133
- const model = process.env.CURSOR_CLOUD_SMOKE_MODEL || "cursor/composer-2-5";
134
- const workspaceDir = buildCloudSmokeWorkspace(artifactDir);
135
- const sessionDir = join(artifactDir, "sessions");
136
- mkdirSync(sessionDir, { recursive: true });
137
- const child = spawn(
138
- findPiBin(),
139
- [
140
- "-e", root,
141
- "--model", model,
142
- "--session-dir", sessionDir,
143
- "--session-id", `cloud-smoke-${Date.now()}`,
144
- "-p", "Reply exactly: cloud-smoke-ok",
145
- ],
146
- {
147
- cwd: workspaceDir,
148
- env: buildCloudSmokeEnv(artifactDir),
149
- stdio: ["ignore", "pipe", "pipe"],
150
- ...CHILD_PROCESS_TREE_SPAWN_OPTIONS,
151
- },
152
- );
153
- let stdout = "";
154
- let stderr = "";
155
- child.stdout.setEncoding("utf8");
156
- child.stderr.setEncoding("utf8");
157
- child.stdout.on("data", (chunk) => { stdout += chunk; });
158
- child.stderr.on("data", (chunk) => { stderr += chunk; });
159
- return new Promise((resolveRun, rejectRun) => {
160
- let timedOut = false;
161
- const timer = setTimeout(() => {
162
- timedOut = true;
163
- void terminateChild(child).then(
164
- () => rejectRun(new Error(`pi cloud smoke timed out after ${timeoutMs}ms`)),
165
- (error) => rejectRun(new Error(`pi cloud smoke timed out and process-tree cleanup failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error })),
166
- );
167
- }, timeoutMs);
168
- child.once("error", (error) => {
169
- if (timedOut) return;
170
- clearTimeout(timer);
171
- rejectRun(error);
172
- });
173
- child.once("close", (code, signal) => {
174
- if (timedOut) return;
175
- clearTimeout(timer);
176
- resolveRun({ code, signal, stdout, stderr });
177
- });
178
- });
179
- }
180
-
181
- function metadataFiles(dir) {
182
- const files = [];
183
- const stack = [dir];
184
- while (stack.length > 0) {
185
- const current = stack.pop();
186
- try {
187
- for (const entry of readdirSync(current, { withFileTypes: true })) {
188
- const path = join(current, entry.name);
189
- if (entry.isDirectory()) stack.push(path);
190
- else if (entry.name === "metadata.json") files.push(path);
191
- }
192
- } catch {}
170
+ export {
171
+ assertCloudSmokeEvidenceSafe,
172
+ assertOwnedThrowawayRepositoryHandle,
173
+ buildCloudSmokeEvidenceProvenance,
174
+ cloudSmokeRepositoryDescription,
175
+ cloudAgentIdsFromLifecycleArtifacts,
176
+ coordinateCloudSmokeReleaseGate,
177
+ listCloudSmokePackageSourcePaths,
178
+ normalizeCloudSmokeGitHubRepo,
179
+ projectCloudSmokeMatrixEvidence,
180
+ validateCloudSmokeMatrixEvidence,
181
+ };
182
+
183
+ const { runPi, startRpc } = createCloudSmokePiRunner({
184
+ root,
185
+ model: MODEL,
186
+ shutdown: cloudSmokeShutdown,
187
+ buildEnv: buildCloudSmokeEnv,
188
+ buildWorkspace: buildCloudSmokeWorkspace,
189
+ });
190
+
191
+ function command(commandName, commandArgs, options = {}) {
192
+ try {
193
+ return runTimedCommand(commandName, commandArgs, { cwd: options.cwd ?? root, ...options });
194
+ } catch (error) {
195
+ fail(error instanceof Error ? error.message : String(error), error?.details ?? "");
193
196
  }
194
- return files.sort();
195
- }
196
-
197
- function readLatestMetadataIfPresent(artifactDir) {
198
- const files = metadataFiles(artifactDir);
199
- if (files.length === 0) return undefined;
200
- const metadataPath = files.at(-1);
201
- return { metadataPath, metadata: JSON.parse(readFileSync(metadataPath, "utf8")) };
202
- }
203
-
204
- function addExactCloudAgentId(ids, value) {
205
- if (typeof value === "string" && CLOUD_AGENT_ID_PATTERN.test(value)) ids.add(value);
206
197
  }
207
198
 
208
- function cloudAgentIdsFromMetadata(artifactDir) {
209
- const ids = new Set();
210
- for (const metadataPath of metadataFiles(artifactDir)) {
211
- try {
212
- const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
213
- addExactCloudAgentId(ids, metadata.run?.agentId ?? metadata.providerMeta?.cloudAgentId);
214
- } catch {
215
- // One corrupt optional debug artifact must not hide IDs available from other evidence.
216
- }
217
- }
218
- return [...ids];
199
+ async function runSuccessfulPrintLane({ artifactDir, envOptions, message, marker, sessionId, timeoutMs }) {
200
+ const run = await runPi({ artifactDir, envOptions, message, sessionId, timeoutMs });
201
+ if (run.code !== 0) fail(`pi cloud lane exited ${run.code}${run.signal ? ` (${run.signal})` : ""}`, `${run.stderr}\n${run.stdout}`.trim());
202
+ if (!run.stdout.includes(marker)) fail(`cloud lane output missing ${marker}`, `${run.stderr}\n${run.stdout}`.trim());
203
+ const latest = readLatestCloudSmokeMetadata(artifactDir);
204
+ if (!latest) fail("cloud lane metadata missing", artifactDir);
205
+ const report = assertCloudMetadata(latest.metadata, latest.metadataPath);
206
+ const agentId = latest.metadata.run.agentId;
207
+ const runId = latest.metadata.run.runId;
208
+ assertLaneEvidence(artifactDir, agentId, runId);
209
+ return { agentId, runId, report };
219
210
  }
220
211
 
221
- function lifecycleArtifactFiles(dir) {
222
- const files = [];
223
- const stack = [dir];
224
- while (stack.length > 0) {
225
- const current = stack.pop();
226
- try {
227
- for (const entry of readdirSync(current, { withFileTypes: true })) {
228
- const path = join(current, entry.name);
229
- if (entry.isDirectory()) stack.push(path);
230
- else if (entry.name.endsWith(".jsonl") || (entry.name.startsWith(`${CLOUD_LIFECYCLE_JOURNAL_PREFIX}-`) && entry.name.endsWith(".journal"))) files.push(path);
212
+ function assertCloudMetadata(metadata, metadataPath, options = {}) {
213
+ if (metadata.providerMeta?.runtime !== "cloud") fail("provider metadata did not record cloud runtime", metadataPath);
214
+ if (metadata.send?.bridgeEnabled !== false) fail("cloud send unexpectedly enabled pi bridge", metadataPath);
215
+ if (metadata.send?.useNativeToolReplay !== false) fail("cloud send unexpectedly enabled native replay", metadataPath);
216
+ if (metadata.send?.agentMode !== "agent") fail("cloud send did not use agent mode", metadataPath);
217
+ if (!CLOUD_AGENT_ID_PATTERN.test(metadata.run?.agentId ?? "")) fail("cloud run did not return an exact cloud agent id", metadataPath);
218
+ if (!CLOUD_RUN_ID_PATTERN.test(metadata.run?.runId ?? "")) fail("cloud run did not return an exact run id", metadataPath);
219
+ const report = readCloudRunReport({ metadataPath, metadata });
220
+ if (!report && options.requireReport === false) return undefined;
221
+ if (!report) fail("cloud report was not retained after the print-mode lane exited", metadataPath);
222
+ if (report.agentId !== metadata.run.agentId || report.runId !== metadata.run.runId) fail("cloud report IDs did not match run metadata", metadataPath);
223
+ if (!Array.isArray(report.branches)) fail("cloud report branches were not an array", metadataPath);
224
+ if (report.artifacts !== undefined) {
225
+ if (!Array.isArray(report.artifacts) || report.artifacts.length > 10) fail("cloud report artifacts exceeded the bounded array contract", metadataPath);
226
+ for (const artifact of report.artifacts) {
227
+ if (typeof artifact?.path !== "string" || artifact.path.length === 0 || artifact.path.length > 240 || !Number.isFinite(artifact.sizeBytes) || artifact.sizeBytes < 0 || typeof artifact.updatedAt !== "string" || artifact.updatedAt.length > 240 || Number.isNaN(Date.parse(artifact.updatedAt))) {
228
+ fail("cloud report artifact did not match the bounded known shape", metadataPath);
231
229
  }
232
- } catch {}
233
- }
234
- return files.sort();
235
- }
236
-
237
- export function cloudAgentIdsFromLifecycleArtifacts(artifactDir) {
238
- const ids = new Set();
239
- for (const path of lifecycleArtifactFiles(artifactDir)) {
240
- const journal = path.split(/[\\/]/).at(-1)?.startsWith(`${CLOUD_LIFECYCLE_JOURNAL_PREFIX}-`) === true;
241
- let lines;
242
- try {
243
- lines = readFileSync(path, "utf8").split(/\r?\n/);
244
- } catch {
245
- continue;
246
230
  }
247
- for (const line of lines) {
248
- if (!line) continue;
249
- try {
250
- const entry = JSON.parse(line);
251
- const data = journal
252
- ? entry
253
- : entry?.type === "custom" && entry.customType === CLOUD_LIFECYCLE_ENTRY_TYPE
254
- ? entry.data
255
- : undefined;
256
- addExactCloudAgentId(ids, data?.agentId);
257
- } catch {
258
- // Journals are newline-framed; one partial line must not hide later IDs.
231
+ }
232
+ if (report.usage !== undefined) {
233
+ if (!report.usage || typeof report.usage !== "object" || Array.isArray(report.usage)) fail("cloud report usage was not an object", metadataPath);
234
+ for (const usage of [report.usage.totalUsage, report.usage.runUsage].filter(Boolean)) {
235
+ for (const key of ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "totalTokens"]) {
236
+ if (!Number.isFinite(usage[key]) || usage[key] < 0) fail("cloud report usage did not match the known token shape", metadataPath);
259
237
  }
260
238
  }
261
239
  }
262
- return [...ids];
240
+ return report;
263
241
  }
264
242
 
265
- function resolveMetadataArtifactPath(metadataPath, artifactPath) {
266
- if (!artifactPath) return undefined;
267
- return resolve(artifactPath) === artifactPath ? artifactPath : join(dirname(metadataPath), artifactPath);
243
+ function assertLaneEvidence(artifactDir, expectedAgentId, expectedRunId) {
244
+ const metadataIds = cloudAgentIdsFromMetadata(artifactDir);
245
+ const lifecycle = cloudLifecycleRecords(artifactDir);
246
+ if (!metadataIds.includes(expectedAgentId)) fail("lane agent ID missing from metadata", expectedAgentId);
247
+ if (!lifecycle.some(({ data }) => data.agentId === expectedAgentId)) fail("lane agent ID missing from canonical lifecycle JSONL/journal", expectedAgentId);
248
+ if (expectedRunId && !lifecycle.some(({ data }) => data.agentId === expectedAgentId && data.runId === expectedRunId)) {
249
+ fail("lane run ID missing from canonical lifecycle JSONL/journal", expectedRunId);
250
+ }
268
251
  }
269
252
 
270
- function readJsonlIfPresent(path) {
271
- if (!path || !existsSync(path)) return [];
272
- return readFileSync(path, "utf8")
273
- .split(/\n+/)
274
- .filter(Boolean)
275
- .map((line) => JSON.parse(line));
253
+ async function waitFor(predicate, timeoutMs, errorMessage) {
254
+ const started = Date.now();
255
+ while (Date.now() - started < timeoutMs) {
256
+ cloudSmokeShutdown.throwIfRequested();
257
+ const value = await predicate();
258
+ if (value) return value;
259
+ await new Promise((resolveWait) => setTimeout(resolveWait, 250));
260
+ }
261
+ throw new Error(errorMessage);
276
262
  }
277
263
 
278
- function startRpc({ artifactDir, contextHandoff, sessionId }) {
279
- const model = process.env.CURSOR_CLOUD_SMOKE_MODEL || "cursor/composer-2-5";
280
- const sessionDir = join(artifactDir, "sessions");
281
- mkdirSync(sessionDir, { recursive: true });
282
- const child = spawn(
283
- findPiBin(),
284
- ["--mode", "rpc", "-e", root, "--model", model, "--approve", "--session-dir", sessionDir, "--session-id", sessionId],
285
- {
286
- cwd: buildCloudSmokeWorkspace(artifactDir),
287
- env: buildCloudSmokeEnv(artifactDir, { contextHandoff }),
288
- stdio: ["pipe", "pipe", "pipe"],
289
- ...CHILD_PROCESS_TREE_SPAWN_OPTIONS,
264
+ async function waitForAgentSettled(rpc, fromIndex, timeoutMs) {
265
+ await waitFor(
266
+ () => {
267
+ rpc.throwIfFailed();
268
+ return rpc.events.slice(fromIndex).some((event) => event.type === "agent_settled");
290
269
  },
270
+ timeoutMs,
271
+ `timeout waiting for agent_settled. Stderr: ${rpc.stderr}`,
291
272
  );
292
- let stderr = "";
293
- const events = [];
294
- const pending = new Map();
295
- let requestId = 0;
296
- let stdoutBuffer = "";
297
- child.stdout.setEncoding("utf8");
298
- child.stderr.setEncoding("utf8");
299
- child.stderr.on("data", (chunk) => { stderr += chunk; });
300
- child.stdout.on("data", (chunk) => {
301
- stdoutBuffer += chunk;
302
- let newlineIndex;
303
- while ((newlineIndex = stdoutBuffer.indexOf("\n")) >= 0) {
304
- const line = stdoutBuffer.slice(0, newlineIndex);
305
- stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
306
- if (!line.trim()) continue;
307
- let message;
308
- try {
309
- message = JSON.parse(line);
310
- } catch {
311
- continue;
312
- }
313
- if (message.type === "response" && pending.has(message.id)) {
314
- const request = pending.get(message.id);
315
- pending.delete(message.id);
316
- clearTimeout(request.timer);
317
- request.resolve(message);
318
- continue;
319
- }
320
- events.push(message);
321
- }
322
- });
323
- const send = (type, extra = {}, timeoutMs = 120000) => new Promise((resolveRequest, rejectRequest) => {
324
- const id = `cloud_smoke_${++requestId}`;
325
- const timer = setTimeout(() => {
326
- pending.delete(id);
327
- rejectRequest(new Error(`timeout waiting for ${type}. Stderr: ${stderr}`));
328
- }, timeoutMs);
329
- pending.set(id, { resolve: resolveRequest, reject: rejectRequest, timer });
330
- child.stdin.write(`${JSON.stringify({ id, type, ...extra })}\n`);
331
- });
332
- const stop = async () => {
333
- for (const request of pending.values()) clearTimeout(request.timer);
334
- pending.clear();
335
- await terminateChild(child);
336
- };
337
- return { events, send, stop, get stderr() { return stderr; } };
338
273
  }
339
274
 
340
- async function waitForAgentSettled(rpc, fromIndex, timeoutMs) {
341
- const started = Date.now();
342
- while (Date.now() - started < timeoutMs) {
343
- if (rpc.events.slice(fromIndex).some((event) => event.type === "agent_settled")) return;
344
- await new Promise((resolveWait) => setTimeout(resolveWait, 250));
345
- }
346
- throw new Error(`timeout waiting for agent_settled. Stderr: ${rpc.stderr}`);
275
+ async function readLastAssistantText(rpc) {
276
+ const response = await rpc.send("get_last_assistant_text");
277
+ if (!response.success) fail("failed to read last assistant text", response.error);
278
+ return typeof response.data?.text === "string" ? response.data.text : "";
347
279
  }
348
280
 
349
281
  async function promptAndRead({ rpc, artifactDir, message, timeoutMs, expectedContextHandoff }) {
350
282
  const fromIndex = rpc.events.length;
351
- await rpc.send("prompt", { message }, timeoutMs);
283
+ const response = await rpc.send("prompt", { message }, timeoutMs);
284
+ if (!response.success) fail("cloud prompt was rejected", response.error);
352
285
  await waitForAgentSettled(rpc, fromIndex, timeoutMs);
353
- const textResponse = await rpc.send("get_last_assistant_text", {}, 120000);
354
- if (!textResponse.success) fail("failed to read last assistant text", textResponse.error);
355
- const latestMetadata = readLatestMetadataIfPresent(artifactDir);
356
- if (!latestMetadata) fail("context smoke metadata missing", artifactDir);
357
- assertCloudMetadata(latestMetadata.metadata, latestMetadata.metadataPath, { requireReport: false });
358
- if (expectedContextHandoff && latestMetadata.metadata.providerMeta?.contextHandoff !== expectedContextHandoff) {
359
- fail("context smoke metadata recorded wrong handoff", JSON.stringify({ expected: expectedContextHandoff, actual: latestMetadata.metadata.providerMeta?.contextHandoff }, null, 2));
286
+ const text = await readLastAssistantText(rpc);
287
+ const latest = readLatestCloudSmokeMetadata(artifactDir);
288
+ if (!latest) fail("cloud smoke metadata missing", artifactDir);
289
+ const report = assertCloudMetadata(latest.metadata, latest.metadataPath, { requireReport: !expectedContextHandoff });
290
+ if (expectedContextHandoff && latest.metadata.providerMeta?.contextHandoff !== expectedContextHandoff) {
291
+ fail("cloud metadata recorded wrong context handoff", JSON.stringify({ expected: expectedContextHandoff, actual: latest.metadata.providerMeta?.contextHandoff }));
360
292
  }
361
- return {
362
- text: typeof textResponse.data?.text === "string" ? textResponse.data.text : "",
363
- agentId: latestMetadata.metadata.run?.agentId ?? latestMetadata.metadata.providerMeta?.cloudAgentId,
364
- runId: latestMetadata.metadata.run?.runId,
365
- };
293
+ const agentId = latest.metadata.run.agentId;
294
+ const runId = latest.metadata.run.runId;
295
+ assertLaneEvidence(artifactDir, agentId, runId);
296
+ return { text, agentId, runId, report };
297
+ }
298
+
299
+ function lastNonEmptyLine(text) {
300
+ return String(text ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean).at(-1) ?? "";
366
301
  }
367
302
 
368
303
  async function runContextScenario({ artifactRoot, contextHandoff, timeoutMs }) {
369
304
  const artifactDir = join(artifactRoot, `context-${contextHandoff}`);
370
305
  mkdirSync(artifactDir, { recursive: true });
371
306
  const marker = `CLOUD_CONTEXT_${contextHandoff}_${Date.now()}`;
372
- const rpc = startRpc({ artifactDir, contextHandoff, sessionId: `cloud-context-${contextHandoff}-${Date.now()}` });
307
+ const rpc = await startRpc({ artifactDir, contextHandoff, sessionId: `cloud-context-${contextHandoff}-${Date.now()}` });
373
308
  try {
374
- const first = await promptAndRead({
375
- rpc,
376
- artifactDir,
377
- message: `Remember exact marker ${marker}. Reply exactly FIRST_OK.`,
378
- timeoutMs,
379
- expectedContextHandoff: contextHandoff,
380
- });
381
- if (firstNonEmptyLine(first.text) !== "FIRST_OK") fail(`cloud ${contextHandoff} setup turn did not return FIRST_OK`, first.text);
382
- const second = await promptAndRead({
383
- rpc,
384
- artifactDir,
385
- message: "What exact CLOUD_CONTEXT marker did I ask you to remember earlier in this pi session? Reply exactly MARKER=<marker> if visible, otherwise NO_MARKER.",
386
- timeoutMs,
387
- expectedContextHandoff: contextHandoff,
388
- });
309
+ const first = await promptAndRead({ rpc, artifactDir, message: `Remember exact marker ${marker}. Reply exactly FIRST_OK.`, timeoutMs, expectedContextHandoff: contextHandoff });
310
+ if (lastNonEmptyLine(first.text) !== "FIRST_OK") fail(`cloud ${contextHandoff} setup turn did not return FIRST_OK`, first.text);
311
+ const second = await promptAndRead({ rpc, artifactDir, message: "What exact CLOUD_CONTEXT marker did I ask you to remember earlier in this pi session? Reply exactly MARKER=<marker> if visible, otherwise NO_MARKER.", timeoutMs, expectedContextHandoff: contextHandoff });
389
312
  return { contextHandoff, marker, first, second };
390
313
  } finally {
391
314
  await rpc.stop();
392
315
  }
393
316
  }
394
317
 
395
- function nonEmptyLines(text) {
396
- return String(text ?? "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
397
- }
398
-
399
- function firstNonEmptyLine(text) {
400
- return nonEmptyLines(text).at(0) ?? "";
318
+ async function runContextMatrix({ artifactRoot, timeoutMs }) {
319
+ const fresh = await runContextScenario({ artifactRoot, contextHandoff: "fresh", timeoutMs });
320
+ if (fresh.second.text.includes(fresh.marker) || lastNonEmptyLine(fresh.second.text) !== "NO_MARKER") fail("fresh cloud context leaked prior context", fresh.second.text);
321
+ const bootstrap = await runContextScenario({ artifactRoot, contextHandoff: "bootstrap", timeoutMs });
322
+ if (lastNonEmptyLine(bootstrap.second.text) !== `MARKER=${bootstrap.marker}`) fail("bootstrap cloud context omitted prior context", bootstrap.second.text);
323
+ return [fresh, bootstrap];
401
324
  }
402
325
 
403
- function lastNonEmptyLine(text) {
404
- return nonEmptyLines(text).at(-1) ?? "";
326
+ async function runBranchAndLifecycleLane({ artifactRoot, repo, timeoutMs, Agent }) {
327
+ const artifactDir = join(artifactRoot, "lane-branch-lifecycle");
328
+ const timestamp = Date.now();
329
+ const marker = `BRANCH_PROOF_${timestamp}`;
330
+ const branchName = `cursor/cloud-smoke-${timestamp}`;
331
+ const sessionId = `cloud-branch-${timestamp}`;
332
+ const envOptions = { repoUrl: repo.repoUrl, startingRef: "starting-ref" };
333
+ const result = await runSuccessfulPrintLane({
334
+ artifactDir,
335
+ envOptions,
336
+ message: `In this repository, create branch-proof.txt containing exactly ${marker} followed by a newline, commit it on a new branch named ${branchName}, push that branch, then reply exactly BRANCH_LANE_OK.`,
337
+ marker: "BRANCH_LANE_OK",
338
+ sessionId,
339
+ timeoutMs,
340
+ });
341
+ for (const branch of result.report.branches) {
342
+ if (normalizeCloudSmokeGitHubRepo(branch.repoUrl) !== repo.fullName.toLowerCase()) fail("branch lane reported a different repository", branch.repoUrl);
343
+ if (branch.prUrl) {
344
+ try {
345
+ await validatePrUrl(repo, branch.prUrl, { cwd: root });
346
+ } catch (error) {
347
+ fail(error instanceof Error ? error.message : String(error), error?.details ?? "");
348
+ }
349
+ }
350
+ }
351
+ command("git", ["check-ref-format", "--branch", branchName], { label: "validate cloud branch name" });
352
+ const remoteRef = "refs/remotes/origin/cloud-smoke-branch-proof";
353
+ command("git", authenticatedGitArgs(["fetch", "origin", `+refs/heads/${branchName}:${remoteRef}`]), { cwd: repo.seedDir, label: "fetch cloud branch proof" });
354
+ const startingCommit = command("git", ["rev-parse", "refs/remotes/origin/starting-ref"], { cwd: repo.seedDir });
355
+ const branchCommit = command("git", ["rev-parse", remoteRef], { cwd: repo.seedDir });
356
+ if (startingCommit === branchCommit) fail("cloud working branch did not advance from startingRef");
357
+ command("git", ["merge-base", "--is-ancestor", startingCommit, branchCommit], { cwd: repo.seedDir, label: "verify cloud branch startingRef ancestry" });
358
+ const remoteContent = command("git", ["show", `${remoteRef}:branch-proof.txt`], { cwd: repo.seedDir, label: "read cloud branch proof" });
359
+ if (remoteContent !== marker) fail("cloud branch proof content did not match marker", remoteContent);
360
+
361
+ const rpc = await startRpc({ artifactDir, sessionId, envOptions });
362
+ try {
363
+ const commandResponse = await rpc.send("prompt", { message: `/cursor-cloud delete ${result.agentId} --yes` }, timeoutMs);
364
+ if (!commandResponse.success) fail("lifecycle delete command failed", commandResponse.error);
365
+ } finally {
366
+ await rpc.stop();
367
+ }
368
+ try {
369
+ await assertAgentDeleted(Agent, result.agentId);
370
+ } catch (error) {
371
+ fail(error instanceof Error ? error.message : String(error), error?.details ?? "");
372
+ }
373
+ if (!cloudLifecycleRecords(artifactDir).some(({ data }) => data.agentId === result.agentId && data.action === "delete")) fail("lifecycle delete result missing from canonical journal/JSONL", result.agentId);
374
+ return [
375
+ {
376
+ name: "explicit-https-repo-starting-ref-branch-pr-reporting",
377
+ status: "passed",
378
+ agentId: result.agentId,
379
+ runId: result.runId,
380
+ branchReportObserved: result.report.branches.some((branch) => Boolean(branch.branch)),
381
+ startingRefAncestryVerified: true,
382
+ remoteContentVerified: true,
383
+ prUrlReturned: result.report.branches.some((branch) => Boolean(branch.prUrl)),
384
+ report: result.report,
385
+ },
386
+ {
387
+ name: "lifecycle-delete",
388
+ status: "passed",
389
+ agentId: result.agentId,
390
+ runId: result.runId,
391
+ lifecycleDeleteVerified: true,
392
+ },
393
+ ];
394
+ }
395
+
396
+ async function runDirectPushLane({ artifactRoot, repo, timeoutMs }) {
397
+ const artifactDir = join(artifactRoot, "lane-direct-push");
398
+ const timestamp = Date.now();
399
+ const marker = `DIRECT_PUSH_PROOF_${timestamp}`;
400
+ const before = command("git", authenticatedGitArgs(["ls-remote", repo.repoUrl, "refs/heads/direct-push"]), { label: "read direct-push branch before lane" }).split(/\s+/)[0];
401
+ const result = await runSuccessfulPrintLane({
402
+ artifactDir,
403
+ envOptions: { repoUrl: repo.repoUrl, startingRef: "direct-push", directPush: true },
404
+ message: `Create direct-push-proof.txt containing exactly ${marker} followed by a newline, commit and push it to the current direct-push branch, then reply exactly DIRECT_PUSH_LANE_OK.`,
405
+ marker: "DIRECT_PUSH_LANE_OK",
406
+ sessionId: `cloud-direct-push-${timestamp}`,
407
+ timeoutMs,
408
+ });
409
+ command("git", authenticatedGitArgs(["fetch", "origin", "+refs/heads/direct-push:refs/remotes/origin/direct-push"]), { cwd: repo.seedDir, label: "fetch direct-push proof" });
410
+ const after = command("git", ["rev-parse", "refs/remotes/origin/direct-push"], { cwd: repo.seedDir });
411
+ if (!before || before === after) fail("direct-push remote branch content did not change");
412
+ const content = command("git", ["show", "refs/remotes/origin/direct-push:direct-push-proof.txt"], { cwd: repo.seedDir, label: "read direct-push proof" });
413
+ if (content !== marker) fail("direct-push remote content did not match marker", content);
414
+ return { name: "direct-push-opt-in", status: "passed", agentId: result.agentId, runId: result.runId, remoteContentChanged: true, report: result.report };
415
+ }
416
+
417
+ async function runCancelLane({ artifactRoot, repo, timeoutMs, Agent }) {
418
+ const artifactDir = join(artifactRoot, "lane-cancel");
419
+ const rpc = await startRpc({ artifactDir, sessionId: `cloud-cancel-${Date.now()}`, envOptions: { repoUrl: repo.repoUrl, startingRef: "starting-ref" } });
420
+ const eventStart = rpc.events.length;
421
+ try {
422
+ const response = await rpc.send("prompt", { message: "Run the shell command `sleep 300`, wait for it, then reply CANCEL_LANE_UNEXPECTED_COMPLETION." }, timeoutMs);
423
+ if (!response.success) fail("cancel lane prompt was rejected", response.error);
424
+ let nextRunProbeAt = 0;
425
+ const identity = await waitFor(async () => {
426
+ rpc.throwIfFailed();
427
+ const latest = readLatestCloudSmokeMetadata(artifactDir);
428
+ const agentId = latest?.metadata.run?.agentId;
429
+ if (!CLOUD_AGENT_ID_PATTERN.test(agentId ?? "")) return undefined;
430
+ if (CLOUD_RUN_ID_PATTERN.test(latest.metadata.run?.runId ?? "")) {
431
+ return { agentId, runId: latest.metadata.run.runId, runIdSource: "metadata" };
432
+ }
433
+ if (Date.now() < nextRunProbeAt) return undefined;
434
+ nextRunProbeAt = Date.now() + 2_000;
435
+ try {
436
+ const page = await Agent.listRuns(agentId, { runtime: "cloud", apiKey: process.env.CURSOR_API_KEY, limit: 10 });
437
+ const run = page.items.find((item) => CLOUD_RUN_ID_PATTERN.test(item.id ?? ""));
438
+ return run ? { agentId, runId: run.id, runIdSource: "agent-list-runs" } : undefined;
439
+ } catch {
440
+ return undefined;
441
+ }
442
+ }, timeoutMs, "cancel lane did not capture exact agent and run IDs before abort");
443
+ const { agentId, runId, runIdSource } = identity;
444
+ const abortResponse = await rpc.send("abort", {}, 120000);
445
+ if (!abortResponse.success) fail("cancel lane abort request failed", abortResponse.error);
446
+ await waitForAgentSettled(rpc, eventStart, timeoutMs);
447
+ await waitFor(() => {
448
+ rpc.throwIfFailed();
449
+ try {
450
+ assertLaneEvidence(artifactDir, agentId, runId);
451
+ return true;
452
+ } catch {
453
+ return false;
454
+ }
455
+ }, timeoutMs, "cancel lane did not retain exact agent/run lifecycle evidence after abort");
456
+ const terminal = await waitFor(async () => {
457
+ rpc.throwIfFailed();
458
+ const run = await Agent.getRun(runId, { runtime: "cloud", agentId, apiKey: process.env.CURSOR_API_KEY });
459
+ return run.status === "cancelled" ? run : run.status === "finished" || run.status === "error" ? fail(`cancel lane reached wrong terminal state ${run.status}`) : undefined;
460
+ }, timeoutMs, "cancel lane SDK status did not reach cancelled");
461
+ return { name: "cancel", status: "passed", agentId, runId, runIdSource, terminalStatus: terminal.status, idsCapturedBeforeAbort: true };
462
+ } finally {
463
+ await rpc.stop();
464
+ }
405
465
  }
406
466
 
407
- async function runContextMatrix({ artifactRoot, timeoutMs }) {
408
- const fresh = await runContextScenario({ artifactRoot, contextHandoff: "fresh", timeoutMs });
409
- if (fresh.second.text.includes(fresh.marker) || lastNonEmptyLine(fresh.second.text) !== "NO_MARKER") {
410
- fail("fresh cloud context handoff leaked prior pi context", JSON.stringify({ expected: "NO_MARKER", actual: fresh.second.text, marker: fresh.marker }, null, 2));
467
+ async function runMissingBranchLane({ artifactRoot, repo, timeoutMs }) {
468
+ const artifactDir = join(artifactRoot, "lane-missing-branch");
469
+ const timestamp = Date.now();
470
+ const missingRef = `missing-${timestamp}`;
471
+ const run = await runPi({
472
+ artifactDir,
473
+ envOptions: { repoUrl: repo.repoUrl, startingRef: missingRef },
474
+ message: "Reply MISSING_BRANCH_UNEXPECTED_SUCCESS.",
475
+ sessionId: `cloud-missing-${timestamp}`,
476
+ timeoutMs,
477
+ });
478
+ const failureOutput = `${run.stdout}\n${run.stderr}`;
479
+ if (run.code === 0 || run.stdout.includes("MISSING_BRANCH_UNEXPECTED_SUCCESS") || !/(branch|ref).*(missing|not found|exist|invalid)|(missing|not found|exist|invalid).*(branch|ref)/i.test(failureOutput)) {
480
+ fail("missing branch did not fail closed with branch/ref evidence", failureOutput);
411
481
  }
412
- const bootstrap = await runContextScenario({ artifactRoot, contextHandoff: "bootstrap", timeoutMs });
413
- if (lastNonEmptyLine(bootstrap.second.text) !== `MARKER=${bootstrap.marker}`) {
414
- fail("bootstrap cloud context handoff did not include prior pi context", JSON.stringify({ expected: `MARKER=${bootstrap.marker}`, actual: bootstrap.second.text }, null, 2));
482
+ const agentIds = new Set();
483
+ for (const { metadataPath, metadata } of readCloudSmokeMetadata(artifactDir)) {
484
+ const agentId = metadata.run?.agentId ?? metadata.providerMeta?.cloudAgentId;
485
+ const runId = metadata.run?.runId;
486
+ if (agentId) {
487
+ if (!CLOUD_AGENT_ID_PATTERN.test(agentId)) fail("missing-branch path recorded malformed agent ID", metadataPath);
488
+ agentIds.add(agentId);
489
+ assertLaneEvidence(artifactDir, agentId, runId);
490
+ }
415
491
  }
416
- return [fresh, bootstrap];
492
+ return { name: "missing-branch-failure", status: "passed", expectedFailureObserved: true, agentIds: [...agentIds].sort() };
417
493
  }
418
494
 
419
- async function archiveCloudAgent(agentId) {
420
- if (!agentId) return;
421
- const { Agent } = await import("@cursor/sdk");
422
- await Agent.archive(agentId, {});
423
- const archived = await Agent.get(agentId, {});
424
- if (archived.archived !== true) {
425
- fail(`cloud agent ${agentId} did not report archived after cleanup`, JSON.stringify(archived, null, 2));
426
- }
427
- console.error(scrubSmokeText(`[cloud-smoke] archived agent ${agentId}`));
495
+ function accountConditionalLane(lanes) {
496
+ const reports = lanes.map((lane) => lane.report).filter(Boolean);
497
+ return {
498
+ name: "passive-artifacts-and-raw-usage",
499
+ status: "passed",
500
+ artifactsObserved: reports.some((report) => Array.isArray(report.artifacts) && report.artifacts.length > 0),
501
+ rawUsageObserved: reports.some((report) => report.usage && Object.keys(report.usage).length > 0),
502
+ observationsValidated: true,
503
+ };
428
504
  }
429
505
 
430
- function assertCloudMetadata(metadata, metadataPath, options = {}) {
431
- if (metadata.providerMeta?.runtime !== "cloud") fail("provider metadata did not record cloud runtime", metadataPath);
432
- if (metadata.send?.bridgeEnabled !== false) fail("cloud send unexpectedly enabled pi bridge", metadataPath);
433
- if (metadata.send?.useNativeToolReplay !== false) fail("cloud send unexpectedly enabled native replay live-run mode", metadataPath);
434
- if (metadata.send?.agentMode !== "agent") fail("cloud send did not use agent mode", metadataPath);
435
- if (!metadata.run?.agentId?.startsWith?.("bc-")) fail("cloud run did not return a cloud agent id", metadataPath);
436
- const providerEventsPath = resolveMetadataArtifactPath(metadataPath, metadata.artifacts?.providerEvents);
437
- const providerEvents = readJsonlIfPresent(providerEventsPath);
438
- const report = providerEvents.find((event) => event.phase === "cloud_run_report")?.payload;
439
- if (!report && options.requireReport === false) return;
440
- if (report?.agentId !== metadata.run.agentId) fail("cloud report did not include the cloud agent id", providerEventsPath ?? metadataPath);
441
- if (report?.runId !== metadata.run.runId) fail("cloud report did not include the cloud run id", providerEventsPath ?? metadataPath);
442
- if (!Array.isArray(report.branches)) fail("cloud report branches were not an array", providerEventsPath ?? metadataPath);
443
- if (report.artifacts !== undefined && !Array.isArray(report.artifacts)) fail("cloud report artifacts were not an array", providerEventsPath ?? metadataPath);
444
- if (report.usage !== undefined && (typeof report.usage !== "object" || report.usage === null)) fail("cloud report usage was not an object", providerEventsPath ?? metadataPath);
506
+ function stageEvidenceSummary(summary) {
507
+ const text = assertCloudSmokeEvidenceSafe(validateCloudSmokeMatrixEvidence(summary));
508
+ mkdirSync(dirname(EVIDENCE_PATH), { recursive: true });
509
+ const temporary = `${EVIDENCE_PATH}.tmp-${process.pid}`;
510
+ writeFileSync(temporary, text, { mode: 0o644 });
511
+ return temporary;
512
+ }
513
+
514
+ function commitEvidenceSummary(temporary) {
515
+ try {
516
+ renameSync(temporary, EVIDENCE_PATH);
517
+ } catch (error) {
518
+ rmSync(temporary, { force: true });
519
+ throw error;
520
+ }
445
521
  }
446
522
 
447
523
  async function main() {
448
- if (!process.env.CURSOR_API_KEY) fail("CURSOR_API_KEY is required for cloud smoke and cleanup");
524
+ if (!process.env.CURSOR_API_KEY) fail("CURSOR_API_KEY is required for cloud smoke and verified cleanup");
449
525
  const timeoutMs = Number(process.env.CURSOR_CLOUD_SMOKE_TIMEOUT_MS || 300000);
450
526
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) fail("CURSOR_CLOUD_SMOKE_TIMEOUT_MS must be a positive number");
451
-
452
527
  const artifactRoot = mkdtempSync(join(tmpdir(), "pi-cursor-cloud-smoke-"));
453
- let agentId;
528
+ installCloudSmokeSignalHandlers(cloudSmokeShutdown, process, () => { process.exitCode = 1; });
454
529
  let failure;
455
530
  console.error(scrubSmokeText(`[cloud-smoke] artifacts: ${artifactRoot}`));
531
+ const { Agent } = await import("@cursor/sdk");
532
+ const contextMatrix = args.has("--context-matrix");
456
533
  try {
457
- if (args.has("--context-matrix")) {
458
- await runContextMatrix({ artifactRoot, timeoutMs });
459
- console.error(scrubSmokeText("[cloud-smoke] context matrix passed"));
460
- } else {
461
- const run = await runPi({ artifactDir: artifactRoot, timeoutMs });
462
- const latestMetadata = readLatestMetadataIfPresent(artifactRoot);
463
- agentId = latestMetadata?.metadata.run?.agentId ?? latestMetadata?.metadata.providerMeta?.cloudAgentId;
464
- if (run.code !== 0) {
465
- fail(`pi cloud smoke exited ${run.code}${run.signal ? ` (${run.signal})` : ""}`, `${run.stderr}\n${run.stdout}`.trim());
466
- }
467
- if (!/cloud-smoke-ok/i.test(run.stdout)) {
468
- fail("cloud smoke output missing exact marker", `${run.stderr}\n${run.stdout}`.trim());
469
- }
470
- if (!latestMetadata) fail("no SDK event debug metadata was written", artifactRoot);
471
- assertCloudMetadata(latestMetadata.metadata, latestMetadata.metadataPath);
472
- }
534
+ await coordinateCloudSmokeReleaseGate({
535
+ throwIfInterrupted: () => cloudSmokeShutdown.throwIfRequested(),
536
+ run: async (state) => {
537
+ cloudSmokeShutdown.throwIfRequested();
538
+ if (contextMatrix) {
539
+ state.lanes = await runContextMatrix({ artifactRoot, timeoutMs });
540
+ console.error("[cloud-smoke] context matrix passed");
541
+ return;
542
+ }
543
+ try {
544
+ state.repository = createThrowawayRepository(
545
+ artifactRoot,
546
+ (created) => { state.repository = created; },
547
+ { cwd: root },
548
+ );
549
+ } catch (error) {
550
+ fail(error instanceof Error ? error.message : String(error), error?.details ?? "");
551
+ }
552
+ const repo = state.repository;
553
+ state.lanes.push(await runCancelLane({ artifactRoot, repo, timeoutMs, Agent }));
554
+ state.lanes.push(...await runBranchAndLifecycleLane({ artifactRoot, repo, timeoutMs, Agent }));
555
+ state.lanes.push(await runDirectPushLane({ artifactRoot, repo, timeoutMs }));
556
+ state.lanes.push(await runMissingBranchLane({ artifactRoot, repo, timeoutMs }));
557
+ state.lanes.push(accountConditionalLane(state.lanes));
558
+ },
559
+ harvestAgentIds: (state) => {
560
+ const allAgentIds = new Set([
561
+ ...cloudAgentIdsFromMetadata(artifactRoot),
562
+ ...cloudAgentIdsFromLifecycleArtifacts(artifactRoot),
563
+ ]);
564
+ for (const lane of state.lanes) {
565
+ if (CLOUD_AGENT_ID_PATTERN.test(lane.agentId ?? "")) allAgentIds.add(lane.agentId);
566
+ for (const turn of [lane.first, lane.second]) {
567
+ if (CLOUD_AGENT_ID_PATTERN.test(turn?.agentId ?? "")) allAgentIds.add(turn.agentId);
568
+ }
569
+ }
570
+ return allAgentIds;
571
+ },
572
+ cleanupAgent: (agentId) => cleanupCloudAgent(Agent, agentId),
573
+ onAgentCleanupError: (error, agentId) => {
574
+ console.error(scrubSmokeText(`[cloud-smoke] cleanup failed for ${agentId}: ${error instanceof Error ? error.message : String(error)}`));
575
+ },
576
+ cleanupRepository: (repo) => deleteThrowawayRepository(repo, { cwd: root }),
577
+ onRepositoryCleanupError: (error, repo) => {
578
+ console.error(scrubSmokeText(`[cloud-smoke] repository cleanup failed for ${repo.fullName}: ${error instanceof Error ? error.message : String(error)}`));
579
+ },
580
+ writeEvidence: async ({ lanes, cleanup, throwawayRepository }) => {
581
+ if (contextMatrix) return;
582
+ const summary = projectCloudSmokeMatrixEvidence({
583
+ model: MODEL,
584
+ lanes,
585
+ cleanup,
586
+ throwawayRepository,
587
+ provenance: buildCloudSmokeEvidenceProvenance({ root }),
588
+ });
589
+ const temporary = stageEvidenceSummary(summary);
590
+ try {
591
+ await checkpointCloudSmokeShutdown(cloudSmokeShutdown);
592
+ commitEvidenceSummary(temporary);
593
+ } catch (error) {
594
+ rmSync(temporary, { force: true });
595
+ throw error;
596
+ }
597
+ },
598
+ });
599
+ await checkpointCloudSmokeShutdown(cloudSmokeShutdown);
473
600
  } catch (error) {
474
601
  failure = error;
475
602
  }
476
- for (const id of new Set([
477
- agentId,
478
- ...cloudAgentIdsFromMetadata(artifactRoot),
479
- ...cloudAgentIdsFromLifecycleArtifacts(artifactRoot),
480
- ].filter((id) => typeof id === "string" && CLOUD_AGENT_ID_PATTERN.test(id)))) {
603
+ if (process.env.CURSOR_CLOUD_SMOKE_KEEP_ARTIFACTS !== "1" && !failure) rmSync(artifactRoot, { recursive: true, force: true });
604
+ else console.error(scrubSmokeText(`[cloud-smoke] retained artifacts: ${artifactRoot}`));
605
+ if (!failure) {
481
606
  try {
482
- await archiveCloudAgent(id);
607
+ await checkpointCloudSmokeShutdown(cloudSmokeShutdown);
483
608
  } catch (error) {
484
- console.error(scrubSmokeText(`[cloud-smoke] cleanup failed for ${id}: ${error instanceof Error ? error.message : String(error)}`));
485
- failure ??= error;
609
+ failure = error;
486
610
  }
487
611
  }
488
- if (process.env.CURSOR_CLOUD_SMOKE_KEEP_ARTIFACTS !== "1" && !failure) {
489
- rmSync(artifactRoot, { recursive: true, force: true });
490
- } else {
491
- console.error(scrubSmokeText(`[cloud-smoke] retained artifacts: ${artifactRoot}`));
492
- }
493
612
  if (failure) throw failure;
494
- console.log("cloud-smoke-ok");
613
+ console.log(contextMatrix ? "cloud-context-smoke-ok" : "cloud-smoke-ok");
495
614
  }
496
615
 
497
616
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {