@stigmer/runner 3.0.9-dev.20260615153829 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
  3. package/dist/__test-utils__/approval-contract/types.js +24 -0
  4. package/dist/__test-utils__/approval-contract/types.js.map +1 -0
  5. package/dist/activities/call-agent-status.d.ts +19 -1
  6. package/dist/activities/call-agent-status.js +64 -5
  7. package/dist/activities/call-agent-status.js.map +1 -1
  8. package/dist/activities/call-llm.js +19 -53
  9. package/dist/activities/call-llm.js.map +1 -1
  10. package/dist/activities/classify-tool-approvals.d.ts +24 -0
  11. package/dist/activities/classify-tool-approvals.js +69 -17
  12. package/dist/activities/classify-tool-approvals.js.map +1 -1
  13. package/dist/activities/discover-mcp-server.d.ts +7 -0
  14. package/dist/activities/discover-mcp-server.js +11 -1
  15. package/dist/activities/discover-mcp-server.js.map +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
  17. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
  18. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
  19. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
  20. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
  21. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
  22. package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
  23. package/dist/activities/execute-cursor/approval-policy.js +8 -120
  24. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  25. package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
  26. package/dist/activities/execute-cursor/approval-state.js +233 -17
  27. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  28. package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
  29. package/dist/activities/execute-cursor/capture-flow.js +234 -0
  30. package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
  31. package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
  32. package/dist/activities/execute-cursor/cas-observations.js +184 -0
  33. package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
  34. package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
  35. package/dist/activities/execute-cursor/command-provenance.js +128 -0
  36. package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
  37. package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
  38. package/dist/activities/execute-cursor/exact-apply.js +204 -0
  39. package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
  40. package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
  41. package/dist/activities/execute-cursor/hook-script.js +310 -47
  42. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  43. package/dist/activities/execute-cursor/index.d.ts +8 -1
  44. package/dist/activities/execute-cursor/index.js +500 -64
  45. package/dist/activities/execute-cursor/index.js.map +1 -1
  46. package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
  47. package/dist/activities/execute-cursor/message-translator.js +836 -90
  48. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  49. package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
  50. package/dist/activities/execute-cursor/prompt-builder.js +74 -15
  51. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  52. package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
  53. package/dist/activities/execute-cursor/workspace-setup.js +212 -33
  54. package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
  55. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
  56. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
  57. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
  58. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
  59. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
  60. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
  61. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
  62. package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
  63. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
  64. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
  65. package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
  66. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  67. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
  68. package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
  69. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
  70. package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
  71. package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
  72. package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
  73. package/dist/activities/execute-deep-agent/hitl.d.ts +16 -3
  74. package/dist/activities/execute-deep-agent/hitl.js +11 -3
  75. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  76. package/dist/activities/execute-deep-agent/index.d.ts +2 -1
  77. package/dist/activities/execute-deep-agent/index.js +393 -33
  78. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  79. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
  80. package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
  81. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  82. package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
  83. package/dist/activities/execute-deep-agent/setup.js +149 -92
  84. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  85. package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
  86. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
  87. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
  88. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
  89. package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
  90. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  91. package/dist/activities/execute-deep-agent/status-builder.d.ts +17 -5
  92. package/dist/activities/execute-deep-agent/status-builder.js +44 -3
  93. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  94. package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
  95. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  96. package/dist/activities/execute-deep-agent/streaming.js +3 -15
  97. package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
  98. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
  99. package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
  100. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  101. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
  102. package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
  103. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  104. package/dist/activities/execute-deep-agent/v3-status-builder.js +32 -2
  105. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  106. package/dist/claimcheck/payload-codec.js +9 -5
  107. package/dist/claimcheck/payload-codec.js.map +1 -1
  108. package/dist/client/stigmer-client.d.ts +2 -0
  109. package/dist/client/stigmer-client.js +2 -0
  110. package/dist/client/stigmer-client.js.map +1 -1
  111. package/dist/middleware/approval-gate.d.ts +85 -4
  112. package/dist/middleware/approval-gate.js +165 -38
  113. package/dist/middleware/approval-gate.js.map +1 -1
  114. package/dist/middleware/types.d.ts +2 -5
  115. package/dist/shared/activity-input.d.ts +43 -0
  116. package/dist/shared/activity-input.js +17 -0
  117. package/dist/shared/activity-input.js.map +1 -0
  118. package/dist/shared/approval-canonicalize.d.ts +19 -0
  119. package/dist/shared/approval-canonicalize.js +119 -0
  120. package/dist/shared/approval-canonicalize.js.map +1 -0
  121. package/dist/shared/approval-fingerprint.d.ts +106 -0
  122. package/dist/shared/approval-fingerprint.js +113 -0
  123. package/dist/shared/approval-fingerprint.js.map +1 -0
  124. package/dist/shared/approval-policy.d.ts +182 -12
  125. package/dist/shared/approval-policy.js +213 -27
  126. package/dist/shared/approval-policy.js.map +1 -1
  127. package/dist/shared/args-preview.d.ts +52 -0
  128. package/dist/shared/args-preview.js +93 -0
  129. package/dist/shared/args-preview.js.map +1 -0
  130. package/dist/shared/artifact-storage.d.ts +19 -1
  131. package/dist/shared/artifact-storage.js +48 -11
  132. package/dist/shared/artifact-storage.js.map +1 -1
  133. package/dist/shared/file-change.d.ts +44 -0
  134. package/dist/shared/file-change.js +57 -0
  135. package/dist/shared/file-change.js.map +1 -0
  136. package/dist/shared/file-tools.d.ts +107 -0
  137. package/dist/shared/file-tools.js +168 -0
  138. package/dist/shared/file-tools.js.map +1 -0
  139. package/dist/shared/filereview/capture.d.ts +202 -0
  140. package/dist/shared/filereview/capture.js +498 -0
  141. package/dist/shared/filereview/capture.js.map +1 -0
  142. package/dist/shared/filereview/cas-substrate.d.ts +190 -0
  143. package/dist/shared/filereview/cas-substrate.js +284 -0
  144. package/dist/shared/filereview/cas-substrate.js.map +1 -0
  145. package/dist/shared/filereview/digest.d.ts +40 -0
  146. package/dist/shared/filereview/digest.js +66 -0
  147. package/dist/shared/filereview/digest.js.map +1 -0
  148. package/dist/shared/filereview/events.d.ts +170 -0
  149. package/dist/shared/filereview/events.js +298 -0
  150. package/dist/shared/filereview/events.js.map +1 -0
  151. package/dist/shared/filereview/git-substrate.d.ts +175 -0
  152. package/dist/shared/filereview/git-substrate.js +439 -0
  153. package/dist/shared/filereview/git-substrate.js.map +1 -0
  154. package/dist/shared/filereview/index.d.ts +11 -0
  155. package/dist/shared/filereview/index.js +12 -0
  156. package/dist/shared/filereview/index.js.map +1 -0
  157. package/dist/shared/filereview/secret-paths.d.ts +63 -0
  158. package/dist/shared/filereview/secret-paths.js +105 -0
  159. package/dist/shared/filereview/secret-paths.js.map +1 -0
  160. package/dist/shared/fingerprint-secret.d.ts +26 -0
  161. package/dist/shared/fingerprint-secret.js +47 -0
  162. package/dist/shared/fingerprint-secret.js.map +1 -0
  163. package/dist/shared/model-client.d.ts +51 -0
  164. package/dist/shared/model-client.js +77 -0
  165. package/dist/shared/model-client.js.map +1 -0
  166. package/dist/shared/plan-artifact.js +0 -2
  167. package/dist/shared/plan-artifact.js.map +1 -1
  168. package/dist/shared/status-offload.d.ts +83 -9
  169. package/dist/shared/status-offload.js +399 -79
  170. package/dist/shared/status-offload.js.map +1 -1
  171. package/dist/shared/status.js +14 -1
  172. package/dist/shared/status.js.map +1 -1
  173. package/dist/shared/tool-kind.d.ts +19 -0
  174. package/dist/shared/tool-kind.js +13 -0
  175. package/dist/shared/tool-kind.js.map +1 -1
  176. package/dist/shared/tool-row.d.ts +88 -0
  177. package/dist/shared/tool-row.js +127 -0
  178. package/dist/shared/tool-row.js.map +1 -0
  179. package/dist/shared/workspace/platform-dir.d.ts +25 -0
  180. package/dist/shared/workspace/platform-dir.js +38 -2
  181. package/dist/shared/workspace/platform-dir.js.map +1 -1
  182. package/dist/workflows/call-agent-orchestrator.js +56 -7
  183. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  184. package/dist/workflows/connect-mcp-server.d.ts +50 -0
  185. package/dist/workflows/connect-mcp-server.js +136 -15
  186. package/dist/workflows/connect-mcp-server.js.map +1 -1
  187. package/dist/workflows/types.d.ts +8 -0
  188. package/package.json +2 -2
  189. package/src/__test-utils__/approval-contract/contract.ts +224 -0
  190. package/src/__test-utils__/approval-contract/types.ts +179 -0
  191. package/src/__test-utils__/fake-artifact-storage.ts +72 -0
  192. package/src/__tests__/approval-gateway-contract.test.ts +29 -0
  193. package/src/__tests__/claimcheck-codec.test.ts +16 -53
  194. package/src/__tests__/golden-e2e.test.ts +2 -0
  195. package/src/__tests__/runner-token-coordinator.test.ts +3 -3
  196. package/src/activities/__tests__/call-agent-status.test.ts +135 -0
  197. package/src/activities/__tests__/call-llm.test.ts +1 -1
  198. package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
  199. package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
  200. package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
  201. package/src/activities/call-agent-status.ts +74 -4
  202. package/src/activities/call-llm.ts +18 -63
  203. package/src/activities/classify-tool-approvals.ts +101 -19
  204. package/src/activities/discover-mcp-server.ts +29 -1
  205. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
  206. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
  207. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
  208. package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
  209. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
  210. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
  211. package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
  212. package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
  213. package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
  214. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
  215. package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
  216. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
  217. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
  218. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
  219. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
  220. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
  221. package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
  222. package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
  223. package/src/activities/execute-cursor/approval-policy.ts +28 -159
  224. package/src/activities/execute-cursor/approval-state.ts +366 -18
  225. package/src/activities/execute-cursor/capture-flow.ts +323 -0
  226. package/src/activities/execute-cursor/cas-observations.ts +204 -0
  227. package/src/activities/execute-cursor/command-provenance.ts +168 -0
  228. package/src/activities/execute-cursor/exact-apply.ts +253 -0
  229. package/src/activities/execute-cursor/hook-script.ts +317 -51
  230. package/src/activities/execute-cursor/index.ts +575 -67
  231. package/src/activities/execute-cursor/message-translator.ts +963 -89
  232. package/src/activities/execute-cursor/prompt-builder.ts +80 -14
  233. package/src/activities/execute-cursor/workspace-setup.ts +257 -42
  234. package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
  235. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
  236. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
  237. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
  238. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
  239. package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
  240. package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
  241. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
  242. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +284 -0
  243. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
  244. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
  245. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
  246. package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
  247. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +91 -10
  248. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
  249. package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
  250. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
  251. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
  252. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
  253. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
  254. package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
  255. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
  256. package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
  257. package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
  258. package/src/activities/execute-deep-agent/hitl.ts +23 -7
  259. package/src/activities/execute-deep-agent/index.ts +463 -40
  260. package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
  261. package/src/activities/execute-deep-agent/setup.ts +223 -125
  262. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
  263. package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
  264. package/src/activities/execute-deep-agent/status-builder.ts +63 -8
  265. package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
  266. package/src/activities/execute-deep-agent/streaming.ts +3 -13
  267. package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
  268. package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
  269. package/src/activities/execute-deep-agent/v3-status-builder.ts +36 -2
  270. package/src/claimcheck/payload-codec.ts +8 -8
  271. package/src/client/stigmer-client.ts +9 -1
  272. package/src/middleware/__tests__/approval-gate.test.ts +488 -4
  273. package/src/middleware/approval-gate.ts +247 -38
  274. package/src/middleware/types.ts +5 -5
  275. package/src/shared/__tests__/activity-input.test.ts +78 -0
  276. package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
  277. package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
  278. package/src/shared/__tests__/approval-policy.test.ts +274 -40
  279. package/src/shared/__tests__/args-preview.test.ts +78 -0
  280. package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
  281. package/src/shared/__tests__/artifact-storage.test.ts +123 -11
  282. package/src/shared/__tests__/file-change.test.ts +85 -0
  283. package/src/shared/__tests__/file-tools.test.ts +90 -0
  284. package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
  285. package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
  286. package/src/shared/__tests__/model-client.test.ts +162 -0
  287. package/src/shared/__tests__/plan-artifact.test.ts +11 -26
  288. package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
  289. package/src/shared/__tests__/status-offload.test.ts +573 -16
  290. package/src/shared/__tests__/status.test.ts +4 -5
  291. package/src/shared/__tests__/tool-kind.test.ts +24 -1
  292. package/src/shared/__tests__/tool-row.test.ts +221 -0
  293. package/src/shared/activity-input.ts +57 -0
  294. package/src/shared/approval-canonicalize.ts +159 -0
  295. package/src/shared/approval-fingerprint.ts +148 -0
  296. package/src/shared/approval-policy.ts +303 -27
  297. package/src/shared/args-preview.ts +98 -0
  298. package/src/shared/artifact-storage.ts +62 -11
  299. package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
  300. package/src/shared/file-change.ts +64 -0
  301. package/src/shared/file-tools.ts +169 -0
  302. package/src/shared/filereview/__tests__/capture.test.ts +856 -0
  303. package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
  304. package/src/shared/filereview/__tests__/digest.test.ts +100 -0
  305. package/src/shared/filereview/__tests__/events.test.ts +245 -0
  306. package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
  307. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
  308. package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
  309. package/src/shared/filereview/capture.ts +727 -0
  310. package/src/shared/filereview/cas-substrate.ts +401 -0
  311. package/src/shared/filereview/digest.ts +83 -0
  312. package/src/shared/filereview/events.ts +449 -0
  313. package/src/shared/filereview/git-substrate.ts +555 -0
  314. package/src/shared/filereview/index.ts +60 -0
  315. package/src/shared/filereview/secret-paths.ts +121 -0
  316. package/src/shared/fingerprint-secret.ts +53 -0
  317. package/src/shared/model-client.ts +122 -0
  318. package/src/shared/plan-artifact.ts +0 -2
  319. package/src/shared/status-offload.ts +433 -77
  320. package/src/shared/status.ts +13 -0
  321. package/src/shared/tool-kind.ts +33 -0
  322. package/src/shared/tool-row.ts +135 -0
  323. package/src/shared/workspace/platform-dir.ts +41 -2
  324. package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
  325. package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
  326. package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
  327. package/src/workflows/call-agent-orchestrator.ts +53 -6
  328. package/src/workflows/connect-mcp-server.ts +179 -24
  329. package/src/workflows/types.ts +8 -0
@@ -0,0 +1,727 @@
1
+ /**
2
+ * Harness-agnostic capture-mode turn orchestration for the apply-then-review
3
+ * HITL subsystem. This is the glue between the git snapshot/restore substrate
4
+ * ({@link ./git-substrate.js}) and the file-review ledger ({@link ./events.js}):
5
+ * it turns "the agent edited files freely this turn" into the append-only
6
+ * `file_review` events the server folds into `file_change_sets` (the single
7
+ * review surface), and on resume reconciles the tree to the user's decisions.
8
+ *
9
+ * It is parameterized by `harnessId` (stamped onto the BASELINE payload, which is
10
+ * the projection's source of truth for `turn_id`/`harness_id`) and `excludePaths`
11
+ * (the runner-owned files a harness writes into the workspace), so the Cursor and
12
+ * deep-agent harnesses author IDENTICAL ledger entries through this one seam. Each
13
+ * harness keeps only its presentation concerns (e.g. hiding flowed transcript
14
+ * rows) in its own adapter.
15
+ *
16
+ * Identity: the change set id is `${executionId}:${turnSeq}` (one turn = one
17
+ * change set), minted by the caller; each file's stable id is
18
+ * `${changeSetId}:${pathAfter||pathBefore}` so the resume can map a per-file
19
+ * decision back to its path without depending on any other identity.
20
+ */
21
+
22
+ import { create } from "@bufbuild/protobuf";
23
+ import {
24
+ FileCaptureClass,
25
+ FileChangeKind,
26
+ FileChangeType,
27
+ FileDecisionAction,
28
+ FileDecisionScope,
29
+ FileReviewBlockReason,
30
+ FileReviewFailureKind,
31
+ SnapshotKind,
32
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
33
+ import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
34
+ import {
35
+ CasManifestRefSchema,
36
+ GitTreeRefSchema,
37
+ SnapshotRefSchema,
38
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
39
+ import type {
40
+ CapturedFileChange,
41
+ FileChangeSet,
42
+ SnapshotRef,
43
+ TurnCommandProvenance,
44
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
45
+ import { utcTimestamp } from "../status.js";
46
+ import {
47
+ appendFileReviewEvents,
48
+ buildBaselineCapturedEvent,
49
+ buildCandidateCapturedEvent,
50
+ buildCapturedFileChange,
51
+ buildFailedEvent,
52
+ buildReconciledEvent,
53
+ contentSha256,
54
+ type CapturedChangeInput,
55
+ type ChangeSetContext,
56
+ } from "./events.js";
57
+ import {
58
+ applyApprovedPaths,
59
+ baselineRef,
60
+ captureChangeSet,
61
+ captureRef,
62
+ dropCaptureRefs,
63
+ recomputeChangeSet,
64
+ restoreToBaseline,
65
+ snapshotApproved,
66
+ snapshotBaseline,
67
+ type GitSubstrateChange as GitCapturedChange,
68
+ } from "./git-substrate.js";
69
+ import type { ArtifactStorage } from "../artifact-storage.js";
70
+ import {
71
+ applyCasApproved,
72
+ loadCasManifest,
73
+ restoreCasToBaseline,
74
+ snapshotCasChangeSet,
75
+ type BlobReader,
76
+ type CasCapturedFile,
77
+ type CasPathCapture,
78
+ type CasSnapshotRef,
79
+ } from "./cas-substrate.js";
80
+
81
+ /**
82
+ * Whether a turn runs in apply-then-review CAPTURE mode (file edits flow and are
83
+ * reviewed post-hoc) rather than the classic pre-write DENY-GATE. Shared by BOTH
84
+ * harnesses (Cursor and deep-agent) so the capture-vs-deny-gate decision is made
85
+ * one way everywhere — the single point where a workspace's substrate availability
86
+ * decides the HITL model.
87
+ *
88
+ * Capture needs a primary workspace AND a substrate to capture into:
89
+ * - a git work tree captures tracked edits from the git diff (no storage needed);
90
+ * - a NON-git workspace captures every write via the path-scoped CAS substrate,
91
+ * which needs artifact storage to persist blobs.
92
+ *
93
+ * So a non-git workspace with NO artifact storage — or no workspace at all — has
94
+ * no capture substrate and falls back to the deny-gate: the agent still runs, but
95
+ * file writes gate pre-execution instead of flowing. In the Cursor harness that
96
+ * fallback is also the ONLY branch that arms the resume-time exact-apply guarantee
97
+ * (see execute-cursor/exact-apply.ts); the deep-agent harness re-applies an
98
+ * approved write via LangGraph checkpoint replay and needs no exact-apply. Keeping
99
+ * this the single, named, truth-table-tested decision pins exactly when the
100
+ * no-storage deny-gate engages in either harness.
101
+ */
102
+ export function deriveCaptureMode(
103
+ primaryWorkspaceDir: string | undefined,
104
+ gitWorkspace: boolean,
105
+ hasArtifactStorage: boolean,
106
+ ): boolean {
107
+ return !!primaryWorkspaceDir && (gitWorkspace || hasArtifactStorage);
108
+ }
109
+
110
+ /**
111
+ * Turn start: pin the pre-turn working tree behind the baseline ref and author
112
+ * the BASELINE_CAPTURED event. The projection reads `turn_id`/`harness_id` ONLY
113
+ * from this payload, so `turnId == changeSetId` (one turn = one change set) and
114
+ * `harnessId` identifies the producer. Returns the baseline tree sha for the
115
+ * turn-end diff. Appends onto `status.fileReviewEventStream`; the event rides the
116
+ * next persist.
117
+ *
118
+ * `gitWorkspace=false` (a non-git workspace, DD-21 D2) has no whole-tree baseline
119
+ * to pin — a file's pre-edit bytes are captured per-path by the CAS observer at
120
+ * mutation time, and the reconcile sources them from the durable CAS manifest,
121
+ * not this snapshot. The BASELINE event is still authored (it is the projection's
122
+ * sole source for `turn_id`/`harness_id`), carrying a `CAS_MANIFEST` snapshot with
123
+ * no manifest yet (the manifest is authored at candidate time). Returns "".
124
+ */
125
+ export async function captureBaselineToLedger(opts: {
126
+ readonly status: AgentExecutionStatus;
127
+ readonly gitRoot: string;
128
+ readonly executionId: string;
129
+ readonly changeSetId: string;
130
+ readonly harnessId: string;
131
+ readonly excludePaths?: readonly string[];
132
+ /** True (default) for a git work tree; false for a CAS-only non-git workspace. */
133
+ readonly gitWorkspace?: boolean;
134
+ }): Promise<string> {
135
+ const { status, gitRoot, executionId, changeSetId, harnessId, excludePaths } = opts;
136
+ const gitWorkspace = opts.gitWorkspace ?? true;
137
+
138
+ if (!gitWorkspace) {
139
+ const event = buildBaselineCapturedEvent(
140
+ changeSetContext(changeSetId, harnessId),
141
+ casManifestSnapshotRef(),
142
+ );
143
+ appendFileReviewEvents(status, executionId, [event]);
144
+ return "";
145
+ }
146
+
147
+ const baselineTree = await snapshotBaseline(gitRoot, executionId, excludePaths);
148
+ const event = buildBaselineCapturedEvent(
149
+ changeSetContext(changeSetId, harnessId),
150
+ gitTreeSnapshotRef(baselineTree, baselineRef(executionId)),
151
+ );
152
+ appendFileReviewEvents(status, executionId, [event]);
153
+ return baselineTree;
154
+ }
155
+
156
+ /**
157
+ * Turn end: capture the change set, LEAVE the working tree in its applied
158
+ * ("after") state (the user reviews the real change; nothing is committed and the
159
+ * next turn is blocked until they decide), and author the CANDIDATE_CAPTURED
160
+ * event. The pinned baseline/after refs are the authoritative source for the
161
+ * resume-time reconcile ({@link applyCaptureDecisions}).
162
+ *
163
+ * Presentation (hiding the harness's flowed transcript rows so `file_change_sets`
164
+ * is the single review surface) is the harness adapter's concern, not this seam's.
165
+ * Returns the captured changes. No-op authoring when the turn changed nothing.
166
+ */
167
+ export async function captureCandidateToLedger(opts: {
168
+ readonly status: AgentExecutionStatus;
169
+ readonly gitRoot: string;
170
+ readonly executionId: string;
171
+ readonly changeSetId: string;
172
+ readonly baselineTree: string;
173
+ readonly harnessId: string;
174
+ readonly excludePaths?: readonly string[];
175
+ /**
176
+ * Ignored / non-git paths captured this turn (before/after bytes the harness
177
+ * recorded at mutation time). When present, they are stored as content-
178
+ * addressed CAS blobs and composed with the git-tracked changes into ONE
179
+ * hybrid change set (design doc 11). Omit for a git-only turn — existing
180
+ * callers are unaffected.
181
+ */
182
+ readonly casCaptures?: readonly CasPathCapture[];
183
+ /** The CAS blob store; required when {@link casCaptures} is non-empty. */
184
+ readonly storage?: ArtifactStorage;
185
+ /**
186
+ * Gitignored paths whose bytes are deliberately NOT captured — the DD-E secret
187
+ * gate refused them. Each is authored as a content-less `DIFF_UNREVIEWABLE`
188
+ * change entry (`diff_complete=false`, no before/after) so the change set is
189
+ * PARTIAL_BLOCKED (approval blocked) and the path is honestly surfaced, while
190
+ * its content never enters the ledger or storage. Never overlaps `casCaptures`.
191
+ */
192
+ readonly unreviewablePaths?: readonly string[];
193
+ /**
194
+ * Capture class stamped on the content-less DIFF_UNREVIEWABLE entries — the CAS
195
+ * class of THIS turn's substrate: `GIT_IGNORED_CAPTURED` for a git work tree's
196
+ * ignored paths, `NON_GIT_CAS` for a non-git workspace. Content-addressed
197
+ * `casCaptures` carry their own per-item class; this only labels the blocked
198
+ * (content-less) ones so a non-git blocked secret is not mislabeled "gitignored".
199
+ * Defaults to `GIT_IGNORED_CAPTURED` (git callers are unaffected).
200
+ */
201
+ readonly unreviewableCaptureClass?: FileCaptureClass;
202
+ /**
203
+ * True (default) for a git work tree — git-tracked edits are diffed from the
204
+ * pinned baseline/after trees and composed with any CAS captures. False for a
205
+ * CAS-only non-git workspace (DD-21 D2): there is no git tree to diff, so the
206
+ * change set is sourced ENTIRELY from `casCaptures` (+ `unreviewablePaths`) and
207
+ * the snapshot is `CAS_MANIFEST`, not `GIT_TREE_REF`/`HYBRID`.
208
+ */
209
+ readonly gitWorkspace?: boolean;
210
+ /**
211
+ * The harness's approved-command turn facts (DD-28): present only when the
212
+ * turn's sole mutation source was consented shell commands. Carried verbatim
213
+ * on the CANDIDATE event for the backend to verify and — on success — author
214
+ * the policy auto-keep decision. Omitted → the set reviews manually.
215
+ */
216
+ readonly commandProvenance?: TurnCommandProvenance;
217
+ }): Promise<readonly GitCapturedChange[]> {
218
+ const {
219
+ status, gitRoot, executionId, changeSetId, baselineTree, harnessId, excludePaths,
220
+ casCaptures, storage, unreviewablePaths, commandProvenance,
221
+ } = opts;
222
+ const gitWorkspace = opts.gitWorkspace ?? true;
223
+ const unreviewableCaptureClass =
224
+ opts.unreviewableCaptureClass ?? FileCaptureClass.GIT_IGNORED_CAPTURED;
225
+
226
+ // A non-git workspace has no tree to diff; git-tracked changes are empty and
227
+ // the whole change set comes from the CAS captures below.
228
+ const { afterTree, changes: gitChanges } = gitWorkspace
229
+ ? await captureChangeSet(gitRoot, executionId, baselineTree, excludePaths)
230
+ : { afterTree: "", changes: [] as readonly GitCapturedChange[] };
231
+
232
+ // Capture the ignored / non-git deltas into CAS (content-addressed, deduped,
233
+ // offloaded), if the harness supplied any this turn.
234
+ let casFiles: readonly CasCapturedFile[] = [];
235
+ let casRef: CasSnapshotRef | undefined;
236
+ if (casCaptures && casCaptures.length > 0) {
237
+ if (!storage) {
238
+ throw new Error("captureCandidateToLedger: casCaptures requires an ArtifactStorage");
239
+ }
240
+ const snap = await snapshotCasChangeSet({ storage, executionId, changeSetId, captures: casCaptures });
241
+ casFiles = snap.manifest.files;
242
+ if (casFiles.length > 0) casRef = snap.ref;
243
+ }
244
+
245
+ const unreviewable = unreviewablePaths ?? [];
246
+
247
+ // A turn that changed nothing (no tracked/ignored change and no blocked path)
248
+ // authors no event.
249
+ if (gitChanges.length === 0 && casFiles.length === 0 && unreviewable.length === 0) {
250
+ return gitChanges;
251
+ }
252
+
253
+ // One combined change set: git-tracked (inline bodies) + CAS (blob refs) +
254
+ // secret-blocked (content-less, DIFF_UNREVIEWABLE). The aggregate digest folds
255
+ // all (buildCandidateCapturedEvent sorts by file digest), so the reviewed diff
256
+ // and its identity span every substrate. deriveDiffCompleteness then rolls the
257
+ // per-file signals up three ways: any content-less (non-binary) entry forces
258
+ // PARTIAL_BLOCKED; a set blocked only by binaries is BINARY_SUMMARY_ONLY
259
+ // (keepable in one acknowledged action); else COMPLETE.
260
+ const captured = [
261
+ ...gitChanges.map((c) => buildCapturedFileChange(toCapturedChangeInput(changeSetId, c))),
262
+ ...casFiles.map((f) => buildCapturedFileChange(casToCapturedChangeInput(changeSetId, f))),
263
+ ...unreviewable.map((p) => buildCapturedFileChange(unreviewableChangeInput(changeSetId, p, unreviewableCaptureClass))),
264
+ ];
265
+ // Snapshot kind follows the substrate(s) that captured this turn: CAS-only for
266
+ // a non-git workspace, HYBRID when a git turn also touched ignored/non-git
267
+ // paths, else a plain git tree.
268
+ const snapshot = !gitWorkspace
269
+ ? casManifestSnapshotRef(casRef)
270
+ : casRef
271
+ ? hybridSnapshotRef(afterTree, captureRef(executionId), casRef)
272
+ : gitTreeSnapshotRef(afterTree, captureRef(executionId));
273
+ const event = buildCandidateCapturedEvent(
274
+ changeSetContext(changeSetId, harnessId),
275
+ snapshot,
276
+ captured,
277
+ commandProvenance,
278
+ );
279
+ appendFileReviewEvents(status, executionId, [event]);
280
+ return gitChanges;
281
+ }
282
+
283
+ /** Outcome of reconciling a DECIDED change set's decisions on resume. */
284
+ export interface CaptureResumeResult {
285
+ /** False when this resume is not a capture turn (no capture ref present). */
286
+ readonly isCaptureTurn: boolean;
287
+ /** Repo-relative paths applied (approved) to the working tree. */
288
+ readonly approvedPaths: readonly string[];
289
+ /** Repo-relative paths discarded (rejected / undecided-then-skipped). */
290
+ readonly rejectedPaths: readonly string[];
291
+ /** True when at least one file was rejected. */
292
+ readonly hadReject: boolean;
293
+ /**
294
+ * True when an approved file's on-disk bytes no longer hash to the digest the
295
+ * user approved: a FileReviewFailure(HASH_MISMATCH) was authored and NOTHING
296
+ * was applied (what-you-approve-is-what-applies is a hard gate).
297
+ */
298
+ readonly failed: boolean;
299
+ /** Human-readable detail when {@link failed} (the diverged paths). */
300
+ readonly failureDetail?: string;
301
+ }
302
+
303
+ /**
304
+ * Resume: reconcile the working tree to the change set's DECIDED decisions
305
+ * (projected server-side onto `FileChangeSet.decisions`), sourced entirely from
306
+ * the pinned baseline/after refs — approved files are ensured at their "after"
307
+ * bytes (uncommitted; normally already on disk from the review window, re-asserted
308
+ * idempotently), rejected/undecided files are snapped back to baseline. Then
309
+ * author RECONCILED with the post-reconcile approved snapshot and release the
310
+ * refs. Because both sides are ref-sourced, the result is correct regardless of
311
+ * the tree's current contents (idempotent under a Temporal retry or a tree reset).
312
+ *
313
+ * Scope precedence is runner-owned, most-specific-wins: the CHANGE_SET decision
314
+ * (if any) seeds every file, then per-file FILE decisions override it.
315
+ *
316
+ * Enforcement ("what you approve is what applies"): before keeping an approved
317
+ * file, the recomputed bytes must still hash to the reviewed digest. Any mismatch
318
+ * authors a FileReviewFailure(HASH_MISMATCH) and applies NOTHING.
319
+ *
320
+ * Mutates `status.fileReviewEventStream` in place (authors RECONCILED / FAILED).
321
+ */
322
+ export async function applyCaptureDecisions(opts: {
323
+ readonly status: AgentExecutionStatus;
324
+ readonly gitRoot: string;
325
+ readonly executionId: string;
326
+ readonly changeSet: FileChangeSet;
327
+ readonly harnessId: string;
328
+ readonly excludePaths?: readonly string[];
329
+ /**
330
+ * The CAS blob reader, used to reconcile ignored / non-git (CAS) files from the
331
+ * durable manifest — approved after-blobs re-written (hash-verified), rejected
332
+ * files snapped back to their before-blobs. Whether the CAS branch runs at all
333
+ * is decided by the change set's CANDIDATE snapshot (a git-only turn carries no
334
+ * CAS ref), NOT by probing storage. Omit for a harness with no artifact storage;
335
+ * a CAS/HYBRID change set then cannot be reconciled.
336
+ *
337
+ * `storage` is accepted so callers can pass the store they derived `readBlob`
338
+ * from (via {@link casBlobReader}); the reconcile itself reads only through
339
+ * `readBlob` and never probes `storage`.
340
+ */
341
+ readonly storage?: ArtifactStorage;
342
+ readonly readBlob?: BlobReader;
343
+ /**
344
+ * True (default) for a git work tree; false for a CAS-only non-git workspace
345
+ * (DD-21 D2). When false, git refs are never consulted — the reconcile is driven
346
+ * entirely by the durable CAS manifest, and RECONCILED carries a `CAS_MANIFEST`
347
+ * snapshot instead of a re-pinned git tree.
348
+ */
349
+ readonly gitWorkspace?: boolean;
350
+ }): Promise<CaptureResumeResult> {
351
+ const { status, gitRoot, executionId, changeSet, harnessId, excludePaths, readBlob } = opts;
352
+ const gitWorkspace = opts.gitWorkspace ?? true;
353
+
354
+ // The two substrates are resolved up front and reconciled independently below:
355
+ // git-tracked files from the pinned baseline/after refs, ignored/non-git files
356
+ // from the durable CAS manifest. A resume is a capture turn iff at least one is
357
+ // present; with neither this is an ordinary (non-capture) resume.
358
+ //
359
+ // Whether this turn captured any CAS (ignored / non-git) files is a fact the
360
+ // ledger already records: the change set's CANDIDATE snapshot carries a CAS
361
+ // manifest ref for a CAS/HYBRID turn and none for a git-only turn. We branch on
362
+ // that ledger fact — NOT on probing artifact storage. A git-only turn writes no
363
+ // manifest, and asking storage "does the manifest exist?" is both redundant and
364
+ // unsafe: a presigned/proxy backend answers true for any key, which turned a
365
+ // git-only reconcile into a doomed manifest download and a 404 crash. Single
366
+ // source of truth: derive the substrate from the ledger, never from storage.
367
+ const recomputed = gitWorkspace ? await recomputeChangeSet(gitRoot, executionId) : undefined;
368
+ const casRef = candidateCasRef(changeSet);
369
+ if (casRef && !readBlob) {
370
+ // The ledger says CAS files were captured, but this caller cannot read blobs
371
+ // back. Skipping would leave those files applied with their decisions
372
+ // unenforced — a silent partial reconcile — so refuse loudly instead.
373
+ throw new Error(
374
+ `applyCaptureDecisions: change set '${changeSet.id}' captured CAS files ` +
375
+ `(manifest '${casRef.artifactUri}') but no blob reader was provided`,
376
+ );
377
+ }
378
+ const manifest = casRef && readBlob ? await loadCasManifest({ readBlob, ref: casRef }) : undefined;
379
+ if (!recomputed && !manifest) {
380
+ return {
381
+ isCaptureTurn: false,
382
+ approvedPaths: [],
383
+ rejectedPaths: [],
384
+ hadReject: false,
385
+ failed: false,
386
+ };
387
+ }
388
+
389
+ const actionByChangeId = resolveDecisions(changeSet);
390
+
391
+ const approved: GitCapturedChange[] = [];
392
+ const rejected: GitCapturedChange[] = [];
393
+ const approvedPaths: string[] = [];
394
+ const rejectedPaths: string[] = [];
395
+ const mismatches: string[] = [];
396
+ let hadReject = false;
397
+
398
+ for (const change of recomputed?.changes ?? []) {
399
+ const protoChange = changeSet.changes.find(
400
+ (c) => c.id === `${changeSet.id}:${change.path}`,
401
+ );
402
+ const action = protoChange
403
+ ? actionByChangeId.get(protoChange.id) ?? FileDecisionAction.UNSPECIFIED
404
+ : FileDecisionAction.UNSPECIFIED;
405
+
406
+ if (action === FileDecisionAction.APPROVE) {
407
+ // Enforcement gate: the bytes we are about to keep must still match what
408
+ // the reviewer approved. A divergence is never silently applied.
409
+ if (protoChange && !digestMatches(change, protoChange)) {
410
+ mismatches.push(change.path);
411
+ continue;
412
+ }
413
+ approved.push(change);
414
+ approvedPaths.push(change.path);
415
+ } else {
416
+ if (action === FileDecisionAction.REJECT) hadReject = true;
417
+ rejected.push(change);
418
+ rejectedPaths.push(change.path);
419
+ }
420
+ }
421
+
422
+ if (mismatches.length > 0) {
423
+ const detail = `on-disk content diverged from the approved digest for: ${mismatches.join(", ")}`;
424
+ appendFileReviewEvents(status, executionId, [
425
+ buildFailedEvent(
426
+ changeSetContext(changeSet.id, harnessId),
427
+ FileReviewFailureKind.HASH_MISMATCH,
428
+ detail,
429
+ ),
430
+ ]);
431
+ // Refuse to apply anything: a partial apply under a verification failure is
432
+ // worse than none. The refs are left in place for diagnosis.
433
+ return {
434
+ isCaptureTurn: true,
435
+ approvedPaths: [],
436
+ rejectedPaths: [],
437
+ hadReject,
438
+ failed: true,
439
+ failureDetail: detail,
440
+ };
441
+ }
442
+
443
+ // Reconcile git-tracked files from the authoritative refs: approved files keep
444
+ // (re-assert) their "after" bytes; rejected/undecided files snap back to baseline.
445
+ if (recomputed) {
446
+ await applyApprovedPaths(gitRoot, recomputed.afterTree, approved);
447
+ await restoreToBaseline(gitRoot, recomputed.baselineTree, rejected);
448
+ }
449
+
450
+ // Reconcile CAS-captured (ignored / non-git) files from the durable manifest,
451
+ // sourced entirely from artifact storage (the CAS analogue of the git refs).
452
+ // Same decision map, keyed by the per-file change id, so the resolution rule is
453
+ // identical across substrates. For a non-git workspace this is the ONLY reconcile.
454
+ if (manifest) {
455
+ const casApproved: CasCapturedFile[] = [];
456
+ const casRejected: CasCapturedFile[] = [];
457
+ for (const file of manifest.files) {
458
+ const path = file.pathAfter || file.pathBefore;
459
+ const action = actionByChangeId.get(`${changeSet.id}:${path}`) ?? FileDecisionAction.UNSPECIFIED;
460
+ if (action === FileDecisionAction.APPROVE) {
461
+ casApproved.push(file);
462
+ approvedPaths.push(path);
463
+ } else {
464
+ if (action === FileDecisionAction.REJECT) hadReject = true;
465
+ casRejected.push(file);
466
+ rejectedPaths.push(path);
467
+ }
468
+ }
469
+ await applyCasApproved({ readBlob: readBlob!, workspaceRoot: gitRoot, files: casApproved });
470
+ await restoreCasToBaseline({ readBlob: readBlob!, workspaceRoot: gitRoot, files: casRejected });
471
+ }
472
+
473
+ // Author RECONCILED carrying the exact post-reconcile snapshot. A git turn
474
+ // re-pins the approved working tree; a CAS-only turn references the durable
475
+ // manifest (the reconciled state is fully derivable from manifest + decisions,
476
+ // and there is no tree to pin).
477
+ let approvedSnapshot: SnapshotRef;
478
+ if (recomputed) {
479
+ const snap = await snapshotApproved(gitRoot, executionId, excludePaths);
480
+ approvedSnapshot = gitTreeSnapshotRef(snap.treeOid, snap.ref);
481
+ } else {
482
+ approvedSnapshot = casManifestSnapshotRef(candidateCasRef(changeSet));
483
+ }
484
+ appendFileReviewEvents(status, executionId, [
485
+ buildReconciledEvent(changeSetContext(changeSet.id, harnessId), approvedSnapshot),
486
+ ]);
487
+
488
+ if (recomputed) await dropCaptureRefs(gitRoot, executionId);
489
+
490
+ return {
491
+ isCaptureTurn: true,
492
+ approvedPaths,
493
+ rejectedPaths,
494
+ hadReject,
495
+ failed: false,
496
+ };
497
+ }
498
+
499
+ // ---------------------------------------------------------------------------
500
+ // Internals
501
+ // ---------------------------------------------------------------------------
502
+
503
+ /**
504
+ * Resolve each file's effective decision (most-specific-wins, runner-owned):
505
+ * seed every change with the CHANGE_SET decision (if present), then override per
506
+ * file with FILE decisions. Returns change-id -> action.
507
+ */
508
+ function resolveDecisions(
509
+ changeSet: FileChangeSet,
510
+ ): Map<string, FileDecisionAction> {
511
+ const byChangeId = new Map<string, FileDecisionAction>();
512
+ const changeSetDecision = changeSet.decisions.find(
513
+ (d) => d.scope === FileDecisionScope.CHANGE_SET,
514
+ );
515
+ if (changeSetDecision) {
516
+ for (const change of changeSet.changes) {
517
+ byChangeId.set(change.id, changeSetDecision.action);
518
+ }
519
+ }
520
+ for (const decision of changeSet.decisions) {
521
+ if (decision.scope === FileDecisionScope.FILE && decision.fileChangeId) {
522
+ byChangeId.set(decision.fileChangeId, decision.action);
523
+ }
524
+ }
525
+ return byChangeId;
526
+ }
527
+
528
+ /**
529
+ * Verify the recomputed content still hashes to the digests the reviewer
530
+ * approved. The "after" side is checked for CREATE/MODIFY (the bytes we keep);
531
+ * the "before" side for MODIFY/DELETE (the baseline we restore from / remove).
532
+ *
533
+ * The sha is taken via the shared {@link contentSha256} over the recomputed
534
+ * {@link CapturedContent} — the SAME function that produced the persisted
535
+ * `before_sha256`/`after_sha256` at capture — so it is byte-true for binary and
536
+ * text alike and matches by construction when the underlying bytes are unchanged.
537
+ * (Re-hashing an inline body string here would be lossy for binaries and would
538
+ * spuriously fail an approved binary, since a binary side carries no body.)
539
+ */
540
+ function digestMatches(
541
+ gitChange: GitCapturedChange,
542
+ protoChange: CapturedFileChange,
543
+ ): boolean {
544
+ if (gitChange.changeType !== FileChangeType.DELETE) {
545
+ if (!gitChange.after || contentSha256(gitChange.after) !== protoChange.afterSha256) {
546
+ return false;
547
+ }
548
+ }
549
+ if (gitChange.changeType !== FileChangeType.CREATE) {
550
+ if (!gitChange.before || contentSha256(gitChange.before) !== protoChange.beforeSha256) {
551
+ return false;
552
+ }
553
+ }
554
+ return true;
555
+ }
556
+
557
+ /** The shared change-set context every event for this turn carries. */
558
+ function changeSetContext(changeSetId: string, harnessId: string): ChangeSetContext {
559
+ return {
560
+ changeSetId,
561
+ turnId: changeSetId,
562
+ harnessId,
563
+ timestamp: utcTimestamp(),
564
+ };
565
+ }
566
+
567
+ /** Build a git-tree {@link SnapshotRef} for the ledger from a pinned ref. */
568
+ function gitTreeSnapshotRef(treeOid: string, ref: string): SnapshotRef {
569
+ return create(SnapshotRefSchema, {
570
+ kind: SnapshotKind.GIT_TREE_REF,
571
+ git: create(GitTreeRefSchema, { treeOid, ref }),
572
+ });
573
+ }
574
+
575
+ /**
576
+ * Build a HYBRID {@link SnapshotRef} composing the git after-tree with the CAS
577
+ * manifest — the shape for a turn that touched both git-tracked and ignored /
578
+ * non-git paths (design doc 06 D3).
579
+ */
580
+ function hybridSnapshotRef(treeOid: string, ref: string, cas: CasSnapshotRef): SnapshotRef {
581
+ return create(SnapshotRefSchema, {
582
+ kind: SnapshotKind.HYBRID,
583
+ git: create(GitTreeRefSchema, { treeOid, ref }),
584
+ cas: create(CasManifestRefSchema, {
585
+ manifestDigest: cas.manifestDigest,
586
+ artifactUri: cas.artifactUri,
587
+ }),
588
+ });
589
+ }
590
+
591
+ /**
592
+ * Build a CAS-only {@link SnapshotRef} — the shape for a non-git workspace, whose
593
+ * every captured path lives in the content-addressed manifest and no git tree
594
+ * exists (DD-21 D2). `cas` is absent for the BASELINE placeholder (the manifest is
595
+ * authored at candidate time); present for CANDIDATE/RECONCILED once it exists.
596
+ */
597
+ function casManifestSnapshotRef(cas?: CasSnapshotRef): SnapshotRef {
598
+ return create(SnapshotRefSchema, {
599
+ kind: SnapshotKind.CAS_MANIFEST,
600
+ cas: cas
601
+ ? create(CasManifestRefSchema, {
602
+ manifestDigest: cas.manifestDigest,
603
+ artifactUri: cas.artifactUri,
604
+ })
605
+ : undefined,
606
+ });
607
+ }
608
+
609
+ /**
610
+ * The CAS manifest reference a change set was captured against, read from its
611
+ * CANDIDATE snapshot — the durable record a CAS-only RECONCILED event points back
612
+ * at (the reconciled state is manifest + decisions; there is no tree to re-pin).
613
+ * Undefined when the candidate carried no CAS manifest.
614
+ */
615
+ function candidateCasRef(changeSet: FileChangeSet): CasSnapshotRef | undefined {
616
+ const cas = changeSet.candidateSnapshot?.cas;
617
+ return cas ? { manifestDigest: cas.manifestDigest, artifactUri: cas.artifactUri } : undefined;
618
+ }
619
+
620
+ /**
621
+ * Map a CAS-captured file to the harness-agnostic producer input. The before/
622
+ * after bodies are carried as blob REFS (already offloaded to artifact storage
623
+ * by the CAS substrate), never re-inlined — so the CANDIDATE event stays small
624
+ * and the bytes are stored exactly once. Enforcement digests come from the blob
625
+ * content addresses, identical in form to the git path.
626
+ */
627
+ function casToCapturedChangeInput(
628
+ changeSetId: string,
629
+ file: CasCapturedFile,
630
+ ): CapturedChangeInput {
631
+ return {
632
+ id: `${changeSetId}:${file.pathAfter || file.pathBefore}`,
633
+ pathBefore: file.pathBefore,
634
+ pathAfter: file.pathAfter,
635
+ kind: file.kind,
636
+ captureClass: file.captureClass,
637
+ before: file.before
638
+ ? {
639
+ kind: "ref",
640
+ sha256: file.before.sha256,
641
+ storageKey: file.before.storageKey,
642
+ sizeBytes: file.before.sizeBytes,
643
+ isBinary: file.before.isBinary,
644
+ }
645
+ : undefined,
646
+ after: file.after
647
+ ? {
648
+ kind: "ref",
649
+ sha256: file.after.sha256,
650
+ storageKey: file.after.storageKey,
651
+ sizeBytes: file.after.sizeBytes,
652
+ isBinary: file.after.isBinary,
653
+ }
654
+ : undefined,
655
+ diffComplete: file.diffComplete,
656
+ };
657
+ }
658
+
659
+ /**
660
+ * Map a secret-blocked gitignored path to a content-less producer input (design
661
+ * doc 12, DD-E). The bytes are deliberately never captured, so both sides are
662
+ * absent (empty enforcement digests) and `diffComplete=false`. Being non-binary
663
+ * incomplete with no keepable bytes, it forces the change set to PARTIAL_BLOCKED
664
+ * (never BINARY_SUMMARY_ONLY) — approval is blocked and the path is surfaced
665
+ * honestly, while its CONTENT never enters the ledger or storage. `blockedReason` records
666
+ * the honest cause (SECRET_WITHHELD) so the review UI can say *why* rather than
667
+ * showing a cause-agnostic "unavailable" (doc 15). Kind is MODIFY: the write was
668
+ * blocked before it ran, so create-vs-modify is unknown and irrelevant (nothing
669
+ * is ever applied or reconciled for this entry). `captureClass` is the turn's CAS
670
+ * substrate class (GIT_IGNORED_CAPTURED | NON_GIT_CAS) so the blocked path is
671
+ * labeled with its true provenance.
672
+ */
673
+ function unreviewableChangeInput(
674
+ changeSetId: string,
675
+ path: string,
676
+ captureClass: FileCaptureClass,
677
+ ): CapturedChangeInput {
678
+ return {
679
+ id: `${changeSetId}:${path}`,
680
+ pathBefore: path,
681
+ pathAfter: path,
682
+ kind: FileChangeKind.MODIFY,
683
+ captureClass,
684
+ diffComplete: false,
685
+ blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
686
+ };
687
+ }
688
+
689
+ /** Map the git capture kind to the file-review {@link FileChangeKind}. */
690
+ function toFileChangeKind(changeType: FileChangeType): FileChangeKind {
691
+ switch (changeType) {
692
+ case FileChangeType.CREATE:
693
+ return FileChangeKind.ADD;
694
+ case FileChangeType.DELETE:
695
+ return FileChangeKind.DELETE;
696
+ default:
697
+ return FileChangeKind.MODIFY;
698
+ }
699
+ }
700
+
701
+ /**
702
+ * Map a git-derived capture (`--no-renames`, so ADD/MODIFY/DELETE) to the
703
+ * harness-agnostic producer input. The before/after content is the
704
+ * {@link CapturedContent} the substrate derived from the exact blob bytes; a
705
+ * binary side (carried as a content address, no body) marks the file incomplete
706
+ * so the change set cannot be approved as a complete text diff.
707
+ */
708
+ function toCapturedChangeInput(
709
+ changeSetId: string,
710
+ change: GitCapturedChange,
711
+ ): CapturedChangeInput {
712
+ const isCreate = change.changeType === FileChangeType.CREATE;
713
+ const isDelete = change.changeType === FileChangeType.DELETE;
714
+ const pathBefore = isCreate ? "" : change.path;
715
+ const pathAfter = isDelete ? "" : change.path;
716
+ const binary = change.before?.kind === "binary" || change.after?.kind === "binary";
717
+ return {
718
+ id: `${changeSetId}:${pathAfter || pathBefore}`,
719
+ pathBefore,
720
+ pathAfter,
721
+ kind: toFileChangeKind(change.changeType),
722
+ captureClass: FileCaptureClass.GIT_TRACKED,
723
+ before: change.before,
724
+ after: change.after,
725
+ diffComplete: !binary,
726
+ };
727
+ }