@stigmer/runner 3.14.0 → 3.14.1

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/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/hermetic-activity.d.ts +245 -0
  3. package/dist/__test-utils__/hermetic-activity.js +369 -0
  4. package/dist/__test-utils__/hermetic-activity.js.map +1 -0
  5. package/dist/__test-utils__/mock-client.d.ts +13 -0
  6. package/dist/__test-utils__/mock-client.js +45 -0
  7. package/dist/__test-utils__/mock-client.js.map +1 -0
  8. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +172 -0
  9. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +331 -0
  10. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -0
  11. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +167 -0
  12. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +239 -0
  13. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -0
  14. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +97 -0
  15. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +132 -0
  16. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -0
  17. package/dist/harness/capabilities.d.ts +71 -0
  18. package/dist/harness/capabilities.js +36 -0
  19. package/dist/harness/capabilities.js.map +1 -0
  20. package/dist/harness/registry.d.ts +67 -0
  21. package/dist/harness/registry.js +112 -0
  22. package/dist/harness/registry.js.map +1 -0
  23. package/dist/harness/types.d.ts +268 -0
  24. package/dist/harness/types.js +55 -0
  25. package/dist/harness/types.js.map +1 -0
  26. package/package.json +4 -4
  27. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +229 -0
  28. package/src/__test-utils__/config-fixture.ts +63 -0
  29. package/src/__test-utils__/harness-contract/contract.ts +536 -0
  30. package/src/__test-utils__/harness-contract/recording-sink.ts +96 -0
  31. package/src/__test-utils__/harness-contract/scripted-adapter.ts +289 -0
  32. package/src/__test-utils__/harness-contract/types.ts +100 -0
  33. package/src/__test-utils__/hermetic-activity.ts +477 -0
  34. package/src/__test-utils__/proto-helpers.ts +25 -0
  35. package/src/__tests__/harness-contract.test.ts +25 -0
  36. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +422 -0
  37. package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +342 -0
  38. package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +166 -0
  39. package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +228 -0
  40. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +180 -0
  41. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn1.status.json +55 -0
  42. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn2.status.json +77 -0
  43. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +126 -0
  44. package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +45 -0
  45. package/src/activities/execute-cursor/__tests__/hermetic/goldens/plain-turn.status.json +48 -0
  46. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-fresh-agent.status.json +53 -0
  47. package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-call.status.json +68 -0
  48. package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +47 -0
  49. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +201 -0
  50. package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +171 -0
  51. package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +156 -0
  52. package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +137 -0
  53. package/src/harness/__tests__/registry.test.ts +167 -0
  54. package/src/harness/capabilities.ts +75 -0
  55. package/src/harness/registry.ts +123 -0
  56. package/src/harness/types.ts +278 -0
@@ -0,0 +1,422 @@
1
+ /**
2
+ * The Cursor slice of the hermetic activity driver: build the fixture record,
3
+ * the scripted SDK and the `Config`, run the REAL `ExecuteCursor` activity under
4
+ * `MockActivityEnvironment`, and hand back what it persisted.
5
+ *
6
+ * Layering (the `approval-contract/` + `gateway-substrate.ts` split): everything
7
+ * harness-agnostic lives in `src/__test-utils__/hermetic-activity.ts` (the
8
+ * activity environment, the execution record behind the client, the temp
9
+ * `~/.stigmer`, the ticking clock, the shutdown signal). This module adds only
10
+ * what is Cursor's: the `@cursor/sdk` double, the model catalog and pricing
11
+ * registry the harness reads at setup, the Cursor `Config` slice, and the
12
+ * per-scenario reset of the harness's module-level caches.
13
+ *
14
+ * What the two `vi.mock` calls a scenario file must carry look like — they are
15
+ * hoisted by vitest and so cannot live here:
16
+ *
17
+ * ```ts
18
+ * vi.mock("@cursor/sdk", async () =>
19
+ * (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule());
20
+ * vi.mock("../../../../client/stigmer-client.js", async () =>
21
+ * (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule());
22
+ * ```
23
+ *
24
+ * Network: the activity's only HTTP fetch is the model registry
25
+ * (`model-pricing-data.ts`, `shared/model-registry.ts`; both fail SOFT to
26
+ * defaults with a 60s failure cache, which would make the goldens depend on
27
+ * DEFAULT_PRICING and log warnings on every run). `fetch` is stubbed to answer
28
+ * one registry document — and to THROW for any other URL, so a new network
29
+ * dependency on the activity path fails the hermetic run instead of leaking.
30
+ *
31
+ * The fixture model is `composer-2.5`, pinned (not Auto) on purpose: Auto short-
32
+ * circuits `resolveServiceTierParams` before the catalog, and a pinned model runs
33
+ * the catalog path, the variant-param pinning, and the `run.wait()` model echo
34
+ * check — more of the production path under the golden.
35
+ */
36
+
37
+ import { execFileSync } from "node:child_process";
38
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
39
+ import { dirname, join } from "node:path";
40
+ import { create } from "@bufbuild/protobuf";
41
+ import type { ModelListItem } from "@cursor/sdk";
42
+ import { vi } from "vitest";
43
+ import {
44
+ AgentExecutionSchema,
45
+ type AgentExecution,
46
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
47
+ import {
48
+ AgentExecutionSpecSchema,
49
+ ExecutionConfigSchema,
50
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
51
+ import { SessionSchema, type Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
52
+ import { SessionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/spec_pb";
53
+ import {
54
+ LocalPathSourceSchema,
55
+ WorkspaceEntrySchema,
56
+ WorkspaceSourceSchema,
57
+ type WorkspaceEntry,
58
+ } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
59
+ import { AgentSchema, type Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
60
+ import { AgentSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
61
+ import {
62
+ AgentInstanceSchema,
63
+ type AgentInstance,
64
+ } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
65
+ import { AgentInstanceSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/spec_pb";
66
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
67
+ import type { Config } from "../../../config.js";
68
+ import type { ExecuteActivityInput } from "../../../shared/activity-input.js";
69
+ import {
70
+ ExecutionRecord,
71
+ ScriptedClock,
72
+ bindHermeticClient,
73
+ runActivityHermetically,
74
+ type ActivityInvocation,
75
+ type HermeticEnvironment,
76
+ type InvocationControls,
77
+ } from "../../../__test-utils__/hermetic-activity.js";
78
+ import { _resetAgentSessionCacheForTests } from "../agent-session-cache.js";
79
+ import { _resetPricingCache } from "../model-pricing-data.js";
80
+ import { resetCatalogCacheForTests } from "../service-tier.js";
81
+ import { _resetRegistryCache } from "../../../shared/model-registry.js";
82
+ import { ScriptedCursorSdk, bindScriptedSdk, type ScriptedSdkOptions } from "./scripted-sdk.js";
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Fixture ids — fixed so goldens are readable and byte-stable
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export const FIXTURE = {
89
+ org: "hermetic-org",
90
+ executionId: "aex_hermetic_0001",
91
+ sessionId: "ses_hermetic_0001",
92
+ agentInstanceId: "ain_hermetic_0001",
93
+ agentId: "agt_hermetic_0001",
94
+ agentName: "hermetic-agent",
95
+ /** The pinned model; in the registry stub AND the SDK catalog below. */
96
+ model: "composer-2.5",
97
+ cursorApiKey: "hermetic-cursor-api-key",
98
+ } as const;
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // The record
102
+ // ---------------------------------------------------------------------------
103
+
104
+ export interface CursorRecordOptions {
105
+ /** The user's message for this execution. */
106
+ readonly message: string;
107
+ /** The agent's instructions (system prompt body). */
108
+ readonly instructions?: string;
109
+ /** Session workspace entries (a `local_path` entry turns on capture mode). */
110
+ readonly workspaceEntries?: WorkspaceEntry[];
111
+ readonly modelName?: string;
112
+ readonly autoApproveAll?: boolean;
113
+ }
114
+
115
+ /** The four resources of one execution, wired by id into a chain. */
116
+ export function cursorExecutionRecord(options: CursorRecordOptions): ExecutionRecord {
117
+ const execution: AgentExecution = create(AgentExecutionSchema, {
118
+ metadata: create(ApiResourceMetadataSchema, {
119
+ id: FIXTURE.executionId,
120
+ org: FIXTURE.org,
121
+ name: FIXTURE.executionId,
122
+ }),
123
+ spec: create(AgentExecutionSpecSchema, {
124
+ sessionId: FIXTURE.sessionId,
125
+ message: options.message,
126
+ autoApproveAll: options.autoApproveAll ?? false,
127
+ executionConfig: create(ExecutionConfigSchema, {
128
+ modelName: options.modelName ?? FIXTURE.model,
129
+ }),
130
+ }),
131
+ });
132
+ const session: Session = create(SessionSchema, {
133
+ metadata: create(ApiResourceMetadataSchema, {
134
+ id: FIXTURE.sessionId,
135
+ org: FIXTURE.org,
136
+ name: FIXTURE.sessionId,
137
+ }),
138
+ spec: create(SessionSpecSchema, {
139
+ agentInstanceId: FIXTURE.agentInstanceId,
140
+ workspaceEntries: options.workspaceEntries ?? [],
141
+ }),
142
+ });
143
+ const agentInstance: AgentInstance = create(AgentInstanceSchema, {
144
+ metadata: create(ApiResourceMetadataSchema, {
145
+ id: FIXTURE.agentInstanceId,
146
+ org: FIXTURE.org,
147
+ name: FIXTURE.agentInstanceId,
148
+ }),
149
+ spec: create(AgentInstanceSpecSchema, { agentId: FIXTURE.agentId }),
150
+ });
151
+ const agent: Agent = create(AgentSchema, {
152
+ metadata: create(ApiResourceMetadataSchema, {
153
+ id: FIXTURE.agentId,
154
+ org: FIXTURE.org,
155
+ name: FIXTURE.agentName,
156
+ }),
157
+ spec: create(AgentSpecSchema, {
158
+ description: "Hermetic fixture agent",
159
+ instructions: options.instructions ?? "You are the hermetic fixture agent. Answer briefly.",
160
+ }),
161
+ });
162
+ return new ExecutionRecord({ execution, session, agentInstance, agent });
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // The registry document and the SDK catalog
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * The model registry document both readers parse (`parsePricingTable` wants
171
+ * `harness: "cursor"` + `pricing`; `parseRegistry` wants `id` + `provider` and
172
+ * reads `capabilities.vision`). One entry, the fixture model, priced at round
173
+ * numbers so the golden's `estimatedCostUsd` is legible.
174
+ */
175
+ export const REGISTRY_DOCUMENT = {
176
+ models: [
177
+ {
178
+ id: FIXTURE.model,
179
+ displayName: "Composer 2.5 (hermetic fixture)",
180
+ provider: "cursor",
181
+ harness: "cursor",
182
+ costTier: "standard",
183
+ featured: true,
184
+ capabilities: { vision: true },
185
+ pricing: {
186
+ inputPricePerMillion: 1.0,
187
+ outputPricePerMillion: 4.0,
188
+ cacheWritePricePerMillion: 1.0,
189
+ cacheReadPricePerMillion: 0.1,
190
+ },
191
+ pricingVariants: {
192
+ fast: {
193
+ inputPricePerMillion: 3.0,
194
+ outputPricePerMillion: 12.0,
195
+ cacheWritePricePerMillion: 3.0,
196
+ cacheReadPricePerMillion: 0.3,
197
+ },
198
+ },
199
+ },
200
+ ],
201
+ } as const;
202
+
203
+ /** The SDK catalog entry for the fixture model: both pinnable params declared. */
204
+ export const SDK_CATALOG: readonly ModelListItem[] = [
205
+ {
206
+ id: FIXTURE.model,
207
+ displayName: "Composer 2.5",
208
+ parameters: [
209
+ { id: "fast", values: [{ value: "true" }, { value: "false" }] },
210
+ { id: "thinking", values: [{ value: "true" }, { value: "false" }] },
211
+ ],
212
+ },
213
+ ];
214
+
215
+ /**
216
+ * Stub `fetch` to answer the registry document and refuse everything else.
217
+ * Returns the URLs fetched, for the "no other network" assertion.
218
+ */
219
+ export function stubRegistryFetch(): { readonly urls: string[]; restore(): void } {
220
+ const urls: string[] = [];
221
+ vi.stubGlobal(
222
+ "fetch",
223
+ vi.fn(async (input: string | URL | Request) => {
224
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
225
+ urls.push(url);
226
+ if (!url.includes("/model-registry")) {
227
+ throw new Error(`hermetic run attempted a non-registry network call: ${url}`);
228
+ }
229
+ return { ok: true, status: 200, json: async () => REGISTRY_DOCUMENT } as unknown as Response;
230
+ }),
231
+ );
232
+ return { urls, restore: () => vi.unstubAllGlobals() };
233
+ }
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Config
237
+ // ---------------------------------------------------------------------------
238
+
239
+ /** The OSS/local runner posture: direct Cursor API key, no proxy, local mode. */
240
+ export function hermeticCursorConfig(env: HermeticEnvironment): Config {
241
+ return {
242
+ taskQueue: "hermetic-test-queue",
243
+ temporalAddress: "localhost:7233",
244
+ temporalNamespace: "default",
245
+ stigmerBackendEndpoint: "http://localhost:7234",
246
+ mcpBridgeEndpoint: null,
247
+ stigmerToken: null,
248
+ cursorApiKey: FIXTURE.cursorApiKey,
249
+ workspaceRootDir: env.workspaceRootDir,
250
+ mode: "local",
251
+ proxyEndpoint: null,
252
+ maxConcurrentActivities: 1,
253
+ idleTimeoutSeconds: null,
254
+ cloudModeEnabled: false,
255
+ checkpointerType: "memory",
256
+ checkpointerProxyEndpoint: null,
257
+ artifactProxyEndpoint: null,
258
+ primaryModel: FIXTURE.model,
259
+ cursorStreamStallTimeoutMs: 180_000,
260
+ agentResolveTimeoutMs: 120_000,
261
+ workspaceLockTimeoutMs: 900_000,
262
+ };
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // The side effects the real SDK performs, for `effect` steps
267
+ // ---------------------------------------------------------------------------
268
+
269
+ /**
270
+ * The primary workspace directory a session with NO workspace entries runs in
271
+ * (`session-root.ts`: `<workspaceRootDir>/sessions/<sessionId>`).
272
+ */
273
+ export function sessionWorkspaceDir(env: HermeticEnvironment): string {
274
+ return join(env.workspaceRootDir, "sessions", FIXTURE.sessionId);
275
+ }
276
+
277
+ /**
278
+ * Run the workspace's installed preToolUse hook exactly as the Cursor SDK does:
279
+ * read `<workspaceRoot>/.cursor/hooks.json`, take the `preToolUse` command the
280
+ * gate installed (an absolute path to the runner's own stable bash script under
281
+ * `~/.stigmer/hitl-gate/<key>/`), and run it with the hook input on stdin. The
282
+ * script resolves the CURRENT turn's state file and denial ledger from the
283
+ * active-turn pointer, walks its parent PIDs to confirm this process is the
284
+ * runner, and prints `{"permission":"allow"|"deny"}`. A deny appends to the
285
+ * ledger the activity's stream loop reads.
286
+ *
287
+ * This is the out-of-process half of deny-and-retry, driven by the real script
288
+ * against the real gate the activity installed — not a simulation of either.
289
+ */
290
+ export function runWorkspaceHook(
291
+ workspaceRoot: string,
292
+ hookInput: object,
293
+ ): { readonly permission: "allow" | "deny" | "?"; readonly raw: string } {
294
+ const hooksJsonPath = join(workspaceRoot, ".cursor", "hooks.json");
295
+ const parsed = JSON.parse(readFileSync(hooksJsonPath, "utf-8")) as {
296
+ hooks?: { preToolUse?: Array<{ command: string }> };
297
+ };
298
+ const command = parsed.hooks?.preToolUse?.[0]?.command;
299
+ if (!command) {
300
+ throw new Error(`runWorkspaceHook: no preToolUse hook installed in ${hooksJsonPath}`);
301
+ }
302
+ const raw = execFileSync("bash", [command], { input: JSON.stringify(hookInput) }).toString();
303
+ const permission = raw.includes('"permission":"deny"')
304
+ ? "deny"
305
+ : raw.includes('"permission":"allow"')
306
+ ? "allow"
307
+ : "?";
308
+ return { permission, raw };
309
+ }
310
+
311
+ /**
312
+ * A git work tree with one committed file, for the file-review capture
313
+ * scenario. Author, committer and both dates are pinned so the commit AND tree
314
+ * object ids are byte-stable — a file-review golden may carry them.
315
+ */
316
+ export function initGitWorkspace(root: string, files: Record<string, string>): void {
317
+ mkdirSync(root, { recursive: true });
318
+ const gitEnv = {
319
+ ...process.env,
320
+ GIT_AUTHOR_NAME: "hermetic",
321
+ GIT_AUTHOR_EMAIL: "hermetic@stigmer.test",
322
+ GIT_COMMITTER_NAME: "hermetic",
323
+ GIT_COMMITTER_EMAIL: "hermetic@stigmer.test",
324
+ GIT_AUTHOR_DATE: "2026-01-01T00:00:00Z",
325
+ GIT_COMMITTER_DATE: "2026-01-01T00:00:00Z",
326
+ };
327
+ const git = (args: string[]): void => {
328
+ execFileSync("git", args, { cwd: root, env: gitEnv, stdio: "ignore" });
329
+ };
330
+ git(["init", "-q", "-b", "main"]);
331
+ for (const [rel, content] of Object.entries(files)) {
332
+ const abs = join(root, rel);
333
+ mkdirSync(dirname(abs), { recursive: true });
334
+ writeFileSync(abs, content, "utf-8");
335
+ }
336
+ git(["add", "-A"]);
337
+ git(["commit", "-q", "-m", "initial"]);
338
+ }
339
+
340
+ /** A session workspace entry mounting an absolute local path (local mode only). */
341
+ export function localPathEntry(name: string, path: string): WorkspaceEntry {
342
+ return create(WorkspaceEntrySchema, {
343
+ name,
344
+ source: create(WorkspaceSourceSchema, {
345
+ source: { case: "localPath", value: create(LocalPathSourceSchema, { path }) },
346
+ }),
347
+ });
348
+ }
349
+
350
+ // ---------------------------------------------------------------------------
351
+ // Running a scenario
352
+ // ---------------------------------------------------------------------------
353
+
354
+ /**
355
+ * Drop every module-level cache the harness keeps across invocations. Called
356
+ * at the START of a scenario, never between its invocations: a deny-and-retry
357
+ * scenario relies on the parked agent surviving from turn 1 into turn 2 exactly
358
+ * as it does in a live worker.
359
+ */
360
+ export function resetCursorModuleState(): void {
361
+ _resetAgentSessionCacheForTests();
362
+ resetCatalogCacheForTests();
363
+ _resetPricingCache();
364
+ _resetRegistryCache();
365
+ }
366
+
367
+ export interface CursorScenario {
368
+ readonly env: HermeticEnvironment;
369
+ readonly clock: ScriptedClock;
370
+ readonly record: ExecutionRecord;
371
+ readonly sdk: ScriptedCursorSdk;
372
+ }
373
+
374
+ export interface CursorScenarioOptions {
375
+ readonly env: HermeticEnvironment;
376
+ readonly clock: ScriptedClock;
377
+ readonly record: ExecutionRecord;
378
+ readonly sdk: ScriptedSdkOptions;
379
+ }
380
+
381
+ /** Bind the record and the SDK for a scenario; resets the harness caches first. */
382
+ export function beginCursorScenario(options: CursorScenarioOptions): CursorScenario {
383
+ resetCursorModuleState();
384
+ const sdk = new ScriptedCursorSdk(options.sdk);
385
+ bindScriptedSdk(sdk);
386
+ bindHermeticClient(options.record.client());
387
+ return { env: options.env, clock: options.clock, record: options.record, sdk };
388
+ }
389
+
390
+ export interface CursorTurnOptions {
391
+ /** Empty for a first invocation; the agent id for a HITL reinvocation. */
392
+ readonly threadId?: string;
393
+ /** The HITL cycle index (0 on the first turn); mints the change-set id. */
394
+ readonly turnSeq?: number;
395
+ readonly onControls?: (controls: InvocationControls) => void;
396
+ }
397
+
398
+ /**
399
+ * Run ONE `ExecuteCursor` invocation for the scenario. The activities are
400
+ * constructed per invocation (the factory constructs its client, which is the
401
+ * one bound at `beginCursorScenario`).
402
+ */
403
+ export async function runCursorTurn(
404
+ scenario: CursorScenario,
405
+ options: CursorTurnOptions = {},
406
+ ): Promise<ActivityInvocation> {
407
+ // Imported lazily so the scenario file's `vi.mock` declarations are in
408
+ // force before the activity module (and, through it, `@cursor/sdk` and the
409
+ // client) is loaded.
410
+ const { createCursorActivities } = await import("../index.js");
411
+ const activities = createCursorActivities(hermeticCursorConfig(scenario.env));
412
+ // The typed wire shape the control plane's workflow sends (activity-input.ts).
413
+ const input: ExecuteActivityInput = {
414
+ execution_id: scenario.record.executionId,
415
+ thread_id: options.threadId ?? "",
416
+ turn_seq: options.turnSeq ?? 0,
417
+ };
418
+ return runActivityHermetically(activities.ExecuteCursor, [input], {
419
+ taskQueue: "hermetic-test-queue",
420
+ onControls: options.onControls,
421
+ });
422
+ }