@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
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Deep-agent adapter for the HITL gateway Contract Test Kit.
3
+ *
4
+ * Drives the REAL in-process gateway: a `createDeepAgent` graph carrying the
5
+ * production {@link createApprovalGateMiddleware}, a `MemorySaver` checkpointer,
6
+ * and a scripted model that proposes the action under test. `authorize` runs the
7
+ * graph to its first pause and, for an approving/non-approving decision, resumes
8
+ * via `Command(resume=…)` — exactly the production pause/resume cycle proven in
9
+ * `__tests__/subagent-approval-propagation.test.ts`. There is no mocked
10
+ * `interrupt()`: the gate's own `interrupt()` fires, so the contract exercises the
11
+ * gateway end-to-end.
12
+ *
13
+ * Capabilities: this substrate IS the side effect, so it observes (and counts)
14
+ * execution; resource-exact leasing is not a property of this gate (sameness comes
15
+ * from checkpoint replay), so `enforcesExactResource` is false and
16
+ * `authorizeAfterGrant` is intentionally not implemented.
17
+ */
18
+
19
+ import { HumanMessage } from "@langchain/core/messages";
20
+ import { tool } from "@langchain/core/tools";
21
+ import { z } from "zod";
22
+ import { Command, MemorySaver } from "@langchain/langgraph";
23
+ import { createDeepAgent, StateBackend } from "deepagents";
24
+
25
+ import { createApprovalGateMiddleware } from "../../../middleware/approval-gate.js";
26
+ import { deriveLeaseScope } from "../../../shared/approval-policy.js";
27
+ import type { ToolApprovalCategory } from "../../../shared/tool-kind.js";
28
+ import { ScriptedModel, readPendingInterrupts } from "./scripted-model.js";
29
+ import type {
30
+ GatewayDecision,
31
+ GatewayOutcome,
32
+ GatewaySubstrate,
33
+ ProposedAction,
34
+ } from "../../../__test-utils__/approval-contract/types.js";
35
+
36
+ /**
37
+ * Translate an abstract action into a deep-agent tool name + args + MCP server.
38
+ * The names are deliberately ALIASES, not deepagents built-ins, so we can attach
39
+ * a counting handler without shadowing an injected built-in, while still hitting
40
+ * the right approval category via the shared classifier:
41
+ * - write → `overwrite_file` (FILE_WRITE), delete → `remove_file` (FILE_DELETE),
42
+ * shell → `execute_command` (SHELL): each gated fail-closed by category.
43
+ * - read → an unknown, non-mutating name: auto-approved (fail-open), no gate.
44
+ * - mcp → the MCP tool name mapped to its server: governed by policy, absent
45
+ * policy ⇒ auto-approved.
46
+ */
47
+ function toDeepAgentTool(action: ProposedAction): {
48
+ name: string;
49
+ args: Record<string, unknown>;
50
+ serverSlug: string;
51
+ } {
52
+ switch (action.kind) {
53
+ case "write":
54
+ return { name: "overwrite_file", args: { path: action.resource, content: "x" }, serverSlug: "" };
55
+ case "shell":
56
+ return { name: "execute_command", args: { command: action.resource }, serverSlug: "" };
57
+ case "delete":
58
+ return { name: "remove_file", args: { path: action.resource }, serverSlug: "" };
59
+ case "read":
60
+ return { name: "inspect_resource", args: { path: action.resource }, serverSlug: "" };
61
+ case "mcp":
62
+ return { name: action.mcpToolName ?? "mcp_tool", args: {}, serverSlug: action.mcpServerSlug ?? "srv" };
63
+ }
64
+ }
65
+
66
+ /** Map a contract decision to the gate's resume verdict (anything unrecognized = unknown). */
67
+ function toResumeAction(decision: GatewayDecision): string {
68
+ switch (decision) {
69
+ case "approve":
70
+ return "approve";
71
+ case "skip":
72
+ return "skip";
73
+ case "reject":
74
+ return "reject";
75
+ default:
76
+ return "unrecognized-verdict";
77
+ }
78
+ }
79
+
80
+ let threadSeq = 0;
81
+
82
+ /**
83
+ * Drive one probe action through a freshly built gate and report the outcome.
84
+ * `leasedCategories` pre-arms a run-lifetime class lease (empty for the plain
85
+ * authorize path); `decision` resumes a pause for the non-lease drives.
86
+ */
87
+ async function runProbe(
88
+ action: ProposedAction,
89
+ decision: GatewayDecision,
90
+ leasedCategories: ReadonlySet<ToolApprovalCategory>,
91
+ ): Promise<GatewayOutcome> {
92
+ const { name, args, serverSlug } = toDeepAgentTool(action);
93
+
94
+ let executionCount = 0;
95
+ const countingTool = tool(
96
+ async () => {
97
+ executionCount += 1;
98
+ return "ok";
99
+ },
100
+ {
101
+ name,
102
+ description: `contract probe tool ${name}`,
103
+ schema: z.object({}).passthrough(),
104
+ },
105
+ );
106
+
107
+ const toolServerMap = serverSlug ? new Map([[name, serverSlug]]) : new Map<string, string>();
108
+ const gate = createApprovalGateMiddleware({
109
+ policies: new Map(),
110
+ toolServerMap,
111
+ leasedCategories,
112
+ });
113
+
114
+ const agent = await createDeepAgent({
115
+ model: new ScriptedModel(() => ({
116
+ toolCalls: [{ name, args, id: "call_1" }],
117
+ done: "done",
118
+ })),
119
+ checkpointer: new MemorySaver() as never,
120
+ backend: new StateBackend(),
121
+ tools: [countingTool],
122
+ middleware: [gate],
123
+ } as unknown as Parameters<typeof createDeepAgent>[0]);
124
+
125
+ const config = { configurable: { thread_id: `contract-${threadSeq++}` }, recursionLimit: 50 };
126
+
127
+ // Run to the first pause. A gated tool interrupts before its handler runs.
128
+ await agent.invoke({ messages: [new HumanMessage({ content: "go" })] }, config);
129
+ const pending = readPendingInterrupts((await agent.getState(config)) as never);
130
+ const gated = pending.length > 0;
131
+ // The gate stamps the authorization provenance on every interrupt it raises;
132
+ // surface it so the contract can assert no side effect is gated without one.
133
+ const policySource = gated ? pending[0].policySource : "";
134
+
135
+ // `none` is the "no decision yet" probe: leave the action withheld.
136
+ if (gated && decision !== "none") {
137
+ const resume: Record<string, { action: string }> = {};
138
+ for (const p of pending) resume[p.interruptId] = { action: toResumeAction(decision) };
139
+ await agent.invoke(new Command({ resume }), config);
140
+ }
141
+
142
+ return { executed: executionCount > 0, gated, executionCount, policySource };
143
+ }
144
+
145
+ const NO_LEASED_CATEGORIES: ReadonlySet<ToolApprovalCategory> = new Set();
146
+
147
+ export function createDeepAgentSubstrate(): GatewaySubstrate {
148
+ return {
149
+ name: "deep-agent",
150
+ available: true,
151
+ capabilities: {
152
+ observesExecution: true,
153
+ enforcesExactResource: false,
154
+ // The deep-agent gate re-checks every distinct call and relies on
155
+ // checkpoint replay for sameness; content isolation is not a property of
156
+ // the gate itself, so the content-exact invariant does not apply here.
157
+ enforcesExactContent: false,
158
+ appliesRunLifetimeLease: true,
159
+ surfacesGatePolicySource: true,
160
+ },
161
+
162
+ async authorize(action: ProposedAction, decision: GatewayDecision): Promise<GatewayOutcome> {
163
+ return runProbe(action, decision, NO_LEASED_CATEGORIES);
164
+ },
165
+
166
+ async authorizeUnderClassLease(
167
+ leased: ProposedAction,
168
+ probe: ProposedAction,
169
+ ): Promise<GatewayOutcome> {
170
+ // Reduce the leased action to its class exactly as the runner does, then
171
+ // arm the gate with that category lease and run the probe with no fresh
172
+ // decision — so only the lease can clear it.
173
+ const leasedTool = toDeepAgentTool(leased);
174
+ const scope = deriveLeaseScope(leasedTool.name, leasedTool.serverSlug);
175
+ const leasedCategories =
176
+ scope?.kind === "category" ? new Set([scope.category]) : NO_LEASED_CATEGORIES;
177
+ return runProbe(probe, "none", leasedCategories);
178
+ },
179
+ };
180
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Scripted deep-agent test harness: a deterministic chat model plus the
3
+ * parent-checkpoint interrupt reader.
4
+ *
5
+ * These two helpers are what every "drive a real deepagents/LangGraph graph
6
+ * through the approval gate" test needs, and they were previously copy-pasted
7
+ * inline. They are shared by:
8
+ * - `__tests__/subagent-approval-propagation.test.ts` (sub-agent interrupt
9
+ * propagation against the real runtime), and
10
+ * - `__test-utils__/gateway-substrate.ts` (the deep-agent adapter for the
11
+ * gateway Contract Test Kit).
12
+ *
13
+ * Keeping one copy means a change to how the runner reads pending interrupts (or
14
+ * how a scripted run is driven) updates every consumer at once.
15
+ */
16
+
17
+ import { BaseChatModel } from "@langchain/core/language_models/chat_models";
18
+ import { AIMessage, ToolMessage, type BaseMessage } from "@langchain/core/messages";
19
+ import type { ChatResult } from "@langchain/core/outputs";
20
+
21
+ /** A single tool call the scripted model proposes on a non-terminal turn. */
22
+ export interface ScriptedToolCall {
23
+ name: string;
24
+ args: Record<string, unknown>;
25
+ id: string;
26
+ }
27
+
28
+ /**
29
+ * What the scripted model does for a given role: propose `toolCalls` while no
30
+ * tool result is pending, then emit the `done` text once a tool result returns.
31
+ */
32
+ export interface ScriptStep {
33
+ toolCalls: ScriptedToolCall[];
34
+ done: string;
35
+ }
36
+
37
+ /**
38
+ * Picks the {@link ScriptStep} for the currently-bound tool set. A single-agent
39
+ * test ignores the argument; a parent/sub-agent test keys the role on a tool
40
+ * unique to one role (e.g. the sub-agent's gated tool), because `createDeepAgent`
41
+ * injects the `task` tool into both the parent and its sub-agents.
42
+ */
43
+ export type ScriptSelector = (boundToolNames: string[]) => ScriptStep;
44
+
45
+ /**
46
+ * A deterministic two-turn chat model: on the first turn it proposes the
47
+ * selected role's tool calls; once a tool result comes back it returns the
48
+ * role's terminating text. No LLM, no network — the role is keyed on the bound
49
+ * tool names so the same model instance drives a parent and its sub-agents.
50
+ */
51
+ export class ScriptedModel extends BaseChatModel {
52
+ toolNames: string[] = [];
53
+ private readonly select: ScriptSelector;
54
+
55
+ constructor(select: ScriptSelector) {
56
+ super({});
57
+ this.select = select;
58
+ }
59
+
60
+ _llmType(): string {
61
+ return "scripted";
62
+ }
63
+
64
+ bindTools(tools: unknown[]): this {
65
+ const next = new ScriptedModel(this.select);
66
+ next.toolNames = (tools as Array<{ name?: string }>).map((t) => t?.name ?? "");
67
+ return next as unknown as this;
68
+ }
69
+
70
+ async _generate(messages: BaseMessage[]): Promise<ChatResult> {
71
+ const step = this.select(this.toolNames);
72
+ const lastIsToolResult = messages[messages.length - 1] instanceof ToolMessage;
73
+
74
+ const message = lastIsToolResult
75
+ ? new AIMessage({ content: step.done })
76
+ : new AIMessage({
77
+ content: "",
78
+ tool_calls: step.toolCalls.map((tc) => ({ ...tc, type: "tool_call" as const })),
79
+ });
80
+
81
+ const text = typeof message.content === "string" ? message.content : "";
82
+ return { generations: [{ message, text }] };
83
+ }
84
+ }
85
+
86
+ /** A pending approval interrupt surfaced at the parent checkpoint. */
87
+ export interface PendingInterrupt {
88
+ taskId: string;
89
+ interruptId: string;
90
+ toolCallId: string;
91
+ toolName: string;
92
+ message: string;
93
+ /**
94
+ * Authorization provenance the gate attached to the interrupt
95
+ * (approval-gate.ts `policy_source`) — the PolicySource union string, or "" for
96
+ * legacy. Lets the gateway contract assert every gated side effect is
97
+ * provenance-tagged.
98
+ */
99
+ policySource: string;
100
+ }
101
+
102
+ /**
103
+ * Read the pending approval interrupts from a graph state, exactly as the
104
+ * production resume path does (`hitl.ts` / `index.ts`): scan the parent's
105
+ * top-level `tasks[].interrupts`, skipping any already carrying a resume value.
106
+ *
107
+ * Crucially `interruptId` is the interrupt's OWN id (`interrupts[].id`), not the
108
+ * owning `task.id` — for a nested sub-agent interrupt the two differ, and only
109
+ * the interrupt id routes a `Command(resume=...)` value back into the nested
110
+ * `interrupt()`.
111
+ */
112
+ export function readPendingInterrupts(state: {
113
+ tasks?: ReadonlyArray<{
114
+ id: string;
115
+ interrupts?: ReadonlyArray<{ id?: string; value?: unknown; resumeValue?: unknown }>;
116
+ }>;
117
+ }): PendingInterrupt[] {
118
+ const out: PendingInterrupt[] = [];
119
+ for (const task of state.tasks ?? []) {
120
+ for (const intr of task.interrupts ?? []) {
121
+ if (intr.resumeValue !== undefined) continue;
122
+ const v = (intr.value ?? {}) as Record<string, unknown>;
123
+ out.push({
124
+ taskId: task.id,
125
+ interruptId: (intr.id as string) ?? task.id,
126
+ toolCallId: (v.tool_call_id as string) ?? "",
127
+ toolName: (v.tool_name as string) ?? "",
128
+ message: (v.message as string) ?? "",
129
+ policySource: (v.policy_source as string) ?? "",
130
+ });
131
+ }
132
+ }
133
+ return out;
134
+ }
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ findAiMessageToolCallArgs,
4
+ captureApprovalArtifacts,
5
+ } from "../approval-file-change.js";
6
+
7
+ /** An AI message shaped like LangChain's, carrying tool calls with args. */
8
+ function aiMessage(toolCalls: Array<{ id: string; name: string; args: unknown }>) {
9
+ return { _getType: () => "ai", tool_calls: toolCalls };
10
+ }
11
+
12
+ describe("findAiMessageToolCallArgs", () => {
13
+ it("returns the args of the tool call matching the id", () => {
14
+ const messages = [
15
+ { _getType: () => "human", content: "do it" },
16
+ aiMessage([
17
+ { id: "call-a", name: "write_file", args: { file_path: "a.txt", content: "A" } },
18
+ { id: "call-b", name: "edit_file", args: { file_path: "b.txt", old_string: "x", new_string: "y" } },
19
+ ]),
20
+ ];
21
+ expect(findAiMessageToolCallArgs(messages, "call-b")).toEqual({
22
+ file_path: "b.txt",
23
+ old_string: "x",
24
+ new_string: "y",
25
+ });
26
+ });
27
+
28
+ it("finds the call across multiple AI messages", () => {
29
+ const messages = [
30
+ aiMessage([{ id: "call-1", name: "write_file", args: { file_path: "1", content: "" } }]),
31
+ aiMessage([{ id: "call-2", name: "write_file", args: { file_path: "2", content: "" } }]),
32
+ ];
33
+ expect(findAiMessageToolCallArgs(messages, "call-2")).toEqual({ file_path: "2", content: "" });
34
+ });
35
+
36
+ it("ignores ToolMessages (singular tool_call_id, no tool_calls array)", () => {
37
+ const messages = [
38
+ { _getType: () => "tool", tool_call_id: "call-x", content: "result" },
39
+ ];
40
+ expect(findAiMessageToolCallArgs(messages, "call-x")).toBeUndefined();
41
+ });
42
+
43
+ it("returns undefined when no message emitted the id (sub-agent-nested miss)", () => {
44
+ const messages = [aiMessage([{ id: "other", name: "write_file", args: {} }])];
45
+ expect(findAiMessageToolCallArgs(messages, "call-missing")).toBeUndefined();
46
+ });
47
+
48
+ it("returns an empty object when the matched call carries no args", () => {
49
+ const messages = [aiMessage([{ id: "call-1", name: "write_file", args: undefined }])];
50
+ expect(findAiMessageToolCallArgs(messages, "call-1")).toEqual({});
51
+ });
52
+ });
53
+
54
+ describe("captureApprovalArtifacts", () => {
55
+ it("returns a sanitized args preview for a correlated tool call", () => {
56
+ const messages = [
57
+ aiMessage([
58
+ { id: "call-1", name: "write_file", args: { file_path: "a.txt", content: "hi", token: "sk-secret" } },
59
+ ]),
60
+ ];
61
+ const { argsPreview } = captureApprovalArtifacts({ toolCallId: "call-1", messages });
62
+
63
+ expect(argsPreview).toBeDefined();
64
+ expect(argsPreview).toContain("a.txt");
65
+ expect(argsPreview).toContain("[REDACTED]");
66
+ expect(argsPreview).not.toContain("sk-secret");
67
+ });
68
+
69
+ it("returns nothing when the interrupt cannot be correlated", () => {
70
+ const result = captureApprovalArtifacts({
71
+ toolCallId: "missing",
72
+ messages: [aiMessage([{ id: "other", name: "write_file", args: { file_path: "a", content: "" } }])],
73
+ });
74
+ expect(result).toEqual({});
75
+ });
76
+
77
+ it("returns a preview for a correlated non-file tool", () => {
78
+ const messages = [
79
+ aiMessage([{ id: "call-1", name: "search", args: { query: "needle" } }]),
80
+ ];
81
+ const { argsPreview } = captureApprovalArtifacts({ toolCallId: "call-1", messages });
82
+ expect(argsPreview).toContain("needle");
83
+ });
84
+ });
@@ -14,6 +14,7 @@ import {
14
14
  MAX_ZIP_EXTRACTED_SIZE,
15
15
  } from "../attachment-injector.js";
16
16
  import { mockWorkspaceBackend } from "../../../__test-utils__/mock-workspace.js";
17
+ import { makeInMemoryArtifactStorage } from "../../../__test-utils__/fake-artifact-storage.js";
17
18
 
18
19
  // ── ZIP Construction Helpers ─────────────────────────────────────────
19
20
 
@@ -156,15 +157,11 @@ function makeAttachment(overrides: Partial<{
156
157
  } as any;
157
158
  }
158
159
 
159
- function makeMockStorage(downloadContent?: Buffer) {
160
- const url = "https://storage.example.com/presigned/test";
161
- return {
162
- upload: vi.fn(),
163
- getDownloadUrl: vi.fn().mockResolvedValue(url),
164
- exists: vi.fn().mockResolvedValue(true),
165
- _content: downloadContent,
166
- _url: url,
167
- };
160
+ function makeMockStorage() {
161
+ // The canonical in-memory double; cloud downloads read from what was uploaded,
162
+ // so tests seed a blob via `storage.upload` and assert on `storage.download`.
163
+ const { storage } = makeInMemoryArtifactStorage();
164
+ return storage;
168
165
  }
169
166
 
170
167
  // ═══════════════════════════════════════════════════════════════════════
@@ -359,15 +356,8 @@ describe("injectAttachments", () => {
359
356
 
360
357
  it("injects single non-ZIP attachment in cloud mode", async () => {
361
358
  const content = Buffer.from("cloud data");
362
- const storage = makeMockStorage(content);
363
-
364
- const fetchMock = vi.fn().mockResolvedValue({
365
- ok: true,
366
- arrayBuffer: () => Promise.resolve(content.buffer.slice(
367
- content.byteOffset, content.byteOffset + content.byteLength,
368
- )),
369
- });
370
- vi.stubGlobal("fetch", fetchMock);
359
+ const storage = makeMockStorage();
360
+ await storage.upload("attachments/xyz/data.csv", content);
371
361
 
372
362
  const backend = mockWorkspaceBackend();
373
363
 
@@ -385,10 +375,7 @@ describe("injectAttachments", () => {
385
375
  expect(result[0].filename).toBe("data.csv");
386
376
  expect(result[0].path).toBe(".stigmer/inputs/data.csv");
387
377
  expect(result[0].sizeBytes).toBe(content.length);
388
- expect(storage.getDownloadUrl).toHaveBeenCalledWith("attachments/xyz/data.csv");
389
- expect(fetchMock).toHaveBeenCalledWith(storage._url);
390
-
391
- vi.unstubAllGlobals();
378
+ expect(storage.download).toHaveBeenCalledWith("attachments/xyz/data.csv");
392
379
  });
393
380
 
394
381
  it("extracts ZIP when extract=true", async () => {
@@ -534,7 +521,7 @@ describe("injectAttachments", () => {
534
521
 
535
522
  it("propagates error when storage download fails", async () => {
536
523
  const storage = makeMockStorage();
537
- storage.getDownloadUrl.mockRejectedValue(new Error("network timeout"));
524
+ storage.download.mockRejectedValue(new Error("network timeout"));
538
525
 
539
526
  const backend = mockWorkspaceBackend();
540
527
 
@@ -602,7 +589,7 @@ describe("injectAttachments", () => {
602
589
  })).rejects.toThrow(/collides with/);
603
590
 
604
591
  // Verify no downloads were attempted
605
- expect(storage.getDownloadUrl).not.toHaveBeenCalled();
592
+ expect(storage.download).not.toHaveBeenCalled();
606
593
  });
607
594
 
608
595
  it("derives filename from storageKey when filename is empty", async () => {
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtemp, mkdir, rm, writeFile, readFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { CasCaptureFilesystemBackend } from "../cas-capture-backend.js";
6
+ import { CasCaptureObserver } from "../cas-capture-observer.js";
7
+
8
+ /**
9
+ * The backend is a thin adapter: it forwards each mutation point to the shared
10
+ * {@link CasCaptureObserver} (which owns all capture state and logic) and then
11
+ * delegates to the base `FilesystemBackend` to apply the write to disk. These
12
+ * tests assert exactly that delegation contract — the write lands on disk AND the
13
+ * observer sees the pre-turn bytes. The recording semantics themselves (first
14
+ * touch, concurrency, memoization) are covered in cas-capture-observer.test.ts.
15
+ */
16
+ describe("CasCaptureFilesystemBackend (thin CAS adapter)", () => {
17
+ let root: string;
18
+
19
+ const isIgnored = async (relPath: string): Promise<boolean> => relPath.startsWith("ignored/");
20
+
21
+ function makeBackend(): { backend: CasCaptureFilesystemBackend; observer: CasCaptureObserver } {
22
+ const observer = new CasCaptureObserver({ rootDir: root, isIgnored });
23
+ const backend = new CasCaptureFilesystemBackend({ rootDir: root }, { observer });
24
+ return { backend, observer };
25
+ }
26
+
27
+ beforeEach(async () => {
28
+ root = await mkdtemp(join(tmpdir(), "cas-backend-"));
29
+ await mkdir(join(root, "ignored"), { recursive: true });
30
+ await mkdir(join(root, "src"), { recursive: true });
31
+ });
32
+
33
+ afterEach(async () => {
34
+ await rm(root, { recursive: true, force: true });
35
+ });
36
+
37
+ it("write applies to disk AND records before=null for a new gitignored file", async () => {
38
+ const { backend, observer } = makeBackend();
39
+
40
+ await backend.write("ignored/new.txt", "hello");
41
+
42
+ expect(await readFile(join(root, "ignored/new.txt"), "utf8")).toBe("hello");
43
+ expect(observer.before.get("ignored/new.txt")).toBeNull();
44
+ });
45
+
46
+ it("edit applies to disk AND records the pre-turn bytes for a gitignored file", async () => {
47
+ await writeFile(join(root, "ignored/cfg.txt"), "v0");
48
+ const { backend, observer } = makeBackend();
49
+
50
+ await backend.edit("ignored/cfg.txt", "v0", "v1");
51
+
52
+ expect(await readFile(join(root, "ignored/cfg.txt"), "utf8")).toBe("v1");
53
+ expect(Buffer.from(observer.before.get("ignored/cfg.txt")!).toString("utf8")).toBe("v0");
54
+ });
55
+
56
+ it("a git-tracked write applies to disk but is NOT observed (the git diff owns it)", async () => {
57
+ const { backend, observer } = makeBackend();
58
+
59
+ await backend.write("src/app.ts", "export const x = 1;");
60
+
61
+ expect(await readFile(join(root, "src/app.ts"), "utf8")).toBe("export const x = 1;");
62
+ expect(observer.before.size).toBe(0);
63
+ });
64
+ });