@cursor/july 0.1.95 → 0.1.96

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 (220) hide show
  1. package/AGENTS.md +3 -4
  2. package/dist/channels/deployments/types.d.ts +1 -1
  3. package/dist/channels/deployments/types.d.ts.map +1 -1
  4. package/dist/channels/deployments/types.js +1 -0
  5. package/dist/channels/origin/api.d.ts +59 -13
  6. package/dist/channels/origin/api.d.ts.map +1 -1
  7. package/dist/channels/origin/api.js +168 -27
  8. package/dist/channels/origin/binding.d.ts +2 -2
  9. package/dist/channels/origin/checks.d.ts +6 -6
  10. package/dist/channels/origin/checks.js +6 -6
  11. package/dist/channels/origin/cursor-account.d.ts +5 -3
  12. package/dist/channels/origin/cursor-account.d.ts.map +1 -1
  13. package/dist/channels/origin/cursor-account.js +5 -3
  14. package/dist/channels/slack/api.d.ts +9 -0
  15. package/dist/channels/slack/api.d.ts.map +1 -1
  16. package/dist/channels/slack/api.js +36 -5
  17. package/dist/channels/slack/defaults.d.ts.map +1 -1
  18. package/dist/channels/slack/defaults.js +17 -46
  19. package/dist/channels/slack/dispatch.d.ts +7 -1
  20. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  21. package/dist/channels/slack/dispatch.js +47 -23
  22. package/dist/channels/slack/inbound.d.ts +8 -0
  23. package/dist/channels/slack/inbound.d.ts.map +1 -1
  24. package/dist/channels/slack/inbound.js +32 -0
  25. package/dist/channels/slack/live-delivery.d.ts +26 -29
  26. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  27. package/dist/channels/slack/live-delivery.js +172 -73
  28. package/dist/channels/slack/manifest.d.ts.map +1 -1
  29. package/dist/channels/slack/manifest.js +5 -1
  30. package/dist/channels/slack/nudge-carrier.d.ts +2 -0
  31. package/dist/channels/slack/nudge-carrier.d.ts.map +1 -1
  32. package/dist/channels/slack/nudge-carrier.js +48 -1
  33. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  34. package/dist/channels/slack/slack-channel.js +2 -2
  35. package/dist/docs/404.html +2 -2
  36. package/dist/docs/ab.html +4 -4
  37. package/dist/docs/assets/{app.BBj0klBO.js → app.jXleCmOc.js} +1 -1
  38. package/dist/docs/assets/chunks/@localSearchIndexroot.DFoGuETX.js +1 -0
  39. package/dist/docs/assets/chunks/{VPLocalSearchBox.CUEXpL78.js → VPLocalSearchBox.8LhyglIT.js} +1 -1
  40. package/dist/docs/assets/chunks/{theme.DabmQnia.js → theme.9ged-pr2.js} +2 -2
  41. package/dist/docs/assets/index.md.CI93Ltlb.js +5 -0
  42. package/dist/docs/assets/{index.md.BoHaMdeZ.lean.js → index.md.CI93Ltlb.lean.js} +1 -1
  43. package/dist/docs/assets/{reference_cli.md.BsMOxDvh.js → reference_cli.md.CCnG-nJx.js} +2 -1
  44. package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.js +2 -0
  45. package/dist/docs/assets/templates_code-wiki.md.CDYZIBFN.lean.js +1 -0
  46. package/dist/docs/building-with-agents.html +4 -4
  47. package/dist/docs/concepts.html +4 -4
  48. package/dist/docs/deployment.html +4 -4
  49. package/dist/docs/evals.html +4 -4
  50. package/dist/docs/guides/agent-to-agent.html +4 -4
  51. package/dist/docs/guides/cloud-runtime.html +4 -4
  52. package/dist/docs/guides/convert-automation.html +4 -4
  53. package/dist/docs/guides/github.html +4 -4
  54. package/dist/docs/guides/human-in-the-loop.html +4 -4
  55. package/dist/docs/guides/mcp-oauth.html +4 -4
  56. package/dist/docs/guides/opentelemetry.html +4 -4
  57. package/dist/docs/guides/slack.html +4 -4
  58. package/dist/docs/guides/webhooks.html +4 -4
  59. package/dist/docs/hashmap.json +1 -1
  60. package/dist/docs/hillclimbing.html +4 -4
  61. package/dist/docs/index.html +6 -6
  62. package/dist/docs/index.md +3 -1
  63. package/dist/docs/llms-full.txt +102 -3
  64. package/dist/docs/llms.txt +1 -0
  65. package/dist/docs/quickstart.html +4 -4
  66. package/dist/docs/reference/agent-config.html +4 -4
  67. package/dist/docs/reference/artifacts.html +4 -4
  68. package/dist/docs/reference/channels.html +4 -4
  69. package/dist/docs/reference/cli.html +7 -6
  70. package/dist/docs/reference/cli.md +3 -2
  71. package/dist/docs/reference/connections.html +4 -4
  72. package/dist/docs/reference/hooks.html +4 -4
  73. package/dist/docs/reference/http-api.html +4 -4
  74. package/dist/docs/reference/instructions.html +4 -4
  75. package/dist/docs/reference/playground.html +4 -4
  76. package/dist/docs/reference/project-layout.html +4 -4
  77. package/dist/docs/reference/prompt.html +4 -4
  78. package/dist/docs/reference/schedules.html +4 -4
  79. package/dist/docs/reference/sessions.html +4 -4
  80. package/dist/docs/reference/skills.html +4 -4
  81. package/dist/docs/reference/subagents.html +4 -4
  82. package/dist/docs/reference/tools.html +4 -4
  83. package/dist/docs/scaffolding-agents.html +4 -4
  84. package/dist/docs/storage.html +4 -4
  85. package/dist/docs/templates/agentic-owners.html +4 -4
  86. package/dist/docs/templates/code-wiki.html +28 -0
  87. package/dist/docs/templates/code-wiki.md +91 -0
  88. package/dist/docs/templates/demo.html +5 -5
  89. package/dist/docs/templates/pr-autofixer.html +4 -4
  90. package/dist/docs/templates/security-help.html +4 -4
  91. package/dist/docs/templates/security-reviewer.html +5 -5
  92. package/dist/docs/templates/triage.html +4 -4
  93. package/dist/docs/troubleshooting.html +4 -4
  94. package/dist/files-backends/cursor-hosted.d.ts +26 -4
  95. package/dist/files-backends/cursor-hosted.d.ts.map +1 -1
  96. package/dist/files-backends/cursor-hosted.js +32 -4
  97. package/dist/internal/advertise-tools.d.ts +2 -0
  98. package/dist/internal/advertise-tools.d.ts.map +1 -1
  99. package/dist/internal/advertise-tools.js +15 -1
  100. package/dist/internal/bounded.d.ts +7 -0
  101. package/dist/internal/bounded.d.ts.map +1 -0
  102. package/dist/internal/bounded.js +32 -0
  103. package/dist/internal/cli-deploy.d.ts.map +1 -1
  104. package/dist/internal/cli-deploy.js +7 -1
  105. package/dist/internal/conversation-mirror.d.ts +11 -8
  106. package/dist/internal/conversation-mirror.d.ts.map +1 -1
  107. package/dist/internal/conversation-mirror.js +116 -55
  108. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  109. package/dist/internal/cursor/backend-client.js +22 -48
  110. package/dist/internal/cursor/session-token.d.ts +45 -0
  111. package/dist/internal/cursor/session-token.d.ts.map +1 -0
  112. package/dist/internal/cursor/session-token.js +99 -0
  113. package/dist/internal/cursor-agent-template.d.ts +1 -1
  114. package/dist/internal/cursor-agent-template.d.ts.map +1 -1
  115. package/dist/internal/cursor-agent-template.js +1 -0
  116. package/dist/internal/framework-file-storage.d.ts +61 -0
  117. package/dist/internal/framework-file-storage.d.ts.map +1 -0
  118. package/dist/internal/framework-file-storage.js +204 -0
  119. package/dist/internal/framework-storage-selection.d.ts +11 -5
  120. package/dist/internal/framework-storage-selection.d.ts.map +1 -1
  121. package/dist/internal/framework-storage-selection.js +38 -11
  122. package/dist/internal/hosted-delivery.d.ts +7 -0
  123. package/dist/internal/hosted-delivery.d.ts.map +1 -1
  124. package/dist/internal/hosted-delivery.js +4 -0
  125. package/dist/internal/init-project.d.ts.map +1 -1
  126. package/dist/internal/init-project.js +2 -0
  127. package/dist/internal/install-cursor-skills.d.ts.map +1 -1
  128. package/dist/internal/install-cursor-skills.js +1 -3
  129. package/dist/internal/managed-delivery.d.ts +80 -0
  130. package/dist/internal/managed-delivery.d.ts.map +1 -0
  131. package/dist/internal/managed-delivery.js +86 -0
  132. package/dist/internal/runtime-dispatch-runner.d.ts +5 -1
  133. package/dist/internal/runtime-dispatch-runner.d.ts.map +1 -1
  134. package/dist/internal/runtime-dispatch-runner.js +7 -0
  135. package/dist/internal/sdk-runner.d.ts +11 -0
  136. package/dist/internal/sdk-runner.d.ts.map +1 -1
  137. package/dist/internal/sdk-runner.js +39 -8
  138. package/dist/internal/server.js +1 -0
  139. package/dist/internal/session-engine.d.ts +13 -0
  140. package/dist/internal/session-engine.d.ts.map +1 -1
  141. package/dist/internal/session-engine.js +62 -3
  142. package/dist/internal/steering.d.ts +15 -0
  143. package/dist/internal/steering.d.ts.map +1 -0
  144. package/dist/internal/steering.js +18 -0
  145. package/dist/playground/assets/index-B1DDvBeC.css +1 -0
  146. package/dist/playground/assets/{index-CZA1uEWA.js → index-Cx9CclK_.js} +51 -51
  147. package/dist/playground/index.html +2 -2
  148. package/dist/storage-backends/cursor-hosted.d.ts +8 -5
  149. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  150. package/dist/storage-backends/cursor-hosted.js +4 -3
  151. package/dist/types.d.ts +2 -1
  152. package/dist/types.d.ts.map +1 -1
  153. package/docs/README.md +3 -1
  154. package/docs/reference/cli.md +3 -2
  155. package/docs/templates/code-wiki.md +96 -0
  156. package/package.json +2 -2
  157. package/skills/ab/SKILL.md +13 -97
  158. package/skills/create-agent/SKILL.md +123 -223
  159. package/skills/debug/SKILL.md +34 -68
  160. package/skills/evals/SKILL.md +55 -180
  161. package/skills/framework-map/SKILL.md +83 -145
  162. package/skills/github/SKILL.md +53 -111
  163. package/skills/hillclimb/SKILL.md +40 -163
  164. package/skills/mcp-auth/SKILL.md +48 -107
  165. package/skills/otel/SKILL.md +22 -68
  166. package/skills/setup-slack/SKILL.md +77 -208
  167. package/src/channels/deployments/types.ts +1 -0
  168. package/src/channels/origin/api.ts +242 -30
  169. package/src/channels/origin/binding.ts +2 -2
  170. package/src/channels/origin/checks.ts +6 -6
  171. package/src/channels/origin/cursor-account.ts +5 -3
  172. package/src/channels/slack/api.ts +41 -5
  173. package/src/channels/slack/defaults.ts +15 -49
  174. package/src/channels/slack/dispatch.ts +56 -22
  175. package/src/channels/slack/inbound.ts +43 -0
  176. package/src/channels/slack/live-delivery.ts +235 -114
  177. package/src/channels/slack/manifest.ts +5 -1
  178. package/src/channels/slack/nudge-carrier.ts +48 -1
  179. package/src/channels/slack/slack-channel.ts +2 -0
  180. package/src/files-backends/cursor-hosted.ts +40 -7
  181. package/src/internal/advertise-tools.ts +18 -2
  182. package/src/internal/bounded.ts +28 -0
  183. package/src/internal/cli-deploy.ts +12 -0
  184. package/src/internal/conversation-mirror.ts +145 -69
  185. package/src/internal/cursor/backend-client.ts +34 -58
  186. package/src/internal/cursor/session-token.ts +115 -0
  187. package/src/internal/cursor-agent-template.ts +1 -0
  188. package/src/internal/framework-file-storage.ts +222 -0
  189. package/src/internal/framework-storage-selection.ts +51 -11
  190. package/src/internal/hosted-delivery.ts +10 -0
  191. package/src/internal/init-project.ts +2 -0
  192. package/src/internal/install-cursor-skills.ts +1 -4
  193. package/src/internal/managed-delivery.ts +125 -0
  194. package/src/internal/runtime-dispatch-runner.ts +10 -0
  195. package/src/internal/sdk-runner.ts +56 -8
  196. package/src/internal/server.ts +2 -0
  197. package/src/internal/session-engine.ts +64 -4
  198. package/src/internal/steering.ts +17 -0
  199. package/src/storage-backends/cursor-hosted.ts +9 -7
  200. package/src/types.ts +2 -1
  201. package/templates/code-wiki/README.md +34 -0
  202. package/templates/code-wiki/agent/agent.ts +8 -0
  203. package/templates/code-wiki/agent/channels/github.ts +144 -0
  204. package/templates/code-wiki/agent/instructions.md +39 -0
  205. package/templates/code-wiki/agent/lib/repos.ts +5 -0
  206. package/templates/code-wiki/agent/lib/wiki.ts +22 -0
  207. package/templates/code-wiki/agent/tools/apply_wiki.ts +219 -0
  208. package/templates/code-wiki/evals/evals.config.ts +5 -0
  209. package/templates/code-wiki/evals/wiki.eval.ts +132 -0
  210. package/templates/code-wiki/init.json +25 -0
  211. package/templates/code-wiki/package.json +18 -0
  212. package/templates/code-wiki/tsconfig.json +12 -0
  213. package/dist/docs/assets/chunks/@localSearchIndexroot.oqPawjiD.js +0 -1
  214. package/dist/docs/assets/index.md.BoHaMdeZ.js +0 -5
  215. package/dist/internal/deployment-scm-event-stream.d.ts +0 -116
  216. package/dist/internal/deployment-scm-event-stream.d.ts.map +0 -1
  217. package/dist/internal/deployment-scm-event-stream.js +0 -474
  218. package/dist/playground/assets/index-CF7hUDnQ.css +0 -1
  219. package/src/internal/deployment-scm-event-stream.ts +0 -593
  220. /package/dist/docs/assets/{reference_cli.md.BsMOxDvh.lean.js → reference_cli.md.CCnG-nJx.lean.js} +0 -0
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Envelope-carried installation config for managed-product deliveries
3
+ * (plan 0002 Stack H1; envelope shape from Stack B1).
4
+ *
5
+ * On the v2 architecture a managed product (security-reviewer, …) is many
6
+ * single-tenant deployments: the pre-signal trigger gate matches the
7
+ * customer's automation trigger before a workflow starts and stamps the
8
+ * matched automation uuid + an opaque installation-config reference on the
9
+ * attributed session envelope. The delivery activity forwards the matched
10
+ * installation-config document to the fork **inside the webhook payload it
11
+ * POSTs to the channel**, under {@link MANAGED_DELIVERY_PAYLOAD_KEY} — the
12
+ * same embed-in-payload pattern the legacy deployment event stream used
13
+ * for its source marker.
14
+ *
15
+ * This module is the one contract both sides share: the control-plane
16
+ * delivery path embeds with {@link withManagedDeliveryConfig}; the agent's
17
+ * channels detect and read with {@link parseManagedDeliveryConfig}. The
18
+ * document mirrors the sync layer's `ManagedAutomationConfigDoc` **policy**
19
+ * fields that "ride the delivery envelope to the fork" (posting mode,
20
+ * managed config, trigger snapshots for the channel's staleness guard);
21
+ * readers ignore unknown fields so a newer sync can add material without
22
+ * breaking older forks. The automation's MCP server refs deliberately do
23
+ * NOT ride here: they travel on the hosted-admission envelope's
24
+ * `managedAutomation` slice (plan 0002 G2, `hosted-delivery-protocol.ts` /
25
+ * `hosted-managed-automation.ts`) so one mechanism owns MCP attach.
26
+ *
27
+ * INTERNAL: first-party host plumbing shared by agents in `factory/*`,
28
+ * consumed via relative source imports. Deliberately excluded from the
29
+ * published `@cursor/july` npm package (see the `files` whitelist).
30
+ */
31
+
32
+ /**
33
+ * Key inside the delivered webhook payload under which the delivery
34
+ * activity embeds the matched installation config. Chosen to never
35
+ * collide with a provider webhook field (GitHub and Origin payloads have
36
+ * no `cursor*` keys).
37
+ */
38
+ export const MANAGED_DELIVERY_PAYLOAD_KEY = "cursorManagedDelivery" as const;
39
+
40
+ /**
41
+ * The matched installation-config document as it rides the delivery to
42
+ * the fork. Required: the matched automation uuid (B1 attribution).
43
+ * Everything else is optional; absent fields keep the channel's default
44
+ * posture (post, all modules enabled, no staleness guard).
45
+ */
46
+ export interface ManagedDeliveryInstallationConfig {
47
+ /** Source managed automation uuid the pre-signal gate matched. */
48
+ matchedAutomationUuid: string;
49
+ /** Opaque `<installationId>@<resourceVersion>` config reference. */
50
+ installationConfigRef?: string;
51
+ managedType?: string;
52
+ name?: string;
53
+ /**
54
+ * Whether product output (PR comments, …) posts for this installation,
55
+ * or the run stays check/shadow-only. Channels post only on `"post"`
56
+ * (or when the field is absent) — any other value fails closed to
57
+ * shadow, so a newer mode name never posts by accident.
58
+ */
59
+ postingMode?: string;
60
+ managedConfig?: Record<string, unknown>;
61
+ /**
62
+ * The automation's git triggers as `GitTrigger.toJson()` snapshots.
63
+ * The channel re-checks the delivery against them (staleness guard:
64
+ * the config may have changed between admission and delivery).
65
+ */
66
+ triggerSnapshots?: unknown[];
67
+ }
68
+
69
+ function isRecord(value: unknown): value is Record<string, unknown> {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
72
+
73
+ function nonEmptyString(value: unknown): string | undefined {
74
+ if (typeof value !== "string") {
75
+ return undefined;
76
+ }
77
+ const trimmed = value.trim();
78
+ return trimmed === "" ? undefined : trimmed;
79
+ }
80
+
81
+ /**
82
+ * Read the embedded installation config out of a delivered webhook
83
+ * payload. Returns undefined for payloads without the key (legacy
84
+ * deliveries, host relay, fixtures) and for documents missing the
85
+ * required matched automation uuid — the channel then runs its legacy
86
+ * path unchanged. Malformed optional fields are dropped, not fatal.
87
+ */
88
+ export function parseManagedDeliveryConfig(
89
+ payload: unknown
90
+ ): ManagedDeliveryInstallationConfig | undefined {
91
+ if (!isRecord(payload)) {
92
+ return undefined;
93
+ }
94
+ const doc = payload[MANAGED_DELIVERY_PAYLOAD_KEY];
95
+ if (!isRecord(doc)) {
96
+ return undefined;
97
+ }
98
+ const matchedAutomationUuid = nonEmptyString(doc.matchedAutomationUuid);
99
+ if (matchedAutomationUuid === undefined) {
100
+ return undefined;
101
+ }
102
+ return {
103
+ matchedAutomationUuid,
104
+ installationConfigRef: nonEmptyString(doc.installationConfigRef),
105
+ managedType: nonEmptyString(doc.managedType),
106
+ name: nonEmptyString(doc.name),
107
+ postingMode: nonEmptyString(doc.postingMode),
108
+ managedConfig: isRecord(doc.managedConfig) ? doc.managedConfig : undefined,
109
+ triggerSnapshots: Array.isArray(doc.triggerSnapshots)
110
+ ? doc.triggerSnapshots
111
+ : undefined,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Embed one matched installation config into the webhook payload the
117
+ * delivery activity POSTs to the channel. Pure; used by the control-plane
118
+ * delivery path and by channel tests building fixture envelopes.
119
+ */
120
+ export function withManagedDeliveryConfig(
121
+ payload: Record<string, unknown>,
122
+ config: ManagedDeliveryInstallationConfig
123
+ ): Record<string, unknown> {
124
+ return { ...payload, [MANAGED_DELIVERY_PAYLOAD_KEY]: config };
125
+ }
@@ -13,6 +13,7 @@ import type {
13
13
  AgentRunner,
14
14
  PrewarmOutcome,
15
15
  RunnerPrewarmRequest,
16
+ SteerAckOutcome,
16
17
  } from "./sdk-runner.js";
17
18
 
18
19
  export interface RuntimeDispatchingRunnerOptions {
@@ -45,6 +46,15 @@ export class RuntimeDispatchingRunner implements AgentRunner {
45
46
  return this.options.sdkRunner.runTurn(request);
46
47
  }
47
48
 
49
+ async steer(request: {
50
+ sessionId: string;
51
+ text: string;
52
+ }): Promise<SteerAckOutcome> {
53
+ return (
54
+ (await this.options.sdkRunner.steer?.(request)) ?? "revert_to_followup"
55
+ );
56
+ }
57
+
48
58
  async prewarm(request: RunnerPrewarmRequest): Promise<PrewarmOutcome> {
49
59
  // Prewarming is a local-harness concern; the engine only calls it for
50
60
  // local-runtime agents.
@@ -16,6 +16,7 @@ import {
16
16
  createAgentPlatform,
17
17
  type LocalAgentStore,
18
18
  type ModelSelection,
19
+ type Run,
19
20
  type RunResult,
20
21
  type SDKAgent,
21
22
  } from "@cursor/sdk";
@@ -31,10 +32,18 @@ import {
31
32
  import { mapSdkTokenUsage, usageTokenSum } from "./cloud-turn-cost.js";
32
33
  import type { ConversationMirror } from "./conversation-mirror.js";
33
34
  import { resolveApiKeySync } from "./cursor/credentials.js";
35
+ import { describeError } from "./describe-error.js";
36
+
37
+ /** Mirrors @cursor/sdk's SteerAckOutcome; the published dist may predate it. */
38
+ export type SteerAckOutcome = "complete_delivered" | "revert_to_followup";
34
39
 
35
40
  /** Internal seam between the session engine and the Cursor SDK harness. */
36
41
  export interface AgentRunner {
37
42
  runTurn(request: RunnerTurnRequest): Promise<TurnOutcome>;
43
+ steer?(request: {
44
+ sessionId: string;
45
+ text: string;
46
+ }): Promise<SteerAckOutcome>;
38
47
  /**
39
48
  * Resolve a local workspace before any session needs it, so the first turn
40
49
  * is not the one that pays for it. Best effort, hence the reported outcome:
@@ -193,6 +202,7 @@ function retryReason(
193
202
 
194
203
  export class CursorSdkRunner implements AgentRunner {
195
204
  private readonly agents = new Map<string, CachedAgentHandle>();
205
+ private readonly activeRuns = new Map<string, Run>();
196
206
  private readonly stores = new Map<string, Promise<LocalAgentStore>>();
197
207
  private readonly prewarmReleases: Array<() => Promise<void>> = [];
198
208
 
@@ -322,15 +332,9 @@ export class CursorSdkRunner implements AgentRunner {
322
332
  ) {
323
333
  await request.onRequestId(run.requestId);
324
334
  }
335
+ this.activeRuns.set(request.sessionId, run);
325
336
  try {
326
337
  const result = await run.wait();
327
- // Mirror the conversation before replying: on hosted v2 the pod can
328
- // be parked as soon as the execution reply lands, so a
329
- // fire-and-forget write would race VM teardown. Failed turns still
330
- // checkpoint the user message, so they persist too.
331
- if (request.runtime === "local") {
332
- await this.persistConversation(request, agent.agentId);
333
- }
334
338
  let outcome = toTurnOutcome(result);
335
339
  if (outcome.status === "error") {
336
340
  // An error result usually means the handle's transport is poisoned
@@ -353,7 +357,25 @@ export class CursorSdkRunner implements AgentRunner {
353
357
  }
354
358
  return outcome;
355
359
  } finally {
360
+ if (this.activeRuns.get(request.sessionId) === run) {
361
+ this.activeRuns.delete(request.sessionId);
362
+ }
356
363
  signal?.removeEventListener("abort", onAbort);
364
+ // Mirror the conversation before replying: on hosted v2 the pod can
365
+ // be parked as soon as the execution reply lands, so a
366
+ // fire-and-forget write would race VM teardown. In the `finally` so
367
+ // a `wait()` that THROWS (transport death mid-turn, an abort with
368
+ // no follow-up) still persists the checkpoints the turn already
369
+ // wrote — the store is local SQLite, safe to read on the throw
370
+ // path, and persistConversation swallows its own errors. Guarded
371
+ // here (not just inside the helper) so unmirrored turns keep their
372
+ // exact post-wait sequencing.
373
+ if (
374
+ request.runtime === "local" &&
375
+ this.options.conversationMirror !== undefined
376
+ ) {
377
+ await this.persistConversation(request, agent.agentId);
378
+ }
357
379
  }
358
380
  } catch (error) {
359
381
  // Drop the cached agent so a dead Connect/HTTP2 session cannot poison
@@ -363,6 +385,28 @@ export class CursorSdkRunner implements AgentRunner {
363
385
  }
364
386
  }
365
387
 
388
+ async steer(request: {
389
+ sessionId: string;
390
+ text: string;
391
+ }): Promise<SteerAckOutcome> {
392
+ const run = this.activeRuns.get(request.sessionId) as
393
+ | (Run & {
394
+ steer?: (text: string) => Promise<SteerAckOutcome>;
395
+ })
396
+ | undefined;
397
+ if (run?.steer === undefined) {
398
+ return "revert_to_followup";
399
+ }
400
+ try {
401
+ return await run.steer(request.text);
402
+ } catch (error) {
403
+ this.log(
404
+ `[agent-sdk] session ${request.sessionId}: steer failed: ${describeError(error)}`
405
+ );
406
+ return "revert_to_followup";
407
+ }
408
+ }
409
+
366
410
  /**
367
411
  * Build the local executor for a workspace before any session asks for it.
368
412
  * The warmed executor is reference counted, so the lease is held until
@@ -484,7 +528,11 @@ export class CursorSdkRunner implements AgentRunner {
484
528
  private async buildLocalOptions(
485
529
  request: RunnerPrewarmRequest
486
530
  ): Promise<AgentOptions> {
487
- const store = await this.resolveStore(request);
531
+ // Not resolveStore: an injected store is used synchronously (no
532
+ // microtask, no thenable assimilation), exactly as before the mirror
533
+ // existed.
534
+ const store =
535
+ this.options.store ?? (await this.getStore(request.runnerStateDir));
488
536
  return {
489
537
  ...this.buildSharedOptions(request),
490
538
  // The built-in tool allowlist rides AgentOptions, not `local`, and is
@@ -1286,6 +1286,8 @@ function mountAgentRoutes(
1286
1286
  channelId,
1287
1287
  hostedSessionId,
1288
1288
  }),
1289
+ drainSessionDurability: (sessionId) =>
1290
+ engine.drainSessionDurability(sessionId),
1289
1291
  });
1290
1292
  }
1291
1293
  // Playground lists/opens Slack (and other channel) sessions across
@@ -136,10 +136,11 @@ import {
136
136
  SCHEDULE_CHANNEL_PREFIX,
137
137
  } from "./prompt-context.js";
138
138
  import { assertCloudCanHonorAccountServersFilters } from "./resolved-connections.js";
139
- import type { AgentRunner } from "./sdk-runner.js";
139
+ import type { AgentRunner, SteerAckOutcome } from "./sdk-runner.js";
140
140
  import { sessionExternalLinks } from "./session-links.js";
141
141
  import { SessionStore } from "./session-store.js";
142
142
  import { syncSkillsToStore } from "./skills-sync.js";
143
+ import { steeringEnabled } from "./steering.js";
143
144
  import { StorageCoordinator } from "./storage-coordinator.js";
144
145
  import type { FrameworkSessionStore } from "./storage-roles.js";
145
146
  import { decideToolCall, resolveEffect } from "./tool-policy.js";
@@ -698,6 +699,9 @@ export class SessionEngine {
698
699
  ) {
699
700
  return;
700
701
  }
702
+ if (steeringEnabled()) {
703
+ return;
704
+ }
701
705
  try {
702
706
  await materializeSlackNudgeCarrier({
703
707
  harnessCwd,
@@ -1359,18 +1363,52 @@ export class SessionEngine {
1359
1363
  });
1360
1364
  }
1361
1365
 
1366
+ private async steerIntoRunningTurn(
1367
+ sessionId: string,
1368
+ message: string
1369
+ ): Promise<SteerAckOutcome> {
1370
+ if (this.runner.steer === undefined) {
1371
+ return "revert_to_followup";
1372
+ }
1373
+ try {
1374
+ return await this.runner.steer({ sessionId, text: message });
1375
+ } catch (error) {
1376
+ this.logger(
1377
+ `[session] steer failed for ${sessionId}: ${describeError(error)}`
1378
+ );
1379
+ return "revert_to_followup";
1380
+ }
1381
+ }
1382
+
1362
1383
  private async admitTurn(
1363
1384
  sessionId: string,
1364
1385
  message: string,
1365
1386
  options: AdmitTurnOptions = {}
1366
1387
  ): Promise<boolean> {
1367
1388
  const admission = options.admission ?? "preempt";
1389
+ if (
1390
+ admission === "coalesce" &&
1391
+ steeringEnabled() &&
1392
+ this.isTurnRunning(sessionId)
1393
+ ) {
1394
+ const toolCtx = this.activeToolContexts.get(sessionId);
1395
+ const steerOutcome = await this.steerIntoRunningTurn(sessionId, message);
1396
+ if (steerOutcome === "complete_delivered") {
1397
+ this.logger(`[session] steered into running turn ${sessionId}`);
1398
+ if (toolCtx !== undefined) {
1399
+ toolCtx.emit({ type: "message.received", data: { text: message } });
1400
+ toolCtx.emit({ type: "nudge.delivered", data: {} });
1401
+ }
1402
+ return true;
1403
+ }
1404
+ }
1405
+ // Re-checked after the steer: a turn that ended during it falls through
1406
+ // and this message starts the next turn itself. Enqueueing here instead
1407
+ // would orphan it — the completion drain already ran.
1368
1408
  if (admission === "coalesce" && this.isTurnRunning(sessionId)) {
1369
1409
  this.enqueueCoalesced(sessionId, {
1370
1410
  message,
1371
- ...(options.sourceTs === undefined
1372
- ? {}
1373
- : { sourceTs: options.sourceTs }),
1411
+ sourceTs: options.sourceTs,
1374
1412
  });
1375
1413
  this.logger(`[session] coalesce enqueue ${sessionId}`);
1376
1414
  return true;
@@ -3394,6 +3432,28 @@ export class SessionEngine {
3394
3432
  // Session handles
3395
3433
  // ==========================================================================
3396
3434
 
3435
+ /**
3436
+ * Wait until everything this session has already emitted is durably
3437
+ * mirrored: its dispatch chain first (the chain's tail runs the turn-end
3438
+ * storage flush, so the record carries the boundary event's handler
3439
+ * mutations), then the storage delivery queue. Hosted v2 awaits this
3440
+ * before the execution reply — the pod can be parked the moment the
3441
+ * reply lands, and an unflushed session record would strand a
3442
+ * conversation whose blobs persisted but whose record (which selects the
3443
+ * conversation to hydrate) never reached the sink. No-op without a
3444
+ * storage mirror.
3445
+ */
3446
+ async drainSessionDurability(sessionId: string): Promise<void> {
3447
+ if (this.storage === undefined) {
3448
+ return;
3449
+ }
3450
+ const chain = this.dispatchChains.get(sessionId);
3451
+ if (chain !== undefined) {
3452
+ await chain.catch(() => {});
3453
+ }
3454
+ await this.storage.whenIdle();
3455
+ }
3456
+
3397
3457
  async waitForCompletion(sessionId: string): Promise<TurnOutcome> {
3398
3458
  // Drain admission first so pendingTurns → activeTurns is visible before we
3399
3459
  // decide the session is idle (reminders use this for queueBehindTurn).
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Deploy-time switch between the two mid-turn follow-up carriers.
3
+ *
4
+ * With ENABLE_STEERING set ("1"/"true"), a Slack follow-up arriving mid-turn
5
+ * is steered into the running turn via Run.steer and the nudge-carrier file
6
+ * store is left uninstalled. Unset (the default), the pre-steering behavior
7
+ * is fully preserved: the carrier is materialized and follow-ups ride the
8
+ * nudge store to the running turn's next tool boundary.
9
+ *
10
+ * Read per call rather than cached so tests can flip it, and because this is
11
+ * a rollout switch: once steering has proven out in production the flag and
12
+ * the carrier both go away.
13
+ */
14
+ export function steeringEnabled(): boolean {
15
+ const value = process.env.ENABLE_STEERING?.trim().toLowerCase();
16
+ return value === "1" || value === "true";
17
+ }
@@ -74,13 +74,16 @@ export interface CursorHostedStorageOptions {
74
74
  * `https://api.cursor.com`).
75
75
  */
76
76
  baseUrl?: string;
77
+ /** Bearer credential (default: `CURSOR_API_KEY`). */
78
+ apiKey?: string;
77
79
  /**
78
- * Bearer credential (default: `CURSOR_API_KEY`). A function is
79
- * re-resolved on every call: hosted freeze snapshots listen unsigned and
80
- * bind the key file after serve start, so a value captured at construct
81
- * time would be permanently absent.
80
+ * Lazy credential, re-resolved on every call and preferred over
81
+ * {@link apiKey}: hosted freeze snapshots listen unsigned and bind the
82
+ * key file after serve start, so a value captured at construct time
83
+ * would be permanently absent. Used by the platform-selected v2
84
+ * defaults; kept separate so `apiKey` stays a plain string for authors.
82
85
  */
83
- apiKey?: string | (() => string | undefined);
86
+ resolveApiKey?: () => string | undefined;
84
87
  /** Injected for tests. */
85
88
  fetch?: typeof fetch;
86
89
  }
@@ -185,8 +188,7 @@ function toCasResult(response: KvCasResponse): KvCasResult {
185
188
  }
186
189
 
187
190
  function resolveApiKey(options: CursorHostedStorageOptions): string {
188
- const configured =
189
- typeof options.apiKey === "function" ? options.apiKey() : options.apiKey;
191
+ const configured = options.resolveApiKey?.() ?? options.apiKey;
190
192
  const key = (configured ?? process.env.CURSOR_API_KEY)?.trim();
191
193
  if (key === undefined || key === "") {
192
194
  throw new Error(
package/src/types.ts CHANGED
@@ -1262,7 +1262,8 @@ export type ConnectionConfig = (
1262
1262
  * Local runtime only, like all server tools: a `runtime: "cloud"` agent
1263
1263
  * fails discovery, and hybrid per-send cloud sessions refuse the turn.
1264
1264
  * The listing runs once per turn; point this at endpoints that can
1265
- * answer `tools/list` cheaply.
1265
+ * answer `tools/list` cheaply. Tools whose MCP `annotations.title` is
1266
+ * `do-not-call` are omitted (vendor host-UI / MCP Apps helpers).
1266
1267
  */
1267
1268
  advertiseTools?: boolean;
1268
1269
  /**
@@ -0,0 +1,34 @@
1
+ # Code wiki
2
+
3
+ Keeps `wiki/` pages current after a pull request merges. Opens a
4
+ follow-up PR against that merge's base when a durable fact changed.
5
+ Skips chores. Same-base updates stack on `wiki/<base>`.
6
+
7
+ ```bash
8
+ npx @cursor/july init ./code-wiki --template code-wiki
9
+ ```
10
+
11
+ `init` asks which repos to watch. Skip the prompt with
12
+ `--var repos=owner/repo`.
13
+
14
+ ## Run
15
+
16
+ Connect GitHub in Cursor for those repos. Then sign the host in:
17
+
18
+ ```bash
19
+ agent-sdk login
20
+ agent-sdk dev
21
+ ```
22
+
23
+ Events and API calls use your Cursor GitHub connection. Replay a merged PR:
24
+
25
+ ```bash
26
+ agent-sdk github replay https://github.com/owner/repo/pull/1 --dir . --action closed
27
+ ```
28
+
29
+ ## Files
30
+
31
+ - `agent/lib/repos.ts`: watched repos
32
+ - `agent/lib/wiki.ts`: wiki path rules
33
+ - `agent/instructions.md`: what counts as a wiki update
34
+ - `evals/wiki.eval.ts`: skip and update smoke cases
@@ -0,0 +1,8 @@
1
+ import { defineAgent } from "@cursor/july";
2
+
3
+ export default defineAgent({
4
+ tools: ["read", "grep"],
5
+ local: {
6
+ sandbox: true,
7
+ },
8
+ });
@@ -0,0 +1,144 @@
1
+ import type { HostGitHubClient } from "@cursor/july";
2
+ import {
3
+ defaultGitHubAuth,
4
+ githubChannel,
5
+ } from "@cursor/july/channels/github";
6
+ import { REPOS, watches } from "../lib/repos.js";
7
+ import { WIKI_DIR, isWikiPath } from "../lib/wiki.js";
8
+
9
+ const MAX_DIFF_CHARS = 150_000;
10
+ const MAX_WIKI_PAGES = 40;
11
+ const LIST_FILES_CAP = 3000;
12
+
13
+ type Octokit = Awaited<ReturnType<HostGitHubClient["getOctokit"]>>;
14
+
15
+ export default githubChannel({
16
+ cursorAccount: { repos: REPOS, permissions: "contents-write" },
17
+ deliverReplies: false,
18
+ onComment: () => null,
19
+ onPullRequest: async (ctx, pr) => {
20
+ if (!watches(ctx.repository.fullName)) {
21
+ return null;
22
+ }
23
+ if (pr.action !== "closed" || !pr.merged) {
24
+ return null;
25
+ }
26
+ if (pr.headRef.startsWith(`${WIKI_DIR}/`)) {
27
+ return null;
28
+ }
29
+ const octokit = await ctx.github.getOctokit();
30
+ const pull = {
31
+ owner: ctx.repository.owner,
32
+ repo: ctx.repository.name,
33
+ pull_number: pr.number,
34
+ };
35
+ const listed = await octokit.paginate(octokit.rest.pulls.listFiles, {
36
+ ...pull,
37
+ per_page: 100,
38
+ });
39
+ const files = listed.map((file) => file.filename);
40
+ if (
41
+ files.length > 0 &&
42
+ files.length < LIST_FILES_CAP &&
43
+ files.every(isWikiPath)
44
+ ) {
45
+ return null;
46
+ }
47
+ return {
48
+ auth: defaultGitHubAuth(ctx),
49
+ context: [
50
+ "This pull request merged. Read pr/files.txt, pr/diff.patch, and wiki/.",
51
+ ],
52
+ async workspaceFiles() {
53
+ const [diff, wiki] = await Promise.all([
54
+ octokit.rest.pulls.get({ ...pull, mediaType: { format: "diff" } }),
55
+ loadWiki(octokit, pull.owner, pull.repo, pr.baseRef),
56
+ ]);
57
+ const text = String(diff.data);
58
+ return {
59
+ "pr/base.ref": `${pr.baseRef}\n`,
60
+ "pr/files.txt": `${files.join("\n")}\n`,
61
+ "pr/diff.patch":
62
+ text.length > MAX_DIFF_CHARS
63
+ ? `${text.slice(0, MAX_DIFF_CHARS)}\n…[truncated]\n`
64
+ : text,
65
+ ...wiki,
66
+ };
67
+ },
68
+ };
69
+ },
70
+ });
71
+
72
+ async function loadWiki(
73
+ octokit: Octokit,
74
+ owner: string,
75
+ repo: string,
76
+ ref: string
77
+ ): Promise<Record<string, string>> {
78
+ const listed = await readContents(octokit, {
79
+ owner,
80
+ repo,
81
+ path: WIKI_DIR,
82
+ ref,
83
+ });
84
+ if (!Array.isArray(listed)) {
85
+ return { "wiki/pages.txt": "" };
86
+ }
87
+ const markdown = listed.filter(
88
+ (item) => item.type === "file" && item.name.endsWith(".md")
89
+ );
90
+ const truncated = markdown.length > MAX_WIKI_PAGES;
91
+ const files = markdown.slice(0, MAX_WIKI_PAGES);
92
+ const pages: Record<string, string> = {};
93
+ await Promise.all(
94
+ files.map(async (file) => {
95
+ const body = await readContents(octokit, {
96
+ owner,
97
+ repo,
98
+ path: `${WIKI_DIR}/${file.name}`,
99
+ ref,
100
+ });
101
+ if (
102
+ body === undefined ||
103
+ Array.isArray(body) ||
104
+ body.encoding !== "base64" ||
105
+ typeof body.content !== "string"
106
+ ) {
107
+ return;
108
+ }
109
+ pages[`${WIKI_DIR}/${file.name}`] = Buffer.from(
110
+ body.content,
111
+ "base64"
112
+ ).toString("utf8");
113
+ })
114
+ );
115
+ const names = Object.keys(pages).sort();
116
+ pages["wiki/pages.txt"] = truncated
117
+ ? `${names.join("\n")}\n…[truncated]\n`
118
+ : `${names.join("\n")}\n`;
119
+ return pages;
120
+ }
121
+
122
+ async function readContents(
123
+ octokit: Octokit,
124
+ args: { owner: string; repo: string; path: string; ref: string }
125
+ ) {
126
+ try {
127
+ const { data } = await octokit.rest.repos.getContent(args);
128
+ return data;
129
+ } catch (error) {
130
+ if (isNotFound(error)) {
131
+ return undefined;
132
+ }
133
+ throw error;
134
+ }
135
+ }
136
+
137
+ function isNotFound(error: unknown): boolean {
138
+ return (
139
+ typeof error === "object" &&
140
+ error !== null &&
141
+ "status" in error &&
142
+ error.status === 404
143
+ );
144
+ }
@@ -0,0 +1,39 @@
1
+ # Code wiki
2
+
3
+ You keep the in-repo wiki current after a pull request merges.
4
+
5
+ ## Read
6
+
7
+ The workspace has `pr/files.txt`, `pr/diff.patch`, and `wiki/`.
8
+ `wiki/pages.txt` lists existing pages. Missing pages mean the wiki is
9
+ empty. Read the file list and the pages that own the change. Use the
10
+ diff only to test a fact.
11
+
12
+ ## Decide
13
+
14
+ Update only when the merge changes a durable fact someone would look
15
+ up later: how a system works, a public or operator contract, a default
16
+ or ownership change, a removed feature.
17
+
18
+ An empty wiki is not a license to document the whole repo. Create a
19
+ page only for a durable contract in this merge.
20
+
21
+ Skip lint, format, lockfiles, dependency pins, tests-only, cherry-picks,
22
+ backports, telemetry-only changes, one-off bug fixes that leave the
23
+ described behavior the same, knobs the wiki never mentioned, and facts
24
+ the matching page already states.
25
+
26
+ Do not write a changelog or a PR list.
27
+
28
+ ## Write
29
+
30
+ Call `apply_wiki` once.
31
+
32
+ - Skip: `{ verdict: "skip", reason }` with one line.
33
+ - Update: `{ verdict: "update", reason, pages }` with full page bodies.
34
+
35
+ Pages stay under `wiki/`, flat, `*.md`. Keep `_index.md` in sync when
36
+ you add a page. Each catalog line names the page and the paths it owns.
37
+ Short declarative sentences. Edit only the stale or missing facts.
38
+
39
+ Reply with `skip` or `update` and the reason.
@@ -0,0 +1,5 @@
1
+ export const REPOS: readonly string[] = ["owner/repo"];
2
+
3
+ export function watches(fullName: string): boolean {
4
+ return REPOS.some((repo) => repo.toLowerCase() === fullName.toLowerCase());
5
+ }