@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,245 @@
1
+ /**
2
+ * Producer unit tests: deterministic event ids (matching the Go/Java EventID
3
+ * format), the captured-change mapping + enforcement digests, candidate
4
+ * completeness derivation, and append-only-by-id stream authoring.
5
+ */
6
+
7
+ import { create } from "@bufbuild/protobuf";
8
+ import { describe, expect, it } from "vitest";
9
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
10
+ import {
11
+ DiffCompleteness,
12
+ FileCaptureClass,
13
+ FileChangeKind,
14
+ FileReviewBlockReason,
15
+ FileReviewEventType,
16
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
17
+ import {
18
+ appendFileReviewEvents,
19
+ buildBaselineCapturedEvent,
20
+ buildCandidateCapturedEvent,
21
+ buildCapturedFileChange,
22
+ deriveDiffCompleteness,
23
+ eventId,
24
+ type ChangeSetContext,
25
+ } from "../events.js";
26
+ import { sha256Bytes } from "../digest.js";
27
+
28
+ const ctx: ChangeSetContext = {
29
+ changeSetId: "exec-1:0",
30
+ turnId: "turn-0",
31
+ harnessId: "cursor",
32
+ timestamp: "2026-06-30T00:00:00Z",
33
+ };
34
+
35
+ // Completeness-derivation fixtures: a fully-reviewable text change, a binary
36
+ // change (no text diff, is_binary on its byte-true side), and a content-less
37
+ // unavailable change (secret-withheld — non-binary, no keepable bytes).
38
+ function reviewableChange(id: string) {
39
+ return buildCapturedFileChange({
40
+ id, pathBefore: id, pathAfter: id, kind: FileChangeKind.MODIFY,
41
+ captureClass: FileCaptureClass.GIT_TRACKED, before: "old\n", after: "new\n",
42
+ });
43
+ }
44
+ function binaryChange(id: string) {
45
+ return buildCapturedFileChange({
46
+ id, pathBefore: "", pathAfter: `${id}.png`, kind: FileChangeKind.ADD,
47
+ captureClass: FileCaptureClass.GIT_TRACKED,
48
+ after: { kind: "binary", sha256: `sha-${id}` },
49
+ diffComplete: false,
50
+ });
51
+ }
52
+ function unavailableChange(id: string) {
53
+ return buildCapturedFileChange({
54
+ id, pathBefore: "", pathAfter: id, kind: FileChangeKind.MODIFY,
55
+ captureClass: FileCaptureClass.GIT_IGNORED_CAPTURED,
56
+ diffComplete: false, blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
57
+ });
58
+ }
59
+
60
+ describe("eventId", () => {
61
+ it("matches the Go/Java deterministic format (changeSetId:scopeId:ENUM_NAME)", () => {
62
+ expect(eventId("cs-1", "cs-1", FileReviewEventType.BASELINE_CAPTURED)).toBe(
63
+ "cs-1:cs-1:FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
64
+ );
65
+ expect(eventId("cs-1", "cs-1", FileReviewEventType.CANDIDATE_CAPTURED)).toBe(
66
+ "cs-1:cs-1:FILE_REVIEW_EVENT_TYPE_CANDIDATE_CAPTURED",
67
+ );
68
+ expect(eventId("cs-1", "cs-1", FileReviewEventType.RECONCILED)).toBe(
69
+ "cs-1:cs-1:FILE_REVIEW_EVENT_TYPE_RECONCILED",
70
+ );
71
+ });
72
+ });
73
+
74
+ describe("buildCapturedFileChange", () => {
75
+ it("computes before/after sha256 and file_digest over the captured bytes", () => {
76
+ const change = buildCapturedFileChange({
77
+ id: "fc-1",
78
+ pathBefore: "src/a.ts",
79
+ pathAfter: "src/a.ts",
80
+ kind: FileChangeKind.MODIFY,
81
+ captureClass: FileCaptureClass.GIT_TRACKED,
82
+ before: "old\n",
83
+ after: "new\n",
84
+ });
85
+ expect(change.beforeSha256).toBe(sha256Bytes(Buffer.from("old\n", "utf8")));
86
+ expect(change.afterSha256).toBe(sha256Bytes(Buffer.from("new\n", "utf8")));
87
+ expect(change.fileDigest).toHaveLength(64);
88
+ expect(change.diffComplete).toBe(true);
89
+ expect(change.before?.body.case).toBe("inline");
90
+ expect(change.after?.body.case).toBe("inline");
91
+ // Producer invariant: a reviewable (diff_complete) file carries no reason.
92
+ expect(change.blockedReason).toBe(FileReviewBlockReason.UNSPECIFIED);
93
+ });
94
+
95
+ it("passes through an explicit blocked_reason (doc 15)", () => {
96
+ const change = buildCapturedFileChange({
97
+ id: "fc-secret",
98
+ pathBefore: ".env",
99
+ pathAfter: ".env",
100
+ kind: FileChangeKind.MODIFY,
101
+ captureClass: FileCaptureClass.GIT_IGNORED_CAPTURED,
102
+ diffComplete: false,
103
+ blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
104
+ });
105
+ expect(change.diffComplete).toBe(false);
106
+ expect(change.blockedReason).toBe(FileReviewBlockReason.SECRET_WITHHELD);
107
+ });
108
+
109
+ it("keeps blocked_reason out of the enforcement digests (informational only)", () => {
110
+ const common = {
111
+ id: "fc-1",
112
+ pathBefore: "a",
113
+ pathAfter: "a",
114
+ kind: FileChangeKind.MODIFY,
115
+ captureClass: FileCaptureClass.GIT_IGNORED_CAPTURED,
116
+ before: "x",
117
+ after: "y",
118
+ diffComplete: false,
119
+ } as const;
120
+ // Two files identical except for the reason must be digest-indistinguishable —
121
+ // structurally guaranteed (FileDigestInput excludes it), locked here anyway.
122
+ const withUnspecified = buildCapturedFileChange(common);
123
+ const withSize = buildCapturedFileChange({
124
+ ...common,
125
+ blockedReason: FileReviewBlockReason.SIZE_ELIDED,
126
+ });
127
+ expect(withSize.fileDigest).toBe(withUnspecified.fileDigest);
128
+
129
+ const aggUnspecified = buildCandidateCapturedEvent(ctx, undefined, [withUnspecified]);
130
+ const aggSize = buildCandidateCapturedEvent(ctx, undefined, [withSize]);
131
+ if (
132
+ aggUnspecified.payload.case === "candidateCaptured" &&
133
+ aggSize.payload.case === "candidateCaptured"
134
+ ) {
135
+ expect(aggSize.payload.value.aggregateDigest).toBe(
136
+ aggUnspecified.payload.value.aggregateDigest,
137
+ );
138
+ }
139
+ });
140
+
141
+ it("omits the before side for an ADD and the after side for a DELETE", () => {
142
+ const add = buildCapturedFileChange({
143
+ id: "fc-add",
144
+ pathBefore: "",
145
+ pathAfter: "src/new.ts",
146
+ kind: FileChangeKind.ADD,
147
+ captureClass: FileCaptureClass.GIT_TRACKED,
148
+ after: "hello\n",
149
+ });
150
+ expect(add.before).toBeUndefined();
151
+ expect(add.beforeSha256).toBe("");
152
+ expect(add.after?.body.case).toBe("inline");
153
+
154
+ const del = buildCapturedFileChange({
155
+ id: "fc-del",
156
+ pathBefore: "src/old.ts",
157
+ pathAfter: "",
158
+ kind: FileChangeKind.DELETE,
159
+ captureClass: FileCaptureClass.GIT_TRACKED,
160
+ before: "bye\n",
161
+ });
162
+ expect(del.after).toBeUndefined();
163
+ expect(del.afterSha256).toBe("");
164
+ });
165
+ });
166
+
167
+ describe("buildCandidateCapturedEvent", () => {
168
+ it("derives COMPLETE when every file is complete", () => {
169
+ const changes = [
170
+ buildCapturedFileChange({
171
+ id: "fc-1", pathBefore: "a", pathAfter: "a", kind: FileChangeKind.MODIFY,
172
+ captureClass: FileCaptureClass.GIT_TRACKED, before: "x", after: "y",
173
+ }),
174
+ ];
175
+ const ev = buildCandidateCapturedEvent(ctx, undefined, changes);
176
+ expect(ev.payload.case).toBe("candidateCaptured");
177
+ if (ev.payload.case === "candidateCaptured") {
178
+ expect(ev.payload.value.diffCompleteness).toBe(DiffCompleteness.COMPLETE);
179
+ expect(ev.payload.value.aggregateDigest).toHaveLength(64);
180
+ }
181
+ });
182
+
183
+ it("derives PARTIAL_BLOCKED when a non-binary file is incomplete", () => {
184
+ const changes = [
185
+ buildCapturedFileChange({
186
+ id: "fc-1", pathBefore: "a", pathAfter: "a", kind: FileChangeKind.MODIFY,
187
+ captureClass: FileCaptureClass.GIT_TRACKED, before: "x", after: "y",
188
+ diffComplete: false,
189
+ }),
190
+ ];
191
+ const ev = buildCandidateCapturedEvent(ctx, undefined, changes);
192
+ if (ev.payload.case === "candidateCaptured") {
193
+ expect(ev.payload.value.diffCompleteness).toBe(DiffCompleteness.PARTIAL_BLOCKED);
194
+ }
195
+ });
196
+
197
+ it("derives BINARY_SUMMARY_ONLY when the only incomplete file is binary", () => {
198
+ const changes = [reviewableChange("fc-text"), binaryChange("fc-bin")];
199
+ const ev = buildCandidateCapturedEvent(ctx, undefined, changes);
200
+ if (ev.payload.case === "candidateCaptured") {
201
+ expect(ev.payload.value.diffCompleteness).toBe(DiffCompleteness.BINARY_SUMMARY_ONLY);
202
+ }
203
+ });
204
+ });
205
+
206
+ describe("deriveDiffCompleteness", () => {
207
+ it("is COMPLETE when every file is complete (and for an empty set)", () => {
208
+ expect(deriveDiffCompleteness([])).toBe(DiffCompleteness.COMPLETE);
209
+ expect(deriveDiffCompleteness([reviewableChange("a"), reviewableChange("b")])).toBe(
210
+ DiffCompleteness.COMPLETE,
211
+ );
212
+ });
213
+
214
+ it("is BINARY_SUMMARY_ONLY when binary is the set's only blocker", () => {
215
+ // Binary-only single file, and a mixed reviewable-text + binary set.
216
+ expect(deriveDiffCompleteness([binaryChange("bin")])).toBe(
217
+ DiffCompleteness.BINARY_SUMMARY_ONLY,
218
+ );
219
+ expect(
220
+ deriveDiffCompleteness([reviewableChange("t1"), reviewableChange("t2"), binaryChange("b")]),
221
+ ).toBe(DiffCompleteness.BINARY_SUMMARY_ONLY);
222
+ });
223
+
224
+ it("is PARTIAL_BLOCKED when a non-binary incomplete file is present, even alongside a binary", () => {
225
+ expect(
226
+ deriveDiffCompleteness([reviewableChange("t"), binaryChange("b"), unavailableChange("secret")]),
227
+ ).toBe(DiffCompleteness.PARTIAL_BLOCKED);
228
+ });
229
+ });
230
+
231
+ describe("appendFileReviewEvents", () => {
232
+ it("seeds the stream and appends append-only by event_id", () => {
233
+ const status = create(AgentExecutionStatusSchema, {});
234
+ const baseline = buildBaselineCapturedEvent(ctx, undefined);
235
+ const candidate = buildCandidateCapturedEvent(ctx, undefined, []);
236
+
237
+ appendFileReviewEvents(status, "exec-1", [baseline, candidate]);
238
+ expect(status.fileReviewEventStream?.executionId).toBe("exec-1");
239
+ expect(status.fileReviewEventStream?.events).toHaveLength(2);
240
+
241
+ // Re-appending the same events (a re-sent heartbeat) is idempotent.
242
+ appendFileReviewEvents(status, "exec-1", [baseline, candidate]);
243
+ expect(status.fileReviewEventStream?.events).toHaveLength(2);
244
+ });
245
+ });
@@ -0,0 +1,362 @@
1
+ /**
2
+ * @regression file-hitl-phase0 — pins file-edit HITL fixes #7, #8 (see _projects/2026-06/20260630.01.file-change-hitl-redesign/tasks/T01_3_regression-manifest.md)
3
+ *
4
+ * Unit tests for the git snapshot/restore capture substrate (the PRIMITIVES:
5
+ * snapshot / capture / restore / apply / recompute / drop).
6
+ *
7
+ * These run against a REAL temporary git repo (the behavior is entirely git
8
+ * plumbing, so a real repo is the only faithful test). They exercise each
9
+ * primitive directly — restoreToBaseline and applyApprovedPaths are the
10
+ * reconcile pair the resume path composes (capture mode leaves the tree applied
11
+ * during review and reconciles to the refs on resume) — plus the hard invariants
12
+ * (the `.stigmer` SDK state and gitignored paths survive; the harness's
13
+ * runner-owned files, passed as excludePaths, never pollute the diff).
14
+ */
15
+
16
+ import { execFile } from "node:child_process";
17
+ import { mkdtemp, mkdir, readFile, rm, writeFile, stat } from "node:fs/promises";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { promisify } from "node:util";
21
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
22
+ import { FileChangeType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
23
+ import {
24
+ applyApprovedPaths,
25
+ captureChangeSet,
26
+ dropCaptureRefs,
27
+ isGitWorkTree,
28
+ isPathCapturable,
29
+ recomputeChangeSet,
30
+ restoreToBaseline,
31
+ snapshotBaseline,
32
+ } from "../git-substrate.js";
33
+ import { sha256Bytes } from "../digest.js";
34
+
35
+ const execFileAsync = promisify(execFile);
36
+
37
+ const EXEC_ID = "exec-test-1";
38
+
39
+ /** The runner-owned files a harness (here, Cursor) writes into the workspace. */
40
+ const RUNNER_OWNED_PATHS: readonly string[] = [
41
+ ".cursor/hooks.json",
42
+ ".cursor/rules/stigmer-tool-approval.mdc",
43
+ ];
44
+
45
+ let repo: string;
46
+
47
+ async function git(args: string[]): Promise<string> {
48
+ const { stdout } = await execFileAsync("git", args, { cwd: repo });
49
+ return stdout;
50
+ }
51
+
52
+ async function exists(rel: string): Promise<boolean> {
53
+ try {
54
+ await stat(join(repo, rel));
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ async function read(rel: string): Promise<string> {
62
+ return readFile(join(repo, rel), "utf-8");
63
+ }
64
+
65
+ async function write(rel: string, content: string): Promise<void> {
66
+ await mkdir(join(repo, rel, ".."), { recursive: true });
67
+ await writeFile(join(repo, rel), content, "utf-8");
68
+ }
69
+
70
+ beforeEach(async () => {
71
+ repo = await mkdtemp(join(tmpdir(), "stigmer-substrate-"));
72
+ await git(["init", "-q"]);
73
+ await git(["config", "user.email", "t@t.local"]);
74
+ await git(["config", "user.name", "t"]);
75
+ // A committed baseline file so HEAD exists and we can exercise modify/delete.
76
+ await write("src/main.ts", "export const x = 1;\n");
77
+ await write("notes.md", "platon notes\n");
78
+ await git(["add", "-A"]);
79
+ await git(["commit", "-q", "-m", "initial"]);
80
+ // A gitignored path and the SDK-state dir that MUST survive capture/restore.
81
+ await write(".gitignore", ".env\n.stigmer/\n");
82
+ await git(["add", ".gitignore"]);
83
+ await git(["commit", "-q", "-m", "gitignore"]);
84
+ // .stigmer is added to .git/info/exclude in production (git.ts); replicate so
85
+ // the test mirrors the real exclude surface.
86
+ await write(".git/info/exclude", ".stigmer\nlost+found\n");
87
+ await mkdir(join(repo, ".stigmer", "cursor-sdk-state"), { recursive: true });
88
+ await writeFile(join(repo, ".stigmer", "cursor-sdk-state", "db.sqlite"), "STATE", "utf-8");
89
+ });
90
+
91
+ afterEach(async () => {
92
+ await rm(repo, { recursive: true, force: true });
93
+ });
94
+
95
+ describe("isGitWorkTree", () => {
96
+ it("returns true inside a repo and false outside", async () => {
97
+ expect(await isGitWorkTree(repo)).toBe(true);
98
+ const plain = await mkdtemp(join(tmpdir(), "stigmer-plain-"));
99
+ try {
100
+ expect(await isGitWorkTree(plain)).toBe(false);
101
+ } finally {
102
+ await rm(plain, { recursive: true, force: true });
103
+ }
104
+ });
105
+ });
106
+
107
+ describe("capture lifecycle", () => {
108
+ it("captures create+modify+delete, restores to baseline, and nothing lands until apply", async () => {
109
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
110
+
111
+ // Agent makes a full turn's worth of edits.
112
+ await write("notes.md", "planton notes\n\n## TODO\n- ship it\n"); // modify
113
+ await write("src/new.ts", "export const y = 2;\n"); // create
114
+ await rm(join(repo, "src/main.ts"), { force: true }); // delete
115
+
116
+ const { changes } = await captureChangeSet(repo, EXEC_ID, baseline);
117
+
118
+ const byPath = new Map(changes.map((c) => [c.path, c]));
119
+ expect(byPath.get("notes.md")?.changeType).toBe(FileChangeType.MODIFY);
120
+ expect(byPath.get("src/new.ts")?.changeType).toBe(FileChangeType.CREATE);
121
+ expect(byPath.get("src/main.ts")?.changeType).toBe(FileChangeType.DELETE);
122
+
123
+ // The capture carries the true before/after net change (text, inline) with a
124
+ // byte-true content address (for valid UTF-8, equal to the utf-8 hash).
125
+ const beforeText = "platon notes\n";
126
+ const afterText = "planton notes\n\n## TODO\n- ship it\n";
127
+ const notes = byPath.get("notes.md")!;
128
+ expect(notes.before).toEqual({
129
+ kind: "inline",
130
+ text: beforeText,
131
+ sha256: sha256Bytes(Buffer.from(beforeText, "utf8")),
132
+ });
133
+ expect(notes.after).toEqual({
134
+ kind: "inline",
135
+ text: afterText,
136
+ sha256: sha256Bytes(Buffer.from(afterText, "utf8")),
137
+ });
138
+
139
+ // Restore: the working tree is byte-identical to pre-turn (nothing landed).
140
+ await restoreToBaseline(repo, baseline, changes);
141
+ expect(await read("notes.md")).toBe("platon notes\n");
142
+ expect(await read("src/main.ts")).toBe("export const x = 1;\n");
143
+ expect(await exists("src/new.ts")).toBe(false);
144
+ });
145
+
146
+ it("applies only the approved subset; rejected files stay at baseline", async () => {
147
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
148
+ await write("notes.md", "planton notes\n");
149
+ await write("src/main.ts", "export const x = 99;\n");
150
+ const { afterTree, changes } = await captureChangeSet(repo, EXEC_ID, baseline);
151
+ await restoreToBaseline(repo, baseline, changes);
152
+
153
+ // Approve notes.md only; reject src/main.ts.
154
+ const approved = changes.filter((c) => c.path === "notes.md");
155
+ await applyApprovedPaths(repo, afterTree, approved);
156
+
157
+ expect(await read("notes.md")).toBe("planton notes\n"); // applied
158
+ expect(await read("src/main.ts")).toBe("export const x = 1;\n"); // untouched
159
+ });
160
+
161
+ it("applies an approved create and an approved delete", async () => {
162
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
163
+ await write("src/new.ts", "export const y = 2;\n"); // create
164
+ await rm(join(repo, "src/main.ts"), { force: true }); // delete
165
+ const { afterTree, changes } = await captureChangeSet(repo, EXEC_ID, baseline);
166
+ await restoreToBaseline(repo, baseline, changes);
167
+
168
+ await applyApprovedPaths(repo, afterTree, changes); // approve all
169
+ expect(await read("src/new.ts")).toBe("export const y = 2;\n");
170
+ expect(await exists("src/main.ts")).toBe(false);
171
+ });
172
+ });
173
+
174
+ describe("binary content identity + reconcile", () => {
175
+ // Non-UTF-8 bytes: a NUL (triggers binary detection) plus high bytes
176
+ // (0xFF/0xFE/0x80) a lossy UTF-8 decode would mangle — so a lossy hash of these
177
+ // would differ from the true byte hash.
178
+ const BIN_V1 = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff, 0xfe, 0x80, 0x01]);
179
+ const BIN_V2 = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff, 0x00, 0x42, 0x99, 0x7f]);
180
+
181
+ async function writeBytes(rel: string, bytes: Buffer): Promise<void> {
182
+ await mkdir(join(repo, rel, ".."), { recursive: true });
183
+ await writeFile(join(repo, rel), bytes);
184
+ }
185
+ async function readBytes(rel: string): Promise<Buffer> {
186
+ return readFile(join(repo, rel));
187
+ }
188
+
189
+ it("captures a binary side as a body-less, byte-true content address", async () => {
190
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
191
+ await writeBytes("assets/logo.png", BIN_V1); // create a binary file
192
+
193
+ const { changes } = await captureChangeSet(repo, EXEC_ID, baseline);
194
+ const change = changes.find((c) => c.path === "assets/logo.png")!;
195
+ expect(change.changeType).toBe(FileChangeType.CREATE);
196
+ // Body-less binary side carrying only its byte-true content address.
197
+ expect(change.after).toEqual({ kind: "binary", sha256: sha256Bytes(BIN_V1) });
198
+ expect(change.before).toBeUndefined();
199
+ // The digest is over the RAW bytes; a lossy UTF-8 re-encode would differ —
200
+ // this is the latent break the byte-true capture closes.
201
+ const lossy = sha256Bytes(Buffer.from(BIN_V1.toString("utf8"), "utf8"));
202
+ expect(sha256Bytes(BIN_V1)).not.toBe(lossy);
203
+ });
204
+
205
+ it("reconciles an approved binary create byte-exact from the git ref", async () => {
206
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
207
+ await writeBytes("assets/logo.png", BIN_V1);
208
+ const { afterTree, changes } = await captureChangeSet(repo, EXEC_ID, baseline);
209
+
210
+ await restoreToBaseline(repo, baseline, changes); // discard removes the create
211
+ expect(await exists("assets/logo.png")).toBe(false);
212
+
213
+ await applyApprovedPaths(repo, afterTree, changes); // approve re-applies exact bytes
214
+ expect((await readBytes("assets/logo.png")).equals(BIN_V1)).toBe(true);
215
+ });
216
+
217
+ it("reconciles a binary modify: approve keeps new bytes, reject restores old (byte-exact)", async () => {
218
+ // Commit a binary baseline so a modify is exercised.
219
+ await writeBytes("assets/logo.png", BIN_V1);
220
+ await git(["add", "-A"]);
221
+ await git(["commit", "-q", "-m", "add binary"]);
222
+
223
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
224
+ await writeBytes("assets/logo.png", BIN_V2); // modify the binary
225
+ const { afterTree, changes } = await captureChangeSet(repo, EXEC_ID, baseline);
226
+ const change = changes.find((c) => c.path === "assets/logo.png")!;
227
+ expect(change.changeType).toBe(FileChangeType.MODIFY);
228
+ expect(change.before).toEqual({ kind: "binary", sha256: sha256Bytes(BIN_V1) });
229
+ expect(change.after).toEqual({ kind: "binary", sha256: sha256Bytes(BIN_V2) });
230
+
231
+ // Approve -> the new bytes are kept byte-exact.
232
+ await applyApprovedPaths(repo, afterTree, changes);
233
+ expect((await readBytes("assets/logo.png")).equals(BIN_V2)).toBe(true);
234
+
235
+ // Reject -> snap back to the exact baseline bytes.
236
+ await restoreToBaseline(repo, baseline, changes);
237
+ expect((await readBytes("assets/logo.png")).equals(BIN_V1)).toBe(true);
238
+ });
239
+ });
240
+
241
+ describe("hard invariants", () => {
242
+ it("never captures or disturbs the gitignored .stigmer SDK state", async () => {
243
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
244
+ await write("notes.md", "planton\n");
245
+ // Simulate the SDK writing more state mid-turn.
246
+ await writeFile(join(repo, ".stigmer", "cursor-sdk-state", "db.sqlite"), "STATE2", "utf-8");
247
+
248
+ const { changes } = await captureChangeSet(repo, EXEC_ID, baseline);
249
+ expect(changes.some((c) => c.path.startsWith(".stigmer"))).toBe(false);
250
+
251
+ await restoreToBaseline(repo, baseline, changes);
252
+ // The SDK state is untouched by restore (it was never in the change set).
253
+ expect(await read(".stigmer/cursor-sdk-state/db.sqlite")).toBe("STATE2");
254
+ });
255
+
256
+ it("excludes the passed runner-owned paths from the captured diff", async () => {
257
+ const baseline = await snapshotBaseline(repo, EXEC_ID, RUNNER_OWNED_PATHS);
258
+ // The gate writes these during the turn.
259
+ await write(".cursor/hooks.json", '{"version":1,"hooks":{}}\n');
260
+ await write(".cursor/rules/stigmer-tool-approval.mdc", "rule\n");
261
+ await write("notes.md", "planton\n");
262
+
263
+ const { changes } = await captureChangeSet(repo, EXEC_ID, baseline, RUNNER_OWNED_PATHS);
264
+ expect(changes.some((c) => c.path === ".cursor/hooks.json")).toBe(false);
265
+ expect(
266
+ changes.some((c) => c.path === ".cursor/rules/stigmer-tool-approval.mdc"),
267
+ ).toBe(false);
268
+ expect(changes.some((c) => c.path === "notes.md")).toBe(true);
269
+ });
270
+
271
+ it("captures everything when no excludePaths are passed (deep-agent: no runner-owned files)", async () => {
272
+ // The deep-agent harness writes no gate files into the work tree, so it passes
273
+ // no excludePaths; a normal source file is captured as usual.
274
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
275
+ await write("src/new.ts", "export const y = 2;\n");
276
+
277
+ const { changes } = await captureChangeSet(repo, EXEC_ID, baseline);
278
+ expect(changes.some((c) => c.path === "src/new.ts")).toBe(true);
279
+ });
280
+
281
+ it("preserves a gitignored file the agent created (NOT in the change set)", async () => {
282
+ // .env is gitignored, so capture cannot see it. (Production gates such
283
+ // writes via git check-ignore; here we assert capture itself never touches
284
+ // it — restore must leave it alone, not delete it.)
285
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
286
+ await write(".env", "SECRET=1\n");
287
+ await write("notes.md", "planton\n");
288
+
289
+ const { changes } = await captureChangeSet(repo, EXEC_ID, baseline);
290
+ expect(changes.some((c) => c.path === ".env")).toBe(false);
291
+
292
+ await restoreToBaseline(repo, baseline, changes);
293
+ expect(await exists(".env")).toBe(true); // not clobbered by restore
294
+ });
295
+ });
296
+
297
+ describe("recomputeChangeSet (resume source of truth)", () => {
298
+ it("returns baselineTree, afterTree, and changes from the pinned refs", async () => {
299
+ const baseline = await snapshotBaseline(repo, EXEC_ID);
300
+ await write("notes.md", "planton notes\n");
301
+ await write("src/new.ts", "export const y = 2;\n");
302
+ const captured = await captureChangeSet(repo, EXEC_ID, baseline);
303
+
304
+ // On resume the refs (not the live tree) are the source of truth: recompute
305
+ // re-derives the exact baseline/after trees and the per-file change set.
306
+ const recomputed = await recomputeChangeSet(repo, EXEC_ID);
307
+ expect(recomputed).toBeDefined();
308
+ expect(recomputed!.baselineTree).toBe(baseline);
309
+ expect(recomputed!.afterTree).toBe(captured.afterTree);
310
+ expect(recomputed!.changes.map((c) => c.path).sort()).toEqual([
311
+ "notes.md",
312
+ "src/new.ts",
313
+ ]);
314
+ });
315
+
316
+ it("returns undefined when the execution never captured (no refs)", async () => {
317
+ expect(await recomputeChangeSet(repo, "never-captured")).toBeUndefined();
318
+ });
319
+ });
320
+
321
+ describe("isPathCapturable (capture-mode gate predicate)", () => {
322
+ beforeEach(async () => {
323
+ // .env + the build/ dir are gitignored; everything else is capturable.
324
+ await write(".gitignore", ".env\nbuild/\n");
325
+ await git(["add", ".gitignore"]);
326
+ await git(["commit", "-q", "-m", "gitignore"]);
327
+ });
328
+
329
+ it("returns true for a git-tracked path (capturable -> flows)", async () => {
330
+ expect(await isPathCapturable(repo, "src/main.ts")).toBe(true);
331
+ });
332
+
333
+ it("returns true for a new untracked-but-not-ignored path (capturable)", async () => {
334
+ expect(await isPathCapturable(repo, "src/brand-new.ts")).toBe(true);
335
+ });
336
+
337
+ it("returns false for a gitignored file (NOT capturable -> stays gated)", async () => {
338
+ expect(await isPathCapturable(repo, ".env")).toBe(false);
339
+ });
340
+
341
+ it("returns false for a path under a gitignored directory", async () => {
342
+ expect(await isPathCapturable(repo, "build/out.js")).toBe(false);
343
+ });
344
+
345
+ it("returns false for an empty path", async () => {
346
+ expect(await isPathCapturable(repo, "")).toBe(false);
347
+ });
348
+ });
349
+
350
+ describe("dropCaptureRefs", () => {
351
+ it("removes the per-execution refs and is idempotent", async () => {
352
+ await snapshotBaseline(repo, EXEC_ID);
353
+ await write("notes.md", "planton\n");
354
+ await captureChangeSet(repo, EXEC_ID, (await git(["rev-parse", "HEAD^{tree}"])).trim());
355
+
356
+ await dropCaptureRefs(repo, EXEC_ID);
357
+ const refs = await git(["for-each-ref", "refs/stigmer/"]);
358
+ expect(refs.trim()).toBe("");
359
+ // Idempotent: a second drop does not throw.
360
+ await dropCaptureRefs(repo, EXEC_ID);
361
+ });
362
+ });