@stigmer/runner 3.40.0 → 3.41.0

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 (278) hide show
  1. package/README.md +3 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  5. package/dist/activities/call-agent.js +1 -1
  6. package/dist/activities/discover-mcp-server.d.ts +7 -1
  7. package/dist/activities/discover-mcp-server.js +13 -29
  8. package/dist/activities/discover-mcp-server.js.map +1 -1
  9. package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
  10. package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
  11. package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
  12. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
  13. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
  15. package/dist/activities/execute-cursor/approval-policy.js +11 -35
  16. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  17. package/dist/activities/execute-cursor/boundary-rows.js +4 -4
  18. package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
  19. package/dist/activities/execute-cursor/translator.d.ts +12 -17
  20. package/dist/activities/execute-cursor/translator.js +13 -30
  21. package/dist/activities/execute-cursor/translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/turn-stream.js +4 -2
  23. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
  25. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
  26. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
  28. package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
  29. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
  31. package/dist/activities/execute-deep-agent/platform-route.js +87 -19
  32. package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
  34. package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
  35. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  36. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
  37. package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
  38. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
  40. package/dist/activities/execute-deep-agent/translator.js +5 -4
  41. package/dist/activities/execute-deep-agent/translator.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
  43. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
  45. package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
  46. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  47. package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
  48. package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
  49. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  50. package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
  51. package/dist/activities/execute-deep-agent/turn.js +15 -6
  52. package/dist/activities/execute-deep-agent/turn.js.map +1 -1
  53. package/dist/activities/workflow-event-activities.js +2 -0
  54. package/dist/activities/workflow-event-activities.js.map +1 -1
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +2 -0
  57. package/dist/config.js.map +1 -1
  58. package/dist/exit-backstop.d.ts +64 -0
  59. package/dist/exit-backstop.js +79 -0
  60. package/dist/exit-backstop.js.map +1 -0
  61. package/dist/harness/terminal-table.d.ts +12 -5
  62. package/dist/harness/terminal-table.js +13 -13
  63. package/dist/harness/terminal-table.js.map +1 -1
  64. package/dist/harness/transcript/builder.d.ts +21 -0
  65. package/dist/harness/transcript/builder.js +53 -31
  66. package/dist/harness/transcript/builder.js.map +1 -1
  67. package/dist/harness/transcript/events.d.ts +22 -26
  68. package/dist/harness/transcript/events.js +6 -5
  69. package/dist/harness/transcript/events.js.map +1 -1
  70. package/dist/harness/turn-context.d.ts +3 -1
  71. package/dist/harness/turn-context.js +5 -3
  72. package/dist/harness/turn-context.js.map +1 -1
  73. package/dist/main.js +10 -4
  74. package/dist/main.js.map +1 -1
  75. package/dist/middleware/cost-advisory.d.ts +6 -3
  76. package/dist/middleware/cost-advisory.js +6 -3
  77. package/dist/middleware/cost-advisory.js.map +1 -1
  78. package/dist/middleware/execution-budget.d.ts +22 -7
  79. package/dist/middleware/execution-budget.js +57 -35
  80. package/dist/middleware/execution-budget.js.map +1 -1
  81. package/dist/middleware/index.d.ts +10 -7
  82. package/dist/middleware/index.js +10 -7
  83. package/dist/middleware/index.js.map +1 -1
  84. package/dist/middleware/types.d.ts +6 -1
  85. package/dist/runner-manager.js +3 -2
  86. package/dist/runner-manager.js.map +1 -1
  87. package/dist/runner.d.ts +25 -0
  88. package/dist/runner.js +41 -14
  89. package/dist/runner.js.map +1 -1
  90. package/dist/shared/approval-policy.d.ts +23 -13
  91. package/dist/shared/approval-policy.js +47 -14
  92. package/dist/shared/approval-policy.js.map +1 -1
  93. package/dist/shared/caller-identity.d.ts +4 -5
  94. package/dist/shared/caller-identity.js +4 -5
  95. package/dist/shared/caller-identity.js.map +1 -1
  96. package/dist/shared/channel-attachment.js +3 -2
  97. package/dist/shared/channel-attachment.js.map +1 -1
  98. package/dist/shared/connect-backfill.d.ts +2 -1
  99. package/dist/shared/connect-backfill.js +2 -2
  100. package/dist/shared/connect-backfill.js.map +1 -1
  101. package/dist/shared/mcp-resolver.d.ts +10 -1
  102. package/dist/shared/mcp-resolver.js +11 -2
  103. package/dist/shared/mcp-resolver.js.map +1 -1
  104. package/dist/shared/memory-attachment.js +3 -2
  105. package/dist/shared/memory-attachment.js.map +1 -1
  106. package/dist/shared/model-error.d.ts +25 -3
  107. package/dist/shared/model-error.js +78 -10
  108. package/dist/shared/model-error.js.map +1 -1
  109. package/dist/shared/platform-server-address.d.ts +71 -0
  110. package/dist/shared/platform-server-address.js +103 -0
  111. package/dist/shared/platform-server-address.js.map +1 -0
  112. package/dist/shared/prompt-sections.d.ts +6 -28
  113. package/dist/shared/prompt-sections.js +6 -35
  114. package/dist/shared/prompt-sections.js.map +1 -1
  115. package/dist/shared/skill-resolver.d.ts +2 -3
  116. package/dist/shared/skill-resolver.js +2 -3
  117. package/dist/shared/skill-resolver.js.map +1 -1
  118. package/dist/shared/synthesized-attachment.d.ts +0 -5
  119. package/dist/shared/synthesized-attachment.js +0 -12
  120. package/dist/shared/synthesized-attachment.js.map +1 -1
  121. package/dist/shared/tool-rounds.d.ts +51 -23
  122. package/dist/shared/tool-rounds.js +67 -24
  123. package/dist/shared/tool-rounds.js.map +1 -1
  124. package/dist/shared/workspace/platform-dir.d.ts +15 -0
  125. package/dist/shared/workspace/platform-dir.js +18 -1
  126. package/dist/shared/workspace/platform-dir.js.map +1 -1
  127. package/dist/shared/workspace/sources/git.js +13 -6
  128. package/dist/shared/workspace/sources/git.js.map +1 -1
  129. package/dist/shared/workspace/stigmer-link.d.ts +21 -14
  130. package/dist/shared/workspace/stigmer-link.js +91 -29
  131. package/dist/shared/workspace/stigmer-link.js.map +1 -1
  132. package/dist/tools/url-guard.d.ts +5 -1
  133. package/dist/tools/url-guard.js +9 -2
  134. package/dist/tools/url-guard.js.map +1 -1
  135. package/dist/worker.d.ts +11 -2
  136. package/dist/worker.js +1 -1
  137. package/dist/worker.js.map +1 -1
  138. package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
  139. package/dist/workflow-engine/tasks/human-input.js +79 -37
  140. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  141. package/dist/workflow-engine/types.d.ts +14 -4
  142. package/dist/workflow-engine/types.js.map +1 -1
  143. package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
  144. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  145. package/dist/workflows/engine-core.d.ts +25 -0
  146. package/dist/workflows/engine-core.js.map +1 -1
  147. package/dist/workflows/execute-from-execution.d.ts +3 -0
  148. package/dist/workflows/execute-from-execution.js.map +1 -1
  149. package/dist/workflows/human-input-orchestrator.d.ts +9 -12
  150. package/dist/workflows/human-input-orchestrator.js +12 -44
  151. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  152. package/package.json +5 -5
  153. package/src/__test-utils__/config-fixture.ts +1 -0
  154. package/src/__test-utils__/execution-record-fixture.ts +2 -1
  155. package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
  156. package/src/__test-utils__/test-gate.ts +8 -0
  157. package/src/__tests__/exit-backstop.test.ts +91 -0
  158. package/src/__tests__/golden-e2e.test.ts +80 -62
  159. package/src/__tests__/history-encryption-e2e.test.ts +51 -27
  160. package/src/__tests__/runner.test.ts +60 -2
  161. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  162. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
  163. package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
  164. package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
  165. package/src/activities/call-agent.ts +1 -1
  166. package/src/activities/discover-mcp-server.ts +27 -37
  167. package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
  168. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
  169. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
  170. package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
  171. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
  172. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
  173. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  174. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
  175. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
  176. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
  177. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
  178. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
  179. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
  180. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
  181. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
  182. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
  183. package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
  184. package/src/activities/execute-cursor/approval-policy.ts +11 -36
  185. package/src/activities/execute-cursor/boundary-rows.ts +3 -3
  186. package/src/activities/execute-cursor/translator.ts +15 -32
  187. package/src/activities/execute-cursor/turn-stream.ts +4 -1
  188. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
  189. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
  190. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
  191. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
  192. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
  193. package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
  194. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
  195. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
  196. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
  197. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
  198. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
  199. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  200. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
  201. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
  202. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
  203. package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
  204. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
  205. package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
  206. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
  207. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
  208. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
  209. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
  210. package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
  211. package/src/activities/execute-deep-agent/platform-route.ts +109 -20
  212. package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
  213. package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
  214. package/src/activities/execute-deep-agent/translator.ts +5 -4
  215. package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
  216. package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
  217. package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
  218. package/src/activities/execute-deep-agent/turn.ts +16 -7
  219. package/src/activities/workflow-event-activities.ts +2 -0
  220. package/src/config.ts +11 -0
  221. package/src/exit-backstop.ts +97 -0
  222. package/src/harness/README.md +5 -5
  223. package/src/harness/terminal-table.ts +13 -13
  224. package/src/harness/transcript/__tests__/builder.test.ts +93 -21
  225. package/src/harness/transcript/builder.ts +53 -30
  226. package/src/harness/transcript/events.ts +22 -27
  227. package/src/harness/turn-context.ts +5 -3
  228. package/src/main.ts +9 -5
  229. package/src/middleware/__tests__/execution-budget.test.ts +181 -103
  230. package/src/middleware/cost-advisory.ts +6 -3
  231. package/src/middleware/execution-budget.ts +60 -34
  232. package/src/middleware/index.ts +10 -7
  233. package/src/middleware/types.ts +6 -1
  234. package/src/runner-manager.ts +3 -2
  235. package/src/runner.ts +57 -15
  236. package/src/shared/__tests__/approval-policy.test.ts +41 -0
  237. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  238. package/src/shared/__tests__/connect-backfill.test.ts +15 -11
  239. package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
  240. package/src/shared/__tests__/model-error.test.ts +71 -3
  241. package/src/shared/__tests__/platform-server-address.test.ts +122 -0
  242. package/src/shared/__tests__/prompt-sections.test.ts +2 -54
  243. package/src/shared/__tests__/tool-rounds.test.ts +64 -14
  244. package/src/shared/approval-policy.ts +57 -14
  245. package/src/shared/caller-identity.ts +4 -5
  246. package/src/shared/channel-attachment.ts +2 -2
  247. package/src/shared/connect-backfill.ts +5 -1
  248. package/src/shared/mcp-resolver.ts +11 -1
  249. package/src/shared/memory-attachment.ts +2 -2
  250. package/src/shared/model-error.ts +87 -9
  251. package/src/shared/platform-server-address.ts +123 -0
  252. package/src/shared/prompt-sections.ts +6 -55
  253. package/src/shared/skill-resolver.ts +2 -3
  254. package/src/shared/synthesized-attachment.ts +0 -12
  255. package/src/shared/tool-rounds.ts +70 -24
  256. package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
  257. package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
  258. package/src/shared/workspace/platform-dir.ts +19 -1
  259. package/src/shared/workspace/sources/git.ts +14 -8
  260. package/src/shared/workspace/stigmer-link.ts +99 -29
  261. package/src/tools/__tests__/url-guard.test.ts +19 -1
  262. package/src/tools/url-guard.ts +9 -1
  263. package/src/worker.ts +12 -2
  264. package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
  265. package/src/workflow-engine/__tests__/loader.test.ts +1 -1
  266. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
  267. package/src/workflow-engine/tasks/human-input.ts +92 -42
  268. package/src/workflow-engine/types.ts +16 -4
  269. package/src/workflows/call-agent-orchestrator.ts +7 -0
  270. package/src/workflows/engine-core.ts +18 -0
  271. package/src/workflows/execute-from-execution.ts +2 -1
  272. package/src/workflows/human-input-orchestrator.ts +15 -56
  273. package/dist/shared/skill-relevance.d.ts +0 -65
  274. package/dist/shared/skill-relevance.js +0 -175
  275. package/dist/shared/skill-relevance.js.map +0 -1
  276. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
  277. package/src/shared/__tests__/skill-relevance.test.ts +0 -292
  278. package/src/shared/skill-relevance.ts +0 -222
@@ -31,6 +31,12 @@ import {
31
31
  } from "../shared/mcp-transport-guard.js";
32
32
  import { detectOAuthChallenge } from "../shared/mcp-oauth-detect.js";
33
33
  import { injectAnonymousCallerIdentityForDiscovery } from "../shared/caller-identity.js";
34
+ import {
35
+ fillPlatformServerAddress,
36
+ platformServerAddress,
37
+ SERVER_ADDRESS_ENV_KEY,
38
+ type PlatformEndpoints,
39
+ } from "../shared/platform-server-address.js";
34
40
  import { startHeartbeat } from "../shared/heartbeat.js";
35
41
  import { withTimeout } from "../shared/with-timeout.js";
36
42
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
@@ -70,10 +76,6 @@ const STDIO_INIT_TIMEOUT_MS = 270_000;
70
76
  */
71
77
  const REDACTED_MARKER = "***REDACTED***";
72
78
 
73
- const PLATFORM_INJECTABLE_MAP: Record<string, string> = {
74
- STIGMER_SERVER_ADDRESS: "STIGMER_MCP_PUBLIC_ENDPOINT",
75
- };
76
-
77
79
  // ─────────────────────────────────────────────────────────────────────────────
78
80
  // Types
79
81
  // ─────────────────────────────────────────────────────────────────────────────
@@ -237,36 +239,6 @@ function structToPlainObject(struct: unknown): Record<string, unknown> | null {
237
239
  }
238
240
  }
239
241
 
240
- // ─────────────────────────────────────────────────────────────────────────────
241
- // Platform Env Injection
242
- // ─────────────────────────────────────────────────────────────────────────────
243
-
244
- export function injectPlatformEnv(
245
- declaredEnvKeys: Set<string>,
246
- envVars: Record<string, string>,
247
- ): Record<string, string> {
248
- if (declaredEnvKeys.size === 0) return envVars;
249
-
250
- let result: Record<string, string> | undefined;
251
-
252
- for (const [targetKey, sourceKey] of Object.entries(PLATFORM_INJECTABLE_MAP)) {
253
- if (!declaredEnvKeys.has(targetKey)) continue;
254
- const value = process.env[sourceKey];
255
- if (!value) continue;
256
-
257
- if (!result) result = { ...envVars };
258
- if (targetKey in result && result[targetKey] !== value) {
259
- console.info(
260
- `Platform env var '${targetKey}' overrides value from ExecutionContext ` +
261
- `(platform infra vars are authoritative)`,
262
- );
263
- }
264
- result[targetKey] = value;
265
- }
266
-
267
- return result ?? envVars;
268
- }
269
-
270
242
  // ─────────────────────────────────────────────────────────────────────────────
271
243
  // Core Discovery Logic (no Temporal coupling)
272
244
  // ─────────────────────────────────────────────────────────────────────────────
@@ -281,6 +253,12 @@ export interface DiscoverDeps {
281
253
  * tool enumeration.
282
254
  */
283
255
  transportPosture: McpTransportPosture;
256
+ /**
257
+ * The runner's endpoints a missing STIGMER_SERVER_ADDRESS is filled from
258
+ * (the runner Config satisfies it) — the same fill execution applies, so
259
+ * discovery dials what the run will dial.
260
+ */
261
+ platformEndpoints: PlatformEndpoints;
284
262
  }
285
263
 
286
264
  export async function discoverMcpServer(
@@ -305,16 +283,25 @@ export async function discoverMcpServer(
305
283
  const previousState = extractPreviousState(mcpServer);
306
284
 
307
285
  const declaredEnv = mcpServer.spec.env ?? {};
286
+ // A key the platform fills for this transport is not a credential the
287
+ // connect flow must deliver, so it never makes an empty EC a failure.
288
+ const platformFillsAddress =
289
+ platformServerAddress(mcpServer.spec.serverType.case, deps.platformEndpoints) !== null;
290
+ const credentialDeclarations = platformFillsAddress
291
+ ? Object.fromEntries(
292
+ Object.entries(declaredEnv).filter(([key]) => key !== SERVER_ADDRESS_ENV_KEY),
293
+ )
294
+ : declaredEnv;
308
295
  const envVars = await resolveEnvVarsForDiscovery(
309
296
  stigmerClient,
310
297
  executionContextId ?? null,
311
298
  executionContextToken ?? null,
312
299
  slug,
313
- declaredEnv,
300
+ credentialDeclarations,
314
301
  );
315
302
 
316
303
  const declaredEnvKeys = new Set(Object.keys(declaredEnv));
317
- const platformEnv = injectPlatformEnv(declaredEnvKeys, envVars);
304
+ const platformEnv = fillPlatformServerAddress(mcpServer, envVars, deps.platformEndpoints);
318
305
 
319
306
  // Discovery runs with no session, so every declared caller-identity key
320
307
  // resolves to the anonymous sentinel — without it, a server templating
@@ -385,7 +372,9 @@ export async function discoverMcpServer(
385
372
  * sentinel can only produce a misleading 401.
386
373
  * - No non-optional declarations → the old lenient path (warn and continue):
387
374
  * servers declaring nothing (or only optional/injected keys like the
388
- * caller-identity family) legitimately discover without an EC.
375
+ * caller-identity family) legitimately discover without an EC. A
376
+ * STIGMER_SERVER_ADDRESS the platform fills for the server's transport is
377
+ * left out of the count by the caller (platform-server-address.ts).
389
378
  */
390
379
  async function resolveEnvVarsForDiscovery(
391
380
  client: StigmerClient,
@@ -635,6 +624,7 @@ export function createDiscoverMcpServerActivities(config: Config) {
635
624
  return await discoverMcpServer(input, {
636
625
  stigmerClient,
637
626
  transportPosture: resolveMcpTransportPosture(config.mode),
627
+ platformEndpoints: config,
638
628
  });
639
629
  } finally {
640
630
  hb.stop();
@@ -21,7 +21,7 @@ import { create } from "@bufbuild/protobuf";
21
21
  import type { InteractionUpdate, SDKMessage } from "@cursor/sdk";
22
22
  import { AgentExecutionStatusSchema, type AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
23
23
  import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
24
- import { TranscriptBuilder } from "../../../harness/transcript/builder.js";
24
+ import { TranscriptBuilder, type TranscriptObserver } from "../../../harness/transcript/builder.js";
25
25
  import { CursorTranslator, type CursorTranslatorOptions } from "../translator.js";
26
26
 
27
27
  export interface CursorFoldOptions {
@@ -35,6 +35,8 @@ export interface CursorFoldOptions {
35
35
  readonly messages?: AgentMessage[];
36
36
  readonly policies?: CursorTranslatorOptions["policies"];
37
37
  readonly leases?: CursorTranslatorOptions["leases"];
38
+ /** The builder's observer, as the runtime passes its turn timeline; a test that moves a fake clock between folds passes one. */
39
+ readonly observer?: TranscriptObserver;
38
40
  }
39
41
 
40
42
  export class CursorFold {
@@ -50,12 +52,12 @@ export class CursorFold {
50
52
  leases: options.leases ?? { global: false, categories: new Set() },
51
53
  seeded: this.status.messages,
52
54
  });
53
- this.builder = new TranscriptBuilder("exec-fold", this.status);
55
+ this.builder = new TranscriptBuilder("exec-fold", this.status, options.observer);
54
56
  }
55
57
 
56
- /** One stream event, translated and folded, then the deltas queued so far — the loop's order. */
58
+ /** One stream event, translated and folded as one observation, then the deltas queued so far — the loop's order. */
57
59
  event(event: SDKMessage): this {
58
- for (const e of this.translator.translate(event)) this.builder.apply(e);
60
+ this.builder.applyObservation(this.translator.translate(event));
59
61
  this.drain();
60
62
  return this;
61
63
  }
@@ -148,6 +148,7 @@ export function hermeticCursorConfig(env: HermeticEnvironment): Config {
148
148
  temporalConnection: {},
149
149
  stigmerBackendEndpoint: "http://localhost:7234",
150
150
  mcpBridgeEndpoint: null,
151
+ mcpPublicEndpoint: null,
151
152
  stigmerTokenRef: { current: null },
152
153
  cursorApiKey: FIXTURE.cursorApiKey,
153
154
  workspaceRootDir: env.workspaceRootDir,
@@ -23,8 +23,6 @@ import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecutio
23
23
 
24
24
  import {
25
25
  approvalCategory,
26
- builtInRequiresApproval,
27
- getBuiltInApprovalMessage,
28
26
  getBuiltInGatedList,
29
27
  getBuiltInGatedCategories,
30
28
  extractArgKey,
@@ -81,22 +79,10 @@ describe("approvalCategory (cross-taxonomy drift-guard)", () => {
81
79
  });
82
80
  });
83
81
 
84
- describe("builtInRequiresApproval", () => {
85
- it("gates mutating/destructive tools in BOTH taxonomies", () => {
86
- for (const t of ["Write", "StrReplace", "EditNotebook", "Shell", "Delete", "edit", "shell", "delete", "execute", "write"]) {
87
- expect(builtInRequiresApproval(t)).toBe(true);
88
- }
89
- });
90
-
91
- it("allows read-only built-in tools", () => {
92
- for (const t of ["Read", "read", "Grep", "grep", "Glob", "glob", "ls", "think", "task"]) {
93
- expect(builtInRequiresApproval(t)).toBe(false);
94
- }
95
- });
96
-
97
- it("fails open for unknown tools (parity with native, avoids denying auto-approved MCP)", () => {
98
- expect(builtInRequiresApproval("SomeFutureTool")).toBe(false);
99
- expect(builtInRequiresApproval("search_services")).toBe(false);
82
+ describe("the hook's gated set", () => {
83
+ it("fails open for unknown tools: no category, so the hook allows them (parity with native, avoids denying auto-approved MCP)", () => {
84
+ expect(approvalCategory("SomeFutureTool")).toBeUndefined();
85
+ expect(approvalCategory("search_services")).toBeUndefined();
100
86
  });
101
87
 
102
88
  it("exposes the gated set in the HOOK taxonomy (what the hook matches)", () => {
@@ -114,17 +100,6 @@ describe("builtInRequiresApproval", () => {
114
100
  });
115
101
  });
116
102
 
117
- describe("getBuiltInApprovalMessage", () => {
118
- it("returns a category template for gated tools in EITHER taxonomy", () => {
119
- expect(getBuiltInApprovalMessage("Write")).toContain("{{args.path}}");
120
- expect(getBuiltInApprovalMessage("edit")).toContain("{{args.path}}");
121
- expect(getBuiltInApprovalMessage("Shell")).toContain("{{args.command}}");
122
- expect(getBuiltInApprovalMessage("shell")).toContain("{{args.command}}");
123
- expect(getBuiltInApprovalMessage("Read")).toBeUndefined();
124
- expect(getBuiltInApprovalMessage("read")).toBeUndefined();
125
- });
126
- });
127
-
128
103
  describe("extractArgKey (spans both taxonomies' field names)", () => {
129
104
  it("extracts the salient value regardless of field name (file_path or path)", () => {
130
105
  expect(extractArgKey({ file_path: "a.txt" })).toBe("a.txt"); // hook shape
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Hermetic: a GATED built-in that runs under the execution's global bypass
3
+ * (`spec.auto_approve_all`), through the whole `ExecuteCursor` activity.
4
+ *
5
+ * Invariant pinned (#1117): `requiresApproval` says whether THIS call was held
6
+ * for a person's decision, on every harness. A shell the bypass lets run is
7
+ * never held, so its row carries no approval fields — the same answer the
8
+ * native harness gives, where a call that starts was authorized by
9
+ * construction. The row still carries the provenance that explains why it
10
+ * ran (`AUTO_APPROVE_ALL`), which is the field that answers "which policy
11
+ * layer governed this call". Until #1117 the Cursor translator stamped the
12
+ * policy's category verdict on the row at tool start, lease- and bypass-blind,
13
+ * so this row read `requiresApproval: true` with an approval message nobody
14
+ * was ever shown.
15
+ */
16
+
17
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
18
+ import { ApprovalPolicySource, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
19
+
20
+ vi.mock("@cursor/sdk", async () =>
21
+ (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
22
+ );
23
+ vi.mock("@cursor/sdk/sqlite", async () =>
24
+ (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSqliteModule(),
25
+ );
26
+ vi.mock("../../../../client/stigmer-client.js", async () =>
27
+ (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
28
+ );
29
+
30
+ import {
31
+ ScriptedClock,
32
+ createHermeticEnvironment,
33
+ type HermeticEnvironment,
34
+ } from "../../../../__test-utils__/hermetic-activity.js";
35
+ import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
36
+ import {
37
+ FIXTURE,
38
+ SDK_CATALOG,
39
+ beginCursorScenario,
40
+ cursorExecutionRecord,
41
+ runCursorTurn,
42
+ } from "../../__test-utils__/hermetic-cursor.js";
43
+ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
44
+
45
+ const AGENT_ID = "agent-hermetic-bypass-0001";
46
+ const RUN_ID = "run-hermetic-bypass-0001";
47
+ const CALL_ID = "call-hermetic-shell-0001";
48
+ const USER_MESSAGE = "Run the tests.";
49
+ const SHELL_ARGS = { command: "npm test" };
50
+ const SHELL_RESULT = "ok 12 tests\n";
51
+ const ASSISTANT_TEXT = "All twelve tests pass.";
52
+
53
+ describe("ExecuteCursor hermetic — a gated built-in under the global bypass", () => {
54
+ let env: HermeticEnvironment;
55
+ let registry: ReturnType<typeof stubRegistryFetch>;
56
+ const clock = new ScriptedClock();
57
+
58
+ beforeAll(() => {
59
+ env = createHermeticEnvironment();
60
+ registry = stubRegistryFetch();
61
+ clock.install();
62
+ });
63
+
64
+ afterAll(() => {
65
+ clock.uninstall();
66
+ registry.restore();
67
+ env.dispose();
68
+ });
69
+
70
+ it("persists the shell it ran as not held: no approval fields, AUTO_APPROVE_ALL provenance", async () => {
71
+ // ── Arrange ──────────────────────────────────────────────────────────────
72
+ const ev = sdkEvents(AGENT_ID, RUN_ID);
73
+ const agent = new ScriptedCursorAgent({
74
+ agentId: AGENT_ID,
75
+ runIds: [RUN_ID],
76
+ observeStep: () => clock.tick(),
77
+ turns: [
78
+ [
79
+ step.event(ev.init()),
80
+ step.event(ev.assistant("Running them now.")),
81
+ step.event(ev.toolCall(CALL_ID, "shell", "running", SHELL_ARGS)),
82
+ step.event(ev.toolCall(CALL_ID, "shell", "completed", SHELL_ARGS, SHELL_RESULT)),
83
+ step.event(ev.assistant(ASSISTANT_TEXT)),
84
+ step.turnEnded({ inputTokens: 1_500, outputTokens: 40, cacheReadTokens: 0, cacheWriteTokens: 0 }),
85
+ step.finished({ result: ASSISTANT_TEXT, model: { id: FIXTURE.model, params: [] } }),
86
+ ],
87
+ ],
88
+ });
89
+ const record = cursorExecutionRecord({ message: USER_MESSAGE, autoApproveAll: true });
90
+ const scenario = beginCursorScenario({
91
+ env,
92
+ clock,
93
+ record,
94
+ sdk: { agents: [agent], catalog: SDK_CATALOG },
95
+ });
96
+
97
+ // ── Act ──────────────────────────────────────────────────────────────────
98
+ const invocation = await runCursorTurn(scenario);
99
+
100
+ // ── Assert: the turn ran to completion, nothing parked ───────────────────
101
+ expect(invocation.outcome.kind).toBe("returned");
102
+ expect((invocation.outcome as { value: Record<string, unknown> }).value.phase).toBe("EXECUTION_COMPLETED");
103
+ expect(record.waitingToolCalls(), "a bypassed call is never parked").toHaveLength(0);
104
+ expect(agent.runs[0].cancelCalls, "nothing was denied, so nothing was cancelled").toHaveLength(0);
105
+
106
+ // ── Assert: the row says it ran, and why, and that nobody was asked ───────
107
+ const rows = record.toolCalls();
108
+ expect(rows).toHaveLength(1);
109
+ const row = rows[0];
110
+ expect(row.name).toBe("shell");
111
+ expect(row.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
112
+ expect(row.approvalPolicySource, "the layer that let it run").toBe(ApprovalPolicySource.AUTO_APPROVE_ALL);
113
+ expect(row.requiresApproval, "never held for a decision").toBe(false);
114
+ expect(row.approvalMessage, "no card was ever shown").toBe("");
115
+ expect(row.approvalRequestedAt, "no approval was ever requested").toBe("");
116
+
117
+ // ── Assert: hermeticity ──────────────────────────────────────────────────
118
+ expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
119
+ });
120
+ });
@@ -160,15 +160,15 @@ describe.skipIf(!hasBash)("ExecuteCursor hermetic — file-review capture on a g
160
160
  expect(candidate.value.changeSetId).toBe(CHANGE_SET_ID);
161
161
  expect(candidate.value.changes.map((c) => c.pathAfter)).toEqual([FILE]);
162
162
 
163
- // The flowed edit row: the built-in policy still classifies `edit` as a
164
- // gated category (`requiresApproval: true` is the policy's verdict, stamped
165
- // at fold time), but capture mode let it FLOW — so the row is COMPLETED,
166
- // never WAITING_APPROVAL, and the review happens on the file card instead.
163
+ // The flowed edit row: `edit` is a gated category, but capture mode let it
164
+ // FLOW, so nobody was asked — the row is COMPLETED, never WAITING_APPROVAL,
165
+ // carries no approval fields (#1117: `requiresApproval` means the call was
166
+ // held), and the review happens on the file card instead.
167
167
  const rows = record.toolCalls();
168
168
  expect(rows).toHaveLength(1);
169
169
  expect(rows[0].name).toBe("edit");
170
170
  expect(rows[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
171
- expect(rows[0].requiresApproval, "policy verdict, not the gate outcome").toBe(true);
171
+ expect(rows[0].requiresApproval, "held for a decision? no: capture mode let it flow").toBe(false);
172
172
  expect(record.waitingToolCalls(), "no gate row — the review is on the file card").toHaveLength(0);
173
173
 
174
174
  // ── Assert: hermeticity ──────────────────────────────────────────────────
@@ -16,8 +16,6 @@
16
16
  "status": "TOOL_CALL_COMPLETED",
17
17
  "startedAt": "2026-01-01T00:00:05.000Z",
18
18
  "completedAt": "2026-01-01T00:00:06.000Z",
19
- "requiresApproval": true,
20
- "approvalMessage": "Write file: notes.md",
21
19
  "argsPreview": "{\"path\":\"notes.md\",\"content\":\"# Notes\\n\\nA fixture file.\\n\\nReviewed by the hermetic run.\\n\"}",
22
20
  "toolKind": "TOOL_KIND_FILE_EDIT",
23
21
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -12,7 +12,7 @@
12
12
  }
13
13
  ],
14
14
  "phase": "EXECUTION_FAILED",
15
- "error": "Execution failed: [Error] hermetic fault injected at getAgent",
15
+ "error": "[Error] hermetic fault injected at getAgent",
16
16
  "startedAt": "2026-01-01T00:00:00.000Z",
17
17
  "completedAt": "2026-01-01T00:00:00.000Z"
18
18
  }
@@ -15,9 +15,6 @@
15
15
  "startedAt": "2026-01-01T00:00:25.000Z",
16
16
  "completedAt": "2026-01-01T00:00:27.000Z",
17
17
  "error": "sh: npm: command not found",
18
- "requiresApproval": true,
19
- "approvalMessage": "Run command: npm test",
20
- "approvalRequestedAt": "2026-01-01T00:00:27.000Z",
21
18
  "argsPreview": "{\"command\":\"npm test\"}",
22
19
  "toolKind": "TOOL_KIND_SHELL",
23
20
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -15,8 +15,6 @@
15
15
  "status": "TOOL_CALL_COMPLETED",
16
16
  "startedAt": "2026-01-01T00:00:16.000Z",
17
17
  "completedAt": "2026-01-01T00:00:19.000Z",
18
- "requiresApproval": true,
19
- "approvalMessage": "Run command: npm test",
20
18
  "argsPreview": "{\"command\":\"npm test\"}",
21
19
  "toolKind": "TOOL_KIND_SHELL",
22
20
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -15,8 +15,6 @@
15
15
  "status": "TOOL_CALL_COMPLETED",
16
16
  "startedAt": "2026-01-01T00:00:03.000Z",
17
17
  "completedAt": "2026-01-01T00:00:08.000Z",
18
- "requiresApproval": true,
19
- "approvalMessage": "Run command: npm test",
20
18
  "argsPreview": "{\"command\":\"npm test\"}",
21
19
  "toolKind": "TOOL_KIND_SHELL",
22
20
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -13,8 +13,6 @@
13
13
  },
14
14
  "status": "TOOL_CALL_RUNNING",
15
15
  "startedAt": "2026-01-01T00:00:03.000Z",
16
- "requiresApproval": true,
17
- "approvalMessage": "Run command: npm test",
18
16
  "argsPreview": "{\"command\":\"npm test\"}",
19
17
  "toolKind": "TOOL_KIND_SHELL",
20
18
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -15,9 +15,6 @@
15
15
  "startedAt": "2026-01-01T00:00:03.000Z",
16
16
  "completedAt": "2026-01-01T00:00:03.000Z",
17
17
  "error": "blocked by a hook",
18
- "requiresApproval": true,
19
- "approvalMessage": "Run command: rm -rf build",
20
- "approvalRequestedAt": "2026-01-01T00:00:03.000Z",
21
18
  "argsPreview": "{\"command\":\"rm -rf build\"}",
22
19
  "toolKind": "TOOL_KIND_SHELL",
23
20
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -17,8 +17,9 @@
17
17
  * the stream's `running` instant; the live output reaches `result`
18
18
  * (`streamingSource: OUTPUT` while it streams, cleared at finalize); the
19
19
  * stream's `completed` stamps `completedAt` and its result wins; the shell
20
- * row is a gated built-in (`requiresApproval`, "Run command: …") that the
21
- * hook never saw, so no `approvalRequestedAt`. Predicted: NO move (the
20
+ * row is a gated built-in the hook never saw, so it was never held and
21
+ * carries no approval fields (#1117 moved `requiresApproval` and "Run
22
+ * command: …" off rows that ran). Predicted: NO move (the
22
23
  * translator drains the queued deltas AFTER the stream event of the
23
24
  * same window, as the enricher applied them, so the stream's instant wins
24
25
  * exactly as before).
@@ -33,9 +34,10 @@
33
34
  * Arm 3 — the completion delta reports an ERROR, and a model event sits
34
35
  * between it and the stream's own `error`. Pinned: the translator honours the
35
36
  * delta's `result.status: "error"` — the row is FAILED from the
36
- * instant the failure was observed (`completedAt` and, for a gated shell,
37
- * `approvalRequestedAt` at the delta's own tick), and the stream's `error`
38
- * event that follows supplies the text. Until #1097 (as this net first
37
+ * instant the failure was observed (`completedAt` at the delta's own tick),
38
+ * and the stream's `error` event that follows supplies the text. Until #1117
39
+ * a gated shell's ordinary failure also stamped `approvalRequestedAt`, though
40
+ * nobody was ever asked; a failure is not a request. Until #1097 (as this net first
39
41
  * pinned it) the enricher promoted every completion delta to
40
42
  * COMPLETED, the monotonic merge then refused the stream's FAILED, and the
41
43
  * row ended COMPLETED carrying an `error` — a row that lied. Moved in #1097
@@ -178,7 +180,7 @@ describe("ExecuteCursor hermetic — the delta channel on a shell row", () => {
178
180
  expect(row.result, "the live output reached the row").toBe(FULL_OUTPUT);
179
181
  expect(row.isStreaming, "finalize closed the OUTPUT stream").toBe(false);
180
182
  expect(row.startedAt < row.completedAt).toBe(true);
181
- expect(row.requiresApproval, "a shell is a gated built-in the hook never saw").toBe(true);
183
+ expect(row.requiresApproval, "a shell the hook never saw was never held").toBe(false);
182
184
  expect(row.approvalRequestedAt).toBe("");
183
185
  expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
184
186
 
@@ -268,7 +270,7 @@ describe("ExecuteCursor hermetic — the delta channel on a shell row", () => {
268
270
  expect(row.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
269
271
  expect(row.error, "the stream's error event supplied the text").toBe(SHELL_ERROR);
270
272
  expect(row.result).toBe("");
271
- expect(row.approvalRequestedAt, "a gated shell's gate stamp shares the observed instant").toBe(row.completedAt);
273
+ expect(row.approvalRequestedAt, "a failure is not an approval request").toBe("");
272
274
 
273
275
  const json = JSON.stringify(toJson(AgentExecutionStatusSchema, record.lastFullStatus!), null, 2) + "\n";
274
276
  await expect(json).toMatchFileSnapshot("./goldens/shell-output-delta.error-delta.status.json");
@@ -37,6 +37,11 @@
37
37
  * golden is unchanged: its turn fails before the workspace is provisioned,
38
38
  * so no capture is pinned.
39
39
  *
40
+ * Golden hunk since stigmer#1122: `resolution-error`'s `status.error` is
41
+ * `[Error] <message>`, the details row's text, where it was
42
+ * `Execution failed: [Error] <message>`; one grammar for a FAILED error,
43
+ * whichever side caught the fault.
44
+ *
40
45
  * Regenerate ONLY after a deliberate behavior change:
41
46
  * npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
42
47
  */
@@ -180,7 +185,7 @@ describe("ExecuteCursor hermetic — thrown-error arms of the outer catch", () =
180
185
  expect(slim.phase).toBe("EXECUTION_FAILED");
181
186
  expect(record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_FAILED]);
182
187
  const final = record.lastFullStatus!;
183
- expect(final.error).toBe(`Execution failed: [Error] ${CONTROL_PLANE_FAULT}`);
188
+ expect(final.error).toBe(`[Error] ${CONTROL_PLANE_FAULT}`);
184
189
  expect(final.completedAt).not.toBe("");
185
190
  expect(systemMessages(final)).toEqual([
186
191
  GENERIC_SYSTEM_MESSAGE,
@@ -296,14 +296,13 @@ describe("Cursor HITL resume — append-only transcript", () => {
296
296
  expect(status.subAgentExecutions.some((s) => s.id === "sub_1")).toBe(true);
297
297
  });
298
298
 
299
- it("FIX: the approved tools are ALLOWED by the hook on resume — not re-denied (the loop's source)", () => {
300
- // The transcript-superset fix keeps the run off the watchdog, but the OTHER
301
- // half of "no loop" is that the approved tools' grants actually let the hook
302
- // ALLOW the re-issued calls. Drive the real bash hook with the grants the
303
- // resume mints from the persisted run-1 decisions and prove the two approved
304
- // MCP tools pass while the genuinely-new `click` is still gated.
305
- if (!hasBash) return; // hook tests require bash; mirrored skip of the d() guard.
306
-
299
+ // The transcript-superset fix keeps the run off the watchdog, but the OTHER
300
+ // half of "no loop" is that the approved tools' grants actually let the hook
301
+ // ALLOW the re-issued calls. Drive the real bash hook with the grants the
302
+ // resume mints from the persisted run-1 decisions and prove the two approved
303
+ // MCP tools pass while the genuinely-new `click` is still gated. The hook
304
+ // needs bash, mirroring the d() guard.
305
+ it.skipIf(!hasBash)("FIX: the approved tools are ALLOWED by the hook on resume — not re-denied (the loop's source)", () => {
307
306
  const { pendingApprovals, decisions } = reconstructAdjudicatedApprovals(persistedRunOneMessages());
308
307
  const grants = buildApprovalGrants(pendingApprovals, decisions);
309
308
  const state = buildApprovalState(new Map(), false, new Set(), grants);
@@ -426,9 +425,7 @@ describe("Cursor HITL resume — two approvals then clean completion (no loop)",
426
425
  expect(guardRejectionReason(committed, seeded)).toBeUndefined();
427
426
  });
428
427
 
429
- it("the built-in grants actually let the hook allow both re-issues (full closure)", () => {
430
- if (!hasBash) return;
431
-
428
+ it.skipIf(!hasBash)("the built-in grants actually let the hook allow both re-issues (full closure)", () => {
432
429
  const { pendingApprovals, decisions } = reconstructAdjudicatedApprovals(committedBuiltInApprovals());
433
430
  const grants = buildApprovalGrants(pendingApprovals, decisions);
434
431
  const state = buildApprovalState(new Map(), false, new Set(), grants);
@@ -170,9 +170,7 @@ describe("Cursor sequential gates A->B across resume", () => {
170
170
  expect(guardRejectionReason(committed, seeded)).toBeUndefined();
171
171
  });
172
172
 
173
- it("the hook ALLOWS gate A's re-issue and DENIES gate B (out-of-process closure)", () => {
174
- if (!hasBash) return;
175
-
173
+ it.skipIf(!hasBash)("the hook ALLOWS gate A's re-issue and DENIES gate B (out-of-process closure)", () => {
176
174
  // Only gate A is approved going into the resume; its grant is what the hook
177
175
  // reads. Gate B has no grant yet (it gates for the first time this turn).
178
176
  const { pendingApprovals, decisions } = reconstructAdjudicatedApprovals(approvedGateA());