@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,146 @@
1
+ /**
2
+ * The persist chokepoint's two contracts: what one write does (the secret
3
+ * backstop, the usage summary, the heartbeat, the STOP), and how requests
4
+ * coalesce (one write in flight, one follow-up at most, every request's
5
+ * promise resolving only once its state is on the wire).
6
+ */
7
+
8
+ import { describe, expect, it, vi } from "vitest";
9
+ import { create } from "@bufbuild/protobuf";
10
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
11
+ import { UpdateStatusResponseSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
12
+ import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
13
+ import { ExecutionControlSignal, MessageType, ServiceTier, ThinkingMode, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
14
+
15
+ import type { StigmerClient } from "../../client/stigmer-client.js";
16
+ import { PersistChokepoint } from "../persist-chokepoint.js";
17
+ import { UsageAccumulator } from "../usage-accumulator.js";
18
+
19
+ interface Gate {
20
+ readonly open: () => void;
21
+ readonly opened: Promise<void>;
22
+ }
23
+
24
+ function gate(): Gate {
25
+ let open!: () => void;
26
+ const opened = new Promise<void>((resolve) => (open = resolve));
27
+ return { open, opened };
28
+ }
29
+
30
+ /** A client whose `updateStatus` answers through a gate the test opens, so writes can be held in flight. */
31
+ function heldClient(answers: ExecutionControlSignal[] = []) {
32
+ const gates: Gate[] = [];
33
+ const writes: number[] = [];
34
+ const client = {
35
+ updateStatus: vi.fn(async (_id: string, status: { messages: unknown[] }) => {
36
+ const g = gate();
37
+ gates.push(g);
38
+ writes.push(status.messages.length);
39
+ await g.opened;
40
+ return create(UpdateStatusResponseSchema, { signal: answers.shift() ?? ExecutionControlSignal.UNSPECIFIED });
41
+ }),
42
+ } as unknown as StigmerClient;
43
+ return { client, gates, writes };
44
+ }
45
+
46
+ function chokepointOver(client: StigmerClient, options: { usage?: UsageAccumulator; onPlatformStop?: () => void } = {}) {
47
+ const status = create(AgentExecutionStatusSchema, {});
48
+ const heartbeat = vi.fn();
49
+ const onPlatformStop = options.onPlatformStop ?? vi.fn();
50
+ const chokepoint = new PersistChokepoint({
51
+ client,
52
+ executionId: "aex_test",
53
+ status,
54
+ offload: undefined,
55
+ usage: () => options.usage,
56
+ heartbeat,
57
+ onPlatformStop,
58
+ });
59
+ return { chokepoint, status, heartbeat, onPlatformStop };
60
+ }
61
+
62
+ describe("PersistChokepoint: one write", () => {
63
+ it("withholds secret content, refreshes streaming_usage, writes, heartbeats, and relays STOP", async () => {
64
+ const { client, gates } = heldClient([ExecutionControlSignal.STOP]);
65
+ const usage = new UsageAccumulator(ServiceTier.STANDARD, ThinkingMode.DISABLED);
66
+ usage.addTurn({ inputTokens: 10, estimatedCostUsd: 0.01, model: "m" });
67
+ const onPlatformStop = vi.fn();
68
+ const { chokepoint, status, heartbeat } = chokepointOver(client, { usage, onPlatformStop });
69
+ status.messages.push(
70
+ create(AgentMessageSchema, {
71
+ type: MessageType.MESSAGE_AI,
72
+ toolCalls: [
73
+ create(ToolCallSchema, {
74
+ id: "w1",
75
+ name: "write",
76
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
77
+ args: { path: ".env", content: "SECRET=1" },
78
+ }),
79
+ ],
80
+ }),
81
+ );
82
+
83
+ const written = chokepoint.write();
84
+ gates[0]!.open();
85
+ await written;
86
+
87
+ expect(status.messages[0]!.toolCalls[0]!.args, "the secret backstop ran before the write").not.toMatchObject({ content: "SECRET=1" });
88
+ expect(status.streamingUsage?.estimatedCostUsd).toBe(0.01);
89
+ expect(heartbeat).toHaveBeenCalledTimes(1);
90
+ expect(onPlatformStop).toHaveBeenCalledTimes(1);
91
+ });
92
+
93
+ it("writes no streaming_usage when no turn was reported", async () => {
94
+ const { client, gates } = heldClient();
95
+ const { chokepoint, status } = chokepointOver(client, { usage: new UsageAccumulator() });
96
+ const written = chokepoint.write();
97
+ gates[0]!.open();
98
+ await written;
99
+ expect(status.streamingUsage).toBeUndefined();
100
+ });
101
+ });
102
+
103
+ describe("PersistChokepoint: coalescing", () => {
104
+ it("a request while a write is in flight schedules ONE follow-up and resolves only when it lands", async () => {
105
+ const { client, gates, writes } = heldClient();
106
+ const { chokepoint, status } = chokepointOver(client);
107
+
108
+ status.messages.push(create(AgentMessageSchema, { content: "one" }));
109
+ const first = chokepoint.request();
110
+ status.messages.push(create(AgentMessageSchema, { content: "two" }));
111
+ const second = chokepoint.request();
112
+ status.messages.push(create(AgentMessageSchema, { content: "three" }));
113
+ const third = chokepoint.request();
114
+
115
+ let secondSettled = false;
116
+ let thirdSettled = false;
117
+ void second.then(() => (secondSettled = true));
118
+ void third.then(() => (thirdSettled = true));
119
+
120
+ expect(gates, "one write in flight, the follow-up waits").toHaveLength(1);
121
+ gates[0]!.open();
122
+ await first;
123
+ await Promise.resolve();
124
+ expect(secondSettled, "the follow-up has not landed yet").toBe(false);
125
+
126
+ await vi.waitFor(() => expect(gates).toHaveLength(2));
127
+ expect(writes, "the follow-up carries the state as of the LAST request").toEqual([1, 3]);
128
+ gates[1]!.open();
129
+ await Promise.all([second, third]);
130
+ expect(secondSettled && thirdSettled).toBe(true);
131
+ expect(client.updateStatus).toHaveBeenCalledTimes(2);
132
+ });
133
+
134
+ it("sequential awaited requests write once each, in order", async () => {
135
+ const { client, gates } = heldClient();
136
+ const { chokepoint } = chokepointOver(client);
137
+ const first = chokepoint.request();
138
+ gates[0]!.open();
139
+ await first;
140
+ const second = chokepoint.request();
141
+ await vi.waitFor(() => expect(gates).toHaveLength(2));
142
+ gates[1]!.open();
143
+ await second;
144
+ expect(client.updateStatus).toHaveBeenCalledTimes(2);
145
+ });
146
+ });
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Pins the harness registry's two load-bearing properties — ORDER and ERROR
3
+ * POSTURE — and the byte-pinned activity names.
4
+ *
5
+ * Order: boot in declaration order, one at a time (the Cursor interceptors
6
+ * must precede anything that dials the control plane); shutdown in reverse.
7
+ * Posture: boot validates the table before touching any adapter and fails
8
+ * fast at the first rejection; shutdown and release continue past a failing
9
+ * adapter and surface every failure in one AggregateError. Names: the map
10
+ * must equal the server's constants byte for byte; the runner cannot import
11
+ * the server, so the literals are asserted here and the server's file is
12
+ * cited beside them.
13
+ */
14
+
15
+ import { describe, it, expect } from "vitest";
16
+
17
+ import { testConfig } from "../../__test-utils__/config-fixture.js";
18
+ import { DEEP_AGENT_VISION_PROFILE } from "../../shared/attachment-vision.js";
19
+ import { HARNESS_ACTIVITY_NAMES, bootHarnesses, releaseHarnessSession, shutdownHarnesses } from "../registry.js";
20
+ import type { HarnessAdapter } from "../types.js";
21
+
22
+ interface StubBehaviour {
23
+ readonly bootRejects?: Error;
24
+ readonly shutdownRejects?: Error;
25
+ readonly releaseRejects?: Error;
26
+ }
27
+
28
+ /** An adapter that only records lifecycle calls into a shared, ordered log. */
29
+ function stubAdapter(name: string, log: string[], behaviour: StubBehaviour = {}): HarnessAdapter {
30
+ return {
31
+ name,
32
+ capabilities: {
33
+ pausePrimitive: "interrupt",
34
+ stateIdSource: "deterministic",
35
+ systemPrompt: true,
36
+ subAgents: false,
37
+ toolRestriction: true,
38
+ visionProfile: DEEP_AGENT_VISION_PROFILE,
39
+ fileReview: { harnessId: "stub", excludePaths: [] },
40
+ },
41
+ async boot() {
42
+ log.push(`boot:${name}`);
43
+ if (behaviour.bootRejects) throw behaviour.bootRejects;
44
+ },
45
+ async shutdown() {
46
+ log.push(`shutdown:${name}`);
47
+ if (behaviour.shutdownRejects) throw behaviour.shutdownRejects;
48
+ },
49
+ async releaseSession(sessionId) {
50
+ log.push(`release:${name}:${sessionId}`);
51
+ if (behaviour.releaseRejects) throw behaviour.releaseRejects;
52
+ },
53
+ async runTurn() {
54
+ throw new Error(`${name}: runTurn is not under test here`);
55
+ },
56
+ };
57
+ }
58
+
59
+ describe("HARNESS_ACTIVITY_NAMES", () => {
60
+ it("equals the server's byte-pinned activity names (stigmer-server temporal/agentexecution/names.ts)", () => {
61
+ expect(HARNESS_ACTIVITY_NAMES.cursor).toBe("ExecuteCursor");
62
+ expect(HARNESS_ACTIVITY_NAMES["deep-agent"]).toBe("ExecuteDeepAgent");
63
+ expect(Object.keys(HARNESS_ACTIVITY_NAMES).sort()).toEqual(["cursor", "deep-agent"]);
64
+ });
65
+ });
66
+
67
+ describe("bootHarnesses", () => {
68
+ it("boots adapters in declaration order, one at a time", async () => {
69
+ const log: string[] = [];
70
+ await bootHarnesses([stubAdapter("first", log), stubAdapter("second", log), stubAdapter("third", log)], testConfig());
71
+ expect(log).toEqual(["boot:first", "boot:second", "boot:third"]);
72
+ });
73
+
74
+ it("hands every adapter the same config", async () => {
75
+ const seen: unknown[] = [];
76
+ const config = testConfig({ taskQueue: "registry-test-queue" });
77
+ const adapters = ["a", "b"].map((name) => ({
78
+ ...stubAdapter(name, []),
79
+ async boot(c: unknown) {
80
+ seen.push(c);
81
+ },
82
+ }));
83
+ await bootHarnesses(adapters, config);
84
+ expect(seen, "each adapter must receive the one worker config").toEqual([config, config]);
85
+ });
86
+
87
+ it("refuses duplicate adapter names before booting anything", async () => {
88
+ const log: string[] = [];
89
+ const adapters = [stubAdapter("cursor", log), stubAdapter("other", log), stubAdapter("cursor", log)];
90
+ await expect(bootHarnesses(adapters, testConfig())).rejects.toThrow(/duplicate adapter name 'cursor'/);
91
+ expect(log, "a table with a duplicate must not boot even its first adapter").toEqual([]);
92
+ });
93
+
94
+ it("fails fast: the first rejection propagates and later adapters never boot", async () => {
95
+ const log: string[] = [];
96
+ const adapters = [
97
+ stubAdapter("first", log),
98
+ stubAdapter("broken", log, { bootRejects: new Error("interceptor install failed") }),
99
+ stubAdapter("third", log),
100
+ ];
101
+ await expect(bootHarnesses(adapters, testConfig())).rejects.toThrow("interceptor install failed");
102
+ expect(log).toEqual(["boot:first", "boot:broken"]);
103
+ });
104
+
105
+ it("boots an empty table as a no-op", async () => {
106
+ await expect(bootHarnesses([], testConfig())).resolves.toBeUndefined();
107
+ });
108
+ });
109
+
110
+ describe("shutdownHarnesses", () => {
111
+ it("shuts adapters down in reverse declaration order", async () => {
112
+ const log: string[] = [];
113
+ await shutdownHarnesses([stubAdapter("first", log), stubAdapter("second", log), stubAdapter("third", log)]);
114
+ expect(log).toEqual(["shutdown:third", "shutdown:second", "shutdown:first"]);
115
+ });
116
+
117
+ it("continues past a failing adapter and reports every failure in one AggregateError", async () => {
118
+ const log: string[] = [];
119
+ const adapters = [
120
+ stubAdapter("first", log, { shutdownRejects: new Error("first leaked") }),
121
+ stubAdapter("second", log),
122
+ stubAdapter("third", log, { shutdownRejects: new Error("third leaked") }),
123
+ ];
124
+ const failure = await shutdownHarnesses(adapters).catch((e: unknown) => e);
125
+ expect(failure).toBeInstanceOf(AggregateError);
126
+ const aggregate = failure as AggregateError;
127
+ expect(aggregate.message).toContain("third: shutdown rejected: third leaked");
128
+ expect(aggregate.message).toContain("first: shutdown rejected: first leaked");
129
+ expect(aggregate.errors.map((e: Error) => e.message)).toEqual([
130
+ "third: shutdown rejected: third leaked",
131
+ "first: shutdown rejected: first leaked",
132
+ ]);
133
+ expect(log, "every adapter must be shut down even when an earlier one failed").toEqual([
134
+ "shutdown:third",
135
+ "shutdown:second",
136
+ "shutdown:first",
137
+ ]);
138
+ });
139
+
140
+ it("preserves the original error as the cause of each reported failure", async () => {
141
+ const original = new Error("socket already closed");
142
+ const failure = await shutdownHarnesses([stubAdapter("only", [], { shutdownRejects: original })]).catch((e: unknown) => e);
143
+ expect((failure as AggregateError).errors[0]?.cause).toBe(original);
144
+ });
145
+ });
146
+
147
+ describe("releaseHarnessSession", () => {
148
+ it("tells every adapter, in declaration order, with the session id", async () => {
149
+ const log: string[] = [];
150
+ await releaseHarnessSession([stubAdapter("first", log), stubAdapter("second", log)], "ses-42");
151
+ expect(log).toEqual(["release:first:ses-42", "release:second:ses-42"]);
152
+ });
153
+
154
+ it("continues past a failing adapter and reports the failure with the session id", async () => {
155
+ const log: string[] = [];
156
+ const adapters = [
157
+ stubAdapter("first", log, { releaseRejects: new Error("executor busy") }),
158
+ stubAdapter("second", log),
159
+ ];
160
+ const failure = await releaseHarnessSession(adapters, "ses-7").catch((e: unknown) => e);
161
+ expect(failure).toBeInstanceOf(AggregateError);
162
+ expect((failure as AggregateError).message).toContain("releaseSession('ses-7') failed");
163
+ expect((failure as AggregateError).errors.map((e: Error) => e.message)).toEqual([
164
+ "first: releaseSession('ses-7') rejected: executor busy",
165
+ ]);
166
+ expect(log).toEqual(["release:first:ses-7", "release:second:ses-7"]);
167
+ });
168
+ });
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The turn runtime, proven without any engine: the scripted fake adapter
3
+ * (`__test-utils__/harness-contract/scripted-adapter.ts`) through the kit's
4
+ * runtime-side half (`__test-utils__/harness-contract/runtime-contract.ts`)
5
+ * under `MockActivityEnvironment`, one arm per row of the terminal table.
6
+ *
7
+ * The runtime-side contract itself — the throw-vs-return rule end to end, the
8
+ * single persist chokepoint, the whole-activity heartbeat, the stop
9
+ * controller's four producers, the reinvocation seam, the byte-pinned copy —
10
+ * is the kit's and is registered here through `describeHarnessRuntimeContract`
11
+ * exactly as `activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts`
12
+ * registers it against the real Cursor adapter. An arm that differs between
13
+ * the fake and the real engine is an adapter defect, not a runtime one.
14
+ *
15
+ * What is asserted HERE and not in the kit are the FAKE's facts: the engine
16
+ * cadence a real engine has no reason to share. The fake's `say` awaits its
17
+ * persist and its `propose` persists nothing before returning
18
+ * `awaiting_approval`, so under the runtime its first full write IS the
19
+ * WAITING write (`persistedPhases === [WAITING_FOR_APPROVAL]`) where a
20
+ * streaming engine's IN_PROGRESS writes precede it; the fake has no setup
21
+ * labels of its own, so the runtime's six are the whole list; the fake states
22
+ * its own price, so the cost copy reads exactly `~$0.6000`; and the fake's
23
+ * failure messages reach the status verbatim, where a real engine's
24
+ * classifier rephrases them.
25
+ *
26
+ * The hermetic environment is created at module level because the subject's
27
+ * `config` is read at collection time; the forks pool keeps it private to
28
+ * this file.
29
+ */
30
+
31
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
32
+ import { ApprovalAction, ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
33
+
34
+ vi.mock("../../client/stigmer-client.js", async () =>
35
+ (await import("../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
36
+ );
37
+
38
+ import { ScriptedClock, createHermeticEnvironment } from "../../__test-utils__/hermetic-activity.js";
39
+ import { stubRegistryFetch } from "../../__test-utils__/model-registry-fixture.js";
40
+ import { scriptedSubject } from "../../__test-utils__/harness-contract/scripted-adapter.js";
41
+ import {
42
+ COST_CAP_FAKE_PRICE_USD,
43
+ FAILURE_MESSAGES,
44
+ NEVER_SEEN,
45
+ RUNTIME_SETUP_LABELS,
46
+ RuntimeExecutionDriver,
47
+ aiMessages,
48
+ assertApprovalRoundTrip,
49
+ assertCompletedTurn,
50
+ assertCostCapTerminates,
51
+ assertFailedSurfaceWritesItsCopy,
52
+ assertRejectedBindFails,
53
+ describeHarnessRuntimeContract,
54
+ slimOf,
55
+ systemMessages,
56
+ type RuntimeContractHarness,
57
+ } from "../../__test-utils__/harness-contract/runtime-contract.js";
58
+ import { scenario } from "../../__test-utils__/harness-contract/types.js";
59
+ import { TERMINAL_COPY } from "../terminal-table.js";
60
+
61
+ const TASK_QUEUE = "runtime-test-queue";
62
+
63
+ const env = createHermeticEnvironment();
64
+ const clock = new ScriptedClock();
65
+ const subject = scriptedSubject({
66
+ pausePrimitive: "interrupt",
67
+ stateIdSource: "engine-minted",
68
+ config: { workspaceRootDir: env.workspaceRootDir, taskQueue: TASK_QUEUE },
69
+ });
70
+ const harness: RuntimeContractHarness = { subject, env, clock };
71
+
72
+ describe("run-turn: the fake adapter through the real runtime", () => {
73
+ let registry: ReturnType<typeof stubRegistryFetch>;
74
+
75
+ beforeAll(() => {
76
+ registry = stubRegistryFetch();
77
+ clock.install();
78
+ });
79
+
80
+ afterAll(() => {
81
+ clock.uninstall();
82
+ registry.restore();
83
+ env.dispose();
84
+ });
85
+
86
+ describeHarnessRuntimeContract(harness);
87
+
88
+ describe("the fake's own facts", () => {
89
+ beforeAll(async () => {
90
+ await subject.adapter.boot(subject.config);
91
+ });
92
+
93
+ it("a completed turn: the runtime's six labels are the whole list, the phases, the stated price and its basis", async () => {
94
+ clock.reset();
95
+ const { driver, final } = await assertCompletedTurn(harness, "fake-completed");
96
+ expect(driver.record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_IN_PROGRESS, ExecutionPhase.EXECUTION_COMPLETED]);
97
+ expect(driver.record.setupProgress).toEqual([...RUNTIME_SETUP_LABELS]);
98
+ expect(final.streamingUsage?.estimatedCostUsd, "the summary is the adapter's stated price, summed").toBeCloseTo(0.00136, 9);
99
+ expect(final.streamingUsage?.model, "the basis the adapter named").toBe("fixture-model");
100
+ expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
101
+ });
102
+
103
+ it("the approval round trip: the WAITING write is the fake's first full persist (M3a finding 3)", async () => {
104
+ clock.reset();
105
+ const { driver } = await assertApprovalRoundTrip(harness, "fake-approval");
106
+ expect(driver.record.persistedPhases).toEqual([
107
+ ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
108
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
109
+ ExecutionPhase.EXECUTION_COMPLETED,
110
+ ]);
111
+ expect(driver.record.sessionUpdates, "an engine-minted adapter binds once across the round trip").toHaveLength(1);
112
+ });
113
+
114
+ it("the cost cap: the fake's stated price reads exactly in the copy", async () => {
115
+ clock.reset();
116
+ const { final } = await assertCostCapTerminates(harness, "fake-cost-cap");
117
+ expect(final.error).toBe(`Agent reached the cost limit for this message (~$${COST_CAP_FAKE_PRICE_USD.toFixed(4)} of the $0.50 budget). Send another message to continue.`);
118
+ expect(final.streamingUsage?.estimatedCostUsd).toBe(COST_CAP_FAKE_PRICE_USD);
119
+ });
120
+
121
+ it.each([
122
+ ["engine", []],
123
+ ["actionable", [`Execution failed: ${FAILURE_MESSAGES.actionable}`]],
124
+ ["internal", [TERMINAL_COPY.internalFailure.row, `Error details: ${FAILURE_MESSAGES.internal}`]],
125
+ ] as const)("a %s failure: the fake's message reaches the status verbatim, with that surface's rows", async (surface, rows) => {
126
+ clock.reset();
127
+ const { driver, final } = await assertFailedSurfaceWritesItsCopy(harness, surface, FAILURE_MESSAGES[surface], `fake-failed-${surface}`);
128
+ expect(driver.record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_IN_PROGRESS, ExecutionPhase.EXECUTION_FAILED]);
129
+ expect(final.error).toBe(FAILURE_MESSAGES[surface]);
130
+ expect(systemMessages(final)).toEqual(rows);
131
+ });
132
+
133
+ it("a rejected bind: the fake names the failure and nothing of the scenario was folded in", async () => {
134
+ clock.reset();
135
+ const { final } = await assertRejectedBindFails(harness, "fake-bind-rejects");
136
+ expect(final.error).toContain("could not bind engine state");
137
+ expect(aiMessages(final)).toEqual([]);
138
+ });
139
+
140
+ it("REJECT of an irreversible action settles the reinvocation FAILED before any engine runs (today's runtime; see F1)", async () => {
141
+ // The runtime fails a reinvocation that carries any REJECT
142
+ // (`decideReinvocation`, moved from Cursor's orchestrator). The proto
143
+ // contract as of #197 — the enum doc, the conformance suite, the native
144
+ // harness's `reconcileNonExecutingDecisions` — says REJECT denies the
145
+ // tool and the run continues with the row SKIPPED. This arm pins the
146
+ // runtime AS IT IS so the S3 change is a visible diff, and is
147
+ // deliberately NOT in the kit (S2 M4 finding F1, Q-M4-1).
148
+ clock.reset();
149
+ const driver = new RuntimeExecutionDriver(harness, "fake-reject");
150
+ const propose = scenario.propose("call-runtime-reject", { kind: "shell", resource: "rm -rf build" });
151
+
152
+ const first = await driver.turn([propose]);
153
+ expect(slimOf(subject, first).phase).toBe("EXECUTION_WAITING_FOR_APPROVAL");
154
+ driver.decide(ApprovalAction.REJECT);
155
+
156
+ const second = await driver.turn([scenario.say(NEVER_SEEN)]);
157
+ expect(slimOf(subject, second).phase).toBe("EXECUTION_FAILED");
158
+ const final = driver.record.lastFullStatus!;
159
+ expect(final.error).toBe(TERMINAL_COPY.rejectedByUser.error);
160
+ expect(systemMessages(final).at(-1)).toBe(TERMINAL_COPY.rejectedByUser.row);
161
+ expect(final.completedAt).not.toBe("");
162
+ expect(aiMessages(final)).not.toContain(NEVER_SEEN);
163
+ });
164
+ });
165
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The stop controller: one signal for every cause, every cause recorded,
3
+ * the first cause the abort's reason. The terminal table's precedence is
4
+ * decided at settlement from the recorded evidence, not from which cause
5
+ * aborted first.
6
+ */
7
+
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ import { StallTimeoutError } from "../../shared/stall-watchdog.js";
11
+ import { StopController } from "../stop-controller.js";
12
+
13
+ describe("StopController", () => {
14
+ it("starts live with no evidence", () => {
15
+ const stop = new StopController();
16
+ expect(stop.signal.aborted).toBe(false);
17
+ expect(stop.evidence).toEqual({ stall: undefined, costCapExceeded: false, platformStop: false, cancellation: false });
18
+ });
19
+
20
+ it("the first cause aborts the signal and names the abort; later causes are recorded but do not re-abort", () => {
21
+ const stop = new StopController();
22
+ stop.stop({ kind: "cost-cap" });
23
+ expect(stop.signal.aborted).toBe(true);
24
+ expect(stop.signal.reason).toBe("cost-cap");
25
+
26
+ stop.stop({ kind: "cancellation" });
27
+ stop.stop({ kind: "platform-stop" });
28
+ expect(stop.signal.reason, "the abort keeps its first reason").toBe("cost-cap");
29
+ expect(stop.evidence).toMatchObject({ costCapExceeded: true, cancellation: true, platformStop: true });
30
+ });
31
+
32
+ it("keeps the FIRST stall error when the watchdog reports twice", () => {
33
+ const stop = new StopController();
34
+ const first = new StallTimeoutError(3_000, "last tool: shell");
35
+ stop.stop({ kind: "stall", error: first });
36
+ stop.stop({ kind: "stall", error: new StallTimeoutError(9_000) });
37
+ expect(stop.evidence.stall).toBe(first);
38
+ });
39
+
40
+ it("fires abort listeners exactly once", () => {
41
+ const stop = new StopController();
42
+ let fired = 0;
43
+ stop.signal.addEventListener("abort", () => fired++);
44
+ stop.stop({ kind: "cancellation" });
45
+ stop.stop({ kind: "cancellation" });
46
+ stop.stop({ kind: "cost-cap" });
47
+ expect(fired).toBe(1);
48
+ });
49
+ });