@stigmer/runner 3.0.9-dev.20260615153829 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
  3. package/dist/__test-utils__/approval-contract/types.js +24 -0
  4. package/dist/__test-utils__/approval-contract/types.js.map +1 -0
  5. package/dist/activities/call-agent-status.d.ts +19 -1
  6. package/dist/activities/call-agent-status.js +64 -5
  7. package/dist/activities/call-agent-status.js.map +1 -1
  8. package/dist/activities/call-llm.js +19 -53
  9. package/dist/activities/call-llm.js.map +1 -1
  10. package/dist/activities/classify-tool-approvals.d.ts +24 -0
  11. package/dist/activities/classify-tool-approvals.js +69 -17
  12. package/dist/activities/classify-tool-approvals.js.map +1 -1
  13. package/dist/activities/discover-mcp-server.d.ts +7 -0
  14. package/dist/activities/discover-mcp-server.js +11 -1
  15. package/dist/activities/discover-mcp-server.js.map +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
  17. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
  18. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
  19. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
  20. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
  21. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
  22. package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
  23. package/dist/activities/execute-cursor/approval-policy.js +8 -120
  24. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  25. package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
  26. package/dist/activities/execute-cursor/approval-state.js +233 -17
  27. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  28. package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
  29. package/dist/activities/execute-cursor/capture-flow.js +234 -0
  30. package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
  31. package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
  32. package/dist/activities/execute-cursor/cas-observations.js +184 -0
  33. package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
  34. package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
  35. package/dist/activities/execute-cursor/command-provenance.js +128 -0
  36. package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
  37. package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
  38. package/dist/activities/execute-cursor/exact-apply.js +204 -0
  39. package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
  40. package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
  41. package/dist/activities/execute-cursor/hook-script.js +310 -47
  42. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  43. package/dist/activities/execute-cursor/index.d.ts +8 -1
  44. package/dist/activities/execute-cursor/index.js +500 -64
  45. package/dist/activities/execute-cursor/index.js.map +1 -1
  46. package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
  47. package/dist/activities/execute-cursor/message-translator.js +836 -90
  48. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  49. package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
  50. package/dist/activities/execute-cursor/prompt-builder.js +74 -15
  51. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  52. package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
  53. package/dist/activities/execute-cursor/workspace-setup.js +212 -33
  54. package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
  55. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
  56. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
  57. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
  58. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
  59. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
  60. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
  61. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
  62. package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
  63. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
  64. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
  65. package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
  66. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  67. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
  68. package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
  69. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
  70. package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
  71. package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
  72. package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
  73. package/dist/activities/execute-deep-agent/hitl.d.ts +16 -3
  74. package/dist/activities/execute-deep-agent/hitl.js +11 -3
  75. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  76. package/dist/activities/execute-deep-agent/index.d.ts +2 -1
  77. package/dist/activities/execute-deep-agent/index.js +393 -33
  78. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  79. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
  80. package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
  81. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  82. package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
  83. package/dist/activities/execute-deep-agent/setup.js +149 -92
  84. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  85. package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
  86. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
  87. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
  88. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
  89. package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
  90. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  91. package/dist/activities/execute-deep-agent/status-builder.d.ts +17 -5
  92. package/dist/activities/execute-deep-agent/status-builder.js +44 -3
  93. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  94. package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
  95. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  96. package/dist/activities/execute-deep-agent/streaming.js +3 -15
  97. package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
  98. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
  99. package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
  100. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  101. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
  102. package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
  103. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  104. package/dist/activities/execute-deep-agent/v3-status-builder.js +32 -2
  105. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  106. package/dist/claimcheck/payload-codec.js +9 -5
  107. package/dist/claimcheck/payload-codec.js.map +1 -1
  108. package/dist/client/stigmer-client.d.ts +2 -0
  109. package/dist/client/stigmer-client.js +2 -0
  110. package/dist/client/stigmer-client.js.map +1 -1
  111. package/dist/middleware/approval-gate.d.ts +85 -4
  112. package/dist/middleware/approval-gate.js +165 -38
  113. package/dist/middleware/approval-gate.js.map +1 -1
  114. package/dist/middleware/types.d.ts +2 -5
  115. package/dist/shared/activity-input.d.ts +43 -0
  116. package/dist/shared/activity-input.js +17 -0
  117. package/dist/shared/activity-input.js.map +1 -0
  118. package/dist/shared/approval-canonicalize.d.ts +19 -0
  119. package/dist/shared/approval-canonicalize.js +119 -0
  120. package/dist/shared/approval-canonicalize.js.map +1 -0
  121. package/dist/shared/approval-fingerprint.d.ts +106 -0
  122. package/dist/shared/approval-fingerprint.js +113 -0
  123. package/dist/shared/approval-fingerprint.js.map +1 -0
  124. package/dist/shared/approval-policy.d.ts +182 -12
  125. package/dist/shared/approval-policy.js +213 -27
  126. package/dist/shared/approval-policy.js.map +1 -1
  127. package/dist/shared/args-preview.d.ts +52 -0
  128. package/dist/shared/args-preview.js +93 -0
  129. package/dist/shared/args-preview.js.map +1 -0
  130. package/dist/shared/artifact-storage.d.ts +19 -1
  131. package/dist/shared/artifact-storage.js +48 -11
  132. package/dist/shared/artifact-storage.js.map +1 -1
  133. package/dist/shared/file-change.d.ts +44 -0
  134. package/dist/shared/file-change.js +57 -0
  135. package/dist/shared/file-change.js.map +1 -0
  136. package/dist/shared/file-tools.d.ts +107 -0
  137. package/dist/shared/file-tools.js +168 -0
  138. package/dist/shared/file-tools.js.map +1 -0
  139. package/dist/shared/filereview/capture.d.ts +202 -0
  140. package/dist/shared/filereview/capture.js +498 -0
  141. package/dist/shared/filereview/capture.js.map +1 -0
  142. package/dist/shared/filereview/cas-substrate.d.ts +190 -0
  143. package/dist/shared/filereview/cas-substrate.js +284 -0
  144. package/dist/shared/filereview/cas-substrate.js.map +1 -0
  145. package/dist/shared/filereview/digest.d.ts +40 -0
  146. package/dist/shared/filereview/digest.js +66 -0
  147. package/dist/shared/filereview/digest.js.map +1 -0
  148. package/dist/shared/filereview/events.d.ts +170 -0
  149. package/dist/shared/filereview/events.js +298 -0
  150. package/dist/shared/filereview/events.js.map +1 -0
  151. package/dist/shared/filereview/git-substrate.d.ts +175 -0
  152. package/dist/shared/filereview/git-substrate.js +439 -0
  153. package/dist/shared/filereview/git-substrate.js.map +1 -0
  154. package/dist/shared/filereview/index.d.ts +11 -0
  155. package/dist/shared/filereview/index.js +12 -0
  156. package/dist/shared/filereview/index.js.map +1 -0
  157. package/dist/shared/filereview/secret-paths.d.ts +63 -0
  158. package/dist/shared/filereview/secret-paths.js +105 -0
  159. package/dist/shared/filereview/secret-paths.js.map +1 -0
  160. package/dist/shared/fingerprint-secret.d.ts +26 -0
  161. package/dist/shared/fingerprint-secret.js +47 -0
  162. package/dist/shared/fingerprint-secret.js.map +1 -0
  163. package/dist/shared/model-client.d.ts +51 -0
  164. package/dist/shared/model-client.js +77 -0
  165. package/dist/shared/model-client.js.map +1 -0
  166. package/dist/shared/plan-artifact.js +0 -2
  167. package/dist/shared/plan-artifact.js.map +1 -1
  168. package/dist/shared/status-offload.d.ts +83 -9
  169. package/dist/shared/status-offload.js +399 -79
  170. package/dist/shared/status-offload.js.map +1 -1
  171. package/dist/shared/status.js +14 -1
  172. package/dist/shared/status.js.map +1 -1
  173. package/dist/shared/tool-kind.d.ts +19 -0
  174. package/dist/shared/tool-kind.js +13 -0
  175. package/dist/shared/tool-kind.js.map +1 -1
  176. package/dist/shared/tool-row.d.ts +88 -0
  177. package/dist/shared/tool-row.js +127 -0
  178. package/dist/shared/tool-row.js.map +1 -0
  179. package/dist/shared/workspace/platform-dir.d.ts +25 -0
  180. package/dist/shared/workspace/platform-dir.js +38 -2
  181. package/dist/shared/workspace/platform-dir.js.map +1 -1
  182. package/dist/workflows/call-agent-orchestrator.js +56 -7
  183. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  184. package/dist/workflows/connect-mcp-server.d.ts +50 -0
  185. package/dist/workflows/connect-mcp-server.js +136 -15
  186. package/dist/workflows/connect-mcp-server.js.map +1 -1
  187. package/dist/workflows/types.d.ts +8 -0
  188. package/package.json +2 -2
  189. package/src/__test-utils__/approval-contract/contract.ts +224 -0
  190. package/src/__test-utils__/approval-contract/types.ts +179 -0
  191. package/src/__test-utils__/fake-artifact-storage.ts +72 -0
  192. package/src/__tests__/approval-gateway-contract.test.ts +29 -0
  193. package/src/__tests__/claimcheck-codec.test.ts +16 -53
  194. package/src/__tests__/golden-e2e.test.ts +2 -0
  195. package/src/__tests__/runner-token-coordinator.test.ts +3 -3
  196. package/src/activities/__tests__/call-agent-status.test.ts +135 -0
  197. package/src/activities/__tests__/call-llm.test.ts +1 -1
  198. package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
  199. package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
  200. package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
  201. package/src/activities/call-agent-status.ts +74 -4
  202. package/src/activities/call-llm.ts +18 -63
  203. package/src/activities/classify-tool-approvals.ts +101 -19
  204. package/src/activities/discover-mcp-server.ts +29 -1
  205. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
  206. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
  207. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
  208. package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
  209. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
  210. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
  211. package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
  212. package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
  213. package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
  214. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
  215. package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
  216. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
  217. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
  218. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
  219. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
  220. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
  221. package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
  222. package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
  223. package/src/activities/execute-cursor/approval-policy.ts +28 -159
  224. package/src/activities/execute-cursor/approval-state.ts +366 -18
  225. package/src/activities/execute-cursor/capture-flow.ts +323 -0
  226. package/src/activities/execute-cursor/cas-observations.ts +204 -0
  227. package/src/activities/execute-cursor/command-provenance.ts +168 -0
  228. package/src/activities/execute-cursor/exact-apply.ts +253 -0
  229. package/src/activities/execute-cursor/hook-script.ts +317 -51
  230. package/src/activities/execute-cursor/index.ts +575 -67
  231. package/src/activities/execute-cursor/message-translator.ts +963 -89
  232. package/src/activities/execute-cursor/prompt-builder.ts +80 -14
  233. package/src/activities/execute-cursor/workspace-setup.ts +257 -42
  234. package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
  235. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
  236. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
  237. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
  238. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
  239. package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
  240. package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
  241. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
  242. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +284 -0
  243. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
  244. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
  245. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
  246. package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
  247. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +91 -10
  248. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
  249. package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
  250. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
  251. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
  252. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
  253. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
  254. package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
  255. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
  256. package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
  257. package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
  258. package/src/activities/execute-deep-agent/hitl.ts +23 -7
  259. package/src/activities/execute-deep-agent/index.ts +463 -40
  260. package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
  261. package/src/activities/execute-deep-agent/setup.ts +223 -125
  262. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
  263. package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
  264. package/src/activities/execute-deep-agent/status-builder.ts +63 -8
  265. package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
  266. package/src/activities/execute-deep-agent/streaming.ts +3 -13
  267. package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
  268. package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
  269. package/src/activities/execute-deep-agent/v3-status-builder.ts +36 -2
  270. package/src/claimcheck/payload-codec.ts +8 -8
  271. package/src/client/stigmer-client.ts +9 -1
  272. package/src/middleware/__tests__/approval-gate.test.ts +488 -4
  273. package/src/middleware/approval-gate.ts +247 -38
  274. package/src/middleware/types.ts +5 -5
  275. package/src/shared/__tests__/activity-input.test.ts +78 -0
  276. package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
  277. package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
  278. package/src/shared/__tests__/approval-policy.test.ts +274 -40
  279. package/src/shared/__tests__/args-preview.test.ts +78 -0
  280. package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
  281. package/src/shared/__tests__/artifact-storage.test.ts +123 -11
  282. package/src/shared/__tests__/file-change.test.ts +85 -0
  283. package/src/shared/__tests__/file-tools.test.ts +90 -0
  284. package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
  285. package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
  286. package/src/shared/__tests__/model-client.test.ts +162 -0
  287. package/src/shared/__tests__/plan-artifact.test.ts +11 -26
  288. package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
  289. package/src/shared/__tests__/status-offload.test.ts +573 -16
  290. package/src/shared/__tests__/status.test.ts +4 -5
  291. package/src/shared/__tests__/tool-kind.test.ts +24 -1
  292. package/src/shared/__tests__/tool-row.test.ts +221 -0
  293. package/src/shared/activity-input.ts +57 -0
  294. package/src/shared/approval-canonicalize.ts +159 -0
  295. package/src/shared/approval-fingerprint.ts +148 -0
  296. package/src/shared/approval-policy.ts +303 -27
  297. package/src/shared/args-preview.ts +98 -0
  298. package/src/shared/artifact-storage.ts +62 -11
  299. package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
  300. package/src/shared/file-change.ts +64 -0
  301. package/src/shared/file-tools.ts +169 -0
  302. package/src/shared/filereview/__tests__/capture.test.ts +856 -0
  303. package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
  304. package/src/shared/filereview/__tests__/digest.test.ts +100 -0
  305. package/src/shared/filereview/__tests__/events.test.ts +245 -0
  306. package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
  307. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
  308. package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
  309. package/src/shared/filereview/capture.ts +727 -0
  310. package/src/shared/filereview/cas-substrate.ts +401 -0
  311. package/src/shared/filereview/digest.ts +83 -0
  312. package/src/shared/filereview/events.ts +449 -0
  313. package/src/shared/filereview/git-substrate.ts +555 -0
  314. package/src/shared/filereview/index.ts +60 -0
  315. package/src/shared/filereview/secret-paths.ts +121 -0
  316. package/src/shared/fingerprint-secret.ts +53 -0
  317. package/src/shared/model-client.ts +122 -0
  318. package/src/shared/plan-artifact.ts +0 -2
  319. package/src/shared/status-offload.ts +433 -77
  320. package/src/shared/status.ts +13 -0
  321. package/src/shared/tool-kind.ts +33 -0
  322. package/src/shared/tool-row.ts +135 -0
  323. package/src/shared/workspace/platform-dir.ts +41 -2
  324. package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
  325. package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
  326. package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
  327. package/src/workflows/call-agent-orchestrator.ts +53 -6
  328. package/src/workflows/connect-mcp-server.ts +179 -24
  329. package/src/workflows/types.ts +8 -0
@@ -1,4 +1,6 @@
1
1
  /**
2
+ * @regression file-hitl-phase0 — pins file-edit HITL fix #2 (see _projects/2026-06/20260630.01.file-change-hitl-redesign/tasks/T01_3_regression-manifest.md)
3
+ *
2
4
  * Behavior tests for the generated preToolUse bash hook.
3
5
  *
4
6
  * These run the ACTUAL bash script the runner writes into the workspace, feeding
@@ -11,118 +13,51 @@
11
13
  * Skipped automatically where bash is unavailable.
12
14
  */
13
15
 
14
- import { describe, it, expect, beforeAll, afterEach } from "vitest";
15
- import { execFileSync, execSync } from "node:child_process";
16
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
16
+ import { describe, it, expect, onTestFinished } from "vitest";
17
+ import { execFileSync } from "node:child_process";
18
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from "node:fs";
17
19
  import { tmpdir } from "node:os";
18
20
  import { join } from "node:path";
19
21
 
20
22
  import { generateHookScript } from "../hook-script.js";
21
- import { buildApprovalState, grantToken, toolIdentity, type ApprovalGrant } from "../approval-state.js";
22
- import type { McpToolPolicyEntry } from "../approval-state.js";
23
-
24
- let hasBash = false;
25
- try {
26
- execSync("bash -c 'exit 0'", { stdio: "ignore" });
27
- hasBash = true;
28
- } catch {
29
- hasBash = false;
23
+ import { buildApprovalState, grantToken, primaryToken, toolIdentity } from "../approval-state.js";
24
+ import { contentDigest } from "../../../shared/file-tools.js";
25
+ import {
26
+ setupCursorHookHarness as setup,
27
+ hasBash,
28
+ hookWrite,
29
+ hookEdit,
30
+ hookShell,
31
+ hookDelete,
32
+ hookRead,
33
+ hookMcp,
34
+ } from "../__test-utils__/cursor-hook-harness.js";
35
+
36
+ /** Decode the base64(JSON(tool_input)) the hook records on a denial. */
37
+ function decodeInput(b64: string | undefined): Record<string, unknown> {
38
+ if (!b64) throw new Error("ledger entry carried no input");
39
+ return JSON.parse(Buffer.from(b64, "base64").toString("utf-8")) as Record<string, unknown>;
30
40
  }
31
41
 
32
42
  const d = hasBash ? describe : describe.skip;
33
43
 
34
- const tempDirs: string[] = [];
35
- afterEach(() => {
36
- for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
37
- });
38
-
39
- interface Harness {
40
- decide(input: object): { permission: string; raw: string };
41
- ledger(): Array<{ toolName: string; token: string }>;
42
- resetLedger(): void;
43
- }
44
-
45
- function setup(opts: {
46
- autoApproveAll?: boolean;
47
- grants?: ApprovalGrant[];
48
- mcpPolicies?: Record<string, McpToolPolicyEntry>;
49
- noStateFile?: boolean;
50
- // Process the hook treats as "the runner". Defaults to this test process,
51
- // which is an ancestor of the bash child execFileSync spawns — so the scope
52
- // guard sees the call as the runner's own agent and applies the gate. Pass a
53
- // non-ancestor PID to exercise the foreign-client path (issue #173).
54
- runnerPid?: number;
55
- }): Harness {
56
- const ws = mkdtempSync(join(tmpdir(), "hook-script-"));
57
- tempDirs.push(ws);
58
- const dir = join(ws, ".cursor", "hooks");
59
- mkdirSync(dir, { recursive: true });
60
- const statePath = join(dir, "state.json");
61
- const ledgerPath = join(dir, "denials.jsonl");
62
- const scriptPath = join(dir, "hook.sh");
63
- writeFileSync(scriptPath, generateHookScript(statePath, ledgerPath, opts.runnerPid ?? process.pid), "utf-8");
64
-
65
- if (!opts.noStateFile) {
66
- const policies = new Map(
67
- Object.entries(opts.mcpPolicies ?? {}).map(([name, p]) => [
68
- `srv/${name}`,
69
- { toolName: name, mcpServerSlug: "srv", requiresApproval: p.requiresApproval, approvalMessage: p.message ?? "" },
70
- ]),
71
- );
72
- const state = buildApprovalState(policies, opts.autoApproveAll ?? false, opts.grants);
73
- writeFileSync(statePath, JSON.stringify(state), "utf-8");
74
- }
75
-
76
- return {
77
- decide(input: object) {
78
- const raw = execFileSync("bash", [scriptPath], { input: JSON.stringify(input) }).toString();
79
- const permission = raw.includes('"permission":"deny"') ? "deny" : raw.includes('"permission":"allow"') ? "allow" : "?";
80
- return { permission, raw };
81
- },
82
- ledger() {
83
- if (!existsSync(ledgerPath)) return [];
84
- return readFileSync(ledgerPath, "utf-8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
85
- },
86
- resetLedger() {
87
- writeFileSync(ledgerPath, "", "utf-8");
88
- },
89
- };
90
- }
91
-
92
- // Real preToolUse hook-input shapes (PascalCase name, file_path/command in
93
- // tool_input). These omit hook_event_name on purpose: a payload with no event
94
- // must still take the built-in arm (the script only diverts to the MCP arm on an
95
- // explicit beforeMCPExecution).
96
- const hookWrite = (filePath: string) => ({ tool_name: "Write", tool_input: { file_path: filePath, content: "x" } });
97
- const hookShell = (command: string) => ({ tool_name: "Shell", tool_input: { command, cwd: "/x", timeout: 30000 } });
98
- const hookDelete = (filePath: string) => ({ tool_name: "Delete", tool_input: { file_path: filePath } });
99
- const hookRead = (filePath: string) => ({ tool_name: "Read", tool_input: { file_path: filePath } });
100
-
101
- // Real beforeMCPExecution shape (captured live): bare tool_name, tool_input as a
102
- // JSON STRING, server identity, and the hook_event_name discriminator.
103
- const hookMcp = (name: string, input: Record<string, unknown> = {}) => ({
104
- tool_name: name,
105
- tool_input: JSON.stringify(input),
106
- mcp_server_name: "srv",
107
- command: "npx -y srv mcp",
108
- hook_event_name: "beforeMCPExecution",
109
- });
110
-
111
44
  d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
112
- it("denies gated built-ins (Write/Shell/Delete) and records a category+salient token", () => {
45
+ it("denies gated built-ins and records the PRIMARY token (content-exact for a write, coarse for shell/delete)", () => {
113
46
  const h = setup({});
114
47
 
115
- for (const [input, category, salient] of [
116
- [hookWrite("/x/a.txt"), "write", "/x/a.txt"],
117
- [hookShell("rm -rf build"), "shell", "rm -rf build"],
118
- [hookDelete("/x/b.txt"), "delete", "/x/b.txt"],
48
+ for (const [input, category, salient, args] of [
49
+ // hookWrite defaults content "x"; the hook records the content-exact token.
50
+ [hookWrite("/x/a.txt"), "write", "/x/a.txt", { content: "x" }],
51
+ [hookShell("rm -rf build"), "shell", "rm -rf build", {}],
52
+ [hookDelete("/x/b.txt"), "delete", "/x/b.txt", {}],
119
53
  ] as const) {
120
54
  h.resetLedger();
121
55
  expect(h.decide(input).permission).toBe("deny");
122
56
  const ledger = h.ledger();
123
57
  expect(ledger).toHaveLength(1);
124
- // Byte-identical to the runner's grantToken(category, salient).
125
- expect(ledger[0].token).toBe(grantToken(category, salient));
58
+ // Byte-identical to the runner's primaryToken: content-exact when the hook
59
+ // can compute an edit digest (the write), else the coarse grantToken.
60
+ expect(ledger[0].token).toBe(primaryToken(category, salient, contentDigest(args)));
126
61
  }
127
62
  });
128
63
 
@@ -137,16 +72,22 @@ d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
137
72
  expect(h.decide(hookWrite("/x/a.txt")).permission).toBe("allow");
138
73
  });
139
74
 
140
- it("allows the EXACT granted resource and re-gates any other (no name-only over-grant)", () => {
141
- const id = toolIdentity("edit", "", { path: "/x/a.txt" });
142
- const h = setup({ grants: [{ toolName: "edit", mcpServerSlug: "", key: id.key, salient: id.salient }] });
143
-
144
- // Same resource the user approved -> allowed on the resumed turn.
145
- expect(h.decide(hookWrite("/x/a.txt")).permission).toBe("allow");
146
- // A different file is NOT covered by the grant -> still gated.
147
- expect(h.decide(hookWrite("/x/OTHER.txt")).permission).toBe("deny");
75
+ it("a run-lifetime category lease allows that category and ONLY that category", () => {
76
+ // "Approve all shell commands" must let later shell calls through while still
77
+ // gating a write the scoped successor to the old global auto-approve-all.
78
+ const h = setup({ leasedCategories: ["shell"] });
79
+ expect(h.decide(hookShell("rm -rf build")).permission).toBe("allow");
80
+ const writeDecision = h.decide(hookWrite("/x/a.txt"));
81
+ expect(writeDecision.permission).toBe("deny");
82
+ // The denied, non-leased write is still recorded for the runner.
83
+ expect(h.ledger().map((e) => e.toolName)).toContain("Write");
148
84
  });
149
85
 
86
+ // Exact-resource lease isolation ("no name-only over-grant") moved to the
87
+ // gateway Contract Test Kit's invariant 10, where the SAME bash hook is driven
88
+ // through the Cursor substrate adapter alongside the deep-agent substrate. See
89
+ // src/__tests__/approval-gateway-contract.test.ts.
90
+
150
91
  it("fails closed (deny) when the state file is missing", () => {
151
92
  const h = setup({ noStateFile: true });
152
93
  expect(h.decide(hookWrite("/x/a.txt")).permission).toBe("deny");
@@ -173,6 +114,11 @@ d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
173
114
  // and that it should continue, NOT stop and wait or ask for permission.
174
115
  expect(res.raw).toContain("submitted to the user for approval automatically");
175
116
  expect(res.raw).toContain("continue with the rest of the task");
117
+ // It must reframe the deny as the approval gate working as intended and
118
+ // forbid the "fix your Cursor settings" narration that the leaky MCP deny
119
+ // path otherwise provokes.
120
+ expect(res.raw.toLowerCase()).toContain("not an error");
121
+ expect(res.raw.toLowerCase()).toContain("cursor settings");
176
122
  // The old propose-then-wait framing and internal sentinel must be gone.
177
123
  expect(res.raw).not.toContain("STIGMER_APPROVAL_REQUIRED");
178
124
  expect(res.raw).not.toContain("Stop and wait");
@@ -181,7 +127,7 @@ d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
181
127
  it("allows a require-approval MCP tool once it has been granted (reinvocation)", () => {
182
128
  const h = setup({
183
129
  mcpPolicies: { click: { requiresApproval: true } },
184
- grants: [{ toolName: "click", mcpServerSlug: "srv", key: "click", salient: "" }],
130
+ grants: [{ toolName: "click", mcpServerSlug: "srv", key: "click", salient: "", contentDigest: "", sourceToolCallId: "consent-1" }],
185
131
  });
186
132
  expect(h.decide(hookMcp("click")).permission).toBe("allow");
187
133
  expect(h.ledger()).toEqual([]);
@@ -227,6 +173,49 @@ d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
227
173
  });
228
174
  });
229
175
 
176
+ // The hook captures the COMPLETE tool_input on every denial (base64(JSON)),
177
+ // so the runner can overlay the proposed change onto the gated tool call for
178
+ // the approval preview — the cursor analog of the native harness reading args
179
+ // from graph state at the interrupt. These pin the capture across taxonomies
180
+ // (built-in object tool_input vs. MCP JSON-string tool_input).
181
+ describe("captures tool_input on the denial ledger", () => {
182
+ it("records the full built-in write input (object tool_input)", () => {
183
+ const h = setup({});
184
+ expect(h.decide(hookWrite("/x/a.txt", "export const x = 1;\n")).permission).toBe("deny");
185
+ const ledger = h.ledger();
186
+ expect(ledger).toHaveLength(1);
187
+ expect(decodeInput(ledger[0].input)).toEqual({
188
+ file_path: "/x/a.txt",
189
+ content: "export const x = 1;\n",
190
+ });
191
+ });
192
+
193
+ it("records an edit's old/new replacement strings", () => {
194
+ const h = setup({});
195
+ expect(h.decide(hookEdit("/x/a.txt", "alpha", "beta")).permission).toBe("deny");
196
+ expect(decodeInput(h.ledger()[0].input)).toEqual({
197
+ file_path: "/x/a.txt",
198
+ old_string: "alpha",
199
+ new_string: "beta",
200
+ });
201
+ });
202
+
203
+ it("parses and records the MCP JSON-STRING tool_input", () => {
204
+ // Cursor delivers MCP tool_input as a JSON string, not an object — the
205
+ // extractor must parse it so the captured input is the same object shape.
206
+ const h = setup({ mcpPolicies: { click: { requiresApproval: true } } });
207
+ expect(h.decide(hookMcp("click", { app: "Slack", element_index: "59" })).permission).toBe("deny");
208
+ expect(decodeInput(h.ledger()[0].input)).toEqual({ app: "Slack", element_index: "59" });
209
+ });
210
+
211
+ it("captures large multi-line content intact (printf, not echo/argv)", () => {
212
+ const content = "line\n".repeat(20_000); // ~100 KB, exercises the printf write
213
+ const h = setup({});
214
+ expect(h.decide(hookWrite("/x/big.ts", content)).permission).toBe("deny");
215
+ expect(decodeInput(h.ledger()[0].input).content).toBe(content);
216
+ });
217
+ });
218
+
230
219
  // Regression: the original grep-based extraction truncated string values at
231
220
  // the first JSON-escaped character, so a shell command containing double
232
221
  // quotes (e.g. `printf '%s' 'x' > "file"`) produced a ledger token that never
@@ -252,13 +241,100 @@ d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
252
241
  it("allows the exact granted shell command even when it contains quotes", () => {
253
242
  const command = 'printf \'%s\' \'hello-resume\' > "/x/resumed-gate.txt"';
254
243
  const id = toolIdentity("shell", "", { command });
255
- const h = setup({ grants: [{ toolName: "shell", mcpServerSlug: "", key: id.key, salient: id.salient }] });
244
+ const h = setup({ grants: [{ toolName: "shell", mcpServerSlug: "", key: id.key, salient: id.salient, contentDigest: "", sourceToolCallId: "consent-1" }] });
256
245
 
257
246
  expect(h.decide(hookShell(command)).permission).toBe("allow");
258
247
  // A different command is NOT covered by the grant -> still gated.
259
248
  expect(h.decide(hookShell('rm -rf "/x"')).permission).toBe("deny");
260
249
  });
261
250
 
251
+ // The full runner<->hook closure, per gated category and for the bytes most
252
+ // likely to drift: the token the RUNNER writes into approval-state.json (via
253
+ // buildApprovalState, exactly as index.ts does on resume) is byte-identical to
254
+ // the token the HOOK records when it denies the same action, and the hook's
255
+ // grep -qF membership check then ALLOWS the re-issued call. This is the precise
256
+ // production safety property — "approve once, allowed on the resumed turn" —
257
+ // closed end to end rather than asserted on either half alone.
258
+ describe("runner grant <-> hook allow closure (tricky args, every gated category)", () => {
259
+ const cases: Array<{
260
+ label: string;
261
+ streamName: string;
262
+ streamArgs: Record<string, unknown>;
263
+ hookInput: object;
264
+ otherHookInput: object;
265
+ }> = [
266
+ {
267
+ label: "write — path with spaces, quotes, and unicode",
268
+ streamName: "edit",
269
+ // Content matches hookWrite's default ("x") so the runner-side grant
270
+ // digest equals the hook-side content digest (content-exact closure).
271
+ streamArgs: { path: '/work/a dir/"café" notes.md', content: "x" },
272
+ hookInput: hookWrite('/work/a dir/"café" notes.md'),
273
+ otherHookInput: hookWrite("/work/other.md"),
274
+ },
275
+ {
276
+ label: "shell — multi-line heredoc with quotes and unicode",
277
+ streamName: "shell",
278
+ streamArgs: { command: "cat > notes.md << 'EOF'\n# Notes — \"x\" café\nEOF" },
279
+ hookInput: hookShell("cat > notes.md << 'EOF'\n# Notes — \"x\" café\nEOF"),
280
+ otherHookInput: hookShell("rm -rf build"),
281
+ },
282
+ {
283
+ label: "delete — path with quotes",
284
+ streamName: "delete",
285
+ streamArgs: { path: '/work/"old" file.tmp' },
286
+ hookInput: hookDelete('/work/"old" file.tmp'),
287
+ otherHookInput: hookDelete("/work/keep.tmp"),
288
+ },
289
+ ];
290
+
291
+ for (const { label, streamName, streamArgs, hookInput, otherHookInput } of cases) {
292
+ it(`${label}: the state-file token the runner writes is the token the hook denies and then allows`, () => {
293
+ // 1. The hook denies the un-granted call and records its identity token.
294
+ const denyHarness = setup({});
295
+ expect(denyHarness.decide(hookInput).permission).toBe("deny");
296
+ const denialLedger = denyHarness.ledger();
297
+ expect(denialLedger).toHaveLength(1);
298
+ const hookDenialToken = denialLedger[0].token;
299
+
300
+ // 2. The runner mints the grant from the STREAM-side identity (the only
301
+ // side it sees) and writes it into the real state file via
302
+ // buildApprovalState — the exact path index.ts takes on resume.
303
+ const id = toolIdentity(streamName, "", streamArgs);
304
+ const state = buildApprovalState(new Map(), false, new Set(), [
305
+ { toolName: streamName, mcpServerSlug: "", key: id.key, salient: id.salient, contentDigest: contentDigest(streamArgs), sourceToolCallId: "consent-1" },
306
+ ]);
307
+
308
+ // 3. The state file carries the byte-exact token the hook recomputed —
309
+ // the grep -qF membership the bash hook performs (string equality).
310
+ expect(state.approvedGrantTokens).toContain(hookDenialToken);
311
+
312
+ // 4. With that state file, the hook ALLOWS the re-issued (fresh-id) call
313
+ // and re-gates a DIFFERENT resource of the same category.
314
+ const grantHarness = setup({ grants: state.approvedGrants });
315
+ expect(grantHarness.decide(hookInput).permission).toBe("allow");
316
+ expect(grantHarness.decide(otherHookInput).permission).toBe("deny");
317
+ });
318
+ }
319
+ });
320
+
321
+ it("sibling isolation: approving one edit does NOT allow a DIFFERENT edit to the same file", () => {
322
+ // The exact reported bug, closed end-to-end through the real hook: approving
323
+ // the rename must re-gate the later `## TODO` edit to the SAME file.
324
+ const path = "/work/notes.md";
325
+ const renameArgs = { path, content: "Planton" };
326
+ const id = toolIdentity("edit", "", renameArgs);
327
+ const state = buildApprovalState(new Map(), false, new Set(), [
328
+ { toolName: "edit", mcpServerSlug: "", key: id.key, salient: id.salient, contentDigest: contentDigest(renameArgs), sourceToolCallId: "consent-1" },
329
+ ]);
330
+ const h = setup({ grants: state.approvedGrants });
331
+
332
+ // The approved edit, re-issued with the SAME content, is allowed.
333
+ expect(h.decide(hookWrite(path, "Planton")).permission).toBe("allow");
334
+ // A DIFFERENT edit to the SAME file is re-gated — the sibling hole is closed.
335
+ expect(h.decide(hookWrite(path, "## TODO")).permission).toBe("deny");
336
+ });
337
+
262
338
  // Issue #173: the hook ships on the workspace's shared .cursor/hooks.json, so
263
339
  // the user's own Cursor IDE (a DIFFERENT process tree) would load and run it
264
340
  // too. The scope guard must allow any invocation that does not descend from
@@ -303,19 +379,217 @@ d("generated approval hook (preToolUse + beforeMCPExecution)", () => {
303
379
  });
304
380
  });
305
381
 
382
+ // Capture mode (git workspaces): file mutations flow during the turn (the
383
+ // runner captures the whole change set with git and gates it per-file at the
384
+ // turn boundary), while shell/MCP and gitignored writes stay on the deny-gate.
385
+ describe("capture mode (git workspaces)", () => {
386
+ it("allows write/edit/delete to flow and records no denial", () => {
387
+ const h = setup({ captureMode: true });
388
+ expect(h.decide(hookWrite("normal.txt")).permission).toBe("allow");
389
+ expect(h.decide(hookEdit("normal.txt")).permission).toBe("allow");
390
+ expect(h.decide(hookDelete("normal.txt")).permission).toBe("allow");
391
+ expect(h.ledger()).toEqual([]);
392
+ });
393
+
394
+ it("still gates shell (it is not git-reversible)", () => {
395
+ const h = setup({ captureMode: true });
396
+ expect(h.decide(hookShell("rm -rf build")).permission).toBe("deny");
397
+ expect(h.ledger().map((e) => e.toolName)).toContain("Shell");
398
+ });
399
+
400
+ it("still gates require-approval MCP tools", () => {
401
+ const h = setup({ captureMode: true, mcpPolicies: { click: { requiresApproval: true } } });
402
+ expect(h.decide(hookMcp("click")).permission).toBe("deny");
403
+ });
404
+
405
+ it("keeps gating a write to a GITIGNORED path (the snapshot cannot revert it)", () => {
406
+ // .env-style ignored paths are invisible to the git snapshot, so capture
407
+ // mode must still gate them for explicit approval.
408
+ const h = setup({ captureMode: true, gitignored: ["secret.txt"] });
409
+ expect(h.decide(hookWrite("secret.txt")).permission).toBe("deny");
410
+ expect(h.ledger().map((e) => e.toolName)).toContain("Write");
411
+ // A non-ignored sibling still flows.
412
+ expect(h.decide(hookWrite("normal.txt")).permission).toBe("allow");
413
+ });
414
+
415
+ it("keeps gating a DELETE of a gitignored path", () => {
416
+ const h = setup({ captureMode: true, gitignored: ["secret.txt"] });
417
+ expect(h.decide(hookDelete("secret.txt")).permission).toBe("deny");
418
+ });
419
+ });
420
+
421
+ // CAS parity (DD-18): with captureIgnored on, a non-secret gitignored write no
422
+ // longer stays on the deny-gate — the hook stages its pre-turn bytes into the
423
+ // cas-observations sidecar and ALLOWS it (apply-then-review), while a secret-
424
+ // like gitignored write is hard-blocked and recorded as unreviewable. The
425
+ // sidecar is read back through the real reader.
426
+ describe("capture mode + captureIgnored (gitignored CAS capture)", () => {
427
+ it("stages a non-secret gitignored ADD and allows it (before=null)", async () => {
428
+ const h = setup({ captureMode: true, captureIgnored: true, gitignored: ["*.log"] });
429
+ expect(h.decide(hookWrite("app.log", "hello")).permission).toBe("allow");
430
+ // Flowed, not denied — it must not become a WAITING_APPROVAL row.
431
+ expect(h.ledger()).toEqual([]);
432
+ const obs = await h.observations();
433
+ expect(obs.secretPaths).toEqual([]);
434
+ expect(obs.captured).toHaveLength(1);
435
+ expect(obs.captured[0].path).toBe("app.log");
436
+ expect(obs.captured[0].before).toBeNull();
437
+ });
438
+
439
+ it("stages a non-secret gitignored MODIFY with the true pre-turn before-bytes", async () => {
440
+ const h = setup({ captureMode: true, captureIgnored: true, gitignored: ["*.log"] });
441
+ writeFileSync(join(h.root, "app.log"), "ORIGINAL", "utf-8");
442
+ expect(h.decide(hookWrite("app.log", "NEW")).permission).toBe("allow");
443
+ const obs = await h.observations();
444
+ expect(obs.captured).toHaveLength(1);
445
+ expect(obs.captured[0].before).not.toBeNull();
446
+ expect(Buffer.from(obs.captured[0].before!).toString("utf8")).toBe("ORIGINAL");
447
+ });
448
+
449
+ it("first-touch-wins: two edits to one gitignored path keep the original before", async () => {
450
+ const h = setup({ captureMode: true, captureIgnored: true, gitignored: ["*.log"] });
451
+ writeFileSync(join(h.root, "app.log"), "ORIGINAL", "utf-8");
452
+ expect(h.decide(hookWrite("app.log", "FIRST")).permission).toBe("allow");
453
+ // Simulate the first write having applied, then a second edit this turn.
454
+ writeFileSync(join(h.root, "app.log"), "FIRST", "utf-8");
455
+ expect(h.decide(hookEdit("app.log")).permission).toBe("allow");
456
+ const obs = await h.observations();
457
+ expect(obs.captured).toHaveLength(1);
458
+ expect(Buffer.from(obs.captured[0].before!).toString("utf8")).toBe("ORIGINAL");
459
+ });
460
+
461
+ it("hard-blocks a secret-like gitignored write and records NO denial-ledger entry", async () => {
462
+ const h = setup({ captureMode: true, captureIgnored: true, gitignored: [".env"] });
463
+ const d = h.decide(hookWrite(".env", "API_KEY=abc"));
464
+ expect(d.permission).toBe("deny");
465
+ expect(d.raw).toContain("blocked for security");
466
+ // A secret is NOT approvable: it must never enter the denial ledger (which
467
+ // drives WAITING_APPROVAL) — it surfaces as DIFF_UNREVIEWABLE instead.
468
+ expect(h.ledger()).toEqual([]);
469
+ const obs = await h.observations();
470
+ expect(obs.captured).toEqual([]);
471
+ expect(obs.secretPaths).toEqual([".env"]);
472
+ });
473
+
474
+ it("captures under auto_approve_all too (capture is a turn property, not authorization)", async () => {
475
+ const h = setup({
476
+ captureMode: true,
477
+ captureIgnored: true,
478
+ autoApproveAll: true,
479
+ gitignored: ["*.log", ".env"],
480
+ });
481
+ // Non-secret gitignored write is staged + allowed even under the bypass...
482
+ expect(h.decide(hookWrite("app.log", "x")).permission).toBe("allow");
483
+ // ...and a secret-like one is STILL hard-blocked under the bypass.
484
+ expect(h.decide(hookWrite(".env", "SECRET")).permission).toBe("deny");
485
+ const obs = await h.observations();
486
+ expect(obs.captured.map((c) => c.path)).toEqual(["app.log"]);
487
+ expect(obs.secretPaths).toEqual([".env"]);
488
+ });
489
+
490
+ it("does not stage a git-tracked write (git captures it) — flows, no observation", async () => {
491
+ const h = setup({ captureMode: true, captureIgnored: true, gitignored: ["*.log"] });
492
+ expect(h.decide(hookWrite("tracked.ts", "x")).permission).toBe("allow");
493
+ const obs = await h.observations();
494
+ expect(obs.captured).toEqual([]);
495
+ expect(obs.secretPaths).toEqual([]);
496
+ });
497
+
498
+ it("still gates a gitignored DELETE (no CAS capture path, parity with deep-agent)", async () => {
499
+ const h = setup({ captureMode: true, captureIgnored: true, gitignored: ["*.log"] });
500
+ expect(h.decide(hookDelete("app.log")).permission).toBe("deny");
501
+ const obs = await h.observations();
502
+ expect(obs.captured).toEqual([]);
503
+ });
504
+
505
+ it("with captureIgnored OFF, a gitignored write stays denied and stages nothing", async () => {
506
+ const h = setup({ captureMode: true, captureIgnored: false, gitignored: ["*.log"] });
507
+ expect(h.decide(hookWrite("app.log", "x")).permission).toBe("deny");
508
+ const obs = await h.observations();
509
+ expect(obs.captured).toEqual([]);
510
+ });
511
+ });
512
+
513
+ // Slice 2c: a NON-git workspace has no git snapshot, so EVERY file write is
514
+ // CAS-staged and flowed for review (not only gitignored ones), a delete stays
515
+ // gated (no CAS delete-capture path, parity with the deep-agent), and shell/MCP
516
+ // gate as always. The workspace is deliberately NOT git-initialized.
517
+ describe("non-git workspace CAS capture (Slice 2c)", () => {
518
+ it("stages EVERY write (not just gitignored) and allows it, no denial", async () => {
519
+ const h = setup({ captureMode: true, captureIgnored: true, gitWorkspace: false });
520
+ writeFileSync(join(h.root, "notes.md"), "ORIGINAL", "utf-8");
521
+ expect(h.decide(hookWrite("notes.md", "NEW")).permission).toBe("allow");
522
+ expect(h.ledger()).toEqual([]);
523
+ const obs = await h.observations();
524
+ expect(obs.secretPaths).toEqual([]);
525
+ expect(obs.captured).toHaveLength(1);
526
+ expect(obs.captured[0].path).toBe("notes.md");
527
+ expect(Buffer.from(obs.captured[0].before!).toString("utf8")).toBe("ORIGINAL");
528
+ });
529
+
530
+ it("stages an ADD (before=null) and allows it", async () => {
531
+ const h = setup({ captureMode: true, captureIgnored: true, gitWorkspace: false });
532
+ expect(h.decide(hookWrite("created.ts", "x")).permission).toBe("allow");
533
+ const obs = await h.observations();
534
+ expect(obs.captured).toHaveLength(1);
535
+ expect(obs.captured[0].before).toBeNull();
536
+ });
537
+
538
+ it("hard-blocks a secret-like write and records NO denial-ledger entry", async () => {
539
+ const h = setup({ captureMode: true, captureIgnored: true, gitWorkspace: false });
540
+ const dcn = h.decide(hookWrite(".env", "API_KEY=abc"));
541
+ expect(dcn.permission).toBe("deny");
542
+ expect(dcn.raw).toContain("blocked for security");
543
+ expect(h.ledger()).toEqual([]);
544
+ const obs = await h.observations();
545
+ expect(obs.captured).toEqual([]);
546
+ expect(obs.secretPaths).toEqual([".env"]);
547
+ });
548
+
549
+ it("still gates a DELETE (deny-gate, parity with deep-agent) and stages nothing", async () => {
550
+ const h = setup({ captureMode: true, captureIgnored: true, gitWorkspace: false });
551
+ expect(h.decide(hookDelete("notes.md")).permission).toBe("deny");
552
+ expect(h.ledger()).toHaveLength(1);
553
+ const obs = await h.observations();
554
+ expect(obs.captured).toEqual([]);
555
+ });
556
+
557
+ it("still gates shell (never git-reversible, never CAS-captured)", async () => {
558
+ const h = setup({ captureMode: true, captureIgnored: true, gitWorkspace: false });
559
+ expect(h.decide(hookShell("rm -rf /")).permission).toBe("deny");
560
+ });
561
+
562
+ it("still gates a require-approval MCP tool", async () => {
563
+ const h = setup({
564
+ captureMode: true,
565
+ captureIgnored: true,
566
+ gitWorkspace: false,
567
+ mcpPolicies: { click: { requiresApproval: true } },
568
+ });
569
+ expect(h.decide(hookMcp("click")).permission).toBe("deny");
570
+ });
571
+ });
572
+
306
573
  it("still denies gated tools via the bash fallback when the Node binary is unavailable", () => {
307
574
  const ws = mkdtempSync(join(tmpdir(), "hook-script-fallback-"));
308
- tempDirs.push(ws);
575
+ onTestFinished(() => rmSync(ws, { recursive: true, force: true }));
309
576
  const dir = join(ws, ".cursor", "hooks");
310
577
  mkdirSync(dir, { recursive: true });
311
578
  const statePath = join(dir, "state.json");
312
579
  const ledgerPath = join(dir, "denials.jsonl");
580
+ const pointerPath = join(dir, "active.json");
313
581
  const scriptPath = join(dir, "hook.sh");
314
- // Break the baked Node path to force the grep/cut fallback.
315
- const script = generateHookScript(statePath, ledgerPath, process.pid)
582
+ // Break the baked Node path to force the grep/cut fallback for BOTH the
583
+ // active-turn pointer parse and the tool identity extraction.
584
+ const script = generateHookScript(pointerPath)
316
585
  .replace(`NODE_BIN="${process.execPath}"`, 'NODE_BIN="/nonexistent/node"');
317
586
  writeFileSync(scriptPath, script, "utf-8");
318
- writeFileSync(statePath, JSON.stringify(buildApprovalState(new Map(), false)), "utf-8");
587
+ writeFileSync(
588
+ pointerPath,
589
+ JSON.stringify({ stateFile: statePath, ledgerFile: ledgerPath, runnerPid: process.pid }),
590
+ "utf-8",
591
+ );
592
+ writeFileSync(statePath, JSON.stringify(buildApprovalState(new Map(), false, new Set())), "utf-8");
319
593
 
320
594
  const raw = execFileSync("bash", [scriptPath], {
321
595
  input: JSON.stringify(hookWrite("/x/a.txt")),