@stigmer/runner 3.12.4 → 3.12.5

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 (146) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +45 -0
  3. package/dist/activities/execute-cursor/cursor-mcp-config.js +76 -0
  4. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -0
  5. package/dist/activities/execute-cursor/index.d.ts +10 -1
  6. package/dist/activities/execute-cursor/index.js +47 -29
  7. package/dist/activities/execute-cursor/index.js.map +1 -1
  8. package/dist/activities/execute-cursor/prompt-builder.d.ts +13 -2
  9. package/dist/activities/execute-cursor/prompt-builder.js +11 -0
  10. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  11. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -1
  12. package/dist/activities/execute-cursor/skill-resolver.d.ts +2 -21
  13. package/dist/activities/execute-cursor/skill-resolver.js +5 -108
  14. package/dist/activities/execute-cursor/skill-resolver.js.map +1 -1
  15. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +9 -0
  16. package/dist/activities/execute-deep-agent/approval-file-change.js +2 -1
  17. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
  18. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +18 -1
  19. package/dist/activities/execute-deep-agent/cas-capture-backend.js +20 -3
  20. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -1
  21. package/dist/activities/execute-deep-agent/index.js +57 -5
  22. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  23. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +9 -0
  24. package/dist/activities/execute-deep-agent/prompt-builder.js +10 -0
  25. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  26. package/dist/activities/execute-deep-agent/setup.js +17 -16
  27. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  28. package/dist/activities/execute-deep-agent/subagent-transformer.js +17 -11
  29. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -3
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/middleware/path-normalization.d.ts +49 -49
  34. package/dist/middleware/path-normalization.js +73 -84
  35. package/dist/middleware/path-normalization.js.map +1 -1
  36. package/dist/middleware/types.d.ts +3 -3
  37. package/dist/runner-manager.d.ts +1 -1
  38. package/dist/runner-manager.js +22 -15
  39. package/dist/runner-manager.js.map +1 -1
  40. package/dist/runner.js +11 -0
  41. package/dist/runner.js.map +1 -1
  42. package/dist/shared/args-preview.d.ts +8 -0
  43. package/dist/shared/args-preview.js +14 -3
  44. package/dist/shared/args-preview.js.map +1 -1
  45. package/dist/shared/artifact-storage.d.ts +11 -1
  46. package/dist/shared/artifact-storage.js +10 -1
  47. package/dist/shared/artifact-storage.js.map +1 -1
  48. package/dist/shared/declared-preferences.d.ts +47 -0
  49. package/dist/shared/declared-preferences.js +64 -0
  50. package/dist/shared/declared-preferences.js.map +1 -0
  51. package/dist/shared/mcp-enabled-tools.d.ts +2 -3
  52. package/dist/shared/mcp-enabled-tools.js +2 -3
  53. package/dist/shared/mcp-enabled-tools.js.map +1 -1
  54. package/dist/shared/mcp-resolver.d.ts +20 -15
  55. package/dist/shared/mcp-resolver.js +11 -12
  56. package/dist/shared/mcp-resolver.js.map +1 -1
  57. package/dist/shared/placeholder-resolver.d.ts +9 -2
  58. package/dist/shared/placeholder-resolver.js +9 -2
  59. package/dist/shared/placeholder-resolver.js.map +1 -1
  60. package/dist/shared/plan-mode-permissions.d.ts +26 -46
  61. package/dist/shared/plan-mode-permissions.js +27 -55
  62. package/dist/shared/plan-mode-permissions.js.map +1 -1
  63. package/dist/shared/skill-mount.d.ts +89 -0
  64. package/dist/shared/skill-mount.js +142 -0
  65. package/dist/shared/skill-mount.js.map +1 -0
  66. package/dist/shared/skill-writer.d.ts +28 -26
  67. package/dist/shared/skill-writer.js +79 -102
  68. package/dist/shared/skill-writer.js.map +1 -1
  69. package/dist/shared/worker-shutdown.d.ts +67 -0
  70. package/dist/shared/worker-shutdown.js +79 -0
  71. package/dist/shared/worker-shutdown.js.map +1 -0
  72. package/dist/shared/workspace/types.d.ts +3 -2
  73. package/dist/workflow-engine/loader.js +38 -1
  74. package/dist/workflow-engine/loader.js.map +1 -1
  75. package/dist/workflow-engine/tasks/human-input.js +23 -2
  76. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  77. package/package.json +15 -11
  78. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +50 -0
  79. package/src/activities/execute-cursor/__tests__/cursor-mcp-config.test.ts +111 -0
  80. package/src/activities/execute-cursor/__tests__/skill-resolver.test.ts +3 -94
  81. package/src/activities/execute-cursor/cursor-mcp-config.ts +107 -0
  82. package/src/activities/execute-cursor/index.ts +63 -35
  83. package/src/activities/execute-cursor/prompt-builder.ts +29 -2
  84. package/src/activities/execute-cursor/session-lifecycle.ts +1 -1
  85. package/src/activities/execute-cursor/skill-resolver.ts +10 -137
  86. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +15 -0
  87. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +75 -0
  88. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +1 -0
  89. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  90. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  91. package/src/activities/execute-deep-agent/__tests__/index.test.ts +1 -0
  92. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +42 -29
  93. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +52 -0
  94. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  95. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +11 -8
  96. package/src/activities/execute-deep-agent/approval-file-change.ts +11 -1
  97. package/src/activities/execute-deep-agent/cas-capture-backend.ts +20 -3
  98. package/src/activities/execute-deep-agent/index.ts +57 -5
  99. package/src/activities/execute-deep-agent/prompt-builder.ts +22 -0
  100. package/src/activities/execute-deep-agent/setup.ts +21 -19
  101. package/src/activities/execute-deep-agent/subagent-transformer.ts +19 -12
  102. package/src/activities/execute-deep-agent/subagent-wiring.ts +3 -3
  103. package/src/index.ts +1 -1
  104. package/src/middleware/__tests__/path-normalization.test.ts +32 -32
  105. package/src/middleware/path-normalization.ts +78 -90
  106. package/src/middleware/types.ts +3 -3
  107. package/src/runner-manager.ts +26 -16
  108. package/src/runner.ts +14 -0
  109. package/src/shared/__tests__/artifact-storage.test.ts +28 -0
  110. package/src/shared/__tests__/bedrock-seam.test.ts +5 -5
  111. package/src/shared/__tests__/declared-preferences.test.ts +109 -0
  112. package/src/shared/__tests__/foundry-seam.test.ts +5 -5
  113. package/src/shared/__tests__/plan-mode-permissions.test.ts +15 -44
  114. package/src/shared/__tests__/skill-mount.test.ts +238 -0
  115. package/src/shared/__tests__/skill-writer.test.ts +151 -128
  116. package/src/shared/__tests__/vertex-adapter.test.ts +30 -3
  117. package/src/shared/__tests__/vertex-seam.test.ts +5 -5
  118. package/src/shared/__tests__/worker-shutdown.test.ts +101 -0
  119. package/src/shared/args-preview.ts +17 -3
  120. package/src/shared/artifact-storage.ts +20 -1
  121. package/src/shared/declared-preferences.ts +84 -0
  122. package/src/shared/mcp-enabled-tools.ts +2 -3
  123. package/src/shared/mcp-resolver.ts +20 -20
  124. package/src/shared/placeholder-resolver.ts +9 -2
  125. package/src/shared/plan-mode-permissions.ts +27 -58
  126. package/src/shared/skill-mount.ts +179 -0
  127. package/src/shared/skill-writer.ts +96 -130
  128. package/src/shared/worker-shutdown.ts +99 -0
  129. package/src/shared/workspace/types.ts +3 -2
  130. package/src/workflow-engine/__tests__/loader.test.ts +51 -0
  131. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +117 -0
  132. package/src/workflow-engine/loader.ts +46 -1
  133. package/src/workflow-engine/tasks/human-input.ts +33 -5
  134. package/dist/activities/execute-cursor/connect-backfill.d.ts +0 -19
  135. package/dist/activities/execute-cursor/connect-backfill.js +0 -27
  136. package/dist/activities/execute-cursor/connect-backfill.js.map +0 -1
  137. package/dist/activities/execute-cursor/mcp-resolver.d.ts +0 -120
  138. package/dist/activities/execute-cursor/mcp-resolver.js +0 -194
  139. package/dist/activities/execute-cursor/mcp-resolver.js.map +0 -1
  140. package/dist/activities/execute-cursor/placeholder-resolver.d.ts +0 -34
  141. package/dist/activities/execute-cursor/placeholder-resolver.js +0 -82
  142. package/dist/activities/execute-cursor/placeholder-resolver.js.map +0 -1
  143. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +0 -125
  144. package/src/activities/execute-cursor/connect-backfill.ts +0 -52
  145. package/src/activities/execute-cursor/mcp-resolver.ts +0 -324
  146. package/src/activities/execute-cursor/placeholder-resolver.ts +0 -109
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The Cursor harness's MCP output adapter: maps the shared resolver's
3
+ * {@link ResolvedMcpServer} servers into the Cursor SDK's mcpServers config
4
+ * for Agent.create(), plus the harness's env pre-flight check.
5
+ *
6
+ * Resolution itself lives in shared/mcp-resolver.ts (oss#387 consolidated the
7
+ * near-duplicate resolver this file used to carry) — this module is the
8
+ * symmetric twin of the deep-agent harness's toMcpClientConfig in
9
+ * shared/mcp-manager.ts: both consume the identical intermediate, only the
10
+ * final SDK serialization differs.
11
+ */
12
+
13
+ import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
14
+ import type { ResolvedMcpServer } from "../../shared/mcp-resolver.js";
15
+
16
+ /**
17
+ * Cursor SDK MCP server config shape (matches @cursor/sdk McpServerConfig).
18
+ * Defined here to avoid tight coupling to the SDK's internal types.
19
+ */
20
+ export type CursorMcpServerConfig =
21
+ | {
22
+ type?: "stdio";
23
+ command: string;
24
+ args?: string[];
25
+ env?: Record<string, string>;
26
+ cwd?: string;
27
+ }
28
+ | {
29
+ type?: "http" | "sse";
30
+ url: string;
31
+ headers?: Record<string, string>;
32
+ };
33
+
34
+ /**
35
+ * Transform resolved MCP servers into the Cursor SDK's mcpServers config
36
+ * for Agent.create().
37
+ *
38
+ * Call it ONCE, after the last mutation of the resolved-server list (backfill,
39
+ * synthesized-attachment injection): the config is a projection of that list,
40
+ * and building it early just creates a stale copy someone must remember to
41
+ * rebuild. The Cursor SDK config cannot hide tools, so enabledTools is NOT
42
+ * expressed here — the HITL hook enforces it (see ResolvedMcpServer.enabledTools).
43
+ */
44
+ export function toCursorMcpConfig(
45
+ servers: ResolvedMcpServer[],
46
+ ): Record<string, CursorMcpServerConfig> {
47
+ const result: Record<string, CursorMcpServerConfig> = {};
48
+
49
+ for (const server of servers) {
50
+ if (server.connectionType === "stdio") {
51
+ if (!server.command) continue;
52
+ result[server.slug] = {
53
+ type: "stdio",
54
+ command: server.command,
55
+ args: server.args,
56
+ env: server.env,
57
+ cwd: server.cwd,
58
+ };
59
+ } else {
60
+ if (!server.url) continue;
61
+ result[server.slug] = {
62
+ type: server.connectionType,
63
+ url: server.url,
64
+ headers: server.headers,
65
+ };
66
+ }
67
+ }
68
+
69
+ return result;
70
+ }
71
+
72
+ /**
73
+ * Validate that resolved MCP servers have their required env vars populated.
74
+ * Returns a list of warnings for servers with empty/missing env.
75
+ * Used as a pre-flight check before agent.send() to surface config issues early.
76
+ */
77
+ export function validateMcpServerEnv(
78
+ servers: ResolvedMcpServer[],
79
+ usages: McpServerUsage[],
80
+ ): string[] {
81
+ const warnings: string[] = [];
82
+
83
+ for (const usage of usages) {
84
+ const slug = usage.mcpServerRef?.slug;
85
+ if (!slug) continue;
86
+
87
+ const resolved = servers.find((s) => s.slug === slug);
88
+ if (!resolved) {
89
+ warnings.push(`MCP server '${slug}': failed to resolve (server may not exist or is inaccessible)`);
90
+ continue;
91
+ }
92
+
93
+ if (resolved.connectionType === "stdio" && resolved.env) {
94
+ const emptyKeys = Object.entries(resolved.env)
95
+ .filter(([, v]) => !v)
96
+ .map(([k]) => k);
97
+ if (emptyKeys.length > 0) {
98
+ warnings.push(
99
+ `MCP server '${slug}': env vars [${emptyKeys.join(", ")}] are empty — ` +
100
+ `server subprocess will likely fail to connect`,
101
+ );
102
+ }
103
+ }
104
+ }
105
+
106
+ return warnings;
107
+ }
@@ -56,6 +56,7 @@ import {
56
56
  resolveCallerIdentity,
57
57
  } from "../../shared/caller-identity.js";
58
58
  import { readSessionContext } from "../../shared/session-context.js";
59
+ import { readDeclaredPreferences } from "../../shared/declared-preferences.js";
59
60
  import { withholdSecretContentFromMessages } from "../../shared/tool-row.js";
60
61
  import { StallTimeoutError, formatStallFailure } from "../../shared/stall-watchdog.js";
61
62
  import { resolveUsableArtifactStorage, loadArtifactStorageConfig, type ArtifactStorage } from "../../shared/artifact-storage.js";
@@ -71,7 +72,8 @@ import { DeltaEnricher } from "./delta-enricher.js";
71
72
  import { TodoTracker } from "./todo-tracker.js";
72
73
  import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/streaming-scheduler.js";
73
74
  import { createCursorEventRecorder } from "./cursor-event-recorder.js";
74
- import { resolveMcpServers, toCursorMcpConfig, validateMcpServerEnv } from "./mcp-resolver.js";
75
+ import { resolveMcpServers } from "../../shared/mcp-resolver.js";
76
+ import { toCursorMcpConfig, validateMcpServerEnv } from "./cursor-mcp-config.js";
75
77
  import { resolveMcpTransportPosture } from "../../shared/mcp-transport-guard.js";
76
78
  import {
77
79
  discoverChannelMessaging,
@@ -85,7 +87,7 @@ import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment
85
87
  import { mergeApprovalPolicies } from "./approval-policy.js";
86
88
  import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
87
89
  import { enabledToolsBySlug } from "../../shared/mcp-enabled-tools.js";
88
- import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
90
+ import { backfillMcpServersIfNeeded } from "../../shared/connect-backfill.js";
89
91
  import { resolveExecutionEnv } from "./env-resolver.js";
90
92
  import { resolveBlueprint } from "./blueprint-resolver.js";
91
93
  import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
@@ -147,7 +149,7 @@ import type { ClassifiedError } from "./error-classifier.js";
147
149
  import { createAgent, createCloudAgent } from "./session-lifecycle.js";
148
150
  import { setMaxListeners } from "node:events";
149
151
  import { startHeartbeat } from "../../shared/heartbeat.js";
150
- import { getShutdownSignalForQueue } from "../../runner-manager.js";
152
+ import { classifyTurnInterruption, getShutdownSignalForQueue } from "../../shared/worker-shutdown.js";
151
153
 
152
154
  /**
153
155
  * Creates the activity functions bound to the runner config.
@@ -627,16 +629,19 @@ async function executeCursorInner(
627
629
  ),
628
630
  sessionId,
629
631
  );
630
- let mcpResolution = await resolveMcpServers(
632
+ // The resolved-server list mutates through backfill and attachment
633
+ // injection below; the Cursor SDK config is projected from it exactly
634
+ // once, after the last mutation (see toCursorMcpConfig).
635
+ let resolvedMcpServers = (await resolveMcpServers(
631
636
  client, blueprint.mergedMcpServerUsages, mcpEnvVars, transportPosture,
632
- );
637
+ )).resolvedServers;
633
638
  setupTiming.mark("resolve_mcp_servers");
634
639
 
635
640
  // Phase 4a: Connect backfill for undiscovered MCP servers
636
641
  heartbeatPhase = "resolving_mcp_servers";
637
642
  const sessionOrg = session.metadata?.org ?? "";
638
- mcpResolution = await backfillMcpServersIfNeeded(
639
- client, mcpResolution, blueprint.mergedMcpServerUsages, mcpEnvVars, sessionOrg,
643
+ resolvedMcpServers = await backfillMcpServersIfNeeded(
644
+ client, resolvedMcpServers, blueprint.mergedMcpServerUsages, mcpEnvVars, sessionOrg,
640
645
  transportPosture, heartbeat, secretKeys,
641
646
  );
642
647
  setupTiming.mark("backfill_mcp");
@@ -684,13 +689,9 @@ async function executeCursorInner(
684
689
  backendEndpoint: config.stigmerBackendEndpoint,
685
690
  });
686
691
  if (attachment) {
687
- const resolvedServers = injectSynthesizedAttachment(
688
- mcpResolution.resolvedServers, attachment, "channel messaging",
692
+ resolvedMcpServers = injectSynthesizedAttachment(
693
+ resolvedMcpServers, attachment, "channel messaging",
689
694
  );
690
- mcpResolution = {
691
- resolvedServers,
692
- cursorConfig: toCursorMcpConfig(resolvedServers),
693
- };
694
695
  }
695
696
  }
696
697
 
@@ -710,15 +711,13 @@ async function executeCursorInner(
710
711
  },
711
712
  );
712
713
  if (conversationAttachment) {
713
- const resolvedServers = injectSynthesizedAttachment(
714
- mcpResolution.resolvedServers, conversationAttachment, "conversation participation",
714
+ resolvedMcpServers = injectSynthesizedAttachment(
715
+ resolvedMcpServers, conversationAttachment, "conversation participation",
715
716
  );
716
- mcpResolution = {
717
- resolvedServers,
718
- cursorConfig: toCursorMcpConfig(resolvedServers),
719
- };
720
717
  }
721
- const mcpConfig = mcpResolution.cursorConfig;
718
+ // The one projection point: every mutation above is now visible in the
719
+ // Cursor SDK config by construction (no per-mutation rebuild to forget).
720
+ const mcpConfig = toCursorMcpConfig(resolvedMcpServers);
722
721
 
723
722
  // Phase 4b: Merge approval policies from all layers.
724
723
  //
@@ -734,16 +733,15 @@ async function executeCursorInner(
734
733
  // see ResolvedMcpServer.toolApprovalOverrides (issue #349) — so there
735
734
  // is no separate override input to pass here.
736
735
  const mergedPolicies = mergeApprovalPolicies(
737
- mcpResolution.resolvedServers,
736
+ resolvedMcpServers,
738
737
  leases,
739
738
  );
740
739
  heartbeat();
741
740
 
742
741
  // Phase 4c: Validate MCP server env health (diagnostic, non-blocking)
743
742
  const mcpWarnings = validateMcpServerEnv(
744
- mcpResolution.resolvedServers,
743
+ resolvedMcpServers,
745
744
  blueprint.mergedMcpServerUsages,
746
- envVars,
747
745
  );
748
746
  if (mcpWarnings.length > 0) {
749
747
  console.warn(
@@ -910,7 +908,7 @@ async function executeCursorInner(
910
908
  // servers' allow-lists, enforced by the hook's "disabled" arm ahead of
911
909
  // every approval bypass. The Cursor SDK config cannot hide a server's
912
910
  // tools, so this deny-at-call is the harness's enforcement.
913
- enabledToolsBySlug(mcpResolution.resolvedServers),
911
+ enabledToolsBySlug(resolvedMcpServers),
914
912
  );
915
913
  const hitlGate = await installHitlGate({
916
914
  workspaceRoot: primaryWorkspaceDir,
@@ -1175,6 +1173,7 @@ async function executeCursorInner(
1175
1173
  contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
1176
1174
  senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
1177
1175
  sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
1176
+ declaredPreferences: readDeclaredPreferences(spec.declaredPreferences),
1178
1177
  conversationCatchup: readConversationCatchup(spec.conversationCatchup),
1179
1178
  // The turn's recorded transcript, seeded from the persisted execution
1180
1179
  // on a reinvocation (Phase 3). Consumed only by the HITL-recovery
@@ -1401,13 +1400,21 @@ async function executeCursorInner(
1401
1400
  // classifies a shutdown from the shutdown signal directly (in
1402
1401
  // resolvePreBoundaryTerminal). The heartbeat timer may set `cancelled` before
1403
1402
  // the AbortSignal microtask propagates; the direct signal check catches that.
1404
- const isShutdown = periodicHeartbeat.workerShutdown || (shutdownSignal?.aborted ?? false);
1405
- if (isShutdown) {
1403
+ // The decision table (including #776's grace-window guard: an aborted
1404
+ // shutdown signal with NO interruption evidence stays "none") lives in
1405
+ // classifyTurnInterruption — shared/worker-shutdown.ts.
1406
+ const interruption = classifyTurnInterruption({
1407
+ heartbeatCancelled: periodicHeartbeat.cancelled,
1408
+ heartbeatWorkerShutdown: periodicHeartbeat.workerShutdown,
1409
+ cancellationSignalAborted: Context.current().cancellationSignal.aborted,
1410
+ shutdownSignalAborted: shutdownSignal?.aborted ?? false,
1411
+ });
1412
+ if (interruption === "worker-shutdown") {
1406
1413
  turnState.pauseDetected = false;
1407
- } else if (periodicHeartbeat.cancelled) {
1414
+ } else if (interruption === "pause") {
1408
1415
  turnState.pauseDetected = true;
1409
1416
  }
1410
- workerShutdownDetected = isShutdown;
1417
+ workerShutdownDetected = interruption === "worker-shutdown";
1411
1418
 
1412
1419
  // Post-stream finalize, shared by the primary turn and both recovery retries:
1413
1420
  // finalize the transcript + streaming flags, mark any in-flight sub-agent
@@ -1505,10 +1512,16 @@ async function executeCursorInner(
1505
1512
  return { kind: "return" };
1506
1513
  }
1507
1514
 
1508
- // Worker shutdown: the runner-manager aborted the shutdown signal. NOT a
1515
+ // Worker shutdown: the runner/manager aborted the shutdown signal. NOT a
1509
1516
  // user pause. Checked via the shutdown signal directly so a retry (whose
1510
- // periodic heartbeat is already stopped) still classifies it correctly.
1511
- if (workerShutdownDetected || (shutdownSignal?.aborted ?? false)) {
1517
+ // periodic heartbeat is already stopped) still classifies it correctly
1518
+ // but only alongside a delivered cancellation: a retry that completed
1519
+ // normally inside the drain grace window must stay a completion (#776's
1520
+ // grace-window guard, mirroring the primary's `interrupted` gate).
1521
+ if (
1522
+ workerShutdownDetected ||
1523
+ ((shutdownSignal?.aborted ?? false) && Context.current().cancellationSignal.aborted)
1524
+ ) {
1512
1525
  status.phase = ExecutionPhase.EXECUTION_FAILED;
1513
1526
  status.error = "Execution interrupted: runner worker was shut down. Retry or resume.";
1514
1527
  status.completedAt = utcTimestamp();
@@ -1865,6 +1878,7 @@ async function executeCursorInner(
1865
1878
  contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
1866
1879
  senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
1867
1880
  sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
1881
+ declaredPreferences: readDeclaredPreferences(spec.declaredPreferences),
1868
1882
  conversationCatchup: readConversationCatchup(spec.conversationCatchup),
1869
1883
  // Composed fresh (not reused from Phase 10): the failed primary
1870
1884
  // stream may have appended partial work onto status.messages,
@@ -2150,9 +2164,12 @@ async function executeCursorInner(
2150
2164
  periodicHeartbeat?.stop();
2151
2165
 
2152
2166
  if (err instanceof CancelledFailure) {
2153
- // workerShutdownDetected means the runner-manager signaled shutdown
2154
- // before the worker drained. This is infrastructure failure, not pause.
2155
- if (workerShutdownDetected) {
2167
+ // Worker shutdown is infrastructure failure, not pause. The direct
2168
+ // signal check covers a CancelledFailure thrown BEFORE the post-stream
2169
+ // classification ran (workerShutdownDetected still false); no extra
2170
+ // interruption-evidence gate is needed here — the caught
2171
+ // CancelledFailure IS the evidence (#776).
2172
+ if (workerShutdownDetected || (shutdownSignal?.aborted ?? false)) {
2156
2173
  console.log(`ExecuteCursor cancelled (worker shutdown) for execution ${executionId}`);
2157
2174
  status.phase = ExecutionPhase.EXECUTION_FAILED;
2158
2175
  status.error = "Execution interrupted: runner worker was shut down. Retry or resume.";
@@ -2451,10 +2468,19 @@ export interface BuildPromptInput {
2451
2468
  * turn.
2452
2469
  */
2453
2470
  sessionContext?: string;
2471
+ /**
2472
+ * Platform-declared standing preferences from the execution spec's
2473
+ * `declared_preferences` (stigmer/stigmer#293). Like the bridge, only
2474
+ * the enhanced-prompt path consumes it — deliberately frozen per Cursor
2475
+ * session (DD-002 D3): the first turn delivers it into the agent's own
2476
+ * conversation store, and repeating it on resumed turns would bloat the
2477
+ * store with identical content.
2478
+ */
2479
+ declaredPreferences?: import("../../shared/declared-preferences.js").DeclaredPreferencesContent;
2454
2480
  /**
2455
2481
  * Conversation catchup from the execution spec's `conversation_catchup`
2456
2482
  * (cloud DD-006): what happened on the channel conversation that the
2457
- * agent has not seen. PER-TURN, so unlike the three standing values
2483
+ * agent has not seen. PER-TURN, so unlike the standing values
2458
2484
  * above it rides BOTH prompt paths — the enhanced prompt and a resumed
2459
2485
  * turn's prefix (the `interaction_mode` shape). Handback lands
2460
2486
  * mid-session on a resumed agent: the resumed path is the one that
@@ -2589,6 +2615,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2589
2615
  contextBridge: input.contextBridge,
2590
2616
  senderIdentity: input.senderIdentity,
2591
2617
  sessionContext: input.sessionContext,
2618
+ declaredPreferences: input.declaredPreferences,
2592
2619
  conversationCatchup,
2593
2620
  },
2594
2621
  {
@@ -2655,6 +2682,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2655
2682
  contextBridge: input.contextBridge,
2656
2683
  senderIdentity: input.senderIdentity,
2657
2684
  sessionContext: input.sessionContext,
2685
+ declaredPreferences: input.declaredPreferences,
2658
2686
  conversationCatchup,
2659
2687
  });
2660
2688
  }
@@ -31,6 +31,10 @@ import {
31
31
  type SenderIdentity,
32
32
  } from "../../shared/sender-identity.js";
33
33
  import { formatSessionContextText } from "../../shared/session-context.js";
34
+ import {
35
+ formatDeclaredPreferencesText,
36
+ type DeclaredPreferencesContent,
37
+ } from "../../shared/declared-preferences.js";
34
38
  import {
35
39
  visionDisclosureLines,
36
40
  type NotViewableEntry,
@@ -147,11 +151,20 @@ export interface EnhancedPromptOptions {
147
151
  * store — the context is constant for the session's lifetime.
148
152
  */
149
153
  sessionContext?: string;
154
+ /**
155
+ * Platform-declared standing preferences (stigmer/stigmer#293): the org's
156
+ * and user's standing context, server-snapshotted onto the execution
157
+ * spec's `declared_preferences` at create. Like the bridge, it lands in
158
+ * the first message and persists in the cursor agent's own conversation
159
+ * store — deliberately frozen per Cursor session (DD-002 D3): repeating
160
+ * it every resumed turn would bloat the store with identical content.
161
+ */
162
+ declaredPreferences?: DeclaredPreferencesContent;
150
163
  /**
151
164
  * Conversation catchup (cloud DD-006): what happened on the channel
152
165
  * conversation that the agent has not seen, read from the execution
153
- * spec's `conversation_catchup`. PER-TURN, unlike the three standing
154
- * siblings above: it rides BOTH prompt paths — this enhanced prompt and
166
+ * spec's `conversation_catchup`. PER-TURN, unlike the standing siblings
167
+ * above: it rides BOTH prompt paths — this enhanced prompt and
155
168
  * a resumed turn's prefix (the `interaction_mode` shape) — because
156
169
  * handback lands mid-session on a resumed agent, the exact case the
157
170
  * metadata lane cannot reach.
@@ -232,6 +245,14 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
232
245
  sections.push(formatSenderIdentitySection(options.senderIdentity));
233
246
  }
234
247
 
248
+ // Platform-declared standing facts precede embedder-supplied context
249
+ // (DD-002 D3): both are standing background, but the declared preferences
250
+ // are platform-authored while session context is the embedder's overlay —
251
+ // the more specific overlay reads later and naturally refines.
252
+ if (options.declaredPreferences) {
253
+ sections.push(formatDeclaredPreferencesSection(options.declaredPreferences));
254
+ }
255
+
235
256
  // Standing facts about the user (session context) come before the
236
257
  // carried conversation (bridge): the bridge may refer back to them.
237
258
  if (options.sessionContext) {
@@ -445,6 +466,12 @@ export function formatSenderIdentitySection(identity: SenderIdentity): string {
445
466
  return `<conversation_sender>\n${formatSenderIdentityText(identity)}\n</conversation_sender>`;
446
467
  }
447
468
 
469
+ export function formatDeclaredPreferencesSection(
470
+ preferences: DeclaredPreferencesContent,
471
+ ): string {
472
+ return `<declared_preferences>\n${formatDeclaredPreferencesText(preferences)}\n</declared_preferences>`;
473
+ }
474
+
448
475
  export function formatSessionContextSection(context: string): string {
449
476
  return `<session_context>\n${formatSessionContextText(context)}\n</session_context>`;
450
477
  }
@@ -47,7 +47,7 @@ import type {
47
47
  ModelParameterValue,
48
48
  } from "@cursor/sdk";
49
49
  import { withTimeout, TimeoutError } from "../../shared/with-timeout.js";
50
- import type { CursorMcpServerConfig } from "./mcp-resolver.js";
50
+ import type { CursorMcpServerConfig } from "./cursor-mcp-config.js";
51
51
 
52
52
  // ---------------------------------------------------------------------------
53
53
  // Constants
@@ -2,8 +2,8 @@
2
2
  * Resolves skill resources and writes them to the platform-managed directory.
3
3
  *
4
4
  * - Fetches skills via gRPC (by reference)
5
- * - Writes SKILL.md to .stigmer/skills/{name}/SKILL.md
6
- * - Downloads and extracts ZIP artifacts (references/, scripts/, etc.)
5
+ * - Mounts each skill via the shared skill-mount mechanics (SKILL.md +
6
+ * extracted artifact, hash-keyed cache see shared/skill-mount.ts)
7
7
  * - Uses a platform-managed directory outside the workspace
8
8
  * - Ensures the workspace `.stigmer` symlink (see stigmer-link.ts)
9
9
  * - Returns metadata for prompt injection
@@ -17,38 +17,20 @@
17
17
  * transfer.
18
18
  */
19
19
 
20
- import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
21
- import { join, dirname } from "node:path";
22
- import { ConnectError, Code } from "@connectrpc/connect";
20
+ import { mkdir } from "node:fs/promises";
21
+ import { join } from "node:path";
23
22
  import type { StigmerClient } from "../../client/stigmer-client.js";
24
- import type { Skill } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
25
23
  import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
26
24
  import type { SkillMetadata } from "./prompt-builder.js";
27
25
  import { getPlatformDir } from "../../shared/workspace/platform-dir.js";
28
- import { extractZipFileEntries } from "../../shared/zip-extract.js";
26
+ import {
27
+ SKILLS_SUBDIR,
28
+ mountIsFresh,
29
+ downloadArtifact,
30
+ writeSkillMount,
31
+ } from "../../shared/skill-mount.js";
29
32
  import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "../../shared/workspace/stigmer-link.js";
30
33
 
31
- const SKILLS_SUBDIR = "skills";
32
-
33
- /**
34
- * Marker recording what a skill's mount directory currently holds. Written
35
- * LAST, after every file of the mount landed — a crash mid-write leaves no
36
- * marker, so the next execution remounts instead of trusting a partial tree.
37
- */
38
- const MOUNT_MARKER_FILE = ".stigmer-mount.json";
39
-
40
- interface MountMarker {
41
- /** Content-addressed version hash (`Skill.status.version_hash`) of the mounted content. */
42
- versionHash: string;
43
- /**
44
- * Whether the artifact's files are part of the mount. `false` when the
45
- * skill has no artifact OR when the download failed and the mount fell
46
- * back to SKILL.md only — the latter makes the next execution retry the
47
- * download rather than cache the degraded mount.
48
- */
49
- artifactMounted: boolean;
50
- }
51
-
52
34
  export interface SkillResolverOptions {
53
35
  sessionId: string;
54
36
  primaryWorkspaceDir: string;
@@ -145,112 +127,3 @@ export async function resolveSkills(
145
127
 
146
128
  return results;
147
129
  }
148
-
149
- /**
150
- * Whether the mount at `skillDir` already holds this version's content.
151
- *
152
- * Fresh means: the marker's hash matches AND the mount isn't a degraded
153
- * SKILL.md-only fallback when the skill does carry an artifact. Any read or
154
- * parse failure counts as stale — the remount is the safe default.
155
- */
156
- async function mountIsFresh(skillDir: string, versionHash: string, wantsArtifact: boolean): Promise<boolean> {
157
- try {
158
- const raw = await readFile(join(skillDir, MOUNT_MARKER_FILE), "utf-8");
159
- const marker = JSON.parse(raw) as Partial<MountMarker>;
160
- return marker.versionHash === versionHash && (marker.artifactMounted === true || !wantsArtifact);
161
- } catch {
162
- return false;
163
- }
164
- }
165
-
166
- /**
167
- * Download a skill artifact's ZIP bytes, transfer lane first (#675).
168
- *
169
- * The URL lane (getArtifactDownloadUrl → HTTP GET) carries any valid skill
170
- * size; the unary getArtifact response is capped by the server's 10MB gRPC
171
- * message limit. Servers that predate the lane (and cloud until its sibling
172
- * lands) answer the mint with UNIMPLEMENTED — those fall back to the unary
173
- * path, which behaves exactly as before for ≤10MB artifacts.
174
- *
175
- * Runs only on a mount-cache miss (#672's hash-keyed marker above) — a hit
176
- * skips the transfer entirely, whichever lane would have carried it.
177
- *
178
- * Exported for tests.
179
- */
180
- export async function downloadArtifact(
181
- client: StigmerClient,
182
- artifactStorageKey: string,
183
- ): Promise<Uint8Array | undefined> {
184
- let minted;
185
- try {
186
- minted = await client.getSkillArtifactDownloadUrl(artifactStorageKey);
187
- } catch (err) {
188
- if (err instanceof ConnectError && err.code === Code.Unimplemented) {
189
- const resp = await client.getSkillArtifact(artifactStorageKey);
190
- return resp.artifact && resp.artifact.length > 0 ? resp.artifact : undefined;
191
- }
192
- throw err;
193
- }
194
-
195
- const resp = await fetch(minted.url);
196
- if (!resp.ok) {
197
- throw new Error(`artifact fetch failed: HTTP ${resp.status} from ${minted.url}`);
198
- }
199
- const bytes = new Uint8Array(await resp.arrayBuffer());
200
- if (minted.sizeBytes > 0n && BigInt(bytes.length) !== minted.sizeBytes) {
201
- throw new Error(
202
- `artifact fetch truncated: got ${bytes.length} bytes, expected ${minted.sizeBytes}`,
203
- );
204
- }
205
- return bytes.length > 0 ? bytes : undefined;
206
- }
207
-
208
- /**
209
- * (Re)write a skill's mount directory from scratch.
210
- *
211
- * The directory is removed first so files deleted between versions don't
212
- * linger in the mount, then SKILL.md and the artifact files are written, and
213
- * the marker is stamped LAST (see MOUNT_MARKER_FILE for the crash-safety
214
- * contract). SKILL.md always comes from `spec.skillMd` — the server's
215
- * authoritative copy — never from the zip; both the zip's SKILL.md and any
216
- * stray marker-named entry are excluded from extraction so the mount's
217
- * ownership of those two files is unconditional.
218
- */
219
- async function writeSkillMount(
220
- skill: Skill,
221
- skillDir: string,
222
- artifactBytes: Uint8Array | undefined,
223
- ): Promise<void> {
224
- await rm(skillDir, { recursive: true, force: true });
225
- await mkdir(skillDir, { recursive: true });
226
-
227
- await writeFile(join(skillDir, "SKILL.md"), skill.spec!.skillMd, "utf-8");
228
-
229
- const artifactMounted = artifactBytes !== undefined && artifactBytes.length > 0;
230
- if (artifactMounted) {
231
- const entries = await extractZipFileEntries(artifactBytes, { exclude: ["SKILL.md", MOUNT_MARKER_FILE] });
232
- for (const entry of entries) {
233
- const filePath = join(skillDir, entry.path);
234
- await mkdir(dirname(filePath), { recursive: true });
235
- await writeFile(filePath, entry.content);
236
- }
237
- }
238
-
239
- const versionHash = skill.status?.versionHash ?? "";
240
- if (versionHash !== "") {
241
- const marker: MountMarker = { versionHash, artifactMounted };
242
- await writeFile(join(skillDir, MOUNT_MARKER_FILE), JSON.stringify(marker), "utf-8");
243
- }
244
- }
245
-
246
- /**
247
- * Clean up platform-managed skill directory for a session.
248
- */
249
- export async function cleanupSkills(sessionId: string): Promise<void> {
250
- const platformDir = getPlatformDir(sessionId);
251
- try {
252
- await rm(platformDir, { recursive: true, force: true });
253
- } catch {
254
- // best-effort
255
- }
256
- }
@@ -81,4 +81,19 @@ describe("captureApprovalArtifacts", () => {
81
81
  const { argsPreview } = captureApprovalArtifacts({ toolCallId: "call-1", messages });
82
82
  expect(argsPreview).toContain("needle");
83
83
  });
84
+
85
+ it("returns the redacted args object for stamping the placeholder row (issue #754)", () => {
86
+ // The interrupt-placeholder ToolCall must carry `args`, not just the
87
+ // preview string: the row header extracts its filename-first path from
88
+ // args, so a placeholder without them rendered a pathless "Write" gate.
89
+ // Redaction posture matches the preview: secret keys never surface.
90
+ const messages = [
91
+ aiMessage([
92
+ { id: "call-1", name: "write_file", args: { file_path: "a.txt", content: "hi", token: "sk-secret" } },
93
+ ]),
94
+ ];
95
+ const { args } = captureApprovalArtifacts({ toolCallId: "call-1", messages });
96
+
97
+ expect(args).toEqual({ file_path: "a.txt", content: "hi", token: "[REDACTED]" });
98
+ });
84
99
  });