@stigmer/runner 3.0.9-dev.20260615153829 → 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 (329) 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 +16 -3
  74. package/dist/activities/execute-deep-agent/hitl.js +11 -3
  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 +393 -33
  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 +17 -5
  92. package/dist/activities/execute-deep-agent/status-builder.js +44 -3
  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 +32 -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 +284 -0
  243. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
  244. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
  245. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
  246. package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
  247. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +91 -10
  248. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
  249. package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
  250. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
  251. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
  252. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
  253. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
  254. package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
  255. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
  256. package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
  257. package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
  258. package/src/activities/execute-deep-agent/hitl.ts +23 -7
  259. package/src/activities/execute-deep-agent/index.ts +463 -40
  260. package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
  261. package/src/activities/execute-deep-agent/setup.ts +223 -125
  262. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
  263. package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
  264. package/src/activities/execute-deep-agent/status-builder.ts +63 -8
  265. package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
  266. package/src/activities/execute-deep-agent/streaming.ts +3 -13
  267. package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
  268. package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
  269. package/src/activities/execute-deep-agent/v3-status-builder.ts +36 -2
  270. package/src/claimcheck/payload-codec.ts +8 -8
  271. package/src/client/stigmer-client.ts +9 -1
  272. package/src/middleware/__tests__/approval-gate.test.ts +488 -4
  273. package/src/middleware/approval-gate.ts +247 -38
  274. package/src/middleware/types.ts +5 -5
  275. package/src/shared/__tests__/activity-input.test.ts +78 -0
  276. package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
  277. package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
  278. package/src/shared/__tests__/approval-policy.test.ts +274 -40
  279. package/src/shared/__tests__/args-preview.test.ts +78 -0
  280. package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
  281. package/src/shared/__tests__/artifact-storage.test.ts +123 -11
  282. package/src/shared/__tests__/file-change.test.ts +85 -0
  283. package/src/shared/__tests__/file-tools.test.ts +90 -0
  284. package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
  285. package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
  286. package/src/shared/__tests__/model-client.test.ts +162 -0
  287. package/src/shared/__tests__/plan-artifact.test.ts +11 -26
  288. package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
  289. package/src/shared/__tests__/status-offload.test.ts +573 -16
  290. package/src/shared/__tests__/status.test.ts +4 -5
  291. package/src/shared/__tests__/tool-kind.test.ts +24 -1
  292. package/src/shared/__tests__/tool-row.test.ts +221 -0
  293. package/src/shared/activity-input.ts +57 -0
  294. package/src/shared/approval-canonicalize.ts +159 -0
  295. package/src/shared/approval-fingerprint.ts +148 -0
  296. package/src/shared/approval-policy.ts +303 -27
  297. package/src/shared/args-preview.ts +98 -0
  298. package/src/shared/artifact-storage.ts +62 -11
  299. package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
  300. package/src/shared/file-change.ts +64 -0
  301. package/src/shared/file-tools.ts +169 -0
  302. package/src/shared/filereview/__tests__/capture.test.ts +856 -0
  303. package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
  304. package/src/shared/filereview/__tests__/digest.test.ts +100 -0
  305. package/src/shared/filereview/__tests__/events.test.ts +245 -0
  306. package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
  307. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
  308. package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
  309. package/src/shared/filereview/capture.ts +727 -0
  310. package/src/shared/filereview/cas-substrate.ts +401 -0
  311. package/src/shared/filereview/digest.ts +83 -0
  312. package/src/shared/filereview/events.ts +449 -0
  313. package/src/shared/filereview/git-substrate.ts +555 -0
  314. package/src/shared/filereview/index.ts +60 -0
  315. package/src/shared/filereview/secret-paths.ts +121 -0
  316. package/src/shared/fingerprint-secret.ts +53 -0
  317. package/src/shared/model-client.ts +122 -0
  318. package/src/shared/plan-artifact.ts +0 -2
  319. package/src/shared/status-offload.ts +433 -77
  320. package/src/shared/status.ts +13 -0
  321. package/src/shared/tool-kind.ts +33 -0
  322. package/src/shared/tool-row.ts +135 -0
  323. package/src/shared/workspace/platform-dir.ts +41 -2
  324. package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
  325. package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
  326. package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
  327. package/src/workflows/call-agent-orchestrator.ts +53 -6
  328. package/src/workflows/connect-mcp-server.ts +179 -24
  329. package/src/workflows/types.ts +8 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * @regression file-hitl-phase0 — pins the no-storage deny-gate exact-apply seam
3
+ * (see _projects/2026-06/20260630.01.file-change-hitl-redesign/tasks/T01_3_regression-manifest.md)
4
+ *
5
+ * Deterministic integration proof of the Cursor deny-gate "what you approve is
6
+ * what gets applied" guarantee, driven through the REAL composition and the REAL
7
+ * deny-oracle hook — the achievable substitute for a pure-Go offline e2e, which
8
+ * is structurally infeasible (no offline Cursor agent driver; see DD-23).
9
+ *
10
+ * It replays the exact chain (and ORDER) `execute-cursor/index.ts` runs on a
11
+ * deny-gate resume, against a REAL {@link LocalWorkspaceBackend} on a temp
12
+ * non-git workspace with NO artifact storage (the `!captureMode` branch):
13
+ *
14
+ * reconstructAdjudicatedApprovals (Phase 3, before apply)
15
+ * -> applyApprovedWholeFileWrites (Phase 5b3, flips applied calls COMPLETED)
16
+ * -> excludeAppliedFromGrants (Phase 5c)
17
+ * -> buildApprovalGrants (Phase 5c)
18
+ * -> the generated bash hook (the pre-write deny-oracle)
19
+ *
20
+ * The order is load-bearing: `reconstructAdjudicatedApprovals` filters on
21
+ * WAITING_APPROVAL and index.ts computes the adjudicated set ONCE (before apply
22
+ * flips status) and never re-derives it, so `excludeAppliedFromGrants` is the
23
+ * only thing that keeps an exact-applied write out of the grants. Seeded tool
24
+ * calls therefore carry BOTH `args` (structured — exact-apply's whole-file
25
+ * source) AND `argsPreview` (grant-identity source).
26
+ *
27
+ * The reinvocation prompt split (an applied write rendered "already applied") is
28
+ * covered separately in build-prompt.test.ts and is NOT re-asserted here.
29
+ *
30
+ * Deterministic; no Cursor API key. The hook cases are skipped where bash is
31
+ * unavailable.
32
+ */
33
+
34
+ import { describe, it, expect, onTestFinished } from "vitest";
35
+ import { create, type JsonObject } from "@bufbuild/protobuf";
36
+ import {
37
+ AgentMessageSchema,
38
+ ToolCallSchema,
39
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
40
+ import type {
41
+ AgentMessage,
42
+ ToolCall,
43
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
44
+ import {
45
+ MessageType,
46
+ ToolCallStatus,
47
+ ApprovalAction,
48
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
49
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
50
+ import { tmpdir } from "node:os";
51
+ import { join } from "node:path";
52
+
53
+ import { applyApprovedWholeFileWrites, excludeAppliedFromGrants } from "../exact-apply.js";
54
+ import { reconstructAdjudicatedApprovals, buildApprovalGrants } from "../approval-state.js";
55
+ import type { ApprovalGrant } from "../approval-state.js";
56
+ import { LocalWorkspaceBackend } from "../../../shared/workspace/local-backend.js";
57
+ import {
58
+ setupCursorHookHarness,
59
+ hasBash,
60
+ hookWrite,
61
+ hookShell,
62
+ } from "../__test-utils__/cursor-hook-harness.js";
63
+
64
+ const EXEC_ID = "exec-deny-gate";
65
+
66
+ // `deriveCaptureMode` (the decision that arms this deny-gate/exact-apply path) is
67
+ // truth-table-tested at its shared home: shared/filereview/__tests__/capture.test.ts.
68
+
69
+ // ── builders ─────────────────────────────────────────────────────────────────
70
+
71
+ /** A fresh temp workspace root (real disk), auto-removed at test end. */
72
+ function tempWorkspace(): string {
73
+ const dir = mkdtempSync(join(tmpdir(), "deny-gate-exact-apply-"));
74
+ onTestFinished(() => rmSync(dir, { recursive: true, force: true }));
75
+ return dir;
76
+ }
77
+
78
+ /**
79
+ * A WAITING_APPROVAL tool call carrying BOTH the structured `args` (exact-apply
80
+ * reads the whole-file content here) AND `argsPreview` (reconstructAdjudicated-
81
+ * Approvals + buildApprovalGrants read grant identity here) — the fixture shape
82
+ * index.ts sees on a deny-gate resume.
83
+ */
84
+ function adjudicatedCall(
85
+ id: string,
86
+ name: string,
87
+ args: Record<string, unknown>,
88
+ approvalAction: ApprovalAction,
89
+ ): ToolCall {
90
+ return create(ToolCallSchema, {
91
+ id,
92
+ name,
93
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
94
+ approvalAction,
95
+ args: args as JsonObject,
96
+ argsPreview: JSON.stringify(args),
97
+ });
98
+ }
99
+
100
+ function messagesOf(...toolCalls: ToolCall[]): AgentMessage[] {
101
+ return [create(AgentMessageSchema, { type: MessageType.MESSAGE_AI, toolCalls })];
102
+ }
103
+
104
+ /**
105
+ * Replay the deny-gate resume chain in the SAME ORDER index.ts runs it (see the
106
+ * module docstring). Returns every intermediate so a test can assert on the exact
107
+ * seam the activity wires.
108
+ */
109
+ async function replayDenyGateResume(root: string, messages: AgentMessage[]): Promise<{
110
+ applied: ReadonlySet<string>;
111
+ grantApprovals: ReturnType<typeof reconstructAdjudicatedApprovals>["pendingApprovals"];
112
+ grants: ApprovalGrant[];
113
+ }> {
114
+ const adjudicated = reconstructAdjudicatedApprovals(messages); // Phase 3 (pre-apply)
115
+ const applied = await applyApprovedWholeFileWrites({
116
+ messages,
117
+ workspaceBackend: new LocalWorkspaceBackend(root),
118
+ workspaceDirs: [root],
119
+ executionId: EXEC_ID,
120
+ }); // Phase 5b3 (mutates applied calls to COMPLETED)
121
+ const grantApprovals = excludeAppliedFromGrants(adjudicated.pendingApprovals, applied); // Phase 5c
122
+ const grants = buildApprovalGrants(grantApprovals, adjudicated.decisions, adjudicated.contentDigests);
123
+ return { applied, grantApprovals, grants };
124
+ }
125
+
126
+ // ── the seam (no hook / bash needed) ─────────────────────────────────────────
127
+
128
+ describe("deny-gate exact-apply seam (real LocalWorkspaceBackend, storage off)", () => {
129
+ it("approve: writes the EXACT bytes to a root-level path, marks the tool COMPLETED, and issues NO grant for it", async () => {
130
+ const root = tempWorkspace();
131
+ const path = "notes.md";
132
+ const content = "# Notes\n- Planton\n";
133
+ const write = adjudicatedCall("tc-write", "write", { file_path: path, content }, ApprovalAction.APPROVE);
134
+
135
+ const { applied, grantApprovals, grants } = await replayDenyGateResume(root, messagesOf(write));
136
+
137
+ // The runner applied exactly the approved bytes to real disk...
138
+ expect(applied).toEqual(new Set(["tc-write"]));
139
+ expect(readFileSync(join(root, path), "utf-8")).toBe(content);
140
+ expect(write.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
141
+ // ...and excluded it from the grants, so a further write to it will re-gate.
142
+ expect(grantApprovals.map((pa) => pa.toolCallId)).not.toContain("tc-write");
143
+ expect(grants).toHaveLength(0);
144
+ });
145
+
146
+ it("reject / undecided: applies nothing and grants nothing", async () => {
147
+ const root = tempWorkspace();
148
+ const rejected = adjudicatedCall("tc-rej", "write", { file_path: "drop.txt", content: "x" }, ApprovalAction.REJECT);
149
+ const undecided = adjudicatedCall("tc-und", "write", { file_path: "maybe.txt", content: "y" }, ApprovalAction.UNSPECIFIED);
150
+
151
+ const { applied, grants } = await replayDenyGateResume(root, messagesOf(rejected, undecided));
152
+
153
+ expect(applied.size).toBe(0);
154
+ expect(existsSync(join(root, "drop.txt"))).toBe(false);
155
+ expect(existsSync(join(root, "maybe.txt"))).toBe(false);
156
+ // A REJECT is never granted; an UNSPECIFIED never even enters the adjudicated set.
157
+ expect(grants).toHaveLength(0);
158
+ });
159
+
160
+ it("safety: an approved write to a not-yet-existing subdirectory falls back (no mkdir, no partial write)", async () => {
161
+ // LocalWorkspaceBackend created without a platformDir does not mkdir parents,
162
+ // so exact-apply's write throws ENOENT and degrades to the grant+reinvocation
163
+ // path — never a partial/corrupt file. This documents that pre-existing, safe
164
+ // behavior (see the plan's out-of-scope note).
165
+ const root = tempWorkspace();
166
+ const nested = adjudicatedCall("tc-nested", "write", { file_path: "sub/nested.txt", content: "z" }, ApprovalAction.APPROVE);
167
+
168
+ const { applied } = await replayDenyGateResume(root, messagesOf(nested));
169
+
170
+ expect(applied.size).toBe(0);
171
+ expect(existsSync(join(root, "sub"))).toBe(false);
172
+ expect(existsSync(join(root, "sub", "nested.txt"))).toBe(false);
173
+ // Left for the existing grant + reinvocation path.
174
+ expect(nested.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
175
+ });
176
+ });
177
+
178
+ // ── the full loop through the REAL deny-oracle hook (needs bash) ──────────────
179
+
180
+ const withBash = hasBash ? describe : describe.skip;
181
+
182
+ withBash("deny-gate exact-apply seam through the real hook", () => {
183
+ it("re-gates a further write to the exact-applied path (DENY) while a co-approved shell still flows (ALLOW)", async () => {
184
+ const root = tempWorkspace();
185
+ const appliedPath = "app.ts";
186
+ const write = adjudicatedCall("tc-write", "write", { file_path: appliedPath, content: "export const x = 1;\n" }, ApprovalAction.APPROVE);
187
+ const shell = adjudicatedCall("tc-shell", "shell", { command: "npm test" }, ApprovalAction.APPROVE);
188
+
189
+ const { applied, grants } = await replayDenyGateResume(root, messagesOf(write, shell));
190
+
191
+ // The write was exact-applied and excluded; the shell (never applied) kept its grant.
192
+ expect(applied).toEqual(new Set(["tc-write"]));
193
+ expect(grants.map((g) => g.toolName)).toEqual(["shell"]);
194
+
195
+ // Drive the REAL generated hook (non-git, capture off) with exactly those grants.
196
+ const hook = setupCursorHookHarness({ grants, gitWorkspace: false, captureMode: false });
197
+
198
+ // A further write to the exact-applied path has no grant at all -> re-gated.
199
+ expect(hook.decide(hookWrite(appliedPath)).permission).toBe("deny");
200
+ // The co-approved shell is allowed by its (coarse) grant.
201
+ expect(hook.decide(hookShell("npm test")).permission).toBe("allow");
202
+ });
203
+ });
@@ -0,0 +1,375 @@
1
+ /**
2
+ * @regression file-hitl-phase0 — pins file-edit HITL fix #4 (see _projects/2026-06/20260630.01.file-change-hitl-redesign/tasks/T01_3_regression-manifest.md)
3
+ *
4
+ * Unit tests for resume-time exact-apply (the HITL "what you approve is what gets
5
+ * applied" guarantee for the Cursor deny-only harness).
6
+ *
7
+ * Since Phase 5 Slice 4 the approved bytes and target path are read from the
8
+ * gated tool call's `args` (the single source — there is no separate captured
9
+ * `file_changes` mirror). These pin the load-bearing invariants:
10
+ * - an APPROVED whole-file write is written to disk with the EXACT approved bytes
11
+ * (args content) and its tool call is marked COMPLETED in place;
12
+ * - the content resolver NEVER writes the elision marker (which would silently
13
+ * corrupt the file) — it degrades to a fall back (no write);
14
+ * - everything that is not an approved whole-file write (hunk edits, shell, MCP,
15
+ * skipped/rejected, already-completed) is left untouched for the existing
16
+ * grant + reinvocation path;
17
+ * - the apply targets the args path resolved under the workspace (multi-root) and
18
+ * refuses a target outside the workspace;
19
+ * - the step is idempotent under Temporal activity retries.
20
+ *
21
+ * Deterministic; no Cursor API key.
22
+ */
23
+
24
+ import { describe, it, expect, vi, afterEach } from "vitest";
25
+ import { create, type JsonObject, type MessageInitShape } from "@bufbuild/protobuf";
26
+ import {
27
+ AgentMessageSchema,
28
+ ToolCallSchema,
29
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
30
+ import type {
31
+ AgentMessage,
32
+ ToolCall,
33
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
34
+ import {
35
+ MessageType,
36
+ ToolCallStatus,
37
+ ApprovalAction,
38
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
39
+
40
+ import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
41
+ import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
42
+
43
+ import {
44
+ applyApprovedWholeFileWrites,
45
+ excludeAppliedFromGrants,
46
+ resolveApprovedWholeFileContent,
47
+ } from "../exact-apply.js";
48
+ import { ELISION_MARKER } from "../../../shared/status-offload.js";
49
+ import { mockWorkspaceBackend } from "../../../__test-utils__/mock-workspace.js";
50
+ import type { WorkspaceBackend } from "../../../shared/workspace/types.js";
51
+
52
+ const ROOT = "/root";
53
+
54
+ afterEach(() => {
55
+ vi.unstubAllGlobals();
56
+ vi.restoreAllMocks();
57
+ });
58
+
59
+ // ── builders ─────────────────────────────────────────────────────────────────
60
+
61
+ /** An approved, still-pending whole-file write whose args carry path + content. */
62
+ function approvedWrite(
63
+ args: Record<string, unknown>,
64
+ overrides: MessageInitShape<typeof ToolCallSchema> = {},
65
+ ): ToolCall {
66
+ return create(ToolCallSchema, {
67
+ id: "tc-1",
68
+ name: "write",
69
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
70
+ approvalAction: ApprovalAction.APPROVE,
71
+ args: args as JsonObject,
72
+ ...overrides,
73
+ });
74
+ }
75
+
76
+ function messagesOf(...toolCalls: ToolCall[]): AgentMessage[] {
77
+ return [create(AgentMessageSchema, { type: MessageType.MESSAGE_AI, toolCalls })];
78
+ }
79
+
80
+ function recordingBackend(): { backend: WorkspaceBackend; writes: Array<{ path: string; content: string }> } {
81
+ const writes: Array<{ path: string; content: string }> = [];
82
+ const backend = mockWorkspaceBackend({
83
+ rootDir: ROOT,
84
+ writeFile: vi.fn(async (path: string, content: string) => {
85
+ writes.push({ path, content });
86
+ }),
87
+ });
88
+ return { backend, writes };
89
+ }
90
+
91
+ const baseOpts = {
92
+ workspaceDirs: [ROOT],
93
+ executionId: "exec-test",
94
+ };
95
+
96
+ // ── applyApprovedWholeFileWrites ─────────────────────────────────────────────
97
+
98
+ describe("applyApprovedWholeFileWrites", () => {
99
+ it("writes the EXACT approved bytes to the resolved path and marks the tool COMPLETED", async () => {
100
+ const tc = approvedWrite({ path: "notes.md", contents: "# Notes\n- Planton\n" });
101
+ const { backend, writes } = recordingBackend();
102
+
103
+ const applied = await applyApprovedWholeFileWrites({
104
+ ...baseOpts,
105
+ messages: messagesOf(tc),
106
+ workspaceBackend: backend,
107
+ });
108
+
109
+ expect(applied).toEqual(new Set(["tc-1"]));
110
+ expect(writes).toEqual([{ path: `${ROOT}/notes.md`, content: "# Notes\n- Planton\n" }]);
111
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
112
+ expect(tc.completedAt).not.toBe("");
113
+ });
114
+
115
+ it("applies the COMPLETE args content verbatim (full-change fidelity regression)", async () => {
116
+ // The gate's args carry the COMPLETE proposed content (stamped from the
117
+ // authoritative hook input via applyGateInput), so exact-apply must land ALL
118
+ // of it — never a partial. This locks that what exact-apply writes equals the
119
+ // approved args content, whatever it contains.
120
+ const complete = "# Notes\n- Planton\n\n## TODO\n- first\n- second\n";
121
+ const tc = approvedWrite({ path: "notes.md", contents: complete });
122
+ const { backend, writes } = recordingBackend();
123
+
124
+ const applied = await applyApprovedWholeFileWrites({
125
+ ...baseOpts,
126
+ messages: messagesOf(tc),
127
+ workspaceBackend: backend,
128
+ });
129
+
130
+ expect(applied).toEqual(new Set(["tc-1"]));
131
+ expect(writes).toEqual([{ path: `${ROOT}/notes.md`, content: complete }]);
132
+ expect(writes[0].content).toContain("## TODO");
133
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
134
+ });
135
+
136
+ it("SAFETY: never writes the elision marker (falls back instead)", async () => {
137
+ // The args content was elided to the marker by the size backstop.
138
+ const tc = approvedWrite({ path: "notes.md", contents: ELISION_MARKER });
139
+ const { backend, writes } = recordingBackend();
140
+
141
+ const applied = await applyApprovedWholeFileWrites({
142
+ ...baseOpts,
143
+ messages: messagesOf(tc),
144
+ workspaceBackend: backend,
145
+ });
146
+
147
+ expect(applied.size).toBe(0);
148
+ expect(writes).toHaveLength(0);
149
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
150
+ });
151
+
152
+ it("preserves an empty-string write (a legitimately emptied / new empty file)", async () => {
153
+ const tc = approvedWrite({ path: "empty.txt", contents: "" });
154
+ const { backend, writes } = recordingBackend();
155
+
156
+ const applied = await applyApprovedWholeFileWrites({
157
+ ...baseOpts,
158
+ messages: messagesOf(tc),
159
+ workspaceBackend: backend,
160
+ });
161
+
162
+ expect(applied).toEqual(new Set(["tc-1"]));
163
+ expect(writes).toEqual([{ path: `${ROOT}/empty.txt`, content: "" }]);
164
+ });
165
+
166
+ it("targets a file in a NON-primary workspace root (multi-root)", async () => {
167
+ const otherRoot = "/other";
168
+ const tc = approvedWrite({ path: `${otherRoot}/pkg/x.ts`, contents: "b" });
169
+ const { backend, writes } = recordingBackend();
170
+
171
+ const applied = await applyApprovedWholeFileWrites({
172
+ ...baseOpts,
173
+ workspaceDirs: [ROOT, otherRoot],
174
+ messages: messagesOf(tc),
175
+ workspaceBackend: backend,
176
+ });
177
+
178
+ expect(applied).toEqual(new Set(["tc-1"]));
179
+ expect(writes[0].path).toBe(`${otherRoot}/pkg/x.ts`);
180
+ });
181
+
182
+ it("refuses a target OUTSIDE every workspace root (falls back, no write)", async () => {
183
+ const tc = approvedWrite({ path: "/etc/passwd", contents: "pwned" });
184
+ const { backend, writes } = recordingBackend();
185
+
186
+ const applied = await applyApprovedWholeFileWrites({
187
+ ...baseOpts,
188
+ messages: messagesOf(tc),
189
+ workspaceBackend: backend,
190
+ });
191
+
192
+ expect(applied.size).toBe(0);
193
+ expect(writes).toHaveLength(0);
194
+ });
195
+
196
+ it("is idempotent: an already-COMPLETED call is skipped, never written twice", async () => {
197
+ const tc = approvedWrite(
198
+ { path: "notes.md", contents: "done\n" },
199
+ { status: ToolCallStatus.TOOL_CALL_COMPLETED },
200
+ );
201
+ const { backend, writes } = recordingBackend();
202
+
203
+ const applied = await applyApprovedWholeFileWrites({
204
+ ...baseOpts,
205
+ messages: messagesOf(tc),
206
+ workspaceBackend: backend,
207
+ });
208
+
209
+ expect(applied.size).toBe(0);
210
+ expect(writes).toHaveLength(0);
211
+ });
212
+
213
+ it("falls back when the workspace write fails (never silently drops)", async () => {
214
+ const tc = approvedWrite({ path: "notes.md", contents: "x" });
215
+ const backend = mockWorkspaceBackend({
216
+ rootDir: ROOT,
217
+ writeFile: vi.fn(async () => {
218
+ throw new Error("EACCES");
219
+ }),
220
+ });
221
+
222
+ const applied = await applyApprovedWholeFileWrites({
223
+ ...baseOpts,
224
+ messages: messagesOf(tc),
225
+ workspaceBackend: backend,
226
+ });
227
+
228
+ expect(applied.size).toBe(0);
229
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
230
+ });
231
+
232
+ it("applies an APPROVE_ALL whole-file write too", async () => {
233
+ const tc = approvedWrite(
234
+ { path: "notes.md", contents: "v2\n" },
235
+ { approvalAction: ApprovalAction.APPROVE_ALL },
236
+ );
237
+ const { backend, writes } = recordingBackend();
238
+
239
+ const applied = await applyApprovedWholeFileWrites({
240
+ ...baseOpts,
241
+ messages: messagesOf(tc),
242
+ workspaceBackend: backend,
243
+ });
244
+
245
+ expect(applied).toEqual(new Set(["tc-1"]));
246
+ expect(writes).toHaveLength(1);
247
+ });
248
+
249
+ it("leaves a hunk edit untouched (old_string/new_string stays on the reinvocation path)", async () => {
250
+ const tc = approvedWrite({ path: "notes.md", old_string: "a", new_string: "b" });
251
+ const { backend, writes } = recordingBackend();
252
+
253
+ const applied = await applyApprovedWholeFileWrites({
254
+ ...baseOpts,
255
+ messages: messagesOf(tc),
256
+ workspaceBackend: backend,
257
+ });
258
+
259
+ expect(applied.size).toBe(0);
260
+ expect(writes).toHaveLength(0);
261
+ expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
262
+ });
263
+
264
+ it("leaves a SKIPPED / REJECTED / undecided whole-file write untouched", async () => {
265
+ const skipped = approvedWrite({ path: "a.md", contents: "x" }, { id: "s", approvalAction: ApprovalAction.SKIP });
266
+ const rejected = approvedWrite({ path: "b.md", contents: "x" }, { id: "r", approvalAction: ApprovalAction.REJECT });
267
+ const undecided = approvedWrite({ path: "c.md", contents: "x" }, { id: "u", approvalAction: ApprovalAction.UNSPECIFIED });
268
+ const { backend, writes } = recordingBackend();
269
+
270
+ const applied = await applyApprovedWholeFileWrites({
271
+ ...baseOpts,
272
+ messages: messagesOf(skipped, rejected, undecided),
273
+ workspaceBackend: backend,
274
+ });
275
+
276
+ expect(applied.size).toBe(0);
277
+ expect(writes).toHaveLength(0);
278
+ });
279
+
280
+ it("leaves a non-file approved tool (shell) untouched", async () => {
281
+ const tc = approvedWrite({ command: "ls" }, { id: "sh", name: "shell" });
282
+ const { backend, writes } = recordingBackend();
283
+
284
+ const applied = await applyApprovedWholeFileWrites({
285
+ ...baseOpts,
286
+ messages: messagesOf(tc),
287
+ workspaceBackend: backend,
288
+ });
289
+
290
+ expect(applied.size).toBe(0);
291
+ expect(writes).toHaveLength(0);
292
+ });
293
+
294
+ it("applies only the approved write among a mixed batch, returning just its id", async () => {
295
+ const write = approvedWrite({ path: "notes.md", contents: "new\n" }, { id: "w" });
296
+ const shell = approvedWrite({ command: "ls" }, { id: "sh", name: "shell" });
297
+ const skipped = approvedWrite({ path: "b.md", contents: "x" }, { id: "s", approvalAction: ApprovalAction.SKIP });
298
+ const { backend, writes } = recordingBackend();
299
+
300
+ const applied = await applyApprovedWholeFileWrites({
301
+ ...baseOpts,
302
+ messages: messagesOf(write, shell, skipped),
303
+ workspaceBackend: backend,
304
+ });
305
+
306
+ expect(applied).toEqual(new Set(["w"]));
307
+ expect(writes).toEqual([{ path: `${ROOT}/notes.md`, content: "new\n" }]);
308
+ });
309
+ });
310
+
311
+ // ── excludeAppliedFromGrants ─────────────────────────────────────────────────
312
+
313
+ describe("excludeAppliedFromGrants", () => {
314
+ function pendingApproval(toolCallId: string, toolName = "write"): PendingApproval {
315
+ return create(PendingApprovalSchema, { toolCallId, toolName });
316
+ }
317
+
318
+ it("removes an exact-applied write so it is never re-granted (a further write re-gates)", () => {
319
+ const approvals = [pendingApproval("w1", "write"), pendingApproval("sh", "shell")];
320
+ const result = excludeAppliedFromGrants(approvals, new Set(["w1"]));
321
+ expect(result.map((p) => p.toolCallId)).toEqual(["sh"]);
322
+ });
323
+
324
+ it("is an identity passthrough when nothing was applied (fresh turn / capture mode)", () => {
325
+ const approvals = [pendingApproval("w1", "write"), pendingApproval("e1", "edit"), pendingApproval("sh", "shell")];
326
+ expect(excludeAppliedFromGrants(approvals, new Set())).toEqual(approvals);
327
+ });
328
+
329
+ it("retains non-applied approvals (a hunk edit, a shell) beside the removed applied write", () => {
330
+ // Exact-apply only applies whole-file writes; a hunk edit and shell are never
331
+ // applied, so they must keep flowing to the grant builder.
332
+ const approvals = [pendingApproval("w1", "write"), pendingApproval("e1", "edit"), pendingApproval("sh", "shell")];
333
+ const result = excludeAppliedFromGrants(approvals, new Set(["w1"]));
334
+ expect(result.map((p) => p.toolCallId)).toEqual(["e1", "sh"]);
335
+ });
336
+
337
+ it("removes only the applied ids from a mixed batch (order preserved)", () => {
338
+ const approvals = ["a", "b", "c", "d"].map((id) => pendingApproval(id));
339
+ const result = excludeAppliedFromGrants(approvals, new Set(["b", "d"]));
340
+ expect(result.map((p) => p.toolCallId)).toEqual(["a", "c"]);
341
+ });
342
+
343
+ it("returns empty for no approvals", () => {
344
+ expect(excludeAppliedFromGrants([], new Set(["x"]))).toEqual([]);
345
+ });
346
+ });
347
+
348
+ // ── resolveApprovedWholeFileContent ──────────────────────────────────────────
349
+
350
+ describe("resolveApprovedWholeFileContent", () => {
351
+ it("reads the whole-file content from args.content", () => {
352
+ const tc = approvedWrite({ file_path: "notes.md", content: "ARGS" });
353
+ expect(resolveApprovedWholeFileContent(tc)).toBe("ARGS");
354
+ });
355
+
356
+ it("reads the whole-file content from args.contents", () => {
357
+ const tc = approvedWrite({ file_path: "notes.md", contents: "ARGS-CONTENTS" });
358
+ expect(resolveApprovedWholeFileContent(tc)).toBe("ARGS-CONTENTS");
359
+ });
360
+
361
+ it("returns null for an elided marker in args", () => {
362
+ const tc = approvedWrite({ file_path: "notes.md", contents: ELISION_MARKER });
363
+ expect(resolveApprovedWholeFileContent(tc)).toBeNull();
364
+ });
365
+
366
+ it("returns null when the args carry no whole-file content (a hunk edit)", () => {
367
+ const tc = approvedWrite({ file_path: "notes.md", old_string: "a", new_string: "b" });
368
+ expect(resolveApprovedWholeFileContent(tc)).toBeNull();
369
+ });
370
+
371
+ it("preserves an empty-string content (a legitimately emptied file)", () => {
372
+ const tc = approvedWrite({ file_path: "notes.md", contents: "" });
373
+ expect(resolveApprovedWholeFileContent(tc)).toBe("");
374
+ });
375
+ });