@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
@@ -27,12 +27,14 @@
27
27
  */
28
28
 
29
29
  import { heartbeat, Context, CancelledFailure } from "@temporalio/activity";
30
- import { create, type JsonObject } from "@bufbuild/protobuf";
30
+ import { create, clone, type JsonObject } from "@bufbuild/protobuf";
31
31
  import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
32
32
  import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
33
+ import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
34
+ import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
33
35
  import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
34
- import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
35
- import { ExecutionControlSignal, ExecutionPhase, InteractionMode, MessageType, ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
36
+ import type { AgentExecution, AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
37
+ import { ExecutionControlSignal, ExecutionPhase, FileChangeSetStatus, InteractionMode, MessageType, ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
36
38
  import type { SDKMessage, Run, ConversationTurn } from "@cursor/sdk";
37
39
 
38
40
  import type { Config } from "../../config.js";
@@ -41,8 +43,9 @@ import { resolveAgent } from "./session-lifecycle.js";
41
43
  import type { AgentResolution, CreateAgentOptions, CreateCloudAgentOptions } from "./session-lifecycle.js";
42
44
  import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
43
45
  import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
44
- import { MessageAccumulator, reconcileDeniedToolCalls, cancelInProgressSubAgentProtos } from "./message-translator.js";
46
+ import { MessageAccumulator, reconcileDeniedToolCalls, clearProvisionalPostDenialNarration, cancelInProgressSubAgentProtos, collapseRedundantToolCallTwins } from "./message-translator.js";
45
47
  import { utcTimestamp, persistStatus, reportSetupProgress, slimStatus } from "../../shared/status.js";
48
+ import { collectSubAgentToolCallIds } from "../../shared/tool-row.js";
46
49
  import { startStallWatchdog, StallTimeoutError, formatStallFailure, type StallWatchdog } from "../../shared/stall-watchdog.js";
47
50
  import { createArtifactStorage, loadArtifactStorageConfig, type ArtifactStorage } from "../../shared/artifact-storage.js";
48
51
  import { publishPlanArtifact } from "../../shared/plan-artifact.js";
@@ -53,7 +56,7 @@ import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/stre
53
56
  import { createCursorEventRecorder } from "./cursor-event-recorder.js";
54
57
  import { resolveMcpServers, validateMcpServerEnv } from "./mcp-resolver.js";
55
58
  import { mergeApprovalPolicies } from "./approval-policy.js";
56
- import { hasApproveAllDecision } from "../../shared/approval-policy.js";
59
+ import { deriveActiveLeases } from "../../shared/approval-policy.js";
57
60
  import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
58
61
  import { resolveExecutionEnv } from "./env-resolver.js";
59
62
  import { resolveBlueprint } from "./blueprint-resolver.js";
@@ -63,7 +66,19 @@ import { resolveAttachments } from "./attachment-resolver.js";
63
66
  import { buildEnhancedPrompt, buildReinvocationPrompt } from "./prompt-builder.js";
64
67
  import { installHitlGate, removeHitlGate } from "./workspace-setup.js";
65
68
  import { ensureHitlDir } from "../../shared/workspace/platform-dir.js";
66
- import { buildApprovalState, buildApprovalGrants, readDenialLedger, reconstructAdjudicatedApprovals } from "./approval-state.js";
69
+ import { LocalWorkspaceBackend } from "../../shared/workspace/local-backend.js";
70
+ import { buildApprovalState, buildApprovalGrants, emitCursorGrantReceipts, primaryToken, readDenialLedger, reconstructAdjudicatedApprovals, watchDenialLedger } from "./approval-state.js";
71
+ import { deriveTurnCommandProvenance } from "./command-provenance.js";
72
+ import { applyApprovedWholeFileWrites, excludeAppliedFromGrants } from "./exact-apply.js";
73
+ import { isGitWorkTree } from "../../shared/filereview/git-substrate.js";
74
+ import {
75
+ captureBaselineToLedger,
76
+ captureTurnToLedger,
77
+ applyCaptureDecisions,
78
+ deriveCaptureMode,
79
+ } from "./capture-flow.js";
80
+ import { deriveExecutionFingerprintKey } from "../../shared/approval-fingerprint.js";
81
+ import { getRunnerHitlMasterSecret } from "../../shared/fingerprint-secret.js";
67
82
  import { provisionCursorWorkspace } from "./workspace-provision.js";
68
83
  import { setInterceptorExecutionId, runWithExecutionContext } from "./fetch-interceptor.js";
69
84
  import { closeProxySessions } from "./http2-interceptor.js";
@@ -71,6 +86,7 @@ import { resolveModelId, ensureLoaded as ensurePricingLoaded } from "./model-pri
71
86
  import { UsageAccumulator } from "./usage-accumulator.js";
72
87
  import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
73
88
  import { activityStarted, activityFinished } from "../../idle-watchdog.js";
89
+ import { normalizeActivityInput, type ExecuteActivityInput } from "../../shared/activity-input.js";
74
90
  import { getCapturedRejection, clearCapturedRejection } from "./rejection-capture.js";
75
91
  import { synthesizeError, formatClassifiedError, shouldRetryWithFreshAgent } from "./error-classifier.js";
76
92
  import type { ClassifiedError } from "./error-classifier.js";
@@ -79,6 +95,12 @@ import { setMaxListeners } from "node:events";
79
95
  import { startHeartbeat } from "../../shared/heartbeat.js";
80
96
  import { getShutdownSignalForQueue } from "../../runner-manager.js";
81
97
 
98
+ // How long Phase 12 waits for the first-denial-stop's run.cancel() to settle
99
+ // before reading the final denial ledger and capturing the turn's tree. Long
100
+ // enough for the SDK's normal teardown, short enough that a wedged cancel
101
+ // cannot noticeably delay the approval pause the user is already waiting on.
102
+ const FIRST_DENIAL_CANCEL_TIMEOUT_MS = 5_000;
103
+
82
104
  /**
83
105
  * Creates the activity functions bound to the runner config.
84
106
  * Returned object is passed to Temporal Worker.create({ activities }).
@@ -91,10 +113,18 @@ export function createCursorActivities(config: Config) {
91
113
  });
92
114
 
93
115
  return {
94
- ExecuteCursor: async (executionId: string, threadId: string): Promise<unknown> => {
116
+ // Accepts the new typed object OR the legacy positional args (transitional
117
+ // dual-shape so the runner can deploy before the control planes — see
118
+ // shared/activity-input.ts). Drop the positional arm once both control
119
+ // planes send the object.
120
+ ExecuteCursor: async (
121
+ arg0: ExecuteActivityInput | string,
122
+ arg1?: string,
123
+ ): Promise<unknown> => {
124
+ const { executionId, threadId, turnSeq } = normalizeActivityInput(arg0, arg1);
95
125
  activityStarted();
96
126
  try {
97
- return await executeCursor(config, client, executionId, threadId);
127
+ return await executeCursor(config, client, executionId, threadId, turnSeq);
98
128
  } finally {
99
129
  activityFinished();
100
130
  }
@@ -107,15 +137,16 @@ async function executeCursor(
107
137
  client: StigmerClient,
108
138
  executionId: string,
109
139
  threadId: string,
140
+ turnSeq: number,
110
141
  ): Promise<unknown> {
111
- console.log(`ExecuteCursor started: execution=${executionId}, threadId=${threadId || "(new)"}`);
142
+ console.log(`ExecuteCursor started: execution=${executionId}, threadId=${threadId || "(new)"}, turnSeq=${turnSeq}`);
112
143
 
113
144
  // Ensure fresh HTTP/2 transport — prevents a degraded session from a
114
145
  // prior workflow task from poisoning this execution's agent stream.
115
146
  closeProxySessions();
116
147
 
117
148
  setInterceptorExecutionId(executionId);
118
- return runWithExecutionContext(executionId, () => executeCursorInner(config, client, executionId, threadId));
149
+ return runWithExecutionContext(executionId, () => executeCursorInner(config, client, executionId, threadId, turnSeq));
119
150
  }
120
151
 
121
152
  async function executeCursorInner(
@@ -123,6 +154,10 @@ async function executeCursorInner(
123
154
  client: StigmerClient,
124
155
  executionId: string,
125
156
  threadId: string,
157
+ // turnSeq is the monotonic HITL-cycle index (0 on the first turn). The
158
+ // file-review producer consumes it to mint the deterministic change-set id
159
+ // (executionId:turnSeq) in the capture phase.
160
+ turnSeq: number,
126
161
  ): Promise<unknown> {
127
162
 
128
163
  const status = create(AgentExecutionStatusSchema, {
@@ -163,6 +198,29 @@ async function executeCursorInner(
163
198
  // message surfaced to the user; both feed the Phase 11a stall branch.
164
199
  let stallDetected = false;
165
200
  let stallError: StallTimeoutError | undefined;
201
+ // Set the moment the preToolUse hook records its first denial in the ledger.
202
+ // We then stop consuming the stream and cancel the run so the model never
203
+ // reacts to Cursor's tool-failure surface (narrate defeat, attempt a second
204
+ // gated tool) — converging the Cursor harness toward the native harness, which
205
+ // pauses BEFORE the model sees a denial. Phase 12 reconciles the denied tool
206
+ // calls into WAITING_FOR_APPROVAL exactly as after a natural stream end.
207
+ let firstDenialDetected = false;
208
+ // Flipped by the denial-ledger fs watcher the instant the hook writes a
209
+ // denial, so the NEXT stream event of ANY type triggers the ledger read —
210
+ // instead of waiting for the next tool_call event, during which the model's
211
+ // full post-denial reaction (thinking, narration, a workaround tool) would
212
+ // stream and persist (observed in production: aex_01kwj07f7g23c3wp9sn8496z5g).
213
+ // The tool_call-event read below remains the backstop where fs.watch is
214
+ // unreliable.
215
+ let denialLedgerDirty = false;
216
+ let stopDenialWatcher: (() => void) | undefined;
217
+ // The in-flight run.cancel() started by the first-denial stop. Awaited
218
+ // (timeboxed) before Phase 12 so the agent process has actually stopped
219
+ // before the final ledger read and the turn-boundary tree capture — closing
220
+ // the race where a post-denial workaround's ledger entry lands after the
221
+ // read (it would then never be collapsed) or a late tool mutates the tree
222
+ // mid-capture.
223
+ let denialCancelSettled: Promise<void> | undefined;
166
224
  let periodicHeartbeat: ReturnType<typeof startHeartbeat> | undefined;
167
225
  // Progress-based stall watchdog (see ../../shared/stall-watchdog.ts). Stopped
168
226
  // in the finally on every exit path; complements the liveness heartbeat.
@@ -209,6 +267,33 @@ async function executeCursorInner(
209
267
  );
210
268
  heartbeat();
211
269
 
270
+ // Apply-then-review is the universal file-review model (Slice 2c). When the
271
+ // primary workspace is a real git work tree, file edits flow during the turn
272
+ // and are captured per-file from the git diff at the turn boundary
273
+ // (capture-flow.ts / shared/filereview/git-substrate.ts). A NON-git workspace
274
+ // has no git snapshot, so it captures every file write via the path-scoped CAS
275
+ // substrate instead — which requires artifact storage to persist blobs; when
276
+ // storage is unavailable a non-git workspace falls back to the classic
277
+ // deny-gate (no regression). `gitWorkspace` selects the substrate; both flow
278
+ // file edits and review post-hoc, and the deny-gate then survives only for
279
+ // shell/MCP/irreversible tools. Detected once from the provisioned primary root.
280
+ const primaryWorkspaceDir = blueprint.workspaceDirs[0];
281
+ const gitWorkspace = primaryWorkspaceDir
282
+ ? await isGitWorkTree(primaryWorkspaceDir)
283
+ : false;
284
+ const captureMode = deriveCaptureMode(primaryWorkspaceDir, gitWorkspace, !!artifactStorage);
285
+ // Pre-turn baseline tree, pinned before the agent runs (capture mode only)
286
+ // so the turn-end capture diffs against it and the tree restores exactly.
287
+ let baselineTree: string | undefined;
288
+ // Deterministic id of the change set this turn may produce:
289
+ // `${executionId}:${turnSeq}`. Minted from the workflow-threaded turn index
290
+ // so it is stable across a Temporal retry (idempotent ledger authoring) and
291
+ // unique per turn. The resume reconcile reads the change set id back from the
292
+ // DECIDED projection, not from turnSeq — so a "wasted" id on a pure-reconcile
293
+ // resume (which never authors a baseline) is harmless.
294
+ const changeSetId = `${executionId}:${turnSeq}`;
295
+ heartbeat();
296
+
212
297
  // Set OTel baggage so downstream calls carry execution context.
213
298
  try {
214
299
  const { setBaggage, BAGGAGE_EXECUTION_ID, BAGGAGE_SESSION_ID, BAGGAGE_ORG_ID } = await import("../../otel.js");
@@ -242,14 +327,76 @@ async function executeCursorInner(
242
327
  // by reinvocation time — the decision survives only on the tool call. This
243
328
  // feeds both the grant builder and the reinvocation prompt below.
244
329
  let adjudicatedApprovals: PendingApproval[] = [];
330
+ // tool-call id -> content digest of the approved edit, threaded into the
331
+ // grant builder so an approved edit is authorized by its exact content (a
332
+ // sibling edit to the same file re-gates). Sourced from the persisted
333
+ // approval_content_digest field (see reconstructAdjudicatedApprovals).
334
+ let adjudicatedContentDigests: Map<string, string> = new Map();
335
+ // Sub-agent executions carried over from the persisted transcript on a
336
+ // resume, handed to the MessageAccumulator so a gated tool inside a
337
+ // delegated sub-agent survives the round-trip (see seeding below).
338
+ let seededSubAgents: SubAgentExecution[] = [];
245
339
 
246
340
  if (isReinvocation) {
247
341
  const existingStatus = execution.status;
342
+ // Seed the in-progress status from the persisted execution BEFORE the
343
+ // MessageAccumulator wraps status.messages, so this resumed turn APPENDS
344
+ // onto prior history rather than rebuilding from empty. A Cursor resume
345
+ // re-issues approved tool calls with fresh ids; a from-empty rebuild would
346
+ // drop the previously-committed ids and the backend's append-only-at-
347
+ // identity guard would reject the whole update, stalling the run (the
348
+ // "approval propagation is broken" watchdog failure). The resumed re-runs
349
+ // are reconciled onto these seeded calls by canonical identity inside the
350
+ // accumulator. Mirrors the deep-agent seedStatusFromExecution.
351
+ seededSubAgents = seedCursorTranscriptFromExecution(status, execution);
352
+
353
+ // File-review reconcile (the dual-source half): reconcile every change set
354
+ // the server projected as DECIDED, sourced from the ledger decisions and
355
+ // the pinned git refs (approved kept at their "after" bytes, rejected
356
+ // snapped back to baseline — all uncommitted, hash-verified). This is
357
+ // independent of tool approvals: a single turn can carry BOTH a DECIDED
358
+ // file change set AND an approved shell/MCP action.
359
+ let reconciledFileReview = false;
360
+ let fileReviewFailed = false;
361
+ let fileReviewFailureDetail = "";
362
+ const discardedPaths: string[] = [];
363
+ if (captureMode && primaryWorkspaceDir) {
364
+ const decidedSets = (existingStatus?.fileChangeSets ?? []).filter(
365
+ (cs) => cs.status === FileChangeSetStatus.DECIDED,
366
+ );
367
+ for (const changeSet of decidedSets) {
368
+ const capResult = await applyCaptureDecisions({
369
+ status,
370
+ gitRoot: primaryWorkspaceDir,
371
+ executionId,
372
+ changeSet,
373
+ // Thread the CAS store so CAS-captured files in the change set
374
+ // reconcile from the durable manifest (approved after-blobs written,
375
+ // rejected snapped back). In a non-git workspace this is the ONLY
376
+ // reconcile; in a git tree it composes with the git-ref reconcile.
377
+ storage: artifactStorage,
378
+ gitWorkspace,
379
+ });
380
+ if (!capResult.isCaptureTurn) continue;
381
+ reconciledFileReview = true;
382
+ if (capResult.failed) {
383
+ fileReviewFailed = true;
384
+ fileReviewFailureDetail = capResult.failureDetail ?? "file review reconcile failed";
385
+ }
386
+ if (capResult.hadReject) discardedPaths.push(...capResult.rejectedPaths);
387
+ }
388
+ }
389
+
390
+ // Tool approvals (shell / MCP / gitignored writes) still resolve from the
391
+ // message transcript — the deny-gate path, unchanged by the file-review
392
+ // cutover.
248
393
  const adjudicated = reconstructAdjudicatedApprovals(existingStatus?.messages ?? []);
249
394
  if (adjudicated.decisions.size > 0) {
250
395
  approvalDecisions = adjudicated.decisions;
251
396
  adjudicatedApprovals = adjudicated.pendingApprovals;
397
+ adjudicatedContentDigests = adjudicated.contentDigests;
252
398
 
399
+ // A reject of an irreversible action (shell/MCP) fails the execution.
253
400
  const hasReject = [...approvalDecisions.values()].some(
254
401
  (a) => a === ApprovalAction.REJECT,
255
402
  );
@@ -265,6 +412,47 @@ async function executeCursorInner(
265
412
  await persist(status);
266
413
  return slimStatus(status);
267
414
  }
415
+ // else: fall through to run the approved shell/MCP. The agent may produce
416
+ // further edits, captured as a new change set in the next cycle.
417
+ } else if (reconciledFileReview) {
418
+ // Pure file review: the agent already finished its full turn during
419
+ // capture, so keeping/discarding a change does NOT re-prompt it
420
+ // (Cursor-like). The reconcile is done; the execution is complete.
421
+ status.phase = ExecutionPhase.EXECUTION_COMPLETED;
422
+ status.completedAt = utcTimestamp();
423
+ if (fileReviewFailed) {
424
+ // What-you-approve-is-what-applies could not be honored (on-disk bytes
425
+ // diverged from the approved digest). Surface it to the human; the
426
+ // FileReviewFailure(HASH_MISMATCH) event is the audit record.
427
+ status.messages.push(create(AgentMessageSchema, {
428
+ type: MessageType.MESSAGE_SYSTEM,
429
+ content:
430
+ "Some approved file changes could not be applied because the file " +
431
+ "changed after review: " + fileReviewFailureDetail + ".",
432
+ timestamp: utcTimestamp(),
433
+ }));
434
+ } else if (discardedPaths.length > 0) {
435
+ // A reject is a DISCARD that COMPLETES (not FAILED) — surface a SYSTEM
436
+ // note listing the reverted files. This note is for the human; it does
437
+ // NOT re-sync the Cursor SDK agent (its native context still believes
438
+ // those edits stuck). The agent self-corrects by re-reading, and any
439
+ // edit it makes from that stale belief is itself re-surfaced as a new
440
+ // change set next turn (the structural safety net). See
441
+ // design-decisions/capture-reject-next-turn-resync-not-built.md.
442
+ status.messages.push(create(AgentMessageSchema, {
443
+ type: MessageType.MESSAGE_SYSTEM,
444
+ content:
445
+ "Some proposed file changes were discarded by the user and were not applied: " +
446
+ discardedPaths.join(", ") + ".",
447
+ timestamp: utcTimestamp(),
448
+ }));
449
+ }
450
+ await persist(status);
451
+ console.log(
452
+ `ExecuteCursor file-review resume short-circuit: execution=${executionId}, ` +
453
+ `failed=${fileReviewFailed}, discarded=${discardedPaths.length}`,
454
+ );
455
+ return slimStatus(status);
268
456
  }
269
457
  }
270
458
 
@@ -284,20 +472,20 @@ async function executeCursorInner(
284
472
 
285
473
  // Phase 4b: Merge approval policies from all layers.
286
474
  //
287
- // Effective auto-approve is true when EITHER the execution was pre-armed via
288
- // spec.auto_approve_all OR a user chose APPROVE_ALL ("approve and don't ask
289
- // again") at some gate earlier in this run. The shared hasApproveAllDecision
290
- // helper (used by the native harness too) keeps this contract defined once.
291
- // When true, the merged policy map is empty and the hook state file disables
292
- // gating for the rest of the run.
293
- const effectiveAutoApproveAll =
294
- (execution.spec?.autoApproveAll ?? false) || hasApproveAllDecision(execution);
475
+ // Two bypasses (see ActiveLeases, shared with the native harness): the
476
+ // pre-armed spec.auto_approve_all is the one whole-run global bypass; an
477
+ // interactive APPROVE_ALL grants a run-lifetime lease scoped to that action's
478
+ // class. deriveActiveLeases keeps this contract defined once. Server-scoped
479
+ // leases drop that server's tools from the merged map (so the hook treats
480
+ // them as auto-approved); the global bypass empties the map entirely.
481
+ const leases = deriveActiveLeases(execution);
482
+ const globalBypass = leases.global;
295
483
  const agentOverrides = blueprint.mergedMcpServerUsages
296
484
  .flatMap((u) => u.toolApprovalOverrides ?? []);
297
485
  const mergedPolicies = mergeApprovalPolicies(
298
486
  mcpResolution.resolvedServers,
299
487
  agentOverrides,
300
- effectiveAutoApproveAll,
488
+ leases,
301
489
  );
302
490
  heartbeat();
303
491
 
@@ -316,7 +504,7 @@ async function executeCursorInner(
316
504
 
317
505
  // Phase 5: Resolve skills (merged from agent + session)
318
506
  await reportSetupProgress(client, executionId, "Resolving skills");
319
- const primaryWorkspaceDir = blueprint.workspaceDirs[0];
507
+ // (primaryWorkspaceDir / captureMode were resolved right after provisioning.)
320
508
  const skillMetadata = await resolveSkills(client, blueprint.mergedSkillRefs, {
321
509
  sessionId,
322
510
  primaryWorkspaceDir,
@@ -332,6 +520,35 @@ async function executeCursorInner(
332
520
  );
333
521
  const attachmentPaths = attachmentResults.map((a) => a.relativePath);
334
522
 
523
+ // Phase 5b3: Exact-apply approved whole-file writes (HITL "what you approve
524
+ // is what gets applied"). The Cursor deny-only harness reinvokes the model,
525
+ // which regenerates content, so a resource grant alone cannot guarantee the
526
+ // bytes that land match the bytes the user approved. The runner therefore
527
+ // writes the EXACT approved whole-file content itself, marks those tool calls
528
+ // COMPLETED, and (below) issues NO grant for them — so any FURTHER change the
529
+ // model makes to those files is re-gated. Hunk edits / shell / MCP stay on
530
+ // the grant + reinvocation path. Every uncertain case degrades to that path,
531
+ // so this can never corrupt a file (see exact-apply.ts).
532
+ let appliedToolCallIds: ReadonlySet<string> = new Set();
533
+ // Exact-apply is the deny-gate path's "what you approve is what gets applied"
534
+ // mechanism (the model regenerates content on reinvocation). Capture mode
535
+ // does not reinvoke the model for file edits — it applies the exact captured
536
+ // bytes itself in applyCaptureDecisions — so exact-apply is scoped OUT of it.
537
+ if (!captureMode && isReinvocation && approvalDecisions) {
538
+ appliedToolCallIds = await applyApprovedWholeFileWrites({
539
+ messages: status.messages,
540
+ workspaceBackend: new LocalWorkspaceBackend(primaryWorkspaceDir),
541
+ workspaceDirs: blueprint.workspaceDirs,
542
+ executionId,
543
+ });
544
+ if (appliedToolCallIds.size > 0) {
545
+ // Persist the applied writes (tool calls now COMPLETED with the approved
546
+ // diff) before reinvocation, so the applied state is durable even if the
547
+ // continuation fails, and the UI reflects it immediately.
548
+ await persist(status);
549
+ }
550
+ }
551
+
335
552
  // Phase 5c: Install the HITL approval gate BEFORE resolving the agent.
336
553
  //
337
554
  // The gate's runtime artifacts (hook script, approval-state file, denial
@@ -345,15 +562,56 @@ async function executeCursorInner(
345
562
  //
346
563
  // On reinvocation, turn the user's approvals into tool-identity grants so
347
564
  // the resumed agent's re-attempt (which carries a fresh tool-call id) is
348
- // allowed through.
565
+ // allowed through. Exact-applied writes are EXCLUDED from the grants: with no
566
+ // grant, a further write to that file is re-gated (the user sees every change).
567
+ // Capture mode: pin the pre-turn baseline tree before the agent runs (and
568
+ // before the gate is installed, though the gate files are excluded from the
569
+ // capture anyway). The turn-end capture diffs the post-turn tree against this
570
+ // to build the per-file cards; the baseline ref is also what a reject reverts
571
+ // to on resume. Covers a fresh turn and the approved-irreversible resume
572
+ // fall-through (the agent will run and may make further edits).
573
+ if (captureMode && primaryWorkspaceDir) {
574
+ // Pin the pre-turn tree AND author BASELINE_CAPTURED so the projection can
575
+ // materialize the change set (status CAPTURING) before any candidate exists.
576
+ // The event rides the next persist; CAPTURING does not arm the unified gate.
577
+ baselineTree = await captureBaselineToLedger({
578
+ status,
579
+ gitRoot: primaryWorkspaceDir,
580
+ executionId,
581
+ changeSetId,
582
+ gitWorkspace,
583
+ });
584
+ }
585
+
349
586
  hitlDir = await ensureHitlDir(sessionId);
587
+ const grantApprovals = excludeAppliedFromGrants(adjudicatedApprovals, appliedToolCallIds);
350
588
  const approvalGrants = approvalDecisions
351
- ? buildApprovalGrants(adjudicatedApprovals, approvalDecisions)
589
+ ? buildApprovalGrants(grantApprovals, approvalDecisions, adjudicatedContentDigests)
352
590
  : undefined;
591
+ if (approvalGrants && approvalGrants.length > 0 && !globalBypass) {
592
+ emitCursorGrantReceipts(
593
+ approvalGrants,
594
+ deriveExecutionFingerprintKey(getRunnerHitlMasterSecret(), executionId),
595
+ executionId,
596
+ );
597
+ }
598
+ // CAS capture requires artifact storage to persist blobs
599
+ // (captureCandidateToLedger throws without it). In a git tree, captureMode
600
+ // alone governs tracked-file capture (no storage needed) and captureIgnored is
601
+ // the narrower switch (git tree + storage) that also captures gitignored
602
+ // writes. In a non-git workspace ALL capture is CAS, so captureMode already
603
+ // required storage — captureIgnored then equals captureMode. When storage is
604
+ // absent a git tree keeps gating gitignored writes and a non-git workspace
605
+ // falls back to the deny-gate entirely (no regression).
606
+ const captureIgnored = captureMode && !!artifactStorage;
353
607
  const approvalState = buildApprovalState(
354
608
  mergedPolicies,
355
- effectiveAutoApproveAll,
609
+ globalBypass,
610
+ leases.categories,
356
611
  approvalGrants,
612
+ captureMode,
613
+ captureIgnored,
614
+ gitWorkspace,
357
615
  );
358
616
  const hitlGate = await installHitlGate({
359
617
  workspaceRoot: primaryWorkspaceDir,
@@ -365,6 +623,12 @@ async function executeCursorInner(
365
623
  await removeHitlGate(hitlGate);
366
624
  await removeStigmerSymlink(primaryWorkspaceDir);
367
625
  };
626
+ // Arm the denial watcher as soon as the gate exists. The per-turn ledger
627
+ // reset may flip the flag once before the run starts; the loop's read then
628
+ // sees an empty ledger and clears it — harmless by construction.
629
+ stopDenialWatcher = watchDenialLedger(hitlDir, () => {
630
+ denialLedgerDirty = true;
631
+ });
368
632
 
369
633
  // Phase 5d: Ensure model pricing registry is populated before validation
370
634
  await ensurePricingLoaded();
@@ -489,6 +753,7 @@ async function executeCursorInner(
489
753
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
490
754
  attachmentPaths,
491
755
  pendingApprovals: adjudicatedApprovals,
756
+ appliedToolCallIds,
492
757
  interactionMode,
493
758
  });
494
759
 
@@ -618,7 +883,17 @@ async function executeCursorInner(
618
883
  }
619
884
  });
620
885
 
621
- const accumulator = new MessageAccumulator(status.messages, { mergedPolicies });
886
+ // Everything at an index >= this was produced by THIS turn's stream — the
887
+ // positional turn boundary the approved-command provenance (DD-28) scopes
888
+ // its qualification to. Snapshotted before the accumulator can append.
889
+ const turnStartMessageIndex = status.messages.length;
890
+
891
+ const accumulator = new MessageAccumulator(status.messages, {
892
+ mergedPolicies,
893
+ provenance: { globalBypass, leasedCategories: leases.categories },
894
+ workspaceRoot: primaryWorkspaceDir,
895
+ seededSubAgents,
896
+ });
622
897
  // Shared cadence with the native harness: discrete state changes force a
623
898
  // flush; high-frequency token deltas ride this scheduler's time cadence
624
899
  // (env-tunable via STREAMING_* — see loadStreamingConfig).
@@ -650,6 +925,53 @@ async function executeCursorInner(
650
925
  );
651
926
  }
652
927
 
928
+ // First-denial stop (HITL clean pause). In CAPTURE mode this fires only for
929
+ // an IRREVERSIBLE tool the hook still gates (shell, MCP, or a gitignored
930
+ // write/delete) — file edits flow freely and are captured at the turn
931
+ // boundary, so they never enter the ledger. In the deny-gate FALLBACK
932
+ // (non-git workspace) it fires for every gated file edit too. Either way:
933
+ // the preToolUse hook appends to the denial ledger the instant it gates a
934
+ // tool — before Cursor surfaces the failure to the model — and the fs
935
+ // watcher flips denialLedgerDirty the moment that write lands. Confirming
936
+ // the flag with a read on the very next event (of ANY type — thinking
937
+ // deltas arrive within milliseconds) ends the turn before the model's
938
+ // reaction can persist: waiting for the next tool_call event let the full
939
+ // post-denial reaction (thinking, narration, a workaround shell) stream
940
+ // and persist live (production case aex_01kwj07f7g23c3wp9sn8496z5g). The
941
+ // tool_call-event read stays as the backstop for platforms where fs.watch
942
+ // is unreliable; the current event was already accumulated above, so the
943
+ // anchor's own row is always present for the Phase 12 gate overlay. This
944
+ // mirrors the native harness's pause-before-react semantics; Phase 12
945
+ // reconciles the denied calls and its trim remains the last-resort
946
+ // backstop for anything that persisted before the stop.
947
+ if (!firstDenialDetected && hitlDir && (denialLedgerDirty || event.type === "tool_call")) {
948
+ denialLedgerDirty = false;
949
+ const denials = await readDenialLedger(hitlDir);
950
+ if (denials.length > 0) {
951
+ firstDenialDetected = true;
952
+ console.log(
953
+ `ExecuteCursor first denial detected (${denials.length} ledger ` +
954
+ `entr${denials.length === 1 ? "y" : "ies"}); stopping turn to pause ` +
955
+ `cleanly for approval: execution=${executionId}`,
956
+ );
957
+ if (run.supports?.("cancel")) {
958
+ // Kept (not fire-and-forget): awaited timeboxed before Phase 12 so
959
+ // the ledger read and tree capture see a stopped agent.
960
+ denialCancelSettled = run.cancel().then(
961
+ () => {},
962
+ (cancelErr: unknown) => {
963
+ console.warn(
964
+ `ExecuteCursor run.cancel() after first denial failed (non-fatal): ` +
965
+ `execution=${executionId}, ` +
966
+ `error=${cancelErr instanceof Error ? cancelErr.message : cancelErr}`,
967
+ );
968
+ },
969
+ );
970
+ }
971
+ break;
972
+ }
973
+ }
974
+
653
975
  deltaEnricher.applyEnrichments(status.messages);
654
976
  eventCount++;
655
977
 
@@ -703,12 +1025,16 @@ async function executeCursorInner(
703
1025
  }
704
1026
  }
705
1027
  } catch (streamErr) {
706
- // run.cancel() from the stall watchdog can make the stream iterator
707
- // reject; that is the expected teardown, so swallow it and fall through
708
- // to the stallDetected branch in Phase 11a. Anything else is a genuine
1028
+ // run.cancel() from the stall watchdog or the first-denial stop — can
1029
+ // make the stream iterator reject as it tears down; that is the expected
1030
+ // teardown for both, so swallow it and fall through (stallDetected ->
1031
+ // Phase 11a; firstDenialDetected -> Phase 12). Anything else is a genuine
709
1032
  // stream failure — rethrow it to the outer error handler.
710
- if (!stallDetected) throw streamErr;
711
- console.warn(`ExecuteCursor stream ended via stall cancel: execution=${executionId}`);
1033
+ if (!stallDetected && !firstDenialDetected) throw streamErr;
1034
+ console.warn(
1035
+ `ExecuteCursor stream ended via cancel: execution=${executionId}, ` +
1036
+ `stall=${stallDetected}, firstDenial=${firstDenialDetected}`,
1037
+ );
712
1038
  }
713
1039
 
714
1040
  periodicHeartbeat.stop();
@@ -864,12 +1190,146 @@ async function executeCursorInner(
864
1190
  // harness — the approval surface is driven entirely by tool-call status. We
865
1191
  // deliberately do NOT set status.pendingApprovals here: any value would be
866
1192
  // discarded by the backend's recompute on the next updateStatus.
1193
+ //
1194
+ // Before reading the ledger, wait (timeboxed) for the first-denial-stop's
1195
+ // run.cancel() to settle. run.cancel() races the SDK's auto-execution: until
1196
+ // it lands, the agent process may still attempt a post-denial workaround
1197
+ // whose hook denial would land AFTER a premature ledger read — the row then
1198
+ // never collapses and renders as RUNNING forever (production case
1199
+ // aex_01kwj07f7g23c3wp9sn8496z5g) — or a late tool could mutate the tree
1200
+ // mid-capture. The timebox keeps a wedged cancel from hanging the pause;
1201
+ // the Phase 12 trims below remain the backstop for that degraded case.
1202
+ if (firstDenialDetected && denialCancelSettled) {
1203
+ await Promise.race([
1204
+ denialCancelSettled,
1205
+ new Promise<void>((resolve) => {
1206
+ const timer = setTimeout(resolve, FIRST_DENIAL_CANCEL_TIMEOUT_MS);
1207
+ timer.unref();
1208
+ }),
1209
+ ]);
1210
+ }
867
1211
  const deniedLedger = await readDenialLedger(hitlDir ?? "");
868
- const deniedToolCalls = reconcileDeniedToolCalls(status.messages, deniedLedger, mergedPolicies);
869
- if (deniedToolCalls.length > 0) {
1212
+
1213
+ // Capture mode: author the net change set to the file_review ledger as the
1214
+ // CANDIDATE_CAPTURED event (projected server-side to a file_change_set
1215
+ // AWAITING_REVIEW — the single review surface). The runner-owned gate files
1216
+ // are excluded from the capture. The agent's edits are LEFT applied on the
1217
+ // working tree (Cursor parity — the user reviews the real change; nothing is
1218
+ // committed and the next turn is blocked until approval, and a reject snaps
1219
+ // each file back on resume). Runs BEFORE the denial reconcile so a denied
1220
+ // (gitignored) write stays on the deny-gate path while every flowed edit is
1221
+ // captured to the ledger.
1222
+ let capturedChangeCount = 0;
1223
+ // `baselineTree !== undefined` means a baseline was authored this turn — the
1224
+ // git tree sha for a git workspace, or "" (empty, but authored) for a non-git
1225
+ // one. A plain truthiness check would wrongly skip the non-git capture.
1226
+ if (captureMode && baselineTree !== undefined && primaryWorkspaceDir) {
1227
+ const deniedTokens = new Set(deniedLedger.map((e) => e.token));
1228
+ // Approved-command turn facts (DD-28): when every mutation-capable call
1229
+ // this turn was a consented shell command, attach the provenance so the
1230
+ // backend can verify the cited consent rows and auto-keep the set instead
1231
+ // of arming a second gate. Fail-closed: any non-qualifying turn attaches
1232
+ // nothing and reviews manually exactly as before.
1233
+ const commandProvenance = deriveTurnCommandProvenance({
1234
+ messages: status.messages,
1235
+ turnStartIndex: turnStartMessageIndex,
1236
+ deniedTokens,
1237
+ grantTokenToConsentId: new Map(
1238
+ (approvalGrants ?? []).map((g) => [
1239
+ primaryToken(g.key, g.salient, g.contentDigest),
1240
+ g.sourceToolCallId,
1241
+ ]),
1242
+ ),
1243
+ globalBypass,
1244
+ });
1245
+ if (commandProvenance) {
1246
+ console.log(
1247
+ `ExecuteCursor capture: turn qualifies for approved-command auto-keep ` +
1248
+ `(consent rows: ${commandProvenance.consentToolCallIds.join(",") || "(auto_approve_all)"}); ` +
1249
+ `attaching provenance to candidate (execution=${executionId})`,
1250
+ );
1251
+ }
1252
+ const captured = await captureTurnToLedger({
1253
+ status,
1254
+ gitRoot: primaryWorkspaceDir,
1255
+ executionId,
1256
+ changeSetId,
1257
+ baselineTree,
1258
+ messages: status.messages,
1259
+ deniedTokens,
1260
+ commandProvenance,
1261
+ // Scope sub-agent row stamping to this turn: the seeded prior sub-agents
1262
+ // (cloned in on resume) are the "before this turn" rows to skip.
1263
+ priorSubAgentToolCallIds: collectSubAgentToolCallIds(seededSubAgents),
1264
+ // The CAS half: read the sidecar the hook staged this turn and compose it
1265
+ // into the change set. hitlDir + storage are present when captureIgnored
1266
+ // was on (a git tree's gitignored writes, or ALL writes in a non-git
1267
+ // workspace). In a git tree this composes with the git diff (HYBRID); in a
1268
+ // non-git workspace it IS the whole change set (CAS-only).
1269
+ hitlDir,
1270
+ storage: artifactStorage,
1271
+ gitWorkspace,
1272
+ });
1273
+ capturedChangeCount = captured.length;
1274
+ if (capturedChangeCount > 0) {
1275
+ console.log(
1276
+ `ExecuteCursor capture: ${capturedChangeCount} file change(s) authored to the ` +
1277
+ `file_review ledger (change_set=${changeSetId}), working tree left applied ` +
1278
+ `for review (execution=${executionId})`,
1279
+ );
1280
+ }
1281
+ }
1282
+
1283
+ // The gate reads each denied file's pre-edit `before` from the workspace the
1284
+ // runner is co-located with (local FS for OSS; the sandbox in cloud), so a
1285
+ // whole-file rewrite gate renders a true before/after diff. The tool was
1286
+ // DENIED, so disk still holds the old content. User files are never platform
1287
+ // paths, so no platformDir routing is needed here.
1288
+ const gateWorkspaceBackend = new LocalWorkspaceBackend(primaryWorkspaceDir);
1289
+ const deniedToolCalls = await reconcileDeniedToolCalls(
1290
+ status.messages,
1291
+ deniedLedger,
1292
+ mergedPolicies,
1293
+ gateWorkspaceBackend,
1294
+ );
1295
+ // Observability: a synthesized placeholder (id `approval:*`) means a denial
1296
+ // correlated to NO streamed tool call in either the exact or the normalized
1297
+ // pass. After the normalized-path fallback this should be ~0; a non-zero rate
1298
+ // is the early-warning signal of a NEW identity drift (the gate would then
1299
+ // show "No preview available" with no diff). Logged, not thrown — the
1300
+ // synthesized gate still safely surfaces the approval.
1301
+ const synthesizedGateCount = deniedToolCalls.filter((tc) =>
1302
+ tc.id.startsWith("approval:"),
1303
+ ).length;
1304
+ if (synthesizedGateCount > 0) {
1305
+ console.warn(
1306
+ `ExecuteCursor reconcile synthesized ${synthesizedGateCount} placeholder gate(s) ` +
1307
+ `with no correlated stream call (execution=${executionId}); ` +
1308
+ `possible hook/stream identity drift — gate(s) will lack a diff`,
1309
+ );
1310
+ }
1311
+ if (deniedToolCalls.length > 0 || capturedChangeCount > 0) {
1312
+ if (deniedToolCalls.length > 0) {
1313
+ // Deterministic clean-pause: a turn that pauses for approval must read as
1314
+ // the same shape the native harness produces — pre-tool text + the gated
1315
+ // tool calls — never the model's provisional reaction to Cursor's deny
1316
+ // (e.g. "blocked by a hook; enable it in your Cursor settings"). We blank
1317
+ // that reaction in place (keeping the message count, so the finalize stays
1318
+ // append-only) rather than removing it. See
1319
+ // clearProvisionalPostDenialNarration for the full rationale.
1320
+ const redactedNarration = clearProvisionalPostDenialNarration(status.messages, deniedToolCalls);
1321
+ if (redactedNarration.length > 0) {
1322
+ console.log(
1323
+ `ExecuteCursor redacted ${redactedNarration.length} provisional post-denial narration message(s) before pausing for approval`,
1324
+ );
1325
+ }
1326
+ }
870
1327
  status.phase = ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL;
871
1328
  await persist(status);
872
- console.log(`ExecuteCursor returning WAITING_FOR_APPROVAL: ${deniedToolCalls.length} tools pending`);
1329
+ console.log(
1330
+ `ExecuteCursor returning WAITING_FOR_APPROVAL: ${deniedToolCalls.length} gated tool(s), ` +
1331
+ `${capturedChangeCount} file card(s) pending`,
1332
+ );
873
1333
  return slimStatus(status);
874
1334
  }
875
1335
 
@@ -1216,6 +1676,22 @@ async function executeCursorInner(
1216
1676
  }
1217
1677
  }
1218
1678
 
1679
+ // Collapse any redundant same-identity tool-call twin born this turn before
1680
+ // the terminal persist. On a resume turn the gated tool is already granted, so
1681
+ // there is no denial ledger and reconcileDeniedToolCalls never runs — the
1682
+ // extra attempt the model emits beside the approved action (a stuck RUNNING
1683
+ // zombie, a denied-reported-as-success COMPLETED, or an all-no-change double)
1684
+ // would otherwise persist as a second "No preview available" card. The shared
1685
+ // routine keeps the diff/output carrier and blanks the rest to hidden SKIPPED
1686
+ // rows in place, preserving each committed id so the finalize stays append-only.
1687
+ const collapsedTwins = collapseRedundantToolCallTwins(status.messages);
1688
+ if (collapsedTwins > 0) {
1689
+ console.log(
1690
+ `ExecuteCursor collapsed ${collapsedTwins} redundant tool-call twin(s) at ` +
1691
+ `terminal finalize (kept in place as hidden SKIPPED rows): execution=${executionId}`,
1692
+ );
1693
+ }
1694
+
1219
1695
  // NOW persist — subscriber sees COMPLETED + structured_output atomically
1220
1696
  await persist(status);
1221
1697
 
@@ -1390,6 +1866,10 @@ async function executeCursorInner(
1390
1866
  // point so no orphaned timer survives the activity.
1391
1867
  stallWatchdog?.stop();
1392
1868
 
1869
+ // Close the denial-ledger watcher on EVERY exit path (idempotent) so no
1870
+ // orphaned fs.watch handle survives the activity.
1871
+ stopDenialWatcher?.();
1872
+
1393
1873
  // Tear down the HITL gate on EVERY exit path (success, error, approval
1394
1874
  // pause, cancellation) so attaching a real repo leaves the user's
1395
1875
  // .cursor/hooks.json and workspace untouched between turns (issue #173).
@@ -1408,6 +1888,45 @@ async function executeCursorInner(
1408
1888
  }
1409
1889
  }
1410
1890
 
1891
+ /**
1892
+ * Seed an in-progress status from the persisted execution on a durable resume
1893
+ * (HITL approval, pause/resume, or transient recovery) so the upcoming turn
1894
+ * APPENDS onto prior history instead of replacing it. This is the Cursor analog
1895
+ * of the deep-agent's seedStatusFromExecution (execute-deep-agent/index.ts).
1896
+ *
1897
+ * Why it is required: a resumed Cursor agent re-issues the previously gated tool
1898
+ * calls with brand-new call ids. Without seeding, the MessageAccumulator would
1899
+ * rebuild the transcript from empty and emit a status that drops the already-
1900
+ * committed tool-call ids. The backend's append-only-at-identity guard
1901
+ * (AgentExecutionUpdateStatusHandler / update_status.go) rejects any non-
1902
+ * terminal update that drops a committed tool-call id, so the resumed progress
1903
+ * would never persist — the run stalls in WAITING_FOR_APPROVAL with no pending
1904
+ * approvals and the workflow watchdog fails it. Seeding makes the resume status
1905
+ * a strict superset; the re-runs are then reconciled in place onto these seeded
1906
+ * calls by canonical identity inside the accumulator.
1907
+ *
1908
+ * The persisted protos are cloned so the input execution stays immutable, and
1909
+ * the seeded messages are pushed into status.messages (which the accumulator
1910
+ * wraps by reference) BEFORE the accumulator is constructed. Sub-agent
1911
+ * executions are returned rather than written to status.subAgentExecutions
1912
+ * directly, because the accumulator owns that array (it overwrites
1913
+ * status.subAgentExecutions with its own on every flush) — handing them to the
1914
+ * accumulator keeps the seeded sub-agent rows from being clobbered.
1915
+ *
1916
+ * @returns the cloned sub-agent executions to seed into the MessageAccumulator.
1917
+ */
1918
+ function seedCursorTranscriptFromExecution(
1919
+ status: AgentExecutionStatus,
1920
+ execution: AgentExecution,
1921
+ ): SubAgentExecution[] {
1922
+ const persisted = execution.status;
1923
+ if (!persisted || persisted.messages.length === 0) return [];
1924
+ for (const message of persisted.messages) {
1925
+ status.messages.push(clone(AgentMessageSchema, message));
1926
+ }
1927
+ return persisted.subAgentExecutions.map((sub) => clone(SubAgentExecutionSchema, sub));
1928
+ }
1929
+
1411
1930
  // ---------------------------------------------------------------------------
1412
1931
  // Structured Output Extraction (Cursor Harness Tier 2)
1413
1932
  // ---------------------------------------------------------------------------
@@ -1417,10 +1936,9 @@ async function executeCursorInner(
1417
1936
  * economy-tier LLM with withStructuredOutput (function-calling).
1418
1937
  * Guarantees schema-conformant JSON output via the API's tool-use mechanism.
1419
1938
  *
1420
- * Provider-aware: resolves the economy model via the registry, infers its
1421
- * provider (anthropic / openai), and constructs the correct LangChain client
1422
- * with the matching proxy endpoint. Follows the same pattern as
1423
- * call-llm.ts constructModel().
1939
+ * Construction (registry-id resolution, provider inference, proxy wiring) is
1940
+ * delegated to the shared buildChatModel so the economy model's registry id is
1941
+ * always resolved to a provider API id before the call.
1424
1942
  */
1425
1943
  async function extractStructuredOutput(
1426
1944
  agentResponse: string,
@@ -1428,39 +1946,18 @@ async function extractStructuredOutput(
1428
1946
  config: Config,
1429
1947
  primaryModel: string,
1430
1948
  ): Promise<unknown | null> {
1431
- const { ChatOpenAI } = await import("@langchain/openai");
1432
- const { ChatAnthropic } = await import("@langchain/anthropic");
1433
- const { inferProvider, resolveProxyBaseUrl, buildProxyHeaders } = await import("../../shared/llm-proxy.js");
1434
1949
  const { getEconomyModel } = await import("../../shared/model-registry.js");
1950
+ const { buildChatModel } = await import("../../shared/model-client.js");
1435
1951
 
1436
1952
  const extractionModel = await getEconomyModel(primaryModel);
1437
- const provider = inferProvider(extractionModel);
1438
-
1439
1953
  const proxyEndpoint = config.proxyEndpoint ?? config.stigmerBackendEndpoint;
1440
- const baseUrl = resolveProxyBaseUrl(proxyEndpoint, provider);
1441
- const headers = config.stigmerToken
1442
- ? buildProxyHeaders(config.stigmerToken, {})
1443
- : {};
1444
-
1445
- const apiKey = provider === "openai"
1446
- ? (config.stigmerToken ?? process.env.OPENAI_API_KEY ?? "proxy-managed")
1447
- : (config.stigmerToken ?? process.env.ANTHROPIC_API_KEY ?? "proxy-managed");
1448
-
1449
- const llm = provider === "openai"
1450
- ? new ChatOpenAI({
1451
- model: extractionModel,
1452
- apiKey,
1453
- temperature: 0,
1454
- maxTokens: 4096,
1455
- configuration: { baseURL: baseUrl, defaultHeaders: headers },
1456
- })
1457
- : new ChatAnthropic({
1458
- model: extractionModel,
1459
- apiKey,
1460
- temperature: 0,
1461
- maxTokens: 4096,
1462
- clientOptions: { baseURL: baseUrl, defaultHeaders: headers },
1463
- });
1954
+
1955
+ const { model: llm } = await buildChatModel({
1956
+ modelName: extractionModel,
1957
+ proxyEndpoint,
1958
+ stigmerToken: config.stigmerToken ?? undefined,
1959
+ maxTokens: 4096,
1960
+ });
1464
1961
 
1465
1962
  const zodSchema = jsonSchemaToZod(schema);
1466
1963
  const structured = llm.withStructuredOutput(zodSchema);
@@ -1492,6 +1989,12 @@ export interface BuildPromptInput {
1492
1989
  workspaceFileRefs: string[];
1493
1990
  attachmentPaths: string[];
1494
1991
  pendingApprovals: import("@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb").PendingApproval[];
1992
+ /**
1993
+ * Approved whole-file writes the runner already applied itself (exact-apply).
1994
+ * The reinvocation prompt marks these as done so the model does not redo them;
1995
+ * the remaining approved actions are the ones it must still carry out.
1996
+ */
1997
+ appliedToolCallIds?: ReadonlySet<string>;
1495
1998
  interactionMode?: InteractionMode;
1496
1999
  }
1497
2000
 
@@ -1528,9 +2031,14 @@ export function buildPrompt(input: BuildPromptInput): string {
1528
2031
  const isHitlReinvocation = approvalDecisions !== undefined && approvalDecisions.size > 0;
1529
2032
 
1530
2033
  // HITL reinvocation: the agent is resumed, so its native context carries the
1531
- // prior conversation; the reinvocation prompt conveys the approval decisions.
2034
+ // prior conversation; the reinvocation prompt conveys the approval decisions
2035
+ // (and which approved writes the runner already exact-applied).
1532
2036
  if (isHitlReinvocation) {
1533
- return buildReinvocationPrompt(input.pendingApprovals, approvalDecisions);
2037
+ return buildReinvocationPrompt(
2038
+ input.pendingApprovals,
2039
+ approvalDecisions,
2040
+ input.appliedToolCallIds,
2041
+ );
1534
2042
  }
1535
2043
 
1536
2044
  // A successfully resumed agent carries its own conversation context via the