@stigmer/runner 3.0.9-dev.20260616060535 → 3.1.1

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,323 @@
1
+ /**
2
+ * Cursor adapter over the harness-agnostic capture-mode orchestration
3
+ * ({@link ../../shared/filereview/capture.js}).
4
+ *
5
+ * The substrate (git snapshot/restore) and the orchestration (baseline/candidate/
6
+ * reconcile authoring) live in `shared/filereview/` so the Cursor and deep-agent
7
+ * harnesses author IDENTICAL ledger entries. This adapter binds the Cursor
8
+ * specifics:
9
+ * - `harnessId = "cursor"` (stamped on the BASELINE payload the projection reads);
10
+ * - the Cursor gate files written into the workspace, excluded from the captured
11
+ * diff (`CURSOR_RUNNER_OWNED_PATHS`);
12
+ * - stamping the streamed file-edit transcript rows that flowed this turn with
13
+ * the change set id (`stampFlowedFileEditRows`) — the rows stay visible as
14
+ * observational/audit records at their transcript positions, while
15
+ * `file_change_sets` remains the single DECISION surface.
16
+ *
17
+ * The three turn-boundary functions keep their original signatures so the
18
+ * activity wiring (index.ts) and the cutover tests are unchanged.
19
+ */
20
+
21
+ import { readFile } from "node:fs/promises";
22
+ import { join } from "node:path";
23
+ import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
24
+ import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
25
+ import type { FileChangeSet, TurnCommandProvenance } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
26
+ import { FileCaptureClass } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
27
+ import { approvalCategory } from "./approval-policy.js";
28
+ import { toolIdentity, primaryToken } from "./approval-state.js";
29
+ import { readCasObservations } from "./cas-observations.js";
30
+ import { contentDigest } from "../../shared/file-tools.js";
31
+ import { isToolCallRowHidden, stampFileEditRow } from "../../shared/tool-row.js";
32
+ import {
33
+ applyCaptureDecisions as sharedApplyCaptureDecisions,
34
+ captureBaselineToLedger as sharedCaptureBaselineToLedger,
35
+ captureCandidateToLedger as sharedCaptureCandidateToLedger,
36
+ type CaptureResumeResult,
37
+ } from "../../shared/filereview/capture.js";
38
+ import { hasCandidateCaptured } from "../../shared/filereview/events.js";
39
+ import { partitionIgnoredPathsBySecret } from "../../shared/filereview/secret-paths.js";
40
+ import { casBlobReader, type CasPathCapture } from "../../shared/filereview/cas-substrate.js";
41
+ import type { GitSubstrateChange as GitCapturedChange } from "../../shared/filereview/git-substrate.js";
42
+ import type { ArtifactStorage } from "../../shared/artifact-storage.js";
43
+
44
+ export type { CaptureResumeResult };
45
+
46
+ /** The harness id the projection reads from the BASELINE payload (load-bearing). */
47
+ const HARNESS_ID = "cursor";
48
+
49
+ /**
50
+ * Workspace-relative paths the Cursor gate writes into the repo. They are excluded
51
+ * from capture so a turn's diff never shows the gate's own machinery. (The
52
+ * workspace-scoped gate dir and SDK state live under `~/.stigmer` / the
53
+ * git-excluded `.stigmer`, so they need no exclusion here.)
54
+ */
55
+ const CURSOR_RUNNER_OWNED_PATHS: readonly string[] = [
56
+ ".cursor/hooks.json",
57
+ ".cursor/rules/stigmer-tool-approval.mdc",
58
+ ];
59
+
60
+ // `deriveCaptureMode` is the single capture-vs-deny-gate decision, now shared by
61
+ // BOTH harnesses from `shared/filereview/capture.ts`. Re-exported here so existing
62
+ // Cursor imports (index.ts, the deny-gate-exact-apply seam test) keep resolving
63
+ // from this adapter.
64
+ export { deriveCaptureMode } from "../../shared/filereview/capture.js";
65
+
66
+ /**
67
+ * Turn start: pin the pre-turn working tree behind the baseline ref and author
68
+ * BASELINE_CAPTURED (harnessId "cursor"). Returns the baseline tree sha for the
69
+ * turn-end diff.
70
+ */
71
+ export function captureBaselineToLedger(opts: {
72
+ readonly status: AgentExecutionStatus;
73
+ readonly gitRoot: string;
74
+ readonly executionId: string;
75
+ readonly changeSetId: string;
76
+ /** True (default) for a git work tree; false for a CAS-only non-git workspace. */
77
+ readonly gitWorkspace?: boolean;
78
+ }): Promise<string> {
79
+ return sharedCaptureBaselineToLedger({
80
+ ...opts,
81
+ harnessId: HARNESS_ID,
82
+ excludePaths: CURSOR_RUNNER_OWNED_PATHS,
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Turn end: capture the change set + author CANDIDATE_CAPTURED, then stamp the
88
+ * streamed file-edit rows that flowed this turn with the change set id — the
89
+ * rows stay visible in place as observational records while `file_change_sets`
90
+ * remains the single decision surface. The working tree is LEFT applied
91
+ * (Cursor parity).
92
+ *
93
+ * The change set is HYBRID (git tree) or CAS-only (non-git): the hook-staged
94
+ * writes from the cas-observations sidecar this turn ({@link readCasObservations})
95
+ * — the Cursor analog of deep-agent's `buildCasTurnCaptures` — composed with the
96
+ * git-tracked diff when `gitWorkspace`. Non-secret staged paths become
97
+ * `GIT_IGNORED_CAPTURED` (git tree) or `NON_GIT_CAS` (non-git) CAS captures
98
+ * (before-bytes from the sidecar, after-bytes re-read from disk);
99
+ * secret-blocked paths become content-less `DIFF_UNREVIEWABLE` entries. The
100
+ * boundary re-runs {@link partitionIgnoredPathsBySecret} as a fail-closed
101
+ * backstop, so a secret that ever slipped into the captured set still has its
102
+ * bytes withheld from durable storage.
103
+ *
104
+ * `deniedTokens` are the identities the hook gated this turn (shell/MCP, or a
105
+ * gitignored delete). A streamed file-edit row whose identity is in that set is
106
+ * left for the deny-gate reconcile path — it did NOT flow. A flowed gitignored
107
+ * write is NOT in that set (the hook allowed it), so it is stamped like any
108
+ * other flowed edit and its captured delta surfaces as a CAS entry in the set.
109
+ *
110
+ * `hitlDir`/`storage` are omitted only by callers with no artifact storage
111
+ * (captureIgnored off); the CAS half is then skipped and this is a git-only
112
+ * capture exactly as before.
113
+ *
114
+ * Mutates `messages` and `status` in place. Returns the captured git changes.
115
+ */
116
+ export async function captureTurnToLedger(opts: {
117
+ readonly status: AgentExecutionStatus;
118
+ readonly gitRoot: string;
119
+ readonly executionId: string;
120
+ readonly changeSetId: string;
121
+ readonly baselineTree: string;
122
+ readonly messages: AgentMessage[];
123
+ readonly deniedTokens: ReadonlySet<string>;
124
+ readonly hitlDir?: string;
125
+ readonly storage?: ArtifactStorage;
126
+ /**
127
+ * Sub-agent tool-call ids that existed BEFORE this turn's stream (the seeded
128
+ * prior sub-agents). Sub-agent edit rows created this turn are stamped with the
129
+ * parent change set id; rows in this set are skipped so a resume never
130
+ * re-stamps a prior turn's sub-agent rows. Omit when there are no prior
131
+ * sub-agents.
132
+ */
133
+ readonly priorSubAgentToolCallIds?: ReadonlySet<string>;
134
+ /**
135
+ * True (default) for a git work tree — the candidate is the git diff composed
136
+ * with the gitignored CAS captures (`GIT_IGNORED_CAPTURED`). False for a non-git
137
+ * workspace (Slice 2c): there is no git diff, so the whole change set is the CAS
138
+ * captures the hook staged for EVERY touched path (`NON_GIT_CAS`).
139
+ */
140
+ readonly gitWorkspace?: boolean;
141
+ /**
142
+ * The approved-command turn facts (DD-28), derived by the caller from the
143
+ * turn's tool calls ({@link ../command-provenance.js}). Present only when the
144
+ * turn qualifies; carried verbatim to the CANDIDATE event.
145
+ */
146
+ readonly commandProvenance?: TurnCommandProvenance;
147
+ }): Promise<readonly GitCapturedChange[]> {
148
+ const { status, gitRoot, executionId, changeSetId, baselineTree, messages, deniedTokens, hitlDir, storage, priorSubAgentToolCallIds, commandProvenance } = opts;
149
+ const gitWorkspace = opts.gitWorkspace ?? true;
150
+
151
+ // The CAS substrate class for this turn's staged writes: gitignored paths in a
152
+ // git tree, all touched paths in a non-git workspace.
153
+ const casCaptureClass = gitWorkspace
154
+ ? FileCaptureClass.GIT_IGNORED_CAPTURED
155
+ : FileCaptureClass.NON_GIT_CAS;
156
+ const { casCaptures, unreviewablePaths } = await buildCasTurnCaptures(
157
+ gitRoot,
158
+ hitlDir,
159
+ storage,
160
+ casCaptureClass,
161
+ );
162
+
163
+ const changes = await sharedCaptureCandidateToLedger({
164
+ status,
165
+ gitRoot,
166
+ executionId,
167
+ changeSetId,
168
+ baselineTree,
169
+ harnessId: HARNESS_ID,
170
+ excludePaths: CURSOR_RUNNER_OWNED_PATHS,
171
+ casCaptures,
172
+ storage,
173
+ unreviewablePaths,
174
+ unreviewableCaptureClass: casCaptureClass,
175
+ gitWorkspace,
176
+ commandProvenance,
177
+ });
178
+
179
+ // Observational rows: the reviewable diff lives on the file_review ledger
180
+ // (projected to file_change_sets); the streamed file-edit rows that flowed
181
+ // this turn stay visible in place, stamped with the change set id so clients
182
+ // badge them and anchor the decision surface. Denied (gitignored-delete/
183
+ // shell) rows stay on the deny-gate path. Gated on an authored CANDIDATE —
184
+ // a no-op turn (every edit reverted before the boundary) authors no event,
185
+ // and a row must never reference a change set that does not exist.
186
+ if (hasCandidateCaptured(status, changeSetId)) {
187
+ stampFlowedFileEditRows(messages, deniedTokens, changeSetId);
188
+ // Sub-agent edit rows fold their files into the SAME parent turn set, so they
189
+ // carry the same change set id. Walked separately (they live under
190
+ // subAgentExecutions, not the top-level transcript) and scoped to this turn
191
+ // via the pre-turn snapshot of seeded sub-agent tool-call ids.
192
+ for (const sa of status.subAgentExecutions) {
193
+ stampFlowedFileEditRows(sa.messages, deniedTokens, changeSetId, priorSubAgentToolCallIds);
194
+ }
195
+ }
196
+
197
+ return changes;
198
+ }
199
+
200
+ /**
201
+ * Compose this turn's CAS captures from the sidecar the hook staged, mirroring
202
+ * deep-agent's `buildCasTurnCaptures`: for each non-secret observed path, the
203
+ * before-bytes come from the sidecar and the after-bytes are re-read from disk now
204
+ * (`null` = the file was removed after the write). Secret-blocked paths are
205
+ * returned as `unreviewablePaths`. The secret partition is re-run as a
206
+ * fail-closed backstop over the observed set. `captureClass` labels each captured
207
+ * path's provenance (GIT_IGNORED_CAPTURED for a git tree's ignored paths,
208
+ * NON_GIT_CAS for a non-git workspace).
209
+ *
210
+ * Returns empty when the harness has no storage (captureIgnored off) or the
211
+ * sidecar is empty — leaving the shared capture unchanged.
212
+ */
213
+ async function buildCasTurnCaptures(
214
+ gitRoot: string,
215
+ hitlDir: string | undefined,
216
+ storage: ArtifactStorage | undefined,
217
+ captureClass: FileCaptureClass,
218
+ ): Promise<{ casCaptures: CasPathCapture[]; unreviewablePaths: string[] }> {
219
+ if (!hitlDir || !storage) return { casCaptures: [], unreviewablePaths: [] };
220
+
221
+ const { captured, secretPaths } = await readCasObservations(hitlDir);
222
+ const beforeByPath = new Map(captured.map((c) => [c.path, c.before] as const));
223
+ const { capturablePaths, unreviewablePaths } = partitionIgnoredPathsBySecret(
224
+ beforeByPath.keys(),
225
+ new Set(secretPaths),
226
+ );
227
+
228
+ const casCaptures: CasPathCapture[] = [];
229
+ for (const relPath of capturablePaths) {
230
+ const after = await readFileOrNull(join(gitRoot, relPath));
231
+ casCaptures.push({
232
+ path: relPath,
233
+ before: beforeByPath.get(relPath) ?? null,
234
+ after,
235
+ captureClass,
236
+ });
237
+ }
238
+ return { casCaptures, unreviewablePaths: [...unreviewablePaths] };
239
+ }
240
+
241
+ /** Raw bytes of a file, or `null` when it does not exist (an ADD-then-removed). */
242
+ async function readFileOrNull(absolutePath: string): Promise<Uint8Array | null> {
243
+ try {
244
+ return await readFile(absolutePath);
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Resume: reconcile the working tree to the change set's DECIDED decisions
252
+ * (keep approved "after" bytes, snap rejected/undecided back to baseline,
253
+ * hash-verified), author RECONCILED / FAILED, and release the refs. Delegates to
254
+ * the shared orchestration with the Cursor harness id + exclude paths.
255
+ *
256
+ * `storage` threads the CAS blob store + reader so gitignored (CAS) files in the
257
+ * change set reconcile from the durable manifest — approved after-blobs written
258
+ * (hash-verified), rejected files snapped back to their before-blobs. Omit for a
259
+ * git-only harness/turn; the shared CAS branch is then skipped. The read path is
260
+ * the same one `exact-apply` already uses (`ArtifactStorage.download`).
261
+ */
262
+ export function applyCaptureDecisions(opts: {
263
+ readonly status: AgentExecutionStatus;
264
+ readonly gitRoot: string;
265
+ readonly executionId: string;
266
+ readonly changeSet: FileChangeSet;
267
+ readonly storage?: ArtifactStorage;
268
+ /**
269
+ * True (default) for a git work tree; false for a CAS-only non-git workspace
270
+ * (Slice 2c). When false the reconcile never consults git refs — it is driven
271
+ * entirely by the durable CAS manifest.
272
+ */
273
+ readonly gitWorkspace?: boolean;
274
+ }): Promise<CaptureResumeResult> {
275
+ const { storage, ...rest } = opts;
276
+ return sharedApplyCaptureDecisions({
277
+ ...rest,
278
+ harnessId: HARNESS_ID,
279
+ excludePaths: CURSOR_RUNNER_OWNED_PATHS,
280
+ storage,
281
+ readBlob: storage ? casBlobReader(storage) : undefined,
282
+ });
283
+ }
284
+
285
+ // ---------------------------------------------------------------------------
286
+ // Cursor-specific presentation
287
+ // ---------------------------------------------------------------------------
288
+
289
+ /**
290
+ * Stamp every streamed file-edit row (category write/delete) that flowed this
291
+ * turn with the change set id, so the row stays visible as an observational
292
+ * record and clients can badge it / anchor the decision surface. Skips:
293
+ * - already-stamped rows — the stamp is the idempotency AND cross-turn guard:
294
+ * a resume seeds prior turns' rows into this transcript, and re-stamping
295
+ * them with this turn's id would mis-attribute them ({@link stampFileEditRow});
296
+ * - legacy hidden rows (sessions persisted before stamping existed) — they
297
+ * belong to an earlier turn's change set, not this one;
298
+ * - denied identities (the deny-gate reconcile path owns those rows);
299
+ * - tool-call ids in `skipToolCallIds` — used for sub-agent rows, which lack
300
+ * the already-stamped/hidden shields the top-level transcript has, to scope
301
+ * the stamp to rows created this turn (the seeded prior sub-agents' ids).
302
+ */
303
+ function stampFlowedFileEditRows(
304
+ messages: readonly AgentMessage[],
305
+ deniedTokens: ReadonlySet<string>,
306
+ changeSetId: string,
307
+ skipToolCallIds?: ReadonlySet<string>,
308
+ ): void {
309
+ for (const msg of messages) {
310
+ for (const tc of msg.toolCalls) {
311
+ if (tc.fileChangeSetId) continue;
312
+ if (skipToolCallIds?.has(tc.id)) continue;
313
+ if (isToolCallRowHidden(tc)) continue;
314
+ const category = approvalCategory(tc.name);
315
+ if (category !== "write" && category !== "delete") continue;
316
+ const args = (tc.args ?? {}) as Record<string, unknown>;
317
+ const id = toolIdentity(tc.name, tc.mcpServerSlug, args);
318
+ const token = primaryToken(id.key, id.salient, contentDigest(args));
319
+ if (deniedTokens.has(token)) continue; // denied -> reconcile path owns it
320
+ stampFileEditRow(tc, changeSetId);
321
+ }
322
+ }
323
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * The Cursor harness's disk-backed observer for gitignored file writes — the
3
+ * out-of-process analog of the deep-agent `CasCaptureFilesystemBackend`
4
+ * ({@link ../execute-deep-agent/cas-capture-backend.js}).
5
+ *
6
+ * Deep-agent runs the agent in-process, so it records a gitignored file's
7
+ * pre-turn bytes in an in-memory `CasBeforeMap` at the mutation point (before
8
+ * `super.write`). Cursor runs the agent as an EXTERNAL CLI subprocess whose only
9
+ * pre-write interception is the generated preToolUse bash hook — and each hook
10
+ * invocation is a FRESH, EPHEMERAL process that cannot hold in-memory state
11
+ * across writes. So Cursor's before-map must be DISK-BACKED: the hook stages each
12
+ * first-touched gitignored path into a per-turn "cas-observations" sidecar, and
13
+ * the runner reads it back at the turn boundary.
14
+ *
15
+ * The sidecar lives under the session HITL directory
16
+ * (`~/.stigmer/sessions/{id}/hitl/cas-observations/`), OUTSIDE the user's
17
+ * workspace — so staged before-bytes and secret markers never land in the repo,
18
+ * cannot pollute the captured git diff, and are never pushed. It is reset every
19
+ * turn (alongside the denial ledger) and consumed within the same activity
20
+ * invocation that produced it; resume reconciles from the durable CAS manifest,
21
+ * never this scratch. A Temporal retry re-runs the reset and re-observes.
22
+ *
23
+ * ONE MODULE OWNS THE ON-DISK FORMAT. Both sides live here so they cannot drift:
24
+ * - the READER ({@link readCasObservations}) the runner boundary calls;
25
+ * - the WRITER-SCRIPT GENERATOR ({@link buildObservationStagingScript}) whose
26
+ * output the hook embeds and runs on the runner's own Node binary. The writer
27
+ * classifies secrets from the SAME {@link isSecretLikePath} pattern arrays the
28
+ * boundary backstop uses (`shared/filereview/secret-paths.ts`), reconstructed
29
+ * as byte-identical `RegExp`s — no bash-regex, no divergence.
30
+ *
31
+ * Each observed path becomes one entry:
32
+ * - `captured` — a non-secret gitignored write/edit: `{path, kind, existed}`
33
+ * metadata plus a `.blob` of the pre-turn bytes (omitted for an ADD, where the
34
+ * file did not exist). First-touch-wins via atomic exclusive-create, so
35
+ * repeated edits to one path keep the true pre-turn before.
36
+ * - `secret` — a secret-like gitignored path the hook hard-blocked (denied,
37
+ * nothing written): `{path, kind}` only. The boundary authors it as a
38
+ * content-less `DIFF_UNREVIEWABLE` change; its bytes never reach storage.
39
+ *
40
+ * @since File-Change HITL Redesign (Cursor CAS parity — DD-18)
41
+ */
42
+
43
+ import { readFile, readdir, rm, mkdir } from "node:fs/promises";
44
+ import { join } from "node:path";
45
+ import { SECRET_BASENAME_PATTERNS, SECRET_PATH_PATTERNS } from "../../shared/filereview/secret-paths.js";
46
+
47
+ /** Sidecar directory name under the session HITL dir. */
48
+ export const CAS_OBSERVATIONS_DIRNAME = "cas-observations";
49
+
50
+ /** Suffix of the per-path metadata file (also the atomic first-touch marker). */
51
+ const META_SUFFIX = ".meta.json";
52
+ /** Suffix of the per-path pre-turn bytes blob (present only for a MODIFY). */
53
+ const BLOB_SUFFIX = ".blob";
54
+
55
+ /** Absolute path of the per-turn cas-observations sidecar dir. */
56
+ export function casObservationsDir(hitlDir: string): string {
57
+ return join(hitlDir, CAS_OBSERVATIONS_DIRNAME);
58
+ }
59
+
60
+ /**
61
+ * Truncate the cas-observations sidecar to empty for a fresh turn, returning its
62
+ * path. Called every turn alongside {@link resetDenialLedger} so the boundary
63
+ * only ever reads observations produced by the current run — deterministic across
64
+ * HITL reinvocations and Temporal activity retries.
65
+ */
66
+ export async function resetCasObservations(hitlDir: string): Promise<string> {
67
+ const dir = casObservationsDir(hitlDir);
68
+ await rm(dir, { recursive: true, force: true });
69
+ await mkdir(dir, { recursive: true });
70
+ return dir;
71
+ }
72
+
73
+ /** One non-secret gitignored path the hook staged this turn. */
74
+ export interface CasObservationCapture {
75
+ /** Workspace-root-relative path. */
76
+ readonly path: string;
77
+ /** Pre-turn bytes; `null` when the file did not exist before (an ADD). */
78
+ readonly before: Uint8Array | null;
79
+ }
80
+
81
+ /** The turn's staged gitignored observations, split by the hook's secret gate. */
82
+ export interface CasObservations {
83
+ /** Non-secret paths to capture into CAS (before-bytes in hand). */
84
+ readonly captured: CasObservationCapture[];
85
+ /** Secret-like paths the hook hard-blocked (path-only; bytes withheld). */
86
+ readonly secretPaths: string[];
87
+ }
88
+
89
+ /**
90
+ * Read the cas-observations sidecar the hook wrote during the turn. Missing dir →
91
+ * no observations. Malformed/partial entries are tolerated and skipped (the hook
92
+ * writes the blob before the metadata marker, so a present marker implies a
93
+ * complete blob; a marker whose blob is unexpectedly missing is skipped rather
94
+ * than mis-captured). Entries are returned in a deterministic (sorted) order.
95
+ */
96
+ export async function readCasObservations(hitlDir: string): Promise<CasObservations> {
97
+ const dir = casObservationsDir(hitlDir);
98
+ let names: string[];
99
+ try {
100
+ names = await readdir(dir);
101
+ } catch {
102
+ return { captured: [], secretPaths: [] };
103
+ }
104
+
105
+ const captured: CasObservationCapture[] = [];
106
+ const secretPaths: string[] = [];
107
+ for (const name of names.sort()) {
108
+ if (!name.endsWith(META_SUFFIX)) continue;
109
+ let meta: { path?: unknown; kind?: unknown; existed?: unknown };
110
+ try {
111
+ meta = JSON.parse(await readFile(join(dir, name), "utf-8"));
112
+ } catch {
113
+ continue; // partial/garbled marker — tolerate
114
+ }
115
+ if (!meta || typeof meta.path !== "string" || !meta.path) continue;
116
+
117
+ if (meta.kind === "secret") {
118
+ secretPaths.push(meta.path);
119
+ continue;
120
+ }
121
+ if (meta.kind === "captured") {
122
+ let before: Uint8Array | null = null;
123
+ if (meta.existed === true) {
124
+ const blobPath = join(dir, name.slice(0, -META_SUFFIX.length) + BLOB_SUFFIX);
125
+ try {
126
+ before = await readFile(blobPath);
127
+ } catch {
128
+ continue; // marker present but blob missing (mid-hook crash) — skip
129
+ }
130
+ }
131
+ captured.push({ path: meta.path, before });
132
+ }
133
+ }
134
+ return { captured, secretPaths };
135
+ }
136
+
137
+ /**
138
+ * Emit a JS `RegExp` constructor byte-identical to a source pattern, safe to
139
+ * embed inside a single-quoted bash string (JSON.stringify emits double quotes;
140
+ * these patterns contain no single quotes).
141
+ */
142
+ function regexLiteral(re: RegExp): string {
143
+ return `new RegExp(${JSON.stringify(re.source)},${JSON.stringify(re.flags)})`;
144
+ }
145
+
146
+ /**
147
+ * Build the standalone Node.js script the hook runs to observe a single
148
+ * gitignored write — the disk-backed mirror of `CasCaptureFilesystemBackend`'s
149
+ * `recordBefore` plus the DD-E secret gate.
150
+ *
151
+ * Invoked as `printf %s "$SALIENT" | node -e '<this>' <workspaceRoot> <obsDir>`
152
+ * — the (arbitrary) salient path arrives on stdin so no argv escaping is needed;
153
+ * the two absolute paths ride argv. Prints one token on stdout for the hook:
154
+ * - `captured` — non-secret: staged the pre-turn bytes (first-touch-wins), the
155
+ * hook then ALLOWS the write to flow;
156
+ * - `secret` — secret-like: recorded a content-less marker, the hook then
157
+ * DENIES with the security message (nothing is written);
158
+ * - `error` — the path escaped the workspace or a filesystem error occurred,
159
+ * the hook then fails closed (deny).
160
+ *
161
+ * The secret classifier is generated from the SAME
162
+ * {@link SECRET_BASENAME_PATTERNS}/{@link SECRET_PATH_PATTERNS} the boundary
163
+ * backstop uses, so the hook-side and boundary-side classifications agree by
164
+ * construction (a table test locks the equivalence). Authored as a
165
+ * single-quoted-bash string, so the JS must contain no single quotes.
166
+ */
167
+ export function buildObservationStagingScript(): string {
168
+ const base = SECRET_BASENAME_PATTERNS.map(regexLiteral).join(",");
169
+ const pathPatterns = SECRET_PATH_PATTERNS.map(regexLiteral).join(",");
170
+ const metaSuffix = JSON.stringify(META_SUFFIX);
171
+ const blobSuffix = JSON.stringify(BLOB_SUFFIX);
172
+ return [
173
+ `const fs=require("fs");const path=require("path");const crypto=require("crypto");`,
174
+ `const wsRoot=process.argv[1]||"";const obsDir=process.argv[2]||"";`,
175
+ `let salient="";try{salient=fs.readFileSync(0,"utf8");}catch(e){salient="";}`,
176
+ `if(!salient||!obsDir){process.stdout.write("error");process.exit(0);}`,
177
+ // Workspace-relative key, git-consistent: an absolute salient stays put, a
178
+ // relative one resolves against the workspace root. A path that escapes the
179
+ // workspace is treated as an error (fail closed) — it should never be flagged
180
+ // gitignored by `git check-ignore -C <root>` in the first place.
181
+ `const abs=path.resolve(wsRoot,salient);`,
182
+ `let rel=path.relative(wsRoot,abs).split(path.sep).join("/");`,
183
+ `if(!rel||rel===".."||rel.startsWith("../")){process.stdout.write("error");process.exit(0);}`,
184
+ // --- secret classifier: byte-identical mirror of isSecretLikePath ---
185
+ `const baseName=(p)=>{const n=p.replace(/\\\\/g,"/");const i=n.lastIndexOf("/");return i>=0?n.slice(i+1):n;};`,
186
+ `const BASE=[${base}];const PATHP=[${pathPatterns}];`,
187
+ `const isSecret=(p)=>{if(!p)return true;const nm=p.replace(/\\\\/g,"/");const b=baseName(nm).toLowerCase();for(const r of BASE){if(r.test(b))return true;}const lp=nm.toLowerCase();for(const r of PATHP){if(r.test(lp))return true;}return false;};`,
188
+ `const key=crypto.createHash("sha256").update(rel,"utf8").digest("hex");`,
189
+ `const metaPath=path.join(obsDir,key+${metaSuffix});const blobPath=path.join(obsDir,key+${blobSuffix});`,
190
+ `try{fs.mkdirSync(obsDir,{recursive:true});}catch(e){}`,
191
+ // Secret: record a content-less marker (idempotent) and signal a hard-block.
192
+ `if(isSecret(rel)){try{fs.writeFileSync(metaPath,JSON.stringify({path:rel,kind:"secret"}),{flag:"wx"});}catch(e){}process.stdout.write("secret");process.exit(0);}`,
193
+ // Non-secret: first-touch-wins. A prior marker means this path is already
194
+ // observed (a later edit) — leave the true pre-turn before intact.
195
+ `if(fs.existsSync(metaPath)){process.stdout.write("captured");process.exit(0);}`,
196
+ // Read the pre-turn bytes NOW (the write has not applied — this is preToolUse
197
+ // and the first touch). Missing file = an ADD (before is null).
198
+ `let existed=false;let before=null;try{before=fs.readFileSync(abs);existed=true;}catch(e){existed=false;}`,
199
+ // Write the blob BEFORE the marker, so a present marker always implies a
200
+ // complete blob. The wx marker is the atomic commit + first-touch lock.
201
+ `try{if(existed){fs.writeFileSync(blobPath,before);}fs.writeFileSync(metaPath,JSON.stringify({path:rel,kind:"captured",existed:existed}),{flag:"wx"});}catch(e){if(e&&e.code==="EEXIST"){process.stdout.write("captured");process.exit(0);}process.stdout.write("error");process.exit(0);}`,
202
+ `process.stdout.write("captured");`,
203
+ ].join("");
204
+ }