@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,502 @@
1
+ /**
2
+ * Hermetic activity driver — run a REAL runner activity end to end with no
3
+ * network, no credentials, and no live Temporal worker, and read back every
4
+ * status it persisted.
5
+ *
6
+ * Why this exists: the runner's activities (`ExecuteCursor`, `ExecuteDeepAgent`)
7
+ * are the wire contract the control plane's workflow keys on — the phases they
8
+ * persist, the copy they write, whether they RETURN a slim status or THROW
9
+ * `CancelledFailure`. Refactoring them safely needs goldens recorded through the
10
+ * activity whole, not through its modules one at a time. The native harness
11
+ * tests already run their activity hermetically by mocking three modules at the
12
+ * boundary (`execute-deep-agent/__tests__/{index,hitl-*,sequential-gate-resume}.test.ts`);
13
+ * this module is that convention made reusable, harness-agnostic, and driven by
14
+ * the framework's own activity environment instead of a hand-written stand-in.
15
+ *
16
+ * What is generic here and what is not: everything an activity touches that is
17
+ * NOT the vendor SDK — the Temporal `Context`, the control-plane client, the
18
+ * runner-owned `~/.stigmer` tree, artifact storage, the model registry, the
19
+ * clock, the worker-shutdown signal — is set up here. The vendor SDK double
20
+ * belongs to the harness (`activities/execute-cursor/__test-utils__/scripted-*.ts`),
21
+ * the same split as `approval-contract/` (runner-wide kit) + each harness's
22
+ * `gateway-substrate.ts`.
23
+ *
24
+ * Three substitutions, and why each is shaped the way it is:
25
+ *
26
+ * 1. `Context.current()` — `MockActivityEnvironment` from `@temporalio/testing`,
27
+ * ONE PER INVOCATION. It gives the activity a real `Context` (real
28
+ * `cancellationSignal`, real `heartbeat()` surfaced as events, real
29
+ * `CancelledFailure` on throw). The native tests' stand-in mints a fresh
30
+ * `AbortController` on every `Context.current()` call and so cannot drive a
31
+ * pause; production hands each attempt one `Context`, and so does this. The
32
+ * environment's documented caveat — once cancelled it stays cancelled — is
33
+ * why it is per invocation and never shared.
34
+ *
35
+ * 2. `StigmerClient` — the test file mocks the module (`vi.mock` is hoisted and
36
+ * must live in the test file; see {@link hermeticStigmerClientModule}) and
37
+ * the constructor hands back whatever client {@link bindHermeticClient} bound
38
+ * for the current run. The client is `mockStigmerClient` over an
39
+ * {@link ExecutionRecord}: `updateStatus` writes back into the record the way
40
+ * the server would, so a REINVOCATION (`threadId` set) reads the transcript
41
+ * the first invocation persisted — the resume path runs on real data, not on
42
+ * a hand-built status.
43
+ *
44
+ * 3. The environment — a temp `HOME` (the runner-owned `~/.stigmer` tree,
45
+ * `platform-dir.ts` reads `process.env.HOME`), a temp workspace root, local
46
+ * artifact storage in a temp dir, and the HITL fingerprint secret pinned by
47
+ * env var (`fingerprint-secret.ts` falls back to `randomBytes` — the ONE
48
+ * source of randomness on the activity path — and memoizes on first call, so
49
+ * the pin must precede the first invocation in the process).
50
+ *
51
+ * Determinism: the clock is faked for `Date` ONLY (`vi.useFakeTimers({ toFake:
52
+ * ["Date"] })`) and TICKS — the harness double advances it a fixed quantum per
53
+ * script step through {@link ScriptedClock}. Frozen time would make
54
+ * `delta-enricher.ts`'s persist debounce (`Date.now() - lastPersistTime`) never
55
+ * elapse and silently skip the mid-stream persist path production always runs;
56
+ * a ticking clock runs the same path and lands on the same instants every run.
57
+ * Timers stay real so the periodic heartbeat and the stall watchdog behave.
58
+ *
59
+ * Not in scope: this module never edits a production module and never
60
+ * normalizes output. If a golden is not byte-stable, the volatile source is
61
+ * controlled at its origin or the surprise is escalated — never redacted here.
62
+ */
63
+
64
+ import { mkdtempSync, rmSync } from "node:fs";
65
+ import { tmpdir } from "node:os";
66
+ import { join } from "node:path";
67
+ import { vi } from "vitest";
68
+ import { MockActivityEnvironment } from "@temporalio/testing";
69
+ import { CancelledFailure } from "@temporalio/activity";
70
+ import { Code, ConnectError } from "@connectrpc/connect";
71
+ import { clone, create } from "@bufbuild/protobuf";
72
+ import {
73
+ AgentExecutionSchema,
74
+ AgentExecutionStatusSchema,
75
+ type AgentExecution,
76
+ type AgentExecutionStatus,
77
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
78
+ import {
79
+ ApprovalAction,
80
+ ExecutionControlSignal,
81
+ ExecutionPhase,
82
+ ToolCallStatus,
83
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
84
+ import { UpdateStatusResponseSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
85
+ import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
86
+ import { SessionSchema, type Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
87
+ import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
88
+ import type { AgentInstance } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
89
+ import type { StigmerClient } from "../client/stigmer-client.js";
90
+ import {
91
+ registerWorkerShutdownSignal,
92
+ signalWorkerShutdown,
93
+ unregisterWorkerShutdownSignal,
94
+ } from "../shared/worker-shutdown.js";
95
+ import { mockStigmerClient } from "./mock-client.js";
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // The execution record: what the control plane would hold for one execution
99
+ // ---------------------------------------------------------------------------
100
+
101
+ /**
102
+ * The four resources the activity's blueprint chain reads
103
+ * (`execution.spec.sessionId -> session.spec.agentInstanceId ->
104
+ * agentInstance.spec.agentId -> agent`) plus the status the server would hold.
105
+ */
106
+ export interface ExecutionRecordInput {
107
+ readonly execution: AgentExecution;
108
+ readonly session: Session;
109
+ readonly agentInstance: AgentInstance;
110
+ readonly agent: Agent;
111
+ /**
112
+ * What `UpdateStatus` answers for each FULL status write — the platform's
113
+ * STOP lever (`ExecutionControlSignal`), decided by the control plane per
114
+ * write and read by the harness stream loop on its mid-stream persist.
115
+ * Evaluated AFTER the write lands, on the snapshot just persisted, so a
116
+ * policy can key on the transcript ("stop once the first assistant message
117
+ * is in"). Defaults to UNSPECIFIED: keep going.
118
+ */
119
+ readonly controlSignal?: (status: AgentExecutionStatus) => ExecutionControlSignal;
120
+ }
121
+
122
+ /**
123
+ * An in-memory stand-in for the server's execution row, with the TWO merge
124
+ * rules and the ONE control lever the activity depends on, and no others:
125
+ *
126
+ * - A status whose phase is UNSPECIFIED is a setup-progress report
127
+ * (`reportSetupProgress`): the server keeps `setup_progress` and leaves the
128
+ * phase and transcript alone. Modelled as: record the label, change nothing else.
129
+ * - Any other status replaces the held status wholesale (the runner is the
130
+ * writer of the transcript; the server's own field-ownership merge — approval
131
+ * fields it owns — is exercised by the test SETTING `approvalAction` on the
132
+ * record between invocations, exactly as `SubmitApproval` would).
133
+ * - Every `UpdateStatus` answers a control signal ({@link ExecutionRecordInput.controlSignal});
134
+ * the server's STOP is the one instruction that travels back to the runner
135
+ * on this channel, and it is a server decision, so it is modelled here and
136
+ * not by the test hand-writing the client.
137
+ *
138
+ * Every persisted status is also kept in order (`persisted`) so a test can
139
+ * assert the phase sequence the activity wrote, independent of the final state.
140
+ */
141
+ export class ExecutionRecord {
142
+ readonly execution: AgentExecution;
143
+ readonly session: Session;
144
+ readonly agentInstance: AgentInstance;
145
+ readonly agent: Agent;
146
+ private readonly controlSignal: (status: AgentExecutionStatus) => ExecutionControlSignal;
147
+ /** Every `updateStatus` payload, in order, snapshotted at write time. */
148
+ readonly persisted: AgentExecutionStatus[] = [];
149
+ /** Setup-progress labels reported before the stream started, in order. */
150
+ readonly setupProgress: string[] = [];
151
+ /**
152
+ * Every `updateSession` payload, in order (the `harness_state_id` write-back),
153
+ * snapshotted at write time: the activity re-binds the SAME session object on
154
+ * a fresh-agent recovery, so a live reference would show the second id twice.
155
+ */
156
+ readonly sessionUpdates: Session[] = [];
157
+
158
+ constructor(input: ExecutionRecordInput) {
159
+ this.execution = clone(AgentExecutionSchema, input.execution);
160
+ this.session = input.session;
161
+ this.agentInstance = input.agentInstance;
162
+ this.agent = input.agent;
163
+ this.controlSignal = input.controlSignal ?? (() => ExecutionControlSignal.UNSPECIFIED);
164
+ }
165
+
166
+ get executionId(): string {
167
+ return this.execution.metadata?.id ?? "";
168
+ }
169
+
170
+ /** The status the server would currently hold (what `getExecution` returns). */
171
+ get status(): AgentExecutionStatus | undefined {
172
+ return this.execution.status;
173
+ }
174
+
175
+ /** The last FULL status written (phase set), or undefined if none yet. */
176
+ get lastFullStatus(): AgentExecutionStatus | undefined {
177
+ for (let i = this.persisted.length - 1; i >= 0; i--) {
178
+ const s = this.persisted[i];
179
+ if (s.phase !== ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED) return s;
180
+ }
181
+ return undefined;
182
+ }
183
+
184
+ /**
185
+ * The ORDERED, DISTINCT phases the activity persisted — the deterministic
186
+ * shape of the persist cadence. The COUNT of persists depends on the
187
+ * debounce timer and is deliberately not exposed as an assertion surface.
188
+ */
189
+ get persistedPhases(): ExecutionPhase[] {
190
+ const out: ExecutionPhase[] = [];
191
+ for (const s of this.persisted) {
192
+ if (s.phase === ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED) continue;
193
+ if (out[out.length - 1] !== s.phase) out.push(s.phase);
194
+ }
195
+ return out;
196
+ }
197
+
198
+ /** Every tool call on the held transcript (top-level messages), in order. */
199
+ toolCalls(): ToolCall[] {
200
+ return this.execution.status?.messages.flatMap((m) => m.toolCalls) ?? [];
201
+ }
202
+
203
+ /** The tool calls currently paused for a decision. */
204
+ waitingToolCalls(): ToolCall[] {
205
+ return this.toolCalls().filter((tc) => tc.status === ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
206
+ }
207
+
208
+ /**
209
+ * What the server's `SubmitApproval` does to the row: record the decision on
210
+ * the tool call the runner wrote. The runner owns `status`; the server owns
211
+ * `approval_action` (field ownership, 005 mandate 2) — so this is the ONE
212
+ * place a test writes it, and the status stays WAITING_APPROVAL until the
213
+ * resumed turn runs the tool, exactly as in production.
214
+ */
215
+ decideWaitingToolCalls(action: ApprovalAction, decidedAt: string): number {
216
+ const waiting = this.waitingToolCalls();
217
+ for (const tc of waiting) {
218
+ tc.approvalAction = action;
219
+ tc.approvalDecidedAt = decidedAt;
220
+ }
221
+ return waiting.length;
222
+ }
223
+
224
+ /**
225
+ * Apply one `UpdateStatus` write and answer the control signal the server
226
+ * would. A setup-progress report never carries a signal (the runner ignores
227
+ * the response there; the server has nothing to say about a label).
228
+ */
229
+ applyStatusUpdate(status: AgentExecutionStatus): ExecutionControlSignal {
230
+ const snapshot = clone(AgentExecutionStatusSchema, status);
231
+ this.persisted.push(snapshot);
232
+ if (snapshot.phase === ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED) {
233
+ if (snapshot.setupProgress?.currentPhase) {
234
+ this.setupProgress.push(snapshot.setupProgress.currentPhase);
235
+ }
236
+ return ExecutionControlSignal.UNSPECIFIED;
237
+ }
238
+ this.execution.status = clone(AgentExecutionStatusSchema, snapshot);
239
+ return this.controlSignal(snapshot);
240
+ }
241
+
242
+ /**
243
+ * The control-plane client the activity sees: every read answers from this
244
+ * record; every write lands in it. Reads the activity makes for optional
245
+ * facets (execution context, channels, skills) answer the everyday shape —
246
+ * NOT_FOUND for the execution context (an execution with no env vars), no
247
+ * channels, no scoped token (the OSS/local posture) — so a scenario opts INTO
248
+ * a facet by overriding.
249
+ */
250
+ client(overrides: Partial<StigmerClient> = {}): StigmerClient {
251
+ return mockStigmerClient({
252
+ getExecution: vi.fn(async () => clone(AgentExecutionSchema, this.execution)),
253
+ getSession: vi.fn(async () => this.session),
254
+ getAgentInstance: vi.fn(async () => this.agentInstance),
255
+ getAgent: vi.fn(async () => this.agent),
256
+ updateStatus: vi.fn(async (_id: string, status: AgentExecutionStatus) => {
257
+ // The activity reads only `.signal`; UNSPECIFIED means "keep going".
258
+ return create(UpdateStatusResponseSchema, { signal: this.applyStatusUpdate(status) });
259
+ }),
260
+ updateSession: vi.fn(async (session: Session) => {
261
+ this.sessionUpdates.push(clone(SessionSchema, session));
262
+ return session;
263
+ }),
264
+ getExecutionContextByExecutionId: vi.fn(async () => {
265
+ throw new ConnectError("execution context not found", Code.NotFound);
266
+ }),
267
+ ...overrides,
268
+ } as Partial<StigmerClient>);
269
+ }
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // The module-boundary seam for StigmerClient
274
+ // ---------------------------------------------------------------------------
275
+
276
+ let boundClient: StigmerClient | undefined;
277
+
278
+ /**
279
+ * Bind the client the mocked `StigmerClient` constructor hands out for the
280
+ * current run. Called by the harness driver immediately before invoking the
281
+ * activity factory (`createCursorActivities(config)` constructs its client
282
+ * inside the factory, so the bind must precede the factory call).
283
+ */
284
+ export function bindHermeticClient(client: StigmerClient): void {
285
+ boundClient = client;
286
+ }
287
+
288
+ /**
289
+ * The factory a test passes to `vi.mock(".../client/stigmer-client.js", ...)`.
290
+ * `vi.mock` is hoisted and must appear in the test file itself; the factory can
291
+ * `await import()` this module. Usage:
292
+ *
293
+ * ```ts
294
+ * vi.mock("../../../../client/stigmer-client.js", async () =>
295
+ * (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
296
+ * );
297
+ * ```
298
+ */
299
+ export function hermeticStigmerClientModule(): { StigmerClient: new () => StigmerClient } {
300
+ return {
301
+ StigmerClient: class {
302
+ constructor() {
303
+ if (!boundClient) {
304
+ throw new Error(
305
+ "hermetic-activity: no StigmerClient bound — call bindHermeticClient(record.client()) " +
306
+ "before constructing the activities",
307
+ );
308
+ }
309
+ return boundClient;
310
+ }
311
+ } as unknown as new () => StigmerClient,
312
+ };
313
+ }
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // The environment: temp HOME, temp workspace root, local artifact storage
317
+ // ---------------------------------------------------------------------------
318
+
319
+ /**
320
+ * The env-var pins a hermetic run needs, and the reason for each:
321
+ * - `HOME`: `platform-dir.ts` roots the runner-owned `~/.stigmer` tree (HITL
322
+ * gate dir, session dirs, denial ledger) on it.
323
+ * - `ARTIFACT_STORAGE_TYPE=local` + `LOCAL_ARTIFACT_PATH`: a writable local
324
+ * store so capture mode and tool-output offload run the production path
325
+ * (an absent store flips capture off — a different code path).
326
+ * - `STIGMER_RUNNER_HITL_SECRET`: the one randomness source on the activity
327
+ * path, pinned so grant tokens and fingerprints are byte-stable.
328
+ * - `CURSOR_EVENT_RECORD_DIR` cleared: never write recordings from a test.
329
+ */
330
+ const PINNED_ENV = {
331
+ ARTIFACT_STORAGE_TYPE: "local",
332
+ STIGMER_RUNNER_HITL_SECRET: "hermetic-fixture-secret-do-not-use-in-production",
333
+ } as const;
334
+
335
+ const CLEARED_ENV = ["CURSOR_EVENT_RECORD_DIR", "STIGMER_PROXY_ENDPOINT", "STIGMER_CLOUD_API_URL"] as const;
336
+
337
+ export interface HermeticEnvironment {
338
+ /** The temp `HOME` (the runner's `~/.stigmer` lands under it). */
339
+ readonly home: string;
340
+ /** The temp workspace root (`config.workspaceRootDir`). */
341
+ readonly workspaceRootDir: string;
342
+ /** The temp local artifact store (`LOCAL_ARTIFACT_PATH`). */
343
+ readonly artifactPath: string;
344
+ /** Restore every env var and remove the temp tree. Idempotent. */
345
+ dispose(): void;
346
+ }
347
+
348
+ /**
349
+ * Create the temp tree and pin the env vars. Call once per test FILE (in
350
+ * `beforeAll`) — the fingerprint secret is memoized per process on first use,
351
+ * and vitest isolates files in forks, so per-file is the natural unit.
352
+ */
353
+ export function createHermeticEnvironment(): HermeticEnvironment {
354
+ const root = mkdtempSync(join(tmpdir(), "stigmer-hermetic-"));
355
+ const home = join(root, "home");
356
+ const workspaceRootDir = join(root, "workspaces");
357
+ const artifactPath = join(root, "artifacts");
358
+
359
+ const saved = new Map<string, string | undefined>();
360
+ const set = (key: string, value: string | undefined): void => {
361
+ if (!saved.has(key)) saved.set(key, process.env[key]);
362
+ if (value === undefined) delete process.env[key];
363
+ else process.env[key] = value;
364
+ };
365
+ set("HOME", home);
366
+ set("LOCAL_ARTIFACT_PATH", artifactPath);
367
+ for (const [k, v] of Object.entries(PINNED_ENV)) set(k, v);
368
+ for (const k of CLEARED_ENV) set(k, undefined);
369
+
370
+ let disposed = false;
371
+ return {
372
+ home,
373
+ workspaceRootDir,
374
+ artifactPath,
375
+ dispose() {
376
+ if (disposed) return;
377
+ disposed = true;
378
+ for (const [k, v] of saved) {
379
+ if (v === undefined) delete process.env[k];
380
+ else process.env[k] = v;
381
+ }
382
+ rmSync(root, { recursive: true, force: true });
383
+ },
384
+ };
385
+ }
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // The clock
389
+ // ---------------------------------------------------------------------------
390
+
391
+ /**
392
+ * A deterministic, TICKING `Date`. Only `Date` is faked; timers stay real (the
393
+ * periodic heartbeat, the stall watchdog, `withTimeout` all need them). The
394
+ * harness double calls {@link ScriptedClock.tick} once per script step, so
395
+ * every `Date.now()`-based decision on the activity path (status timestamps,
396
+ * the enricher's persist debounce, cache TTLs) sees the same instants run after
397
+ * run and runs the same branches production runs.
398
+ */
399
+ export class ScriptedClock {
400
+ /** A fixed epoch: 2026-01-01T00:00:00.000Z. Chosen for readability in goldens. */
401
+ static readonly EPOCH_MS = Date.UTC(2026, 0, 1, 0, 0, 0, 0);
402
+ /** One second per step — well under any stall or cache horizon. */
403
+ static readonly STEP_MS = 1_000;
404
+
405
+ private nowMs = ScriptedClock.EPOCH_MS;
406
+
407
+ install(): void {
408
+ vi.useFakeTimers({ toFake: ["Date"], now: this.nowMs });
409
+ }
410
+
411
+ tick(ms: number = ScriptedClock.STEP_MS): void {
412
+ this.nowMs += ms;
413
+ vi.setSystemTime(this.nowMs);
414
+ }
415
+
416
+ /** Back to the epoch — for a second scenario in the same file. */
417
+ reset(): void {
418
+ this.nowMs = ScriptedClock.EPOCH_MS;
419
+ vi.setSystemTime(this.nowMs);
420
+ }
421
+
422
+ uninstall(): void {
423
+ vi.useRealTimers();
424
+ }
425
+ }
426
+
427
+ // ---------------------------------------------------------------------------
428
+ // Running one activity invocation
429
+ // ---------------------------------------------------------------------------
430
+
431
+ /** How an activity invocation ended: the return value or the error it threw. */
432
+ export type ActivityOutcome =
433
+ | { readonly kind: "returned"; readonly value: unknown }
434
+ | { readonly kind: "threw"; readonly error: unknown };
435
+
436
+ export interface ActivityInvocation {
437
+ readonly outcome: ActivityOutcome;
438
+ /** Heartbeat details the activity reported, in order. */
439
+ readonly heartbeats: unknown[];
440
+ /** The `taskQueue` the invocation ran on (the shutdown signal is keyed by it). */
441
+ readonly taskQueue: string;
442
+ }
443
+
444
+ /**
445
+ * Handles a scenario uses to interrupt the invocation FROM A SCRIPT STEP —
446
+ * never from a timer. `cancel()` aborts the activity's `cancellationSignal`
447
+ * (a user pause, as the workflow delivers it). `signalWorkerShutdown()` aborts
448
+ * the queue's shutdown signal first, which the activity reads to tell a worker
449
+ * drain from a user pause (`classifyTurnInterruption`).
450
+ */
451
+ export interface InvocationControls {
452
+ cancel(): void;
453
+ signalWorkerShutdown(): void;
454
+ }
455
+
456
+ export interface RunActivityOptions {
457
+ readonly taskQueue?: string;
458
+ /**
459
+ * Receives the invocation's controls before the activity starts, so a
460
+ * scenario can hand them to the harness double's `effect` steps.
461
+ */
462
+ readonly onControls?: (controls: InvocationControls) => void;
463
+ }
464
+
465
+ /**
466
+ * Run one activity function under a fresh `MockActivityEnvironment` and
467
+ * capture how it ended. `CancelledFailure` is caught like any other throw and
468
+ * reported as `{ kind: "threw" }` — the throw-vs-return table is the thing
469
+ * under test, so the driver never interprets it.
470
+ */
471
+ export async function runActivityHermetically<A extends unknown[]>(
472
+ activity: (...args: A) => Promise<unknown>,
473
+ args: A,
474
+ options: RunActivityOptions = {},
475
+ ): Promise<ActivityInvocation> {
476
+ const taskQueue = options.taskQueue ?? "hermetic-test-queue";
477
+ const env = new MockActivityEnvironment({ taskQueue });
478
+ const heartbeats: unknown[] = [];
479
+ env.on("heartbeat", (details: unknown) => heartbeats.push(details));
480
+
481
+ // The worker-shutdown signal is process-global per queue; register for this
482
+ // invocation and always unregister so no state leaks into the next one.
483
+ registerWorkerShutdownSignal(taskQueue);
484
+ options.onControls?.({
485
+ cancel: () => env.cancel(),
486
+ signalWorkerShutdown: () => signalWorkerShutdown(taskQueue),
487
+ });
488
+
489
+ try {
490
+ const value = await env.run(activity, ...args);
491
+ return { outcome: { kind: "returned", value }, heartbeats, taskQueue };
492
+ } catch (error) {
493
+ return { outcome: { kind: "threw", error }, heartbeats, taskQueue };
494
+ } finally {
495
+ unregisterWorkerShutdownSignal(taskQueue);
496
+ }
497
+ }
498
+
499
+ /** True when the outcome is a thrown Temporal `CancelledFailure`. */
500
+ export function threwCancelledFailure(outcome: ActivityOutcome): outcome is { kind: "threw"; error: CancelledFailure } {
501
+ return outcome.kind === "threw" && outcome.error instanceof CancelledFailure;
502
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The model registry document a hermetic activity run reads, and the `fetch`
3
+ * stub that serves it and refuses everything else.
4
+ *
5
+ * The activity's only HTTP fetch is the model registry (`shared/model-
6
+ * registry.ts` for vision capability; the Cursor harness's `model-pricing-
7
+ * data.ts` for pricing). Both fail SOFT to defaults with a 60 s failure
8
+ * cache, which would make a golden depend on DEFAULT_PRICING and log a
9
+ * warning on every run, so a hermetic run stubs `fetch` to answer one
10
+ * registry document — and to THROW for any other URL, so a new network
11
+ * dependency on the activity path fails the run instead of leaking.
12
+ *
13
+ * One entry, `composer-2.5`, priced at round numbers so a golden's
14
+ * `estimatedCostUsd` is legible (600 000 input tokens = $0.60). The entry is
15
+ * a Cursor-harness model because that is what the registry lists; the
16
+ * document itself is the control plane's, read by every harness's runtime.
17
+ * `parsePricingTable` wants `harness: "cursor"` + `pricing`; `parseRegistry`
18
+ * wants `id` + `provider` and reads `capabilities.vision`.
19
+ */
20
+
21
+ import { vi } from "vitest";
22
+
23
+ /** The pinned model of every hermetic run; in the registry document AND in the Cursor double's catalog. */
24
+ export const FIXTURE_MODEL = "composer-2.5";
25
+
26
+ export const REGISTRY_DOCUMENT = {
27
+ models: [
28
+ {
29
+ id: FIXTURE_MODEL,
30
+ displayName: "Composer 2.5 (hermetic fixture)",
31
+ provider: "cursor",
32
+ harness: "cursor",
33
+ costTier: "standard",
34
+ featured: true,
35
+ capabilities: { vision: true },
36
+ pricing: {
37
+ inputPricePerMillion: 1.0,
38
+ outputPricePerMillion: 4.0,
39
+ cacheWritePricePerMillion: 1.0,
40
+ cacheReadPricePerMillion: 0.1,
41
+ },
42
+ pricingVariants: {
43
+ fast: {
44
+ inputPricePerMillion: 3.0,
45
+ outputPricePerMillion: 12.0,
46
+ cacheWritePricePerMillion: 3.0,
47
+ cacheReadPricePerMillion: 0.3,
48
+ },
49
+ },
50
+ },
51
+ ],
52
+ } as const;
53
+
54
+ /**
55
+ * Stub `fetch` to answer the registry document and refuse everything else.
56
+ * Returns the URLs fetched, for the "no other network" assertion.
57
+ */
58
+ export function stubRegistryFetch(): { readonly urls: string[]; restore(): void } {
59
+ const urls: string[] = [];
60
+ vi.stubGlobal(
61
+ "fetch",
62
+ vi.fn(async (input: string | URL | Request) => {
63
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
64
+ urls.push(url);
65
+ if (!url.includes("/model-registry")) {
66
+ throw new Error(`hermetic run attempted a non-registry network call: ${url}`);
67
+ }
68
+ return { ok: true, status: 200, json: async () => REGISTRY_DOCUMENT } as unknown as Response;
69
+ }),
70
+ );
71
+ return { urls, restore: () => vi.unstubAllGlobals() };
72
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The module specifiers a TypeScript file names, read off its syntax tree —
3
+ * the primitive behind the runner's import fences.
4
+ *
5
+ * Two fences stand on it: `src/harness/` and production `src/shared/` never
6
+ * import `src/activities/` (`harness/__tests__/import-direction.test.ts`,
7
+ * the rule the turn runtime is written under), and the Cursor adapter never
8
+ * imports `@temporalio/*` (`execute-cursor/__tests__/adapter-is-temporal-
9
+ * free.test.ts`, what lets the contract kit run it outside an activity).
10
+ *
11
+ * A syntax tree, not a regex, so a path quoted in a header comment cannot
12
+ * trip a fence, and no import form can slip past one: static, type-only,
13
+ * side-effect, re-export and dynamic `import()` are all module specifiers to
14
+ * the parser. A dynamic import whose argument is not a string literal names
15
+ * nothing and is ignored (no fence has a literal to match).
16
+ */
17
+
18
+ import { readdirSync, readFileSync } from "node:fs";
19
+ import { dirname, join, resolve, sep } from "node:path";
20
+ import ts from "typescript";
21
+
22
+ /** Every module specifier a file names, in source order, whatever the form. */
23
+ export function moduleSpecifiers(fileName: string, source: string): string[] {
24
+ const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, /* setParentNodes */ false);
25
+ const specifiers: string[] = [];
26
+ const visit = (node: ts.Node): void => {
27
+ if (
28
+ (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
29
+ node.moduleSpecifier !== undefined &&
30
+ ts.isStringLiteral(node.moduleSpecifier)
31
+ ) {
32
+ specifiers.push(node.moduleSpecifier.text);
33
+ } else if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
34
+ const [argument] = node.arguments;
35
+ if (argument !== undefined && ts.isStringLiteralLike(argument)) {
36
+ specifiers.push(argument.text);
37
+ }
38
+ }
39
+ ts.forEachChild(node, visit);
40
+ };
41
+ visit(sourceFile);
42
+ return specifiers;
43
+ }
44
+
45
+ /**
46
+ * The relative specifiers in `source` that resolve to a path inside
47
+ * `forbiddenRoot`. Package specifiers are ignored: no alias maps a package
48
+ * name onto a source directory.
49
+ */
50
+ export function forbiddenImports(filePath: string, source: string, forbiddenRoot: string): string[] {
51
+ return moduleSpecifiers(filePath, source).filter((specifier) => {
52
+ if (!specifier.startsWith(".")) return false;
53
+ const target = resolve(dirname(filePath), specifier);
54
+ return target === forbiddenRoot || target.startsWith(forbiddenRoot + sep);
55
+ });
56
+ }
57
+
58
+ /** The package specifiers in `source` whose name is `pkg` or starts with `${pkg}/`. */
59
+ export function packageImports(filePath: string, source: string, pkg: string): string[] {
60
+ return moduleSpecifiers(filePath, source).filter((specifier) => specifier === pkg || specifier.startsWith(`${pkg}/`));
61
+ }
62
+
63
+ /** Every `.ts` file under `dir`, sorted, skipping the named directories at any depth. */
64
+ export function typeScriptFilesUnder(dir: string, skipDirectories: ReadonlySet<string>): string[] {
65
+ const files: string[] = [];
66
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
67
+ const path = join(dir, entry.name);
68
+ if (entry.isDirectory()) {
69
+ if (!skipDirectories.has(entry.name)) {
70
+ files.push(...typeScriptFilesUnder(path, skipDirectories));
71
+ }
72
+ } else if (entry.name.endsWith(".ts")) {
73
+ files.push(path);
74
+ }
75
+ }
76
+ return files.sort();
77
+ }
78
+
79
+ /** Read a file for a sweep; one place so every fence reads the same bytes the compiler does. */
80
+ export function readSource(filePath: string): string {
81
+ return readFileSync(filePath, "utf-8");
82
+ }
@@ -39,3 +39,28 @@ export function toolCall(
39
39
  ): ToolCall {
40
40
  return create(ToolCallSchema, { id, name, status });
41
41
  }
42
+
43
+ /** The tool-call row with this id, wherever it sits in the transcript. */
44
+ export function findToolCallRow(status: AgentExecutionStatus, toolCallId: string): ToolCall | undefined {
45
+ for (const message of status.messages) {
46
+ const row = message.toolCalls.find((tc) => tc.id === toolCallId);
47
+ if (row) return row;
48
+ }
49
+ return undefined;
50
+ }
51
+
52
+ /**
53
+ * A tool call withheld for approval — the row a harness writes when it
54
+ * proposes a gated side effect: WAITING_APPROVAL, `requiresApproval` set, and
55
+ * the approval fields the server owns (`approvalAction`, `approvalDecidedAt`)
56
+ * left at their defaults for the decision to fill in.
57
+ */
58
+ export function waitingToolCall(id: string, name: string, approvalMessage: string): ToolCall {
59
+ return create(ToolCallSchema, {
60
+ id,
61
+ name,
62
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
63
+ requiresApproval: true,
64
+ approvalMessage,
65
+ });
66
+ }