@stigmer/runner 3.14.0 → 3.15.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 (331) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/execution-record-fixture.d.ts +47 -0
  3. package/dist/__test-utils__/execution-record-fixture.js +68 -0
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -0
  5. package/dist/__test-utils__/harness-contract/types.d.ts +166 -0
  6. package/dist/__test-utils__/harness-contract/types.js +49 -0
  7. package/dist/__test-utils__/harness-contract/types.js.map +1 -0
  8. package/dist/__test-utils__/hermetic-activity.d.ts +268 -0
  9. package/dist/__test-utils__/hermetic-activity.js +385 -0
  10. package/dist/__test-utils__/hermetic-activity.js.map +1 -0
  11. package/dist/__test-utils__/mock-client.d.ts +13 -0
  12. package/dist/__test-utils__/mock-client.js +45 -0
  13. package/dist/__test-utils__/mock-client.js.map +1 -0
  14. package/dist/__test-utils__/mock-workspace.d.ts +15 -0
  15. package/dist/__test-utils__/mock-workspace.js +21 -0
  16. package/dist/__test-utils__/mock-workspace.js.map +1 -0
  17. package/dist/__test-utils__/model-registry-fixture.d.ts +56 -0
  18. package/dist/__test-utils__/model-registry-fixture.js +66 -0
  19. package/dist/__test-utils__/model-registry-fixture.js.map +1 -0
  20. package/dist/__test-utils__/turn-input-fixture.d.ts +60 -0
  21. package/dist/__test-utils__/turn-input-fixture.js +118 -0
  22. package/dist/__test-utils__/turn-input-fixture.js.map +1 -0
  23. package/dist/activities/execute-cursor/__test-utils__/contract-subject.d.ts +107 -0
  24. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +344 -0
  25. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -0
  26. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +20 -0
  27. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +58 -0
  28. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  29. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +2 -32
  30. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -1
  31. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +148 -0
  32. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +257 -0
  33. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -0
  34. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +183 -0
  35. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +262 -0
  36. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -0
  37. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +138 -0
  38. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +191 -0
  39. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -0
  40. package/dist/activities/execute-cursor/adapter.d.ts +55 -0
  41. package/dist/activities/execute-cursor/adapter.js +119 -0
  42. package/dist/activities/execute-cursor/adapter.js.map +1 -0
  43. package/dist/activities/execute-cursor/approval-state.d.ts +2 -2
  44. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  45. package/dist/activities/execute-cursor/capture-flow.d.ts +15 -30
  46. package/dist/activities/execute-cursor/capture-flow.js +17 -26
  47. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  48. package/dist/activities/execute-cursor/cursor-capabilities.d.ts +23 -0
  49. package/dist/activities/execute-cursor/cursor-capabilities.js +33 -0
  50. package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -0
  51. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +2 -2
  52. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -1
  53. package/dist/activities/execute-cursor/fetch-interceptor.d.ts +15 -25
  54. package/dist/activities/execute-cursor/fetch-interceptor.js +39 -33
  55. package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
  56. package/dist/activities/execute-cursor/http2-interceptor.d.ts +21 -17
  57. package/dist/activities/execute-cursor/http2-interceptor.js +38 -31
  58. package/dist/activities/execute-cursor/http2-interceptor.js.map +1 -1
  59. package/dist/activities/execute-cursor/message-translator.d.ts +28 -19
  60. package/dist/activities/execute-cursor/message-translator.js +76 -27
  61. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  62. package/dist/activities/execute-cursor/prompt-builder.d.ts +180 -11
  63. package/dist/activities/execute-cursor/prompt-builder.js +171 -0
  64. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  65. package/dist/activities/execute-cursor/rejection-capture.d.ts +3 -6
  66. package/dist/activities/execute-cursor/rejection-capture.js +5 -7
  67. package/dist/activities/execute-cursor/rejection-capture.js.map +1 -1
  68. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -4
  69. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  70. package/dist/activities/execute-cursor/turn-boundary.d.ts +1 -1
  71. package/dist/activities/execute-cursor/turn-settle.d.ts +49 -0
  72. package/dist/activities/execute-cursor/turn-settle.js +507 -0
  73. package/dist/activities/execute-cursor/turn-settle.js.map +1 -0
  74. package/dist/activities/execute-cursor/turn-setup.d.ts +173 -0
  75. package/dist/activities/execute-cursor/turn-setup.js +496 -0
  76. package/dist/activities/execute-cursor/turn-setup.js.map +1 -0
  77. package/dist/activities/execute-cursor/turn-stream.d.ts +41 -71
  78. package/dist/activities/execute-cursor/turn-stream.js +70 -135
  79. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  80. package/dist/activities/execute-cursor/turn.d.ts +43 -0
  81. package/dist/activities/execute-cursor/turn.js +222 -0
  82. package/dist/activities/execute-cursor/turn.js.map +1 -0
  83. package/dist/activities/execute-cursor/usage-pricing.d.ts +38 -0
  84. package/dist/activities/execute-cursor/usage-pricing.js +50 -0
  85. package/dist/activities/execute-cursor/usage-pricing.js.map +1 -0
  86. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +1 -1
  87. package/dist/activities/execute-deep-agent/approval-file-change.js +1 -1
  88. package/dist/activities/execute-deep-agent/environment.js +1 -1
  89. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  90. package/dist/activities/execute-deep-agent/setup.js +3 -2
  91. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  92. package/dist/activities/hydrate-workflow-execution.js +1 -1
  93. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  94. package/dist/config.d.ts +15 -0
  95. package/dist/config.js.map +1 -1
  96. package/dist/harness/capabilities.d.ts +87 -0
  97. package/dist/harness/capabilities.js +37 -0
  98. package/dist/harness/capabilities.js.map +1 -0
  99. package/dist/harness/persist-chokepoint.d.ts +74 -0
  100. package/dist/harness/persist-chokepoint.js +88 -0
  101. package/dist/harness/persist-chokepoint.js.map +1 -0
  102. package/dist/harness/registry.d.ts +106 -0
  103. package/dist/harness/registry.js +168 -0
  104. package/dist/harness/registry.js.map +1 -0
  105. package/dist/harness/run-turn.d.ts +53 -0
  106. package/dist/harness/run-turn.js +528 -0
  107. package/dist/harness/run-turn.js.map +1 -0
  108. package/dist/harness/stop-controller.d.ts +50 -0
  109. package/dist/harness/stop-controller.js +62 -0
  110. package/dist/harness/stop-controller.js.map +1 -0
  111. package/dist/harness/terminal-table.d.ts +141 -0
  112. package/dist/harness/terminal-table.js +239 -0
  113. package/dist/harness/terminal-table.js.map +1 -0
  114. package/dist/harness/turn-context.d.ts +523 -0
  115. package/dist/harness/turn-context.js +739 -0
  116. package/dist/harness/turn-context.js.map +1 -0
  117. package/dist/harness/types.d.ts +448 -0
  118. package/dist/harness/types.js +64 -0
  119. package/dist/harness/types.js.map +1 -0
  120. package/dist/harness/usage-accumulator.d.ts +68 -0
  121. package/dist/harness/usage-accumulator.js +101 -0
  122. package/dist/harness/usage-accumulator.js.map +1 -0
  123. package/dist/harness-adapters.d.ts +27 -0
  124. package/dist/harness-adapters.js +28 -0
  125. package/dist/harness-adapters.js.map +1 -0
  126. package/dist/main.js.map +1 -1
  127. package/dist/runner-manager.js +64 -48
  128. package/dist/runner-manager.js.map +1 -1
  129. package/dist/runner.d.ts +5 -4
  130. package/dist/runner.js +65 -64
  131. package/dist/runner.js.map +1 -1
  132. package/dist/shared/approval-policy.d.ts +1 -1
  133. package/dist/shared/approval-policy.js.map +1 -1
  134. package/dist/{activities/execute-cursor → shared}/attachment-resolver.d.ts +2 -2
  135. package/dist/{activities/execute-cursor → shared}/attachment-resolver.js +5 -5
  136. package/dist/shared/attachment-resolver.js.map +1 -0
  137. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.d.ts +7 -5
  138. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.js +3 -63
  139. package/dist/shared/blueprint-resolver.js.map +1 -0
  140. package/dist/shared/channel-attachment.d.ts +1 -1
  141. package/dist/shared/channel-attachment.js.map +1 -1
  142. package/dist/{activities/execute-cursor → shared}/cost-guard.d.ts +5 -1
  143. package/dist/{activities/execute-cursor → shared}/cost-guard.js +5 -1
  144. package/dist/shared/cost-guard.js.map +1 -0
  145. package/dist/{activities/execute-cursor → shared}/env-resolver.d.ts +1 -1
  146. package/dist/shared/env-resolver.js.map +1 -0
  147. package/dist/{activities/execute-cursor → shared}/exact-apply.d.ts +1 -1
  148. package/dist/{activities/execute-cursor → shared}/exact-apply.js +6 -6
  149. package/dist/shared/exact-apply.js.map +1 -0
  150. package/dist/shared/execution-context.d.ts +35 -0
  151. package/dist/shared/execution-context.js +38 -0
  152. package/dist/shared/execution-context.js.map +1 -0
  153. package/dist/{activities/execute-cursor → shared}/extract-structured-output.d.ts +5 -2
  154. package/dist/{activities/execute-cursor → shared}/extract-structured-output.js +9 -6
  155. package/dist/shared/extract-structured-output.js.map +1 -0
  156. package/dist/shared/filereview/capture.d.ts +1 -1
  157. package/dist/shared/filereview/capture.js +1 -1
  158. package/dist/shared/heartbeat.d.ts +13 -11
  159. package/dist/shared/heartbeat.js +14 -29
  160. package/dist/shared/heartbeat.js.map +1 -1
  161. package/dist/{activities/execute-cursor → shared}/persist-decision.d.ts +5 -2
  162. package/dist/{activities/execute-cursor → shared}/persist-decision.js +4 -1
  163. package/dist/shared/persist-decision.js.map +1 -0
  164. package/dist/{activities/execute-cursor → shared}/skill-resolver.d.ts +16 -3
  165. package/dist/{activities/execute-cursor → shared}/skill-resolver.js +12 -4
  166. package/dist/shared/skill-resolver.js.map +1 -0
  167. package/dist/shared/subagent-rows.d.ts +21 -0
  168. package/dist/shared/subagent-rows.js +34 -0
  169. package/dist/shared/subagent-rows.js.map +1 -0
  170. package/dist/shared/worker-shutdown.d.ts +43 -16
  171. package/dist/shared/worker-shutdown.js +28 -13
  172. package/dist/shared/worker-shutdown.js.map +1 -1
  173. package/dist/shared/workspace/platform-dir.d.ts +1 -1
  174. package/dist/shared/workspace/platform-dir.js +1 -1
  175. package/dist/shared/workspace/session-provision.d.ts +54 -0
  176. package/dist/{activities/execute-cursor/workspace-provision.js → shared/workspace/session-provision.js} +15 -12
  177. package/dist/shared/workspace/session-provision.js.map +1 -0
  178. package/package.json +4 -4
  179. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +325 -0
  180. package/src/__test-utils__/config-fixture.ts +63 -0
  181. package/src/__test-utils__/execution-record-fixture.ts +104 -0
  182. package/src/__test-utils__/harness-boot-order-child.ts +58 -0
  183. package/src/__test-utils__/harness-contract/contract.ts +581 -0
  184. package/src/__test-utils__/harness-contract/recording-sink.ts +107 -0
  185. package/src/__test-utils__/harness-contract/runtime-contract.ts +666 -0
  186. package/src/__test-utils__/harness-contract/scripted-adapter.ts +349 -0
  187. package/src/__test-utils__/harness-contract/types.ts +176 -0
  188. package/src/__test-utils__/hermetic-activity.ts +502 -0
  189. package/src/__test-utils__/model-registry-fixture.ts +72 -0
  190. package/src/__test-utils__/module-specifiers.ts +82 -0
  191. package/src/__test-utils__/proto-helpers.ts +25 -0
  192. package/src/__test-utils__/turn-input-fixture.ts +155 -0
  193. package/src/__tests__/harness-boot-order.test.ts +77 -0
  194. package/src/__tests__/harness-contract.test.ts +27 -0
  195. package/src/__tests__/runner-manager.test.ts +18 -0
  196. package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +102 -0
  197. package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +396 -0
  198. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +66 -0
  199. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +2 -43
  200. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +342 -0
  201. package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +368 -0
  202. package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +254 -0
  203. package/src/activities/execute-cursor/__tests__/adapter-is-temporal-free.test.ts +45 -0
  204. package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +90 -0
  205. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -8
  206. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +28 -1
  207. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +1 -1
  208. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +64 -6
  209. package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +228 -0
  210. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +180 -0
  211. package/src/activities/execute-cursor/__tests__/hermetic/goldens/cost-cap.status.json +53 -0
  212. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn1.status.json +55 -0
  213. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn2.status.json +77 -0
  214. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +126 -0
  215. package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +40 -0
  216. package/src/activities/execute-cursor/__tests__/hermetic/goldens/plain-turn.status.json +48 -0
  217. package/src/activities/execute-cursor/__tests__/hermetic/goldens/platform-stop.status.json +56 -0
  218. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-fresh-agent.status.json +53 -0
  219. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-transport-timeout.status.json +48 -0
  220. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
  221. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-cancelled.status.json +36 -0
  222. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-error-non-retryable.status.json +37 -0
  223. package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +38 -0
  224. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +59 -0
  225. package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-call.status.json +68 -0
  226. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +79 -0
  227. package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +42 -0
  228. package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +170 -0
  229. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +205 -0
  230. package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +171 -0
  231. package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +156 -0
  232. package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +286 -0
  233. package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +332 -0
  234. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +193 -0
  235. package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +137 -0
  236. package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +185 -0
  237. package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +151 -0
  238. package/src/activities/execute-cursor/__tests__/http2-interceptor.test.ts +65 -35
  239. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +0 -40
  240. package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +171 -0
  241. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +118 -243
  242. package/src/activities/execute-cursor/__tests__/usage-pricing.test.ts +84 -0
  243. package/src/activities/execute-cursor/adapter.ts +138 -0
  244. package/src/activities/execute-cursor/approval-state.ts +2 -2
  245. package/src/activities/execute-cursor/capture-flow.ts +18 -41
  246. package/src/activities/execute-cursor/cursor-capabilities.ts +35 -0
  247. package/src/activities/execute-cursor/cursor-mcp-config.ts +2 -2
  248. package/src/activities/execute-cursor/fetch-interceptor.ts +48 -45
  249. package/src/activities/execute-cursor/http2-interceptor.ts +47 -34
  250. package/src/activities/execute-cursor/message-translator.ts +90 -45
  251. package/src/activities/execute-cursor/prompt-builder.ts +327 -12
  252. package/src/activities/execute-cursor/rejection-capture.ts +5 -12
  253. package/src/activities/execute-cursor/session-lifecycle.ts +1 -5
  254. package/src/activities/execute-cursor/turn-boundary.ts +1 -1
  255. package/src/activities/execute-cursor/turn-settle.ts +601 -0
  256. package/src/activities/execute-cursor/turn-setup.ts +645 -0
  257. package/src/activities/execute-cursor/turn-stream.ts +84 -210
  258. package/src/activities/execute-cursor/turn.ts +240 -0
  259. package/src/activities/execute-cursor/usage-pricing.ts +67 -0
  260. package/src/activities/execute-deep-agent/approval-file-change.ts +1 -1
  261. package/src/activities/execute-deep-agent/environment.ts +1 -1
  262. package/src/activities/execute-deep-agent/setup.ts +3 -2
  263. package/src/activities/hydrate-workflow-execution.ts +1 -1
  264. package/src/config.ts +13 -0
  265. package/src/harness/__tests__/import-direction.test.ts +168 -0
  266. package/src/harness/__tests__/persist-chokepoint.test.ts +146 -0
  267. package/src/harness/__tests__/registry.test.ts +168 -0
  268. package/src/harness/__tests__/run-turn.test.ts +165 -0
  269. package/src/harness/__tests__/stop-controller.test.ts +49 -0
  270. package/src/harness/__tests__/turn-context.test.ts +178 -0
  271. package/src/harness/__tests__/usage-accumulator.test.ts +83 -0
  272. package/src/harness/capabilities.ts +92 -0
  273. package/src/harness/persist-chokepoint.ts +112 -0
  274. package/src/harness/registry.ts +197 -0
  275. package/src/harness/run-turn.ts +618 -0
  276. package/src/harness/stop-controller.ts +83 -0
  277. package/src/harness/terminal-table.ts +276 -0
  278. package/src/harness/turn-context.ts +1028 -0
  279. package/src/harness/types.ts +473 -0
  280. package/src/harness/usage-accumulator.ts +122 -0
  281. package/src/harness-adapters.ts +30 -0
  282. package/src/main.ts +1 -1
  283. package/src/runner-manager.ts +68 -61
  284. package/src/runner.ts +67 -78
  285. package/src/shared/__tests__/attachment-naming.test.ts +1 -1
  286. package/src/{activities/execute-cursor → shared}/__tests__/attachment-resolver.test.ts +6 -4
  287. package/src/{activities/execute-cursor → shared}/__tests__/cost-guard.test.ts +2 -1
  288. package/src/{activities/execute-cursor → shared}/__tests__/exact-apply.test.ts +3 -3
  289. package/src/{activities/execute-cursor → shared}/__tests__/extract-structured-output.test.ts +8 -8
  290. package/src/{activities/execute-cursor → shared}/__tests__/persist-decision.test.ts +4 -2
  291. package/src/{activities/execute-cursor → shared}/__tests__/skill-resolver.test.ts +1 -1
  292. package/src/shared/__tests__/subagent-rows.test.ts +34 -0
  293. package/src/shared/__tests__/worker-shutdown.test.ts +44 -39
  294. package/src/shared/approval-policy.ts +1 -1
  295. package/src/{activities/execute-cursor → shared}/attachment-resolver.ts +6 -6
  296. package/src/{activities/execute-cursor → shared}/blueprint-resolver.ts +7 -78
  297. package/src/shared/channel-attachment.ts +1 -1
  298. package/src/{activities/execute-cursor → shared}/cost-guard.ts +5 -1
  299. package/src/{activities/execute-cursor → shared}/env-resolver.ts +1 -1
  300. package/src/{activities/execute-cursor → shared}/exact-apply.ts +7 -7
  301. package/src/shared/execution-context.ts +45 -0
  302. package/src/{activities/execute-cursor → shared}/extract-structured-output.ts +10 -7
  303. package/src/shared/filereview/capture.ts +1 -1
  304. package/src/shared/heartbeat.ts +13 -33
  305. package/src/{activities/execute-cursor → shared}/persist-decision.ts +5 -2
  306. package/src/{activities/execute-cursor → shared}/skill-resolver.ts +20 -6
  307. package/src/shared/subagent-rows.ts +39 -0
  308. package/src/shared/worker-shutdown.ts +55 -25
  309. package/src/{activities/execute-cursor/__tests__/workspace-provision.test.ts → shared/workspace/__tests__/session-provision.test.ts} +25 -28
  310. package/src/shared/workspace/platform-dir.ts +1 -1
  311. package/src/{activities/execute-cursor/workspace-provision.ts → shared/workspace/session-provision.ts} +29 -16
  312. package/dist/activities/execute-cursor/attachment-resolver.js.map +0 -1
  313. package/dist/activities/execute-cursor/blueprint-resolver.js.map +0 -1
  314. package/dist/activities/execute-cursor/cost-guard.js.map +0 -1
  315. package/dist/activities/execute-cursor/env-resolver.js.map +0 -1
  316. package/dist/activities/execute-cursor/exact-apply.js.map +0 -1
  317. package/dist/activities/execute-cursor/extract-structured-output.js.map +0 -1
  318. package/dist/activities/execute-cursor/index.d.ts +0 -209
  319. package/dist/activities/execute-cursor/index.js +0 -2345
  320. package/dist/activities/execute-cursor/index.js.map +0 -1
  321. package/dist/activities/execute-cursor/persist-decision.js.map +0 -1
  322. package/dist/activities/execute-cursor/skill-resolver.js.map +0 -1
  323. package/dist/activities/execute-cursor/usage-accumulator.d.ts +0 -89
  324. package/dist/activities/execute-cursor/usage-accumulator.js +0 -121
  325. package/dist/activities/execute-cursor/usage-accumulator.js.map +0 -1
  326. package/dist/activities/execute-cursor/workspace-provision.d.ts +0 -42
  327. package/dist/activities/execute-cursor/workspace-provision.js.map +0 -1
  328. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +0 -164
  329. package/src/activities/execute-cursor/index.ts +0 -2876
  330. package/src/activities/execute-cursor/usage-accumulator.ts +0 -169
  331. /package/dist/{activities/execute-cursor → shared}/env-resolver.js +0 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Two readers of one fact must agree: the turn runtime reads the adjudicated
3
+ * approvals as the VERDICT (`harness/turn-context.ts` `approvalDecisionsOf`,
4
+ * the `TurnInput.approvalDecisions` the contract carries) and this adapter
5
+ * reads the same rows for the FACTS beside the verdict (the pending-approval
6
+ * protos the grant builder and the reinvocation prompt render, the content
7
+ * digest that authorizes an approved edit by its exact bytes —
8
+ * `reconstructAdjudicatedApprovals`). The contract's rule is that an adapter
9
+ * never re-derives the verdict itself, so the two functions must select the
10
+ * same rows in the same order or the grants would be minted for a different
11
+ * set than the runtime decided on.
12
+ *
13
+ * Lives on the adapter's side because `src/harness/` never imports
14
+ * `src/activities/`, tests included (`harness/__tests__/import-direction.test.ts`).
15
+ */
16
+
17
+ import { describe, expect, it } from "vitest";
18
+ import { create } from "@bufbuild/protobuf";
19
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
20
+ import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
21
+ import { ApprovalAction, MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
22
+
23
+ import { approvalDecisionsOf } from "../../../harness/turn-context.js";
24
+ import { reconstructAdjudicatedApprovals } from "../approval-state.js";
25
+
26
+ describe("the runtime's approvalDecisionsOf and the adapter's reconstructAdjudicatedApprovals", () => {
27
+ it("select the same rows in the same order over every row shape a transcript carries", () => {
28
+ const status = create(AgentExecutionStatusSchema, {
29
+ messages: [
30
+ create(AgentMessageSchema, {
31
+ type: MessageType.MESSAGE_AI,
32
+ toolCalls: [
33
+ create(ToolCallSchema, {
34
+ id: "tc-approved",
35
+ name: "Shell",
36
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
37
+ approvalAction: ApprovalAction.APPROVE,
38
+ approvalContentDigest: "sha256:approved",
39
+ }),
40
+ create(ToolCallSchema, {
41
+ id: "tc-undecided",
42
+ name: "Shell",
43
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
44
+ approvalAction: ApprovalAction.UNSPECIFIED,
45
+ }),
46
+ ],
47
+ }),
48
+ create(AgentMessageSchema, {
49
+ type: MessageType.MESSAGE_AI,
50
+ toolCalls: [
51
+ create(ToolCallSchema, {
52
+ id: "tc-completed",
53
+ name: "Read",
54
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
55
+ approvalAction: ApprovalAction.APPROVE,
56
+ }),
57
+ create(ToolCallSchema, {
58
+ id: "tc-skipped",
59
+ name: "Write",
60
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
61
+ approvalAction: ApprovalAction.SKIP,
62
+ }),
63
+ create(ToolCallSchema, {
64
+ id: "tc-rejected",
65
+ name: "Write",
66
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
67
+ approvalAction: ApprovalAction.REJECT,
68
+ }),
69
+ ],
70
+ }),
71
+ ],
72
+ });
73
+
74
+ const runtime = approvalDecisionsOf(status);
75
+ const adapter = reconstructAdjudicatedApprovals(status.messages);
76
+
77
+ expect([...runtime.entries()]).toEqual([...adapter.decisions.entries()]);
78
+ expect(
79
+ adapter.pendingApprovals.map((pa) => pa.toolCallId),
80
+ "the adapter's row facts cover exactly the rows the runtime decided on",
81
+ ).toEqual([...runtime.keys()]);
82
+ expect([...runtime.keys()]).toEqual(["tc-approved", "tc-skipped", "tc-rejected"]);
83
+ });
84
+
85
+ it("both read nothing from a fresh transcript", () => {
86
+ const status = create(AgentExecutionStatusSchema, {});
87
+ expect(approvalDecisionsOf(status).size).toBe(0);
88
+ expect(reconstructAdjudicatedApprovals(status.messages).decisions.size).toBe(0);
89
+ });
90
+ });
@@ -12,8 +12,8 @@ import { create } from "@bufbuild/protobuf";
12
12
  import { ApprovalAction, InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
13
13
  import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
14
14
 
15
- import { appendStructuredOutputDirective, buildPrompt, isHitlReinvocation, primarySendCarriesImages, promptCarriesStandingContext } from "../index.js";
16
- import type { BuildPromptInput } from "../index.js";
15
+ import { appendStructuredOutputDirective, buildPrompt, isHitlReinvocation, primarySendCarriesImages, promptCarriesStandingContext } from "../prompt-builder.js";
16
+ import type { BuildPromptInput } from "../prompt-builder.js";
17
17
  import { buildReinvocationPrompt, formatInteractionModePrefix, formatImplementPlanSection, formatToolApprovalProtocol, buildToolApprovalRuleFile } from "../prompt-builder.js";
18
18
  import { PLAN_MODE_DIRECTIVE } from "../../../shared/plan-mode-prompt.js";
19
19
  import type { AgentResolution, AgentResolutionReason } from "../session-lifecycle.js";
@@ -38,7 +38,7 @@ function resolution(
38
38
  function input(overrides: Partial<BuildPromptInput>): BuildPromptInput {
39
39
  return {
40
40
  resolution: resolution("local", "resumed_successfully"),
41
- approvalDecisions: undefined,
41
+ approvalDecisions: new Map(),
42
42
  instructions: "You are a test agent.",
43
43
  userMessage: USER_MESSAGE,
44
44
  skills: [],
@@ -357,7 +357,7 @@ describe("buildPrompt", () => {
357
357
  const prompt = buildPrompt(
358
358
  input({
359
359
  resolution: resolution("local", reason),
360
- approvalDecisions: hitl ? hitlDecisions : undefined,
360
+ approvalDecisions: hitl ? hitlDecisions : new Map(),
361
361
  pendingApprovals: hitl ? hitlApprovals : [],
362
362
  recalledMemories: { facts: ["Prefers OpenTofu."] },
363
363
  }),
@@ -395,8 +395,7 @@ describe("buildPrompt", () => {
395
395
  });
396
396
 
397
397
  describe("isHitlReinvocation (paired with resolution.reason at every consumer — issue #366)", () => {
398
- it("is false with no decisions and false with an empty map", () => {
399
- expect(isHitlReinvocation(undefined)).toBe(false);
398
+ it("is false with an empty map (the shape a turn with no adjudicated approvals carries)", () => {
400
399
  expect(isHitlReinvocation(new Map())).toBe(false);
401
400
  });
402
401
 
@@ -504,8 +503,8 @@ describe("HITL recovery — fresh agent mid-HITL (issue #366)", () => {
504
503
  // resolution-time resume failure.
505
504
  expect(primarySendCarriesImages(decisions(), "created_after_resume_failure")).toBe(true);
506
505
  // Non-HITL turns always carry the message's images, resumed or not.
507
- expect(primarySendCarriesImages(undefined, "resumed_successfully")).toBe(true);
508
- expect(primarySendCarriesImages(undefined, "created_first_execution")).toBe(true);
506
+ expect(primarySendCarriesImages(new Map(), "resumed_successfully")).toBe(true);
507
+ expect(primarySendCarriesImages(new Map(), "created_first_execution")).toBe(true);
509
508
  });
510
509
 
511
510
  it("the structured-output directive is a pure append shared by the primary and recovery sends — absent schema, absent suffix", () => {
@@ -44,7 +44,7 @@ import {
44
44
  ToolCallStatus,
45
45
  } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
46
46
  import {
47
- applyCaptureDecisions,
47
+ CURSOR_FILE_REVIEW_IDENTITY,
48
48
  captureBaselineToLedger,
49
49
  captureTurnToLedger,
50
50
  } from "../capture-flow.js";
@@ -52,6 +52,33 @@ import { toolIdentity, primaryToken } from "../approval-state.js";
52
52
  import { contentDigest } from "../../../shared/file-tools.js";
53
53
  import { buildObservationStagingScript, casObservationsDir } from "../cas-observations.js";
54
54
  import { makeInMemoryArtifactStorage } from "../../../__test-utils__/fake-artifact-storage.js";
55
+ import { applyCaptureDecisions as sharedApplyCaptureDecisions } from "../../../shared/filereview/capture.js";
56
+ import { casBlobReader } from "../../../shared/filereview/cas-substrate.js";
57
+ import type { ArtifactStorage } from "../../../shared/artifact-storage.js";
58
+ import type { FileChangeSet } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
59
+
60
+ /**
61
+ * The resume reconcile as the turn runtime runs it for this harness
62
+ * (`harness/turn-context.ts` `reconcileReinvocation`): the shared reconcile
63
+ * under Cursor's file-review identity, with the CAS blob reader derived from
64
+ * the same storage. Bound here so every resume case below proves the Cursor
65
+ * pair (harness id, excluded gate paths) over the shared code, exactly as
66
+ * production composes it.
67
+ */
68
+ function applyCaptureDecisions(opts: {
69
+ readonly status: AgentExecutionStatus;
70
+ readonly gitRoot: string;
71
+ readonly executionId: string;
72
+ readonly changeSet: FileChangeSet;
73
+ readonly storage?: ArtifactStorage;
74
+ readonly gitWorkspace?: boolean;
75
+ }) {
76
+ return sharedApplyCaptureDecisions({
77
+ ...opts,
78
+ ...CURSOR_FILE_REVIEW_IDENTITY,
79
+ readBlob: opts.storage ? casBlobReader(opts.storage) : undefined,
80
+ });
81
+ }
55
82
 
56
83
  const execFileAsync = promisify(execFile);
57
84
  const EXEC_ID = "exec-capflow-1";
@@ -50,7 +50,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
50
50
  import { tmpdir } from "node:os";
51
51
  import { join } from "node:path";
52
52
 
53
- import { applyApprovedWholeFileWrites, excludeAppliedFromGrants } from "../exact-apply.js";
53
+ import { applyApprovedWholeFileWrites, excludeAppliedFromGrants } from "../../../shared/exact-apply.js";
54
54
  import { reconstructAdjudicatedApprovals, buildApprovalGrants } from "../approval-state.js";
55
55
  import type { ApprovalGrant } from "../approval-state.js";
56
56
  import { LocalWorkspaceBackend } from "../../../shared/workspace/local-backend.js";
@@ -19,7 +19,7 @@ describe("fetch-interceptor", () => {
19
19
  let calls: CapturedCall[];
20
20
  let installFetchInterceptor: typeof import("../fetch-interceptor.js").installFetchInterceptor;
21
21
  let uninstallFetchInterceptor: typeof import("../fetch-interceptor.js").uninstallFetchInterceptor;
22
- let getExecutionContext: typeof import("../fetch-interceptor.js").getExecutionContext;
22
+ let getExecutionContext: typeof import("../../../shared/execution-context.js").getExecutionContext;
23
23
 
24
24
  beforeEach(async () => {
25
25
  calls = [];
@@ -40,7 +40,9 @@ describe("fetch-interceptor", () => {
40
40
  const mod = await import("../fetch-interceptor.js");
41
41
  installFetchInterceptor = mod.installFetchInterceptor;
42
42
  uninstallFetchInterceptor = mod.uninstallFetchInterceptor;
43
- getExecutionContext = mod.getExecutionContext;
43
+ // The store lives in shared/ and must be the SAME fresh instance the
44
+ // interceptor just imported, so it is resolved after the reset too.
45
+ getExecutionContext = (await import("../../../shared/execution-context.js")).getExecutionContext;
44
46
  });
45
47
 
46
48
  afterEach(() => {
@@ -50,7 +52,7 @@ describe("fetch-interceptor", () => {
50
52
 
51
53
  describe("Connect-RPC paths via fetch (BiDi proxy auth injection)", () => {
52
54
  beforeEach(() => {
53
- installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, stigmerToken: STIGMER_TOKEN });
55
+ installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, proxyTokenRef: { current: STIGMER_TOKEN } });
54
56
  });
55
57
 
56
58
  it("injects x-stigmer-auth on /aiserver.v1 path targeting proxy endpoint", async () => {
@@ -139,7 +141,7 @@ describe("fetch-interceptor", () => {
139
141
 
140
142
  describe("REST path URL rewriting (existing behavior)", () => {
141
143
  beforeEach(() => {
142
- installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, stigmerToken: STIGMER_TOKEN });
144
+ installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, proxyTokenRef: { current: STIGMER_TOKEN } });
143
145
  });
144
146
 
145
147
  it("rewrites Cursor-domain REST URLs and replaces auth", async () => {
@@ -171,9 +173,65 @@ describe("fetch-interceptor", () => {
171
173
  });
172
174
  });
173
175
 
176
+ describe("the token ref is read per request (the root rotates it in place; Q-S2-7)", () => {
177
+ let proxyTokenRef: { current: string | null };
178
+
179
+ beforeEach(() => {
180
+ proxyTokenRef = { current: STIGMER_TOKEN };
181
+ installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, proxyTokenRef });
182
+ });
183
+
184
+ it("a rotation reaches the next REST rewrite and the next BiDi-auth fetch without a call into the module", async () => {
185
+ proxyTokenRef.current = "refreshed-stigmer-jwt-token";
186
+
187
+ await globalThis.fetch("https://api.cursor.com/v1/models", { headers: { authorization: "Bearer original-key" } });
188
+ await globalThis.fetch(`${PROXY_ENDPOINT}/aiserver.v1.AnalyticsService/BootstrapStatsig`, { method: "POST" });
189
+
190
+ expect(calls).toHaveLength(2);
191
+ expect(new Headers(calls[0].init?.headers).get("authorization")).toBe("Bearer refreshed-stigmer-jwt-token");
192
+ expect(new Headers(calls[1].init?.headers).get("x-stigmer-auth")).toBe("Bearer refreshed-stigmer-jwt-token");
193
+ });
194
+
195
+ it("an emptied ref sends no proxy credential and never leaks the SDK's own authorization to the proxy", async () => {
196
+ proxyTokenRef.current = null;
197
+
198
+ await globalThis.fetch("https://api.cursor.com/v1/models", { headers: { authorization: "Bearer cursor-secret" } });
199
+ await globalThis.fetch(`${PROXY_ENDPOINT}/aiserver.v1.AnalyticsService/BootstrapStatsig`, { method: "POST" });
200
+
201
+ expect(new Headers(calls[0].init?.headers).get("authorization")).toBeNull();
202
+ expect(new Headers(calls[1].init?.headers).get("x-stigmer-auth")).toBeNull();
203
+ });
204
+ });
205
+
206
+ describe("install guard", () => {
207
+ it("a proxy endpoint with no bound ref fails the install, as a missing token always has, and leaves fetch untouched", () => {
208
+ const before = globalThis.fetch;
209
+ expect(() => installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, proxyTokenRef: undefined })).toThrow(
210
+ /STIGMER_TOKEN is required when STIGMER_PROXY_ENDPOINT is set/,
211
+ );
212
+ expect(() => installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, proxyTokenRef: { current: null } })).toThrow(
213
+ /STIGMER_TOKEN is required when STIGMER_PROXY_ENDPOINT is set/,
214
+ );
215
+ expect(globalThis.fetch).toBe(before);
216
+ });
217
+
218
+ it("is idempotent: a second install rebinds the ref and keeps delegating to the fetch captured at load", async () => {
219
+ const first = { current: "first-token" };
220
+ const second = { current: "second-token" };
221
+ installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, proxyTokenRef: first });
222
+ installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, proxyTokenRef: second });
223
+
224
+ await globalThis.fetch(`${PROXY_ENDPOINT}/aiserver.v1.AnalyticsService/BootstrapStatsig`, { method: "POST" });
225
+
226
+ // One hop to the captured fetch (a self-capture would recurse or double-wrap).
227
+ expect(calls).toHaveLength(1);
228
+ expect(new Headers(calls[0].init?.headers).get("x-stigmer-auth")).toBe("Bearer second-token");
229
+ });
230
+ });
231
+
174
232
  describe("timed REST path timing", () => {
175
233
  beforeEach(() => {
176
- installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, stigmerToken: STIGMER_TOKEN });
234
+ installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, proxyTokenRef: { current: STIGMER_TOKEN } });
177
235
  });
178
236
 
179
237
  /**
@@ -284,7 +342,7 @@ describe("fetch-interceptor", () => {
284
342
 
285
343
  describe("passthrough (no interception)", () => {
286
344
  beforeEach(() => {
287
- installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, stigmerToken: STIGMER_TOKEN });
345
+ installFetchInterceptor({ proxyEndpoint: PROXY_ENDPOINT, proxyTokenRef: { current: STIGMER_TOKEN } });
288
346
  });
289
347
 
290
348
  it("does NOT intercept Connect-RPC paths on non-proxy hosts", async () => {
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Hermetic golden: the DENY-AND-RETRY approval round trip through two real
3
+ * `ExecuteCursor` invocations, with the REAL bash hook as the out-of-process
4
+ * half.
5
+ *
6
+ * Invariant pinned (the Cursor pause primitive, parent §4a `pausePrimitive:
7
+ * "deny-and-retry"`): the model proposes a gated built-in (`shell`); the
8
+ * workspace hook the activity installed DENIES it and appends to the denial
9
+ * ledger; the stream loop reads the ledger on the next `tool_call` event, cancels
10
+ * the run, and the turn boundary overlays the row as WAITING_APPROVAL; the
11
+ * activity persists WAITING_FOR_APPROVAL, parks the agent, and RETURNS a slim
12
+ * status. The user approves (the server writes `approval_action` on the row —
13
+ * here the record does, per field ownership). The reinvocation (`thread_id` set,
14
+ * `turn_seq` 1) seeds its transcript from the persisted execution, derives a
15
+ * grant from the approved row, reinstalls the gate, and the SAME hook now ALLOWS
16
+ * the re-issued call — which the model emits under a FRESH `call_id`, and which
17
+ * the transcript reconciles onto the committed row by canonical identity (a
18
+ * superset, never a duplicate, never a second gate). The turn ends COMPLETED.
19
+ *
20
+ * Two goldens, one per invocation: `goldens/deny-and-retry.turn1.status.json`
21
+ * (the paused transcript) and `goldens/deny-and-retry.turn2.status.json` (the
22
+ * resumed, completed transcript). S2 must reproduce both byte for byte.
23
+ *
24
+ * Parent phase rows exercised beyond `tool-call`: reinvocation detection and
25
+ * transcript seeding (Phase 3); reading approval decisions from the persisted
26
+ * transcript (`reconstructAdjudicatedApprovals`); grants → approval state →
27
+ * the gate (Phase 5c); the first-denial early stop in the stream loop; the
28
+ * turn boundary's denial overlay; `enterApprovalPause` (WAITING_FOR_APPROVAL,
29
+ * park, return); the parked-agent checkout on the resume (#215, no `Agent.resume`
30
+ * call); the resume prompt.
31
+ *
32
+ * The hook is the runner's own generated bash script, found the way the SDK
33
+ * finds it (`.cursor/hooks.json` in the workspace) and run with the SDK's real
34
+ * preToolUse input shape (`cursor-hook-harness.ts` builders). Skipped where
35
+ * `bash` is unavailable — reported as SKIPPED, never a silent pass.
36
+ *
37
+ * Regenerate ONLY after a deliberate behavior change:
38
+ * npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
39
+ */
40
+
41
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
42
+ import { toJson } from "@bufbuild/protobuf";
43
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
44
+ import {
45
+ ApprovalAction,
46
+ ExecutionPhase,
47
+ ToolCallStatus,
48
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
49
+
50
+ vi.mock("@cursor/sdk", async () =>
51
+ (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
52
+ );
53
+ vi.mock("../../../../client/stigmer-client.js", async () =>
54
+ (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
55
+ );
56
+
57
+ import {
58
+ ScriptedClock,
59
+ createHermeticEnvironment,
60
+ type HermeticEnvironment,
61
+ } from "../../../../__test-utils__/hermetic-activity.js";
62
+ import { hasBash, hookShell } from "../../__test-utils__/cursor-hook-harness.js";
63
+ import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
64
+ import {
65
+ FIXTURE,
66
+ SDK_CATALOG,
67
+ beginCursorScenario,
68
+ cursorExecutionRecord,
69
+ runCursorTurn,
70
+ runWorkspaceHook,
71
+ sessionWorkspaceDir,
72
+ } from "../../__test-utils__/hermetic-cursor.js";
73
+ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
74
+
75
+ const AGENT_ID = "agent-hermetic-deny-0001";
76
+ const RUN_1 = "run-hermetic-deny-0001";
77
+ const RUN_2 = "run-hermetic-deny-0002";
78
+ const CALL_TURN_1 = "call-hermetic-shell-0001";
79
+ const CALL_TURN_2 = "call-hermetic-shell-0002";
80
+ const USER_MESSAGE = "Run the build and tell me if it passes.";
81
+ const COMMAND = "npm run build";
82
+ const SHELL_ARGS = { command: COMMAND };
83
+ const BUILD_OUTPUT = "build ok (hermetic)\n";
84
+ const FINAL_TEXT = "The build passes.";
85
+
86
+ describe.skipIf(!hasBash)("ExecuteCursor hermetic — deny-and-retry approval round trip", () => {
87
+ let env: HermeticEnvironment;
88
+ let registry: ReturnType<typeof stubRegistryFetch>;
89
+ const clock = new ScriptedClock();
90
+
91
+ beforeAll(() => {
92
+ env = createHermeticEnvironment();
93
+ registry = stubRegistryFetch();
94
+ clock.install();
95
+ });
96
+
97
+ afterAll(() => {
98
+ clock.uninstall();
99
+ registry.restore();
100
+ env.dispose();
101
+ });
102
+
103
+ it("pauses on the hook's deny, then completes on the approved re-issue", async () => {
104
+ // ── Arrange ──────────────────────────────────────────────────────────────
105
+ const workspaceRoot = sessionWorkspaceDir(env);
106
+ const hookDecisions: string[] = [];
107
+ const runHook = (label: string) =>
108
+ step.effect(label, () => {
109
+ hookDecisions.push(runWorkspaceHook(workspaceRoot, hookShell(COMMAND)).permission);
110
+ });
111
+
112
+ const ev1 = sdkEvents(AGENT_ID, RUN_1);
113
+ const ev2 = sdkEvents(AGENT_ID, RUN_2);
114
+ const agent = new ScriptedCursorAgent({
115
+ agentId: AGENT_ID,
116
+ runIds: [RUN_1, RUN_2],
117
+ observeStep: () => clock.tick(),
118
+ turns: [
119
+ // Turn 1: the SDK runs the hook before executing the gated tool; the
120
+ // hook denies and writes the ledger; the SDK reports the call blocked.
121
+ [
122
+ step.event(ev1.init()),
123
+ step.event(ev1.assistant("I'll run the build now.")),
124
+ step.event(ev1.toolCall(CALL_TURN_1, "shell", "running", SHELL_ARGS)),
125
+ runHook("hook: turn 1 (expect deny)"),
126
+ step.event(ev1.toolCall(CALL_TURN_1, "shell", "error", SHELL_ARGS, "Blocked by hook")),
127
+ // Never reached: the loop detects the denial on the event above and
128
+ // cancels the run. Present so a regression that does NOT cancel is
129
+ // caught by the golden, not by a missing-result throw.
130
+ step.event(ev1.assistant("The build was blocked.")),
131
+ step.finished({ result: "The build was blocked." }),
132
+ ],
133
+ // Turn 2 (resumed): the hook runs again for the re-issued call and now
134
+ // allows it (the grant); the tool runs under a FRESH call id.
135
+ [
136
+ runHook("hook: turn 2 (expect allow)"),
137
+ step.event(ev2.toolCall(CALL_TURN_2, "shell", "running", SHELL_ARGS)),
138
+ step.event(ev2.toolCall(CALL_TURN_2, "shell", "completed", SHELL_ARGS, BUILD_OUTPUT)),
139
+ step.event(ev2.assistant(FINAL_TEXT)),
140
+ step.turnEnded({ inputTokens: 3_100, outputTokens: 60 }),
141
+ step.finished({ result: FINAL_TEXT, model: { id: FIXTURE.model, params: [] } }),
142
+ ],
143
+ ],
144
+ });
145
+ const record = cursorExecutionRecord({ message: USER_MESSAGE });
146
+ const scenario = beginCursorScenario({
147
+ env,
148
+ clock,
149
+ record,
150
+ sdk: { agents: [agent], catalog: SDK_CATALOG },
151
+ });
152
+
153
+ // ── Act 1: the first invocation ──────────────────────────────────────────
154
+ const turn1 = await runCursorTurn(scenario, { threadId: "", turnSeq: 0 });
155
+
156
+ // ── Assert 1: paused for approval, returned, agent parked ────────────────
157
+ expect(hookDecisions, "the real hook denied the gated shell call").toEqual(["deny"]);
158
+ expect(turn1.outcome.kind, "an approval pause RETURNS to the workflow").toBe("returned");
159
+ expect((turn1.outcome as { value: Record<string, unknown> }).value.phase).toBe(
160
+ "EXECUTION_WAITING_FOR_APPROVAL",
161
+ );
162
+ expect(record.persistedPhases).toEqual([
163
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
164
+ ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
165
+ ]);
166
+ expect(agent.runs[0].cancelCalls, "the first denial cancels the run exactly once").toHaveLength(1);
167
+ expect(agent.closeCalls, "the agent is parked for the resume, not closed").toBe(0);
168
+
169
+ const waiting = record.waitingToolCalls();
170
+ expect(waiting, "exactly one gate row").toHaveLength(1);
171
+ expect(waiting[0].id).toBe(CALL_TURN_1);
172
+ expect(waiting[0].name).toBe("shell");
173
+ expect(waiting[0].requiresApproval).toBe(true);
174
+ // The gate row carries what the HOOK saw (the ledger's captured tool_input:
175
+ // command, cwd, timeout), overlaid on the stream's args — the golden pins
176
+ // the full shape; here only the shared salient field is asserted.
177
+ expect(waiting[0].args).toMatchObject(SHELL_ARGS);
178
+ // The model's post-denial narration never reaches the user as fact.
179
+ expect(record.status?.messages.some((m) => m.content === "The build was blocked.")).toBe(false);
180
+
181
+ const turn1Json = JSON.stringify(toJson(AgentExecutionStatusSchema, record.lastFullStatus!), null, 2) + "\n";
182
+ await expect(turn1Json).toMatchFileSnapshot("./goldens/deny-and-retry.turn1.status.json");
183
+
184
+ // ── The user approves (the server's SubmitApproval effect on the row) ────
185
+ clock.tick();
186
+ expect(record.decideWaitingToolCalls(ApprovalAction.APPROVE, new Date().toISOString())).toBe(1);
187
+
188
+ // ── Act 2: the reinvocation ──────────────────────────────────────────────
189
+ const turn2 = await runCursorTurn(scenario, { threadId: AGENT_ID, turnSeq: 1 });
190
+
191
+ // ── Assert 2: allowed, reconciled, completed ─────────────────────────────
192
+ expect(hookDecisions, "the SAME hook allows the approved re-issue").toEqual(["deny", "allow"]);
193
+ expect(turn2.outcome.kind).toBe("returned");
194
+ const slim2 = (turn2.outcome as { value: Record<string, unknown> }).value;
195
+ expect(slim2.phase).toBe("EXECUTION_COMPLETED");
196
+ expect(slim2.final_text).toBe(FINAL_TEXT);
197
+ expect(record.persistedPhases).toEqual([
198
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
199
+ ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
200
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
201
+ ExecutionPhase.EXECUTION_COMPLETED,
202
+ ]);
203
+
204
+ // #215: the resume checks the parked agent out of the session cache; the
205
+ // SDK is asked to create once for the whole round trip and never to resume.
206
+ expect(scenario.sdk.resolutions.map((r) => r.kind)).toEqual(["create"]);
207
+ expect(agent.sends).toHaveLength(2);
208
+
209
+ // Each tool gates exactly once: the committed row survives under its
210
+ // ORIGINAL id, now COMPLETED with the tool's result; the fresh call id the
211
+ // model emitted is reconciled onto it, never a second row.
212
+ const rows = record.toolCalls();
213
+ expect(rows.filter((tc) => tc.name === "shell")).toHaveLength(1);
214
+ const shell = rows.find((tc) => tc.name === "shell")!;
215
+ expect(shell.id).toBe(CALL_TURN_1);
216
+ expect(shell.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
217
+ expect(shell.result).toBe(BUILD_OUTPUT);
218
+ expect(shell.approvalAction, "the decision survives on the row").toBe(ApprovalAction.APPROVE);
219
+ expect(rows.some((tc) => tc.id === CALL_TURN_2)).toBe(false);
220
+ expect(record.waitingToolCalls()).toHaveLength(0);
221
+
222
+ // ── Assert: hermeticity ──────────────────────────────────────────────────
223
+ expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
224
+
225
+ const turn2Json = JSON.stringify(toJson(AgentExecutionStatusSchema, record.lastFullStatus!), null, 2) + "\n";
226
+ await expect(turn2Json).toMatchFileSnapshot("./goldens/deny-and-retry.turn2.status.json");
227
+ });
228
+ });