@stigmer/runner 3.0.9-dev.20260616060535 → 3.1.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 (328) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
  3. package/dist/__test-utils__/approval-contract/types.js +24 -0
  4. package/dist/__test-utils__/approval-contract/types.js.map +1 -0
  5. package/dist/activities/call-agent-status.d.ts +19 -1
  6. package/dist/activities/call-agent-status.js +64 -5
  7. package/dist/activities/call-agent-status.js.map +1 -1
  8. package/dist/activities/call-llm.js +19 -53
  9. package/dist/activities/call-llm.js.map +1 -1
  10. package/dist/activities/classify-tool-approvals.d.ts +24 -0
  11. package/dist/activities/classify-tool-approvals.js +69 -17
  12. package/dist/activities/classify-tool-approvals.js.map +1 -1
  13. package/dist/activities/discover-mcp-server.d.ts +7 -0
  14. package/dist/activities/discover-mcp-server.js +11 -1
  15. package/dist/activities/discover-mcp-server.js.map +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
  17. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
  18. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
  19. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
  20. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
  21. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
  22. package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
  23. package/dist/activities/execute-cursor/approval-policy.js +8 -120
  24. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  25. package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
  26. package/dist/activities/execute-cursor/approval-state.js +233 -17
  27. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  28. package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
  29. package/dist/activities/execute-cursor/capture-flow.js +234 -0
  30. package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
  31. package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
  32. package/dist/activities/execute-cursor/cas-observations.js +184 -0
  33. package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
  34. package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
  35. package/dist/activities/execute-cursor/command-provenance.js +128 -0
  36. package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
  37. package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
  38. package/dist/activities/execute-cursor/exact-apply.js +204 -0
  39. package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
  40. package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
  41. package/dist/activities/execute-cursor/hook-script.js +310 -47
  42. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  43. package/dist/activities/execute-cursor/index.d.ts +8 -1
  44. package/dist/activities/execute-cursor/index.js +500 -64
  45. package/dist/activities/execute-cursor/index.js.map +1 -1
  46. package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
  47. package/dist/activities/execute-cursor/message-translator.js +836 -90
  48. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  49. package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
  50. package/dist/activities/execute-cursor/prompt-builder.js +74 -15
  51. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  52. package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
  53. package/dist/activities/execute-cursor/workspace-setup.js +212 -33
  54. package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
  55. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
  56. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
  57. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
  58. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
  59. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
  60. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
  61. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
  62. package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
  63. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
  64. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
  65. package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
  66. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  67. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
  68. package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
  69. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
  70. package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
  71. package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
  72. package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
  73. package/dist/activities/execute-deep-agent/hitl.d.ts +10 -0
  74. package/dist/activities/execute-deep-agent/hitl.js +5 -1
  75. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  76. package/dist/activities/execute-deep-agent/index.d.ts +2 -1
  77. package/dist/activities/execute-deep-agent/index.js +370 -56
  78. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  79. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
  80. package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
  81. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  82. package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
  83. package/dist/activities/execute-deep-agent/setup.js +149 -92
  84. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  85. package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
  86. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
  87. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
  88. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
  89. package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
  90. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  91. package/dist/activities/execute-deep-agent/status-builder.d.ts +11 -5
  92. package/dist/activities/execute-deep-agent/status-builder.js +6 -2
  93. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  94. package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
  95. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  96. package/dist/activities/execute-deep-agent/streaming.js +3 -15
  97. package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
  98. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
  99. package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
  100. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  101. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
  102. package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
  103. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  104. package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -2
  105. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  106. package/dist/claimcheck/payload-codec.js +9 -5
  107. package/dist/claimcheck/payload-codec.js.map +1 -1
  108. package/dist/client/stigmer-client.d.ts +2 -0
  109. package/dist/client/stigmer-client.js +2 -0
  110. package/dist/client/stigmer-client.js.map +1 -1
  111. package/dist/middleware/approval-gate.d.ts +85 -4
  112. package/dist/middleware/approval-gate.js +165 -38
  113. package/dist/middleware/approval-gate.js.map +1 -1
  114. package/dist/middleware/types.d.ts +2 -5
  115. package/dist/shared/activity-input.d.ts +43 -0
  116. package/dist/shared/activity-input.js +17 -0
  117. package/dist/shared/activity-input.js.map +1 -0
  118. package/dist/shared/approval-canonicalize.d.ts +19 -0
  119. package/dist/shared/approval-canonicalize.js +119 -0
  120. package/dist/shared/approval-canonicalize.js.map +1 -0
  121. package/dist/shared/approval-fingerprint.d.ts +106 -0
  122. package/dist/shared/approval-fingerprint.js +113 -0
  123. package/dist/shared/approval-fingerprint.js.map +1 -0
  124. package/dist/shared/approval-policy.d.ts +182 -12
  125. package/dist/shared/approval-policy.js +213 -27
  126. package/dist/shared/approval-policy.js.map +1 -1
  127. package/dist/shared/args-preview.d.ts +52 -0
  128. package/dist/shared/args-preview.js +93 -0
  129. package/dist/shared/args-preview.js.map +1 -0
  130. package/dist/shared/artifact-storage.d.ts +19 -1
  131. package/dist/shared/artifact-storage.js +48 -11
  132. package/dist/shared/artifact-storage.js.map +1 -1
  133. package/dist/shared/file-change.d.ts +44 -0
  134. package/dist/shared/file-change.js +57 -0
  135. package/dist/shared/file-change.js.map +1 -0
  136. package/dist/shared/file-tools.d.ts +107 -0
  137. package/dist/shared/file-tools.js +168 -0
  138. package/dist/shared/file-tools.js.map +1 -0
  139. package/dist/shared/filereview/capture.d.ts +202 -0
  140. package/dist/shared/filereview/capture.js +498 -0
  141. package/dist/shared/filereview/capture.js.map +1 -0
  142. package/dist/shared/filereview/cas-substrate.d.ts +190 -0
  143. package/dist/shared/filereview/cas-substrate.js +284 -0
  144. package/dist/shared/filereview/cas-substrate.js.map +1 -0
  145. package/dist/shared/filereview/digest.d.ts +40 -0
  146. package/dist/shared/filereview/digest.js +66 -0
  147. package/dist/shared/filereview/digest.js.map +1 -0
  148. package/dist/shared/filereview/events.d.ts +170 -0
  149. package/dist/shared/filereview/events.js +298 -0
  150. package/dist/shared/filereview/events.js.map +1 -0
  151. package/dist/shared/filereview/git-substrate.d.ts +175 -0
  152. package/dist/shared/filereview/git-substrate.js +439 -0
  153. package/dist/shared/filereview/git-substrate.js.map +1 -0
  154. package/dist/shared/filereview/index.d.ts +11 -0
  155. package/dist/shared/filereview/index.js +12 -0
  156. package/dist/shared/filereview/index.js.map +1 -0
  157. package/dist/shared/filereview/secret-paths.d.ts +63 -0
  158. package/dist/shared/filereview/secret-paths.js +105 -0
  159. package/dist/shared/filereview/secret-paths.js.map +1 -0
  160. package/dist/shared/fingerprint-secret.d.ts +26 -0
  161. package/dist/shared/fingerprint-secret.js +47 -0
  162. package/dist/shared/fingerprint-secret.js.map +1 -0
  163. package/dist/shared/model-client.d.ts +51 -0
  164. package/dist/shared/model-client.js +77 -0
  165. package/dist/shared/model-client.js.map +1 -0
  166. package/dist/shared/plan-artifact.js +0 -2
  167. package/dist/shared/plan-artifact.js.map +1 -1
  168. package/dist/shared/status-offload.d.ts +83 -9
  169. package/dist/shared/status-offload.js +399 -79
  170. package/dist/shared/status-offload.js.map +1 -1
  171. package/dist/shared/status.js +14 -1
  172. package/dist/shared/status.js.map +1 -1
  173. package/dist/shared/tool-kind.d.ts +19 -0
  174. package/dist/shared/tool-kind.js +13 -0
  175. package/dist/shared/tool-kind.js.map +1 -1
  176. package/dist/shared/tool-row.d.ts +88 -0
  177. package/dist/shared/tool-row.js +127 -0
  178. package/dist/shared/tool-row.js.map +1 -0
  179. package/dist/shared/workspace/platform-dir.d.ts +25 -0
  180. package/dist/shared/workspace/platform-dir.js +38 -2
  181. package/dist/shared/workspace/platform-dir.js.map +1 -1
  182. package/dist/workflows/call-agent-orchestrator.js +56 -7
  183. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  184. package/dist/workflows/connect-mcp-server.d.ts +50 -0
  185. package/dist/workflows/connect-mcp-server.js +136 -15
  186. package/dist/workflows/connect-mcp-server.js.map +1 -1
  187. package/dist/workflows/types.d.ts +8 -0
  188. package/package.json +2 -2
  189. package/src/__test-utils__/approval-contract/contract.ts +224 -0
  190. package/src/__test-utils__/approval-contract/types.ts +179 -0
  191. package/src/__test-utils__/fake-artifact-storage.ts +72 -0
  192. package/src/__tests__/approval-gateway-contract.test.ts +29 -0
  193. package/src/__tests__/claimcheck-codec.test.ts +16 -53
  194. package/src/__tests__/golden-e2e.test.ts +2 -0
  195. package/src/__tests__/runner-token-coordinator.test.ts +3 -3
  196. package/src/activities/__tests__/call-agent-status.test.ts +135 -0
  197. package/src/activities/__tests__/call-llm.test.ts +1 -1
  198. package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
  199. package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
  200. package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
  201. package/src/activities/call-agent-status.ts +74 -4
  202. package/src/activities/call-llm.ts +18 -63
  203. package/src/activities/classify-tool-approvals.ts +101 -19
  204. package/src/activities/discover-mcp-server.ts +29 -1
  205. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
  206. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
  207. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
  208. package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
  209. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
  210. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
  211. package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
  212. package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
  213. package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
  214. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
  215. package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
  216. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
  217. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
  218. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
  219. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
  220. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
  221. package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
  222. package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
  223. package/src/activities/execute-cursor/approval-policy.ts +28 -159
  224. package/src/activities/execute-cursor/approval-state.ts +366 -18
  225. package/src/activities/execute-cursor/capture-flow.ts +323 -0
  226. package/src/activities/execute-cursor/cas-observations.ts +204 -0
  227. package/src/activities/execute-cursor/command-provenance.ts +168 -0
  228. package/src/activities/execute-cursor/exact-apply.ts +253 -0
  229. package/src/activities/execute-cursor/hook-script.ts +317 -51
  230. package/src/activities/execute-cursor/index.ts +575 -67
  231. package/src/activities/execute-cursor/message-translator.ts +963 -89
  232. package/src/activities/execute-cursor/prompt-builder.ts +80 -14
  233. package/src/activities/execute-cursor/workspace-setup.ts +257 -42
  234. package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
  235. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
  236. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
  237. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
  238. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
  239. package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
  240. package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
  241. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
  242. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +2 -5
  243. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
  244. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
  245. package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
  246. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +12 -11
  247. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
  248. package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
  249. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
  250. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
  251. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
  252. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
  253. package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
  254. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
  255. package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
  256. package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
  257. package/src/activities/execute-deep-agent/hitl.ts +15 -1
  258. package/src/activities/execute-deep-agent/index.ts +434 -64
  259. package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
  260. package/src/activities/execute-deep-agent/setup.ts +223 -125
  261. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
  262. package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
  263. package/src/activities/execute-deep-agent/status-builder.ts +19 -7
  264. package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
  265. package/src/activities/execute-deep-agent/streaming.ts +3 -13
  266. package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
  267. package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
  268. package/src/activities/execute-deep-agent/v3-status-builder.ts +8 -2
  269. package/src/claimcheck/payload-codec.ts +8 -8
  270. package/src/client/stigmer-client.ts +9 -1
  271. package/src/middleware/__tests__/approval-gate.test.ts +488 -4
  272. package/src/middleware/approval-gate.ts +247 -38
  273. package/src/middleware/types.ts +5 -5
  274. package/src/shared/__tests__/activity-input.test.ts +78 -0
  275. package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
  276. package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
  277. package/src/shared/__tests__/approval-policy.test.ts +274 -40
  278. package/src/shared/__tests__/args-preview.test.ts +78 -0
  279. package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
  280. package/src/shared/__tests__/artifact-storage.test.ts +123 -11
  281. package/src/shared/__tests__/file-change.test.ts +85 -0
  282. package/src/shared/__tests__/file-tools.test.ts +90 -0
  283. package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
  284. package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
  285. package/src/shared/__tests__/model-client.test.ts +162 -0
  286. package/src/shared/__tests__/plan-artifact.test.ts +11 -26
  287. package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
  288. package/src/shared/__tests__/status-offload.test.ts +573 -16
  289. package/src/shared/__tests__/status.test.ts +4 -5
  290. package/src/shared/__tests__/tool-kind.test.ts +24 -1
  291. package/src/shared/__tests__/tool-row.test.ts +221 -0
  292. package/src/shared/activity-input.ts +57 -0
  293. package/src/shared/approval-canonicalize.ts +159 -0
  294. package/src/shared/approval-fingerprint.ts +148 -0
  295. package/src/shared/approval-policy.ts +303 -27
  296. package/src/shared/args-preview.ts +98 -0
  297. package/src/shared/artifact-storage.ts +62 -11
  298. package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
  299. package/src/shared/file-change.ts +64 -0
  300. package/src/shared/file-tools.ts +169 -0
  301. package/src/shared/filereview/__tests__/capture.test.ts +856 -0
  302. package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
  303. package/src/shared/filereview/__tests__/digest.test.ts +100 -0
  304. package/src/shared/filereview/__tests__/events.test.ts +245 -0
  305. package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
  306. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
  307. package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
  308. package/src/shared/filereview/capture.ts +727 -0
  309. package/src/shared/filereview/cas-substrate.ts +401 -0
  310. package/src/shared/filereview/digest.ts +83 -0
  311. package/src/shared/filereview/events.ts +449 -0
  312. package/src/shared/filereview/git-substrate.ts +555 -0
  313. package/src/shared/filereview/index.ts +60 -0
  314. package/src/shared/filereview/secret-paths.ts +121 -0
  315. package/src/shared/fingerprint-secret.ts +53 -0
  316. package/src/shared/model-client.ts +122 -0
  317. package/src/shared/plan-artifact.ts +0 -2
  318. package/src/shared/status-offload.ts +433 -77
  319. package/src/shared/status.ts +13 -0
  320. package/src/shared/tool-kind.ts +33 -0
  321. package/src/shared/tool-row.ts +135 -0
  322. package/src/shared/workspace/platform-dir.ts +41 -2
  323. package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
  324. package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
  325. package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
  326. package/src/workflows/call-agent-orchestrator.ts +53 -6
  327. package/src/workflows/connect-mcp-server.ts +179 -24
  328. package/src/workflows/types.ts +8 -0
@@ -26,16 +26,18 @@
26
26
  * starts from the user message plus re-injected instructions.
27
27
  */
28
28
  import { heartbeat, Context, CancelledFailure } from "@temporalio/activity";
29
- import { create } from "@bufbuild/protobuf";
29
+ import { create, clone } from "@bufbuild/protobuf";
30
30
  import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
31
31
  import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
32
- import { ExecutionControlSignal, ExecutionPhase, InteractionMode, MessageType, ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
32
+ import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
33
+ import { ExecutionControlSignal, ExecutionPhase, FileChangeSetStatus, InteractionMode, MessageType, ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
33
34
  import { StigmerClient } from "../../client/stigmer-client.js";
34
35
  import { resolveAgent } from "./session-lifecycle.js";
35
36
  import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
36
37
  import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
37
- import { MessageAccumulator, reconcileDeniedToolCalls, cancelInProgressSubAgentProtos } from "./message-translator.js";
38
+ import { MessageAccumulator, reconcileDeniedToolCalls, clearProvisionalPostDenialNarration, cancelInProgressSubAgentProtos, collapseRedundantToolCallTwins } from "./message-translator.js";
38
39
  import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "../../shared/status.js";
40
+ import { collectSubAgentToolCallIds } from "../../shared/tool-row.js";
39
41
  import { startStallWatchdog, StallTimeoutError, formatStallFailure } from "../../shared/stall-watchdog.js";
40
42
  import { createArtifactStorage, loadArtifactStorageConfig } from "../../shared/artifact-storage.js";
41
43
  import { publishPlanArtifact } from "../../shared/plan-artifact.js";
@@ -46,7 +48,7 @@ import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/stre
46
48
  import { createCursorEventRecorder } from "./cursor-event-recorder.js";
47
49
  import { resolveMcpServers, validateMcpServerEnv } from "./mcp-resolver.js";
48
50
  import { mergeApprovalPolicies } from "./approval-policy.js";
49
- import { hasApproveAllDecision } from "../../shared/approval-policy.js";
51
+ import { deriveActiveLeases } from "../../shared/approval-policy.js";
50
52
  import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
51
53
  import { resolveExecutionEnv } from "./env-resolver.js";
52
54
  import { resolveBlueprint } from "./blueprint-resolver.js";
@@ -56,7 +58,14 @@ import { resolveAttachments } from "./attachment-resolver.js";
56
58
  import { buildEnhancedPrompt, buildReinvocationPrompt } from "./prompt-builder.js";
57
59
  import { installHitlGate, removeHitlGate } from "./workspace-setup.js";
58
60
  import { ensureHitlDir } from "../../shared/workspace/platform-dir.js";
59
- import { buildApprovalState, buildApprovalGrants, readDenialLedger, reconstructAdjudicatedApprovals } from "./approval-state.js";
61
+ import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
62
+ import { buildApprovalState, buildApprovalGrants, emitCursorGrantReceipts, primaryToken, readDenialLedger, reconstructAdjudicatedApprovals, watchDenialLedger } from "./approval-state.js";
63
+ import { deriveTurnCommandProvenance } from "./command-provenance.js";
64
+ import { applyApprovedWholeFileWrites, excludeAppliedFromGrants } from "./exact-apply.js";
65
+ import { isGitWorkTree } from "../../shared/filereview/git-substrate.js";
66
+ import { captureBaselineToLedger, captureTurnToLedger, applyCaptureDecisions, deriveCaptureMode, } from "./capture-flow.js";
67
+ import { deriveExecutionFingerprintKey } from "../../shared/approval-fingerprint.js";
68
+ import { getRunnerHitlMasterSecret } from "../../shared/fingerprint-secret.js";
60
69
  import { provisionCursorWorkspace } from "./workspace-provision.js";
61
70
  import { setInterceptorExecutionId, runWithExecutionContext } from "./fetch-interceptor.js";
62
71
  import { closeProxySessions } from "./http2-interceptor.js";
@@ -64,12 +73,18 @@ import { resolveModelId, ensureLoaded as ensurePricingLoaded } from "./model-pri
64
73
  import { UsageAccumulator } from "./usage-accumulator.js";
65
74
  import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
66
75
  import { activityStarted, activityFinished } from "../../idle-watchdog.js";
76
+ import { normalizeActivityInput } from "../../shared/activity-input.js";
67
77
  import { getCapturedRejection, clearCapturedRejection } from "./rejection-capture.js";
68
78
  import { synthesizeError, formatClassifiedError, shouldRetryWithFreshAgent } from "./error-classifier.js";
69
79
  import { createAgent, createCloudAgent } from "./session-lifecycle.js";
70
80
  import { setMaxListeners } from "node:events";
71
81
  import { startHeartbeat } from "../../shared/heartbeat.js";
72
82
  import { getShutdownSignalForQueue } from "../../runner-manager.js";
83
+ // How long Phase 12 waits for the first-denial-stop's run.cancel() to settle
84
+ // before reading the final denial ledger and capturing the turn's tree. Long
85
+ // enough for the SDK's normal teardown, short enough that a wedged cancel
86
+ // cannot noticeably delay the approval pause the user is already waiting on.
87
+ const FIRST_DENIAL_CANCEL_TIMEOUT_MS = 5_000;
73
88
  /**
74
89
  * Creates the activity functions bound to the runner config.
75
90
  * Returned object is passed to Temporal Worker.create({ activities }).
@@ -81,10 +96,15 @@ export function createCursorActivities(config) {
81
96
  tokenRef: config.stigmerTokenRef,
82
97
  });
83
98
  return {
84
- ExecuteCursor: async (executionId, threadId) => {
99
+ // Accepts the new typed object OR the legacy positional args (transitional
100
+ // dual-shape so the runner can deploy before the control planes — see
101
+ // shared/activity-input.ts). Drop the positional arm once both control
102
+ // planes send the object.
103
+ ExecuteCursor: async (arg0, arg1) => {
104
+ const { executionId, threadId, turnSeq } = normalizeActivityInput(arg0, arg1);
85
105
  activityStarted();
86
106
  try {
87
- return await executeCursor(config, client, executionId, threadId);
107
+ return await executeCursor(config, client, executionId, threadId, turnSeq);
88
108
  }
89
109
  finally {
90
110
  activityFinished();
@@ -92,15 +112,19 @@ export function createCursorActivities(config) {
92
112
  },
93
113
  };
94
114
  }
95
- async function executeCursor(config, client, executionId, threadId) {
96
- console.log(`ExecuteCursor started: execution=${executionId}, threadId=${threadId || "(new)"}`);
115
+ async function executeCursor(config, client, executionId, threadId, turnSeq) {
116
+ console.log(`ExecuteCursor started: execution=${executionId}, threadId=${threadId || "(new)"}, turnSeq=${turnSeq}`);
97
117
  // Ensure fresh HTTP/2 transport — prevents a degraded session from a
98
118
  // prior workflow task from poisoning this execution's agent stream.
99
119
  closeProxySessions();
100
120
  setInterceptorExecutionId(executionId);
101
- return runWithExecutionContext(executionId, () => executeCursorInner(config, client, executionId, threadId));
121
+ return runWithExecutionContext(executionId, () => executeCursorInner(config, client, executionId, threadId, turnSeq));
102
122
  }
103
- async function executeCursorInner(config, client, executionId, threadId) {
123
+ async function executeCursorInner(config, client, executionId, threadId,
124
+ // turnSeq is the monotonic HITL-cycle index (0 on the first turn). The
125
+ // file-review producer consumes it to mint the deterministic change-set id
126
+ // (executionId:turnSeq) in the capture phase.
127
+ turnSeq) {
104
128
  const status = create(AgentExecutionStatusSchema, {
105
129
  phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
106
130
  startedAt: utcTimestamp(),
@@ -135,6 +159,29 @@ async function executeCursorInner(config, client, executionId, threadId) {
135
159
  // message surfaced to the user; both feed the Phase 11a stall branch.
136
160
  let stallDetected = false;
137
161
  let stallError;
162
+ // Set the moment the preToolUse hook records its first denial in the ledger.
163
+ // We then stop consuming the stream and cancel the run so the model never
164
+ // reacts to Cursor's tool-failure surface (narrate defeat, attempt a second
165
+ // gated tool) — converging the Cursor harness toward the native harness, which
166
+ // pauses BEFORE the model sees a denial. Phase 12 reconciles the denied tool
167
+ // calls into WAITING_FOR_APPROVAL exactly as after a natural stream end.
168
+ let firstDenialDetected = false;
169
+ // Flipped by the denial-ledger fs watcher the instant the hook writes a
170
+ // denial, so the NEXT stream event of ANY type triggers the ledger read —
171
+ // instead of waiting for the next tool_call event, during which the model's
172
+ // full post-denial reaction (thinking, narration, a workaround tool) would
173
+ // stream and persist (observed in production: aex_01kwj07f7g23c3wp9sn8496z5g).
174
+ // The tool_call-event read below remains the backstop where fs.watch is
175
+ // unreliable.
176
+ let denialLedgerDirty = false;
177
+ let stopDenialWatcher;
178
+ // The in-flight run.cancel() started by the first-denial stop. Awaited
179
+ // (timeboxed) before Phase 12 so the agent process has actually stopped
180
+ // before the final ledger read and the turn-boundary tree capture — closing
181
+ // the race where a post-denial workaround's ledger entry lands after the
182
+ // read (it would then never be collapsed) or a late tool mutates the tree
183
+ // mid-capture.
184
+ let denialCancelSettled;
138
185
  let periodicHeartbeat;
139
186
  // Progress-based stall watchdog (see ../../shared/stall-watchdog.ts). Stopped
140
187
  // in the finally on every exit path; complements the liveness heartbeat.
@@ -174,6 +221,32 @@ async function executeCursorInner(config, client, executionId, threadId) {
174
221
  await reportSetupProgress(client, executionId, "Provisioning workspace");
175
222
  blueprint.workspaceDirs = await provisionCursorWorkspace(config, session, envVars, sessionId ?? "");
176
223
  heartbeat();
224
+ // Apply-then-review is the universal file-review model (Slice 2c). When the
225
+ // primary workspace is a real git work tree, file edits flow during the turn
226
+ // and are captured per-file from the git diff at the turn boundary
227
+ // (capture-flow.ts / shared/filereview/git-substrate.ts). A NON-git workspace
228
+ // has no git snapshot, so it captures every file write via the path-scoped CAS
229
+ // substrate instead — which requires artifact storage to persist blobs; when
230
+ // storage is unavailable a non-git workspace falls back to the classic
231
+ // deny-gate (no regression). `gitWorkspace` selects the substrate; both flow
232
+ // file edits and review post-hoc, and the deny-gate then survives only for
233
+ // shell/MCP/irreversible tools. Detected once from the provisioned primary root.
234
+ const primaryWorkspaceDir = blueprint.workspaceDirs[0];
235
+ const gitWorkspace = primaryWorkspaceDir
236
+ ? await isGitWorkTree(primaryWorkspaceDir)
237
+ : false;
238
+ const captureMode = deriveCaptureMode(primaryWorkspaceDir, gitWorkspace, !!artifactStorage);
239
+ // Pre-turn baseline tree, pinned before the agent runs (capture mode only)
240
+ // so the turn-end capture diffs against it and the tree restores exactly.
241
+ let baselineTree;
242
+ // Deterministic id of the change set this turn may produce:
243
+ // `${executionId}:${turnSeq}`. Minted from the workflow-threaded turn index
244
+ // so it is stable across a Temporal retry (idempotent ledger authoring) and
245
+ // unique per turn. The resume reconcile reads the change set id back from the
246
+ // DECIDED projection, not from turnSeq — so a "wasted" id on a pure-reconcile
247
+ // resume (which never authors a baseline) is harmless.
248
+ const changeSetId = `${executionId}:${turnSeq}`;
249
+ heartbeat();
177
250
  // Set OTel baggage so downstream calls carry execution context.
178
251
  try {
179
252
  const { setBaggage, BAGGAGE_EXECUTION_ID, BAGGAGE_SESSION_ID, BAGGAGE_ORG_ID } = await import("../../otel.js");
@@ -202,12 +275,72 @@ async function executeCursorInner(config, client, executionId, threadId) {
202
275
  // by reinvocation time — the decision survives only on the tool call. This
203
276
  // feeds both the grant builder and the reinvocation prompt below.
204
277
  let adjudicatedApprovals = [];
278
+ // tool-call id -> content digest of the approved edit, threaded into the
279
+ // grant builder so an approved edit is authorized by its exact content (a
280
+ // sibling edit to the same file re-gates). Sourced from the persisted
281
+ // approval_content_digest field (see reconstructAdjudicatedApprovals).
282
+ let adjudicatedContentDigests = new Map();
283
+ // Sub-agent executions carried over from the persisted transcript on a
284
+ // resume, handed to the MessageAccumulator so a gated tool inside a
285
+ // delegated sub-agent survives the round-trip (see seeding below).
286
+ let seededSubAgents = [];
205
287
  if (isReinvocation) {
206
288
  const existingStatus = execution.status;
289
+ // Seed the in-progress status from the persisted execution BEFORE the
290
+ // MessageAccumulator wraps status.messages, so this resumed turn APPENDS
291
+ // onto prior history rather than rebuilding from empty. A Cursor resume
292
+ // re-issues approved tool calls with fresh ids; a from-empty rebuild would
293
+ // drop the previously-committed ids and the backend's append-only-at-
294
+ // identity guard would reject the whole update, stalling the run (the
295
+ // "approval propagation is broken" watchdog failure). The resumed re-runs
296
+ // are reconciled onto these seeded calls by canonical identity inside the
297
+ // accumulator. Mirrors the deep-agent seedStatusFromExecution.
298
+ seededSubAgents = seedCursorTranscriptFromExecution(status, execution);
299
+ // File-review reconcile (the dual-source half): reconcile every change set
300
+ // the server projected as DECIDED, sourced from the ledger decisions and
301
+ // the pinned git refs (approved kept at their "after" bytes, rejected
302
+ // snapped back to baseline — all uncommitted, hash-verified). This is
303
+ // independent of tool approvals: a single turn can carry BOTH a DECIDED
304
+ // file change set AND an approved shell/MCP action.
305
+ let reconciledFileReview = false;
306
+ let fileReviewFailed = false;
307
+ let fileReviewFailureDetail = "";
308
+ const discardedPaths = [];
309
+ if (captureMode && primaryWorkspaceDir) {
310
+ const decidedSets = (existingStatus?.fileChangeSets ?? []).filter((cs) => cs.status === FileChangeSetStatus.DECIDED);
311
+ for (const changeSet of decidedSets) {
312
+ const capResult = await applyCaptureDecisions({
313
+ status,
314
+ gitRoot: primaryWorkspaceDir,
315
+ executionId,
316
+ changeSet,
317
+ // Thread the CAS store so CAS-captured files in the change set
318
+ // reconcile from the durable manifest (approved after-blobs written,
319
+ // rejected snapped back). In a non-git workspace this is the ONLY
320
+ // reconcile; in a git tree it composes with the git-ref reconcile.
321
+ storage: artifactStorage,
322
+ gitWorkspace,
323
+ });
324
+ if (!capResult.isCaptureTurn)
325
+ continue;
326
+ reconciledFileReview = true;
327
+ if (capResult.failed) {
328
+ fileReviewFailed = true;
329
+ fileReviewFailureDetail = capResult.failureDetail ?? "file review reconcile failed";
330
+ }
331
+ if (capResult.hadReject)
332
+ discardedPaths.push(...capResult.rejectedPaths);
333
+ }
334
+ }
335
+ // Tool approvals (shell / MCP / gitignored writes) still resolve from the
336
+ // message transcript — the deny-gate path, unchanged by the file-review
337
+ // cutover.
207
338
  const adjudicated = reconstructAdjudicatedApprovals(existingStatus?.messages ?? []);
208
339
  if (adjudicated.decisions.size > 0) {
209
340
  approvalDecisions = adjudicated.decisions;
210
341
  adjudicatedApprovals = adjudicated.pendingApprovals;
342
+ adjudicatedContentDigests = adjudicated.contentDigests;
343
+ // A reject of an irreversible action (shell/MCP) fails the execution.
211
344
  const hasReject = [...approvalDecisions.values()].some((a) => a === ApprovalAction.REJECT);
212
345
  if (hasReject) {
213
346
  status.phase = ExecutionPhase.EXECUTION_FAILED;
@@ -221,6 +354,45 @@ async function executeCursorInner(config, client, executionId, threadId) {
221
354
  await persist(status);
222
355
  return slimStatus(status);
223
356
  }
357
+ // else: fall through to run the approved shell/MCP. The agent may produce
358
+ // further edits, captured as a new change set in the next cycle.
359
+ }
360
+ else if (reconciledFileReview) {
361
+ // Pure file review: the agent already finished its full turn during
362
+ // capture, so keeping/discarding a change does NOT re-prompt it
363
+ // (Cursor-like). The reconcile is done; the execution is complete.
364
+ status.phase = ExecutionPhase.EXECUTION_COMPLETED;
365
+ status.completedAt = utcTimestamp();
366
+ if (fileReviewFailed) {
367
+ // What-you-approve-is-what-applies could not be honored (on-disk bytes
368
+ // diverged from the approved digest). Surface it to the human; the
369
+ // FileReviewFailure(HASH_MISMATCH) event is the audit record.
370
+ status.messages.push(create(AgentMessageSchema, {
371
+ type: MessageType.MESSAGE_SYSTEM,
372
+ content: "Some approved file changes could not be applied because the file " +
373
+ "changed after review: " + fileReviewFailureDetail + ".",
374
+ timestamp: utcTimestamp(),
375
+ }));
376
+ }
377
+ else if (discardedPaths.length > 0) {
378
+ // A reject is a DISCARD that COMPLETES (not FAILED) — surface a SYSTEM
379
+ // note listing the reverted files. This note is for the human; it does
380
+ // NOT re-sync the Cursor SDK agent (its native context still believes
381
+ // those edits stuck). The agent self-corrects by re-reading, and any
382
+ // edit it makes from that stale belief is itself re-surfaced as a new
383
+ // change set next turn (the structural safety net). See
384
+ // design-decisions/capture-reject-next-turn-resync-not-built.md.
385
+ status.messages.push(create(AgentMessageSchema, {
386
+ type: MessageType.MESSAGE_SYSTEM,
387
+ content: "Some proposed file changes were discarded by the user and were not applied: " +
388
+ discardedPaths.join(", ") + ".",
389
+ timestamp: utcTimestamp(),
390
+ }));
391
+ }
392
+ await persist(status);
393
+ console.log(`ExecuteCursor file-review resume short-circuit: execution=${executionId}, ` +
394
+ `failed=${fileReviewFailed}, discarded=${discardedPaths.length}`);
395
+ return slimStatus(status);
224
396
  }
225
397
  }
226
398
  // Phase 4: Resolve MCP servers with approval policies
@@ -232,16 +404,17 @@ async function executeCursorInner(config, client, executionId, threadId) {
232
404
  const mcpConfig = mcpResolution.cursorConfig;
233
405
  // Phase 4b: Merge approval policies from all layers.
234
406
  //
235
- // Effective auto-approve is true when EITHER the execution was pre-armed via
236
- // spec.auto_approve_all OR a user chose APPROVE_ALL ("approve and don't ask
237
- // again") at some gate earlier in this run. The shared hasApproveAllDecision
238
- // helper (used by the native harness too) keeps this contract defined once.
239
- // When true, the merged policy map is empty and the hook state file disables
240
- // gating for the rest of the run.
241
- const effectiveAutoApproveAll = (execution.spec?.autoApproveAll ?? false) || hasApproveAllDecision(execution);
407
+ // Two bypasses (see ActiveLeases, shared with the native harness): the
408
+ // pre-armed spec.auto_approve_all is the one whole-run global bypass; an
409
+ // interactive APPROVE_ALL grants a run-lifetime lease scoped to that action's
410
+ // class. deriveActiveLeases keeps this contract defined once. Server-scoped
411
+ // leases drop that server's tools from the merged map (so the hook treats
412
+ // them as auto-approved); the global bypass empties the map entirely.
413
+ const leases = deriveActiveLeases(execution);
414
+ const globalBypass = leases.global;
242
415
  const agentOverrides = blueprint.mergedMcpServerUsages
243
416
  .flatMap((u) => u.toolApprovalOverrides ?? []);
244
- const mergedPolicies = mergeApprovalPolicies(mcpResolution.resolvedServers, agentOverrides, effectiveAutoApproveAll);
417
+ const mergedPolicies = mergeApprovalPolicies(mcpResolution.resolvedServers, agentOverrides, leases);
245
418
  heartbeat();
246
419
  // Phase 4c: Validate MCP server env health (diagnostic, non-blocking)
247
420
  const mcpWarnings = validateMcpServerEnv(mcpResolution.resolvedServers, blueprint.mergedMcpServerUsages, envVars);
@@ -251,7 +424,7 @@ async function executeCursorInner(config, client, executionId, threadId) {
251
424
  }
252
425
  // Phase 5: Resolve skills (merged from agent + session)
253
426
  await reportSetupProgress(client, executionId, "Resolving skills");
254
- const primaryWorkspaceDir = blueprint.workspaceDirs[0];
427
+ // (primaryWorkspaceDir / captureMode were resolved right after provisioning.)
255
428
  const skillMetadata = await resolveSkills(client, blueprint.mergedSkillRefs, {
256
429
  sessionId,
257
430
  primaryWorkspaceDir,
@@ -260,6 +433,34 @@ async function executeCursorInner(config, client, executionId, threadId) {
260
433
  // Phase 5b: Resolve attachments
261
434
  const attachmentResults = await resolveAttachments(spec.attachments, sessionId, primaryWorkspaceDir, config.mode);
262
435
  const attachmentPaths = attachmentResults.map((a) => a.relativePath);
436
+ // Phase 5b3: Exact-apply approved whole-file writes (HITL "what you approve
437
+ // is what gets applied"). The Cursor deny-only harness reinvokes the model,
438
+ // which regenerates content, so a resource grant alone cannot guarantee the
439
+ // bytes that land match the bytes the user approved. The runner therefore
440
+ // writes the EXACT approved whole-file content itself, marks those tool calls
441
+ // COMPLETED, and (below) issues NO grant for them — so any FURTHER change the
442
+ // model makes to those files is re-gated. Hunk edits / shell / MCP stay on
443
+ // the grant + reinvocation path. Every uncertain case degrades to that path,
444
+ // so this can never corrupt a file (see exact-apply.ts).
445
+ let appliedToolCallIds = new Set();
446
+ // Exact-apply is the deny-gate path's "what you approve is what gets applied"
447
+ // mechanism (the model regenerates content on reinvocation). Capture mode
448
+ // does not reinvoke the model for file edits — it applies the exact captured
449
+ // bytes itself in applyCaptureDecisions — so exact-apply is scoped OUT of it.
450
+ if (!captureMode && isReinvocation && approvalDecisions) {
451
+ appliedToolCallIds = await applyApprovedWholeFileWrites({
452
+ messages: status.messages,
453
+ workspaceBackend: new LocalWorkspaceBackend(primaryWorkspaceDir),
454
+ workspaceDirs: blueprint.workspaceDirs,
455
+ executionId,
456
+ });
457
+ if (appliedToolCallIds.size > 0) {
458
+ // Persist the applied writes (tool calls now COMPLETED with the approved
459
+ // diff) before reinvocation, so the applied state is durable even if the
460
+ // continuation fails, and the UI reflects it immediately.
461
+ await persist(status);
462
+ }
463
+ }
263
464
  // Phase 5c: Install the HITL approval gate BEFORE resolving the agent.
264
465
  //
265
466
  // The gate's runtime artifacts (hook script, approval-state file, denial
@@ -273,12 +474,44 @@ async function executeCursorInner(config, client, executionId, threadId) {
273
474
  //
274
475
  // On reinvocation, turn the user's approvals into tool-identity grants so
275
476
  // the resumed agent's re-attempt (which carries a fresh tool-call id) is
276
- // allowed through.
477
+ // allowed through. Exact-applied writes are EXCLUDED from the grants: with no
478
+ // grant, a further write to that file is re-gated (the user sees every change).
479
+ // Capture mode: pin the pre-turn baseline tree before the agent runs (and
480
+ // before the gate is installed, though the gate files are excluded from the
481
+ // capture anyway). The turn-end capture diffs the post-turn tree against this
482
+ // to build the per-file cards; the baseline ref is also what a reject reverts
483
+ // to on resume. Covers a fresh turn and the approved-irreversible resume
484
+ // fall-through (the agent will run and may make further edits).
485
+ if (captureMode && primaryWorkspaceDir) {
486
+ // Pin the pre-turn tree AND author BASELINE_CAPTURED so the projection can
487
+ // materialize the change set (status CAPTURING) before any candidate exists.
488
+ // The event rides the next persist; CAPTURING does not arm the unified gate.
489
+ baselineTree = await captureBaselineToLedger({
490
+ status,
491
+ gitRoot: primaryWorkspaceDir,
492
+ executionId,
493
+ changeSetId,
494
+ gitWorkspace,
495
+ });
496
+ }
277
497
  hitlDir = await ensureHitlDir(sessionId);
498
+ const grantApprovals = excludeAppliedFromGrants(adjudicatedApprovals, appliedToolCallIds);
278
499
  const approvalGrants = approvalDecisions
279
- ? buildApprovalGrants(adjudicatedApprovals, approvalDecisions)
500
+ ? buildApprovalGrants(grantApprovals, approvalDecisions, adjudicatedContentDigests)
280
501
  : undefined;
281
- const approvalState = buildApprovalState(mergedPolicies, effectiveAutoApproveAll, approvalGrants);
502
+ if (approvalGrants && approvalGrants.length > 0 && !globalBypass) {
503
+ emitCursorGrantReceipts(approvalGrants, deriveExecutionFingerprintKey(getRunnerHitlMasterSecret(), executionId), executionId);
504
+ }
505
+ // CAS capture requires artifact storage to persist blobs
506
+ // (captureCandidateToLedger throws without it). In a git tree, captureMode
507
+ // alone governs tracked-file capture (no storage needed) and captureIgnored is
508
+ // the narrower switch (git tree + storage) that also captures gitignored
509
+ // writes. In a non-git workspace ALL capture is CAS, so captureMode already
510
+ // required storage — captureIgnored then equals captureMode. When storage is
511
+ // absent a git tree keeps gating gitignored writes and a non-git workspace
512
+ // falls back to the deny-gate entirely (no regression).
513
+ const captureIgnored = captureMode && !!artifactStorage;
514
+ const approvalState = buildApprovalState(mergedPolicies, globalBypass, leases.categories, approvalGrants, captureMode, captureIgnored, gitWorkspace);
282
515
  const hitlGate = await installHitlGate({
283
516
  workspaceRoot: primaryWorkspaceDir,
284
517
  hitlDir,
@@ -289,6 +522,12 @@ async function executeCursorInner(config, client, executionId, threadId) {
289
522
  await removeHitlGate(hitlGate);
290
523
  await removeStigmerSymlink(primaryWorkspaceDir);
291
524
  };
525
+ // Arm the denial watcher as soon as the gate exists. The per-turn ledger
526
+ // reset may flip the flag once before the run starts; the loop's read then
527
+ // sees an empty ledger and clears it — harmless by construction.
528
+ stopDenialWatcher = watchDenialLedger(hitlDir, () => {
529
+ denialLedgerDirty = true;
530
+ });
292
531
  // Phase 5d: Ensure model pricing registry is populated before validation
293
532
  await ensurePricingLoaded();
294
533
  // Phase 6: Validate model selection
@@ -383,6 +622,7 @@ async function executeCursorInner(config, client, executionId, threadId) {
383
622
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
384
623
  attachmentPaths,
385
624
  pendingApprovals: adjudicatedApprovals,
625
+ appliedToolCallIds,
386
626
  interactionMode,
387
627
  });
388
628
  // Phase 10a: Inject structured output instruction for Cursor harness
@@ -493,7 +733,16 @@ async function executeCursorInner(config, client, executionId, threadId) {
493
733
  });
494
734
  }
495
735
  });
496
- const accumulator = new MessageAccumulator(status.messages, { mergedPolicies });
736
+ // Everything at an index >= this was produced by THIS turn's stream — the
737
+ // positional turn boundary the approved-command provenance (DD-28) scopes
738
+ // its qualification to. Snapshotted before the accumulator can append.
739
+ const turnStartMessageIndex = status.messages.length;
740
+ const accumulator = new MessageAccumulator(status.messages, {
741
+ mergedPolicies,
742
+ provenance: { globalBypass, leasedCategories: leases.categories },
743
+ workspaceRoot: primaryWorkspaceDir,
744
+ seededSubAgents,
745
+ });
497
746
  // Shared cadence with the native harness: discrete state changes force a
498
747
  // flush; high-frequency token deltas ride this scheduler's time cadence
499
748
  // (env-tunable via STREAMING_* — see loadStreamingConfig).
@@ -518,6 +767,45 @@ async function executeCursorInner(config, client, executionId, threadId) {
518
767
  if (event.type === "tool_call" && event.name === "task") {
519
768
  accumulator.trackSubAgentExecution(event);
520
769
  }
770
+ // First-denial stop (HITL clean pause). In CAPTURE mode this fires only for
771
+ // an IRREVERSIBLE tool the hook still gates (shell, MCP, or a gitignored
772
+ // write/delete) — file edits flow freely and are captured at the turn
773
+ // boundary, so they never enter the ledger. In the deny-gate FALLBACK
774
+ // (non-git workspace) it fires for every gated file edit too. Either way:
775
+ // the preToolUse hook appends to the denial ledger the instant it gates a
776
+ // tool — before Cursor surfaces the failure to the model — and the fs
777
+ // watcher flips denialLedgerDirty the moment that write lands. Confirming
778
+ // the flag with a read on the very next event (of ANY type — thinking
779
+ // deltas arrive within milliseconds) ends the turn before the model's
780
+ // reaction can persist: waiting for the next tool_call event let the full
781
+ // post-denial reaction (thinking, narration, a workaround shell) stream
782
+ // and persist live (production case aex_01kwj07f7g23c3wp9sn8496z5g). The
783
+ // tool_call-event read stays as the backstop for platforms where fs.watch
784
+ // is unreliable; the current event was already accumulated above, so the
785
+ // anchor's own row is always present for the Phase 12 gate overlay. This
786
+ // mirrors the native harness's pause-before-react semantics; Phase 12
787
+ // reconciles the denied calls and its trim remains the last-resort
788
+ // backstop for anything that persisted before the stop.
789
+ if (!firstDenialDetected && hitlDir && (denialLedgerDirty || event.type === "tool_call")) {
790
+ denialLedgerDirty = false;
791
+ const denials = await readDenialLedger(hitlDir);
792
+ if (denials.length > 0) {
793
+ firstDenialDetected = true;
794
+ console.log(`ExecuteCursor first denial detected (${denials.length} ledger ` +
795
+ `entr${denials.length === 1 ? "y" : "ies"}); stopping turn to pause ` +
796
+ `cleanly for approval: execution=${executionId}`);
797
+ if (run.supports?.("cancel")) {
798
+ // Kept (not fire-and-forget): awaited timeboxed before Phase 12 so
799
+ // the ledger read and tree capture see a stopped agent.
800
+ denialCancelSettled = run.cancel().then(() => { }, (cancelErr) => {
801
+ console.warn(`ExecuteCursor run.cancel() after first denial failed (non-fatal): ` +
802
+ `execution=${executionId}, ` +
803
+ `error=${cancelErr instanceof Error ? cancelErr.message : cancelErr}`);
804
+ });
805
+ }
806
+ break;
807
+ }
808
+ }
521
809
  deltaEnricher.applyEnrichments(status.messages);
522
810
  eventCount++;
523
811
  if (event.type === "status") {
@@ -561,13 +849,15 @@ async function executeCursorInner(config, client, executionId, threadId) {
561
849
  }
562
850
  }
563
851
  catch (streamErr) {
564
- // run.cancel() from the stall watchdog can make the stream iterator
565
- // reject; that is the expected teardown, so swallow it and fall through
566
- // to the stallDetected branch in Phase 11a. Anything else is a genuine
852
+ // run.cancel() from the stall watchdog or the first-denial stop — can
853
+ // make the stream iterator reject as it tears down; that is the expected
854
+ // teardown for both, so swallow it and fall through (stallDetected ->
855
+ // Phase 11a; firstDenialDetected -> Phase 12). Anything else is a genuine
567
856
  // stream failure — rethrow it to the outer error handler.
568
- if (!stallDetected)
857
+ if (!stallDetected && !firstDenialDetected)
569
858
  throw streamErr;
570
- console.warn(`ExecuteCursor stream ended via stall cancel: execution=${executionId}`);
859
+ console.warn(`ExecuteCursor stream ended via cancel: execution=${executionId}, ` +
860
+ `stall=${stallDetected}, firstDenial=${firstDenialDetected}`);
571
861
  }
572
862
  periodicHeartbeat.stop();
573
863
  stallWatchdog.stop();
@@ -716,12 +1006,125 @@ async function executeCursorInner(config, client, executionId, threadId) {
716
1006
  // harness — the approval surface is driven entirely by tool-call status. We
717
1007
  // deliberately do NOT set status.pendingApprovals here: any value would be
718
1008
  // discarded by the backend's recompute on the next updateStatus.
1009
+ //
1010
+ // Before reading the ledger, wait (timeboxed) for the first-denial-stop's
1011
+ // run.cancel() to settle. run.cancel() races the SDK's auto-execution: until
1012
+ // it lands, the agent process may still attempt a post-denial workaround
1013
+ // whose hook denial would land AFTER a premature ledger read — the row then
1014
+ // never collapses and renders as RUNNING forever (production case
1015
+ // aex_01kwj07f7g23c3wp9sn8496z5g) — or a late tool could mutate the tree
1016
+ // mid-capture. The timebox keeps a wedged cancel from hanging the pause;
1017
+ // the Phase 12 trims below remain the backstop for that degraded case.
1018
+ if (firstDenialDetected && denialCancelSettled) {
1019
+ await Promise.race([
1020
+ denialCancelSettled,
1021
+ new Promise((resolve) => {
1022
+ const timer = setTimeout(resolve, FIRST_DENIAL_CANCEL_TIMEOUT_MS);
1023
+ timer.unref();
1024
+ }),
1025
+ ]);
1026
+ }
719
1027
  const deniedLedger = await readDenialLedger(hitlDir ?? "");
720
- const deniedToolCalls = reconcileDeniedToolCalls(status.messages, deniedLedger, mergedPolicies);
721
- if (deniedToolCalls.length > 0) {
1028
+ // Capture mode: author the net change set to the file_review ledger as the
1029
+ // CANDIDATE_CAPTURED event (projected server-side to a file_change_set
1030
+ // AWAITING_REVIEW — the single review surface). The runner-owned gate files
1031
+ // are excluded from the capture. The agent's edits are LEFT applied on the
1032
+ // working tree (Cursor parity — the user reviews the real change; nothing is
1033
+ // committed and the next turn is blocked until approval, and a reject snaps
1034
+ // each file back on resume). Runs BEFORE the denial reconcile so a denied
1035
+ // (gitignored) write stays on the deny-gate path while every flowed edit is
1036
+ // captured to the ledger.
1037
+ let capturedChangeCount = 0;
1038
+ // `baselineTree !== undefined` means a baseline was authored this turn — the
1039
+ // git tree sha for a git workspace, or "" (empty, but authored) for a non-git
1040
+ // one. A plain truthiness check would wrongly skip the non-git capture.
1041
+ if (captureMode && baselineTree !== undefined && primaryWorkspaceDir) {
1042
+ const deniedTokens = new Set(deniedLedger.map((e) => e.token));
1043
+ // Approved-command turn facts (DD-28): when every mutation-capable call
1044
+ // this turn was a consented shell command, attach the provenance so the
1045
+ // backend can verify the cited consent rows and auto-keep the set instead
1046
+ // of arming a second gate. Fail-closed: any non-qualifying turn attaches
1047
+ // nothing and reviews manually exactly as before.
1048
+ const commandProvenance = deriveTurnCommandProvenance({
1049
+ messages: status.messages,
1050
+ turnStartIndex: turnStartMessageIndex,
1051
+ deniedTokens,
1052
+ grantTokenToConsentId: new Map((approvalGrants ?? []).map((g) => [
1053
+ primaryToken(g.key, g.salient, g.contentDigest),
1054
+ g.sourceToolCallId,
1055
+ ])),
1056
+ globalBypass,
1057
+ });
1058
+ if (commandProvenance) {
1059
+ console.log(`ExecuteCursor capture: turn qualifies for approved-command auto-keep ` +
1060
+ `(consent rows: ${commandProvenance.consentToolCallIds.join(",") || "(auto_approve_all)"}); ` +
1061
+ `attaching provenance to candidate (execution=${executionId})`);
1062
+ }
1063
+ const captured = await captureTurnToLedger({
1064
+ status,
1065
+ gitRoot: primaryWorkspaceDir,
1066
+ executionId,
1067
+ changeSetId,
1068
+ baselineTree,
1069
+ messages: status.messages,
1070
+ deniedTokens,
1071
+ commandProvenance,
1072
+ // Scope sub-agent row stamping to this turn: the seeded prior sub-agents
1073
+ // (cloned in on resume) are the "before this turn" rows to skip.
1074
+ priorSubAgentToolCallIds: collectSubAgentToolCallIds(seededSubAgents),
1075
+ // The CAS half: read the sidecar the hook staged this turn and compose it
1076
+ // into the change set. hitlDir + storage are present when captureIgnored
1077
+ // was on (a git tree's gitignored writes, or ALL writes in a non-git
1078
+ // workspace). In a git tree this composes with the git diff (HYBRID); in a
1079
+ // non-git workspace it IS the whole change set (CAS-only).
1080
+ hitlDir,
1081
+ storage: artifactStorage,
1082
+ gitWorkspace,
1083
+ });
1084
+ capturedChangeCount = captured.length;
1085
+ if (capturedChangeCount > 0) {
1086
+ console.log(`ExecuteCursor capture: ${capturedChangeCount} file change(s) authored to the ` +
1087
+ `file_review ledger (change_set=${changeSetId}), working tree left applied ` +
1088
+ `for review (execution=${executionId})`);
1089
+ }
1090
+ }
1091
+ // The gate reads each denied file's pre-edit `before` from the workspace the
1092
+ // runner is co-located with (local FS for OSS; the sandbox in cloud), so a
1093
+ // whole-file rewrite gate renders a true before/after diff. The tool was
1094
+ // DENIED, so disk still holds the old content. User files are never platform
1095
+ // paths, so no platformDir routing is needed here.
1096
+ const gateWorkspaceBackend = new LocalWorkspaceBackend(primaryWorkspaceDir);
1097
+ const deniedToolCalls = await reconcileDeniedToolCalls(status.messages, deniedLedger, mergedPolicies, gateWorkspaceBackend);
1098
+ // Observability: a synthesized placeholder (id `approval:*`) means a denial
1099
+ // correlated to NO streamed tool call in either the exact or the normalized
1100
+ // pass. After the normalized-path fallback this should be ~0; a non-zero rate
1101
+ // is the early-warning signal of a NEW identity drift (the gate would then
1102
+ // show "No preview available" with no diff). Logged, not thrown — the
1103
+ // synthesized gate still safely surfaces the approval.
1104
+ const synthesizedGateCount = deniedToolCalls.filter((tc) => tc.id.startsWith("approval:")).length;
1105
+ if (synthesizedGateCount > 0) {
1106
+ console.warn(`ExecuteCursor reconcile synthesized ${synthesizedGateCount} placeholder gate(s) ` +
1107
+ `with no correlated stream call (execution=${executionId}); ` +
1108
+ `possible hook/stream identity drift — gate(s) will lack a diff`);
1109
+ }
1110
+ if (deniedToolCalls.length > 0 || capturedChangeCount > 0) {
1111
+ if (deniedToolCalls.length > 0) {
1112
+ // Deterministic clean-pause: a turn that pauses for approval must read as
1113
+ // the same shape the native harness produces — pre-tool text + the gated
1114
+ // tool calls — never the model's provisional reaction to Cursor's deny
1115
+ // (e.g. "blocked by a hook; enable it in your Cursor settings"). We blank
1116
+ // that reaction in place (keeping the message count, so the finalize stays
1117
+ // append-only) rather than removing it. See
1118
+ // clearProvisionalPostDenialNarration for the full rationale.
1119
+ const redactedNarration = clearProvisionalPostDenialNarration(status.messages, deniedToolCalls);
1120
+ if (redactedNarration.length > 0) {
1121
+ console.log(`ExecuteCursor redacted ${redactedNarration.length} provisional post-denial narration message(s) before pausing for approval`);
1122
+ }
1123
+ }
722
1124
  status.phase = ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL;
723
1125
  await persist(status);
724
- console.log(`ExecuteCursor returning WAITING_FOR_APPROVAL: ${deniedToolCalls.length} tools pending`);
1126
+ console.log(`ExecuteCursor returning WAITING_FOR_APPROVAL: ${deniedToolCalls.length} gated tool(s), ` +
1127
+ `${capturedChangeCount} file card(s) pending`);
725
1128
  return slimStatus(status);
726
1129
  }
727
1130
  // Phase 13: Map final result
@@ -1020,6 +1423,19 @@ async function executeCursorInner(config, client, executionId, threadId) {
1020
1423
  }
1021
1424
  }
1022
1425
  }
1426
+ // Collapse any redundant same-identity tool-call twin born this turn before
1427
+ // the terminal persist. On a resume turn the gated tool is already granted, so
1428
+ // there is no denial ledger and reconcileDeniedToolCalls never runs — the
1429
+ // extra attempt the model emits beside the approved action (a stuck RUNNING
1430
+ // zombie, a denied-reported-as-success COMPLETED, or an all-no-change double)
1431
+ // would otherwise persist as a second "No preview available" card. The shared
1432
+ // routine keeps the diff/output carrier and blanks the rest to hidden SKIPPED
1433
+ // rows in place, preserving each committed id so the finalize stays append-only.
1434
+ const collapsedTwins = collapseRedundantToolCallTwins(status.messages);
1435
+ if (collapsedTwins > 0) {
1436
+ console.log(`ExecuteCursor collapsed ${collapsedTwins} redundant tool-call twin(s) at ` +
1437
+ `terminal finalize (kept in place as hidden SKIPPED rows): execution=${executionId}`);
1438
+ }
1023
1439
  // NOW persist — subscriber sees COMPLETED + structured_output atomically
1024
1440
  await persist(status);
1025
1441
  console.log(`ExecuteCursor completed: execution=${executionId}, phase=${ExecutionPhase[status.phase]}, ` +
@@ -1180,6 +1596,9 @@ async function executeCursorInner(config, client, executionId, threadId) {
1180
1596
  // path stops it after the stream loop; this covers throws before that
1181
1597
  // point so no orphaned timer survives the activity.
1182
1598
  stallWatchdog?.stop();
1599
+ // Close the denial-ledger watcher on EVERY exit path (idempotent) so no
1600
+ // orphaned fs.watch handle survives the activity.
1601
+ stopDenialWatcher?.();
1183
1602
  // Tear down the HITL gate on EVERY exit path (success, error, approval
1184
1603
  // pause, cancellation) so attaching a real repo leaves the user's
1185
1604
  // .cursor/hooks.json and workspace untouched between turns (issue #173).
@@ -1196,6 +1615,42 @@ async function executeCursorInner(config, client, executionId, threadId) {
1196
1615
  }
1197
1616
  }
1198
1617
  }
1618
+ /**
1619
+ * Seed an in-progress status from the persisted execution on a durable resume
1620
+ * (HITL approval, pause/resume, or transient recovery) so the upcoming turn
1621
+ * APPENDS onto prior history instead of replacing it. This is the Cursor analog
1622
+ * of the deep-agent's seedStatusFromExecution (execute-deep-agent/index.ts).
1623
+ *
1624
+ * Why it is required: a resumed Cursor agent re-issues the previously gated tool
1625
+ * calls with brand-new call ids. Without seeding, the MessageAccumulator would
1626
+ * rebuild the transcript from empty and emit a status that drops the already-
1627
+ * committed tool-call ids. The backend's append-only-at-identity guard
1628
+ * (AgentExecutionUpdateStatusHandler / update_status.go) rejects any non-
1629
+ * terminal update that drops a committed tool-call id, so the resumed progress
1630
+ * would never persist — the run stalls in WAITING_FOR_APPROVAL with no pending
1631
+ * approvals and the workflow watchdog fails it. Seeding makes the resume status
1632
+ * a strict superset; the re-runs are then reconciled in place onto these seeded
1633
+ * calls by canonical identity inside the accumulator.
1634
+ *
1635
+ * The persisted protos are cloned so the input execution stays immutable, and
1636
+ * the seeded messages are pushed into status.messages (which the accumulator
1637
+ * wraps by reference) BEFORE the accumulator is constructed. Sub-agent
1638
+ * executions are returned rather than written to status.subAgentExecutions
1639
+ * directly, because the accumulator owns that array (it overwrites
1640
+ * status.subAgentExecutions with its own on every flush) — handing them to the
1641
+ * accumulator keeps the seeded sub-agent rows from being clobbered.
1642
+ *
1643
+ * @returns the cloned sub-agent executions to seed into the MessageAccumulator.
1644
+ */
1645
+ function seedCursorTranscriptFromExecution(status, execution) {
1646
+ const persisted = execution.status;
1647
+ if (!persisted || persisted.messages.length === 0)
1648
+ return [];
1649
+ for (const message of persisted.messages) {
1650
+ status.messages.push(clone(AgentMessageSchema, message));
1651
+ }
1652
+ return persisted.subAgentExecutions.map((sub) => clone(SubAgentExecutionSchema, sub));
1653
+ }
1199
1654
  // ---------------------------------------------------------------------------
1200
1655
  // Structured Output Extraction (Cursor Harness Tier 2)
1201
1656
  // ---------------------------------------------------------------------------
@@ -1204,41 +1659,21 @@ async function executeCursorInner(config, client, executionId, threadId) {
1204
1659
  * economy-tier LLM with withStructuredOutput (function-calling).
1205
1660
  * Guarantees schema-conformant JSON output via the API's tool-use mechanism.
1206
1661
  *
1207
- * Provider-aware: resolves the economy model via the registry, infers its
1208
- * provider (anthropic / openai), and constructs the correct LangChain client
1209
- * with the matching proxy endpoint. Follows the same pattern as
1210
- * call-llm.ts constructModel().
1662
+ * Construction (registry-id resolution, provider inference, proxy wiring) is
1663
+ * delegated to the shared buildChatModel so the economy model's registry id is
1664
+ * always resolved to a provider API id before the call.
1211
1665
  */
1212
1666
  async function extractStructuredOutput(agentResponse, schema, config, primaryModel) {
1213
- const { ChatOpenAI } = await import("@langchain/openai");
1214
- const { ChatAnthropic } = await import("@langchain/anthropic");
1215
- const { inferProvider, resolveProxyBaseUrl, buildProxyHeaders } = await import("../../shared/llm-proxy.js");
1216
1667
  const { getEconomyModel } = await import("../../shared/model-registry.js");
1668
+ const { buildChatModel } = await import("../../shared/model-client.js");
1217
1669
  const extractionModel = await getEconomyModel(primaryModel);
1218
- const provider = inferProvider(extractionModel);
1219
1670
  const proxyEndpoint = config.proxyEndpoint ?? config.stigmerBackendEndpoint;
1220
- const baseUrl = resolveProxyBaseUrl(proxyEndpoint, provider);
1221
- const headers = config.stigmerToken
1222
- ? buildProxyHeaders(config.stigmerToken, {})
1223
- : {};
1224
- const apiKey = provider === "openai"
1225
- ? (config.stigmerToken ?? process.env.OPENAI_API_KEY ?? "proxy-managed")
1226
- : (config.stigmerToken ?? process.env.ANTHROPIC_API_KEY ?? "proxy-managed");
1227
- const llm = provider === "openai"
1228
- ? new ChatOpenAI({
1229
- model: extractionModel,
1230
- apiKey,
1231
- temperature: 0,
1232
- maxTokens: 4096,
1233
- configuration: { baseURL: baseUrl, defaultHeaders: headers },
1234
- })
1235
- : new ChatAnthropic({
1236
- model: extractionModel,
1237
- apiKey,
1238
- temperature: 0,
1239
- maxTokens: 4096,
1240
- clientOptions: { baseURL: baseUrl, defaultHeaders: headers },
1241
- });
1671
+ const { model: llm } = await buildChatModel({
1672
+ modelName: extractionModel,
1673
+ proxyEndpoint,
1674
+ stigmerToken: config.stigmerToken ?? undefined,
1675
+ maxTokens: 4096,
1676
+ });
1242
1677
  const zodSchema = jsonSchemaToZod(schema);
1243
1678
  const structured = llm.withStructuredOutput(zodSchema);
1244
1679
  const result = await structured.invoke([
@@ -1270,9 +1705,10 @@ export function buildPrompt(input) {
1270
1705
  const { resolution, approvalDecisions, instructions, userMessage, skills, subAgents, workspaceDirs, workspaceFileRefs, attachmentPaths, interactionMode, } = input;
1271
1706
  const isHitlReinvocation = approvalDecisions !== undefined && approvalDecisions.size > 0;
1272
1707
  // HITL reinvocation: the agent is resumed, so its native context carries the
1273
- // prior conversation; the reinvocation prompt conveys the approval decisions.
1708
+ // prior conversation; the reinvocation prompt conveys the approval decisions
1709
+ // (and which approved writes the runner already exact-applied).
1274
1710
  if (isHitlReinvocation) {
1275
- return buildReinvocationPrompt(input.pendingApprovals, approvalDecisions);
1711
+ return buildReinvocationPrompt(input.pendingApprovals, approvalDecisions, input.appliedToolCallIds);
1276
1712
  }
1277
1713
  // A successfully resumed agent carries its own conversation context via the
1278
1714
  // SDK's native store — send the raw user message with no preamble.