@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,401 @@
1
+ /**
2
+ * Content-addressed snapshot/restore substrate for the apply-then-review HITL
3
+ * subsystem — the sibling of {@link ./git-substrate.js} for paths git cannot
4
+ * snapshot (`.gitignored` and non-git files). It closes the "gap" the git
5
+ * substrate documents at its top: shell/MCP and gitignored writes are not
6
+ * reversible by git, so today they stay on the deny-gate; this module makes
7
+ * gitignored / non-git file edits capturable, reviewable, and reversible.
8
+ *
9
+ * THE MODEL (mirrors the git substrate, different storage domain)
10
+ * --------------------------------------------------------------
11
+ * The git substrate snapshots the whole working tree at the turn boundary and
12
+ * diffs two pinned tree objects. CAS cannot do that: git does not see ignored
13
+ * paths, and a plain directory has no history to recover a pre-edit byte from.
14
+ * So the CAS substrate is fed the exact before/after bytes of each touched path
15
+ * (captured by the harness at mutation time — the deep-agent's
16
+ * CasCaptureFilesystemBackend, or the Cursor hook staging the file before it
17
+ * allows the write) and:
18
+ *
19
+ * 1. Stores each distinct before/after body as a CONTENT-ADDRESSED blob under
20
+ * `artifacts/{executionId}/filereview/cas/blobs/{sha256}` (deduped by hash,
21
+ * idempotent), reusing the existing {@link ArtifactStorage} — no new store.
22
+ * 2. Writes a per-change-set MANIFEST (path -> before/after blob refs) under
23
+ * `artifacts/{executionId}/filereview/cas/{changeSetId}.manifest.json`. The
24
+ * manifest is the durable, authoritative source for the resume-time
25
+ * reconcile — the CAS analogue of the git substrate's pinned refs.
26
+ * 3. On resume, reconciles the workspace to the user's per-file decisions
27
+ * sourced ENTIRELY from the manifest: approved files are written to their
28
+ * "after" bytes; rejected/undecided files are snapped back to their "before"
29
+ * bytes (or removed, for a rejected create). Symmetric and idempotent.
30
+ *
31
+ * DURABILITY DOMAIN (design doc 11 D3)
32
+ * -----------------------------------
33
+ * Unlike git refs, which live inside the repo, CAS blobs + manifests live in
34
+ * artifact storage (a fixed host path in OSS, R2 via the proxy in Cloud) — a
35
+ * DIFFERENT durability domain that must outlive the multi-day approval wait and
36
+ * a sandbox recycle. Because reconcile reads only from the manifest + blobs
37
+ * (never the live tree), it converges regardless of the tree's current state,
38
+ * so a Temporal retry or a recycled workspace is harmless.
39
+ *
40
+ * WHAT THIS MODULE IS NOT
41
+ * -----------------------
42
+ * It is harness-agnostic and wiring-agnostic: it takes explicit before/after
43
+ * bytes, never reads gitignore, never talks to a hook. WHICH paths flow here (vs
44
+ * stay on the deny-gate or block as DIFF_UNREVIEWABLE), and the secret-safety
45
+ * gate (design doc 12), are the harness adapter's concern, applied BEFORE bytes
46
+ * reach this module. This module never persists a path it is not given.
47
+ *
48
+ * @since File-Change HITL Redesign (Phase 3 — CAS)
49
+ */
50
+
51
+ import { mkdir, rm, writeFile } from "node:fs/promises";
52
+ import { dirname, join } from "node:path";
53
+ import {
54
+ FileCaptureClass,
55
+ FileChangeKind,
56
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
57
+ import type { ArtifactStorage } from "../artifact-storage.js";
58
+ import { bytesLookBinary } from "../file-change.js";
59
+ import { sha256Bytes, sha256Hex } from "./digest.js";
60
+
61
+ /**
62
+ * Reads an artifact's raw bytes back by storage key. Injected so this module
63
+ * stays free of HTTP/transport concerns and is trivially testable; production
64
+ * callers build one with {@link casBlobReader}.
65
+ */
66
+ export type BlobReader = (storageKey: string) => Promise<Buffer>;
67
+
68
+ /** A stored, content-addressed blob and where to find it. */
69
+ export interface CasBlobRef {
70
+ /** Lowercase-hex SHA-256 of the bytes (the content address + enforcement digest). */
71
+ readonly sha256: string;
72
+ /** Artifact-storage key the bytes live under. */
73
+ readonly storageKey: string;
74
+ readonly sizeBytes: number;
75
+ /** A NUL byte was present — the diff cannot render as text (blocks COMPLETE). */
76
+ readonly isBinary: boolean;
77
+ }
78
+
79
+ /** One captured file within a CAS change set (before/after blob refs). */
80
+ export interface CasCapturedFile {
81
+ /** Path before the change (workspace-relative). Empty for ADD. */
82
+ readonly pathBefore: string;
83
+ /** Path after the change. Empty for DELETE. */
84
+ readonly pathAfter: string;
85
+ readonly kind: FileChangeKind;
86
+ /** Which substrate class captured it (GIT_IGNORED_CAPTURED | NON_GIT_CAS). */
87
+ readonly captureClass: FileCaptureClass;
88
+ /** Pre-edit blob; absent for ADD. */
89
+ readonly before?: CasBlobRef;
90
+ /** Post-edit blob; absent for DELETE. */
91
+ readonly after?: CasBlobRef;
92
+ /** False when a side is binary — the change set cannot be approved as complete. */
93
+ readonly diffComplete: boolean;
94
+ }
95
+
96
+ /** The durable manifest for one change set — the reconcile's source of truth. */
97
+ export interface CasManifest {
98
+ readonly changeSetId: string;
99
+ readonly files: readonly CasCapturedFile[];
100
+ }
101
+
102
+ /** A pointer to a persisted {@link CasManifest} (mirrors the proto CasManifestRef). */
103
+ export interface CasSnapshotRef {
104
+ /** SHA-256 over the canonical manifest JSON (tamper/integrity check). */
105
+ readonly manifestDigest: string;
106
+ /** Artifact-storage key the manifest JSON lives under. */
107
+ readonly artifactUri: string;
108
+ }
109
+
110
+ /** The before/after bytes of one path the harness captured this turn. */
111
+ export interface CasPathCapture {
112
+ /** Workspace-root-relative path. */
113
+ readonly path: string;
114
+ /** Pre-edit bytes; `null` when the file did not exist before (an ADD). */
115
+ readonly before: Uint8Array | null;
116
+ /** Post-edit bytes; `null` when the file was removed (a DELETE). */
117
+ readonly after: Uint8Array | null;
118
+ /** Which substrate class this path belongs to (for the ledger). */
119
+ readonly captureClass: FileCaptureClass;
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Key namespace (design doc 08: execution-scoped, reuse existing authz)
124
+ // ---------------------------------------------------------------------------
125
+
126
+ /** The CAS blob key for a content hash, under the execution's artifact prefix. */
127
+ export function casBlobKey(executionId: string, sha256: string): string {
128
+ return `artifacts/${executionId}/filereview/cas/blobs/${sha256}`;
129
+ }
130
+
131
+ /**
132
+ * The manifest key for a change set. The change-set id (`{exec}:{turnSeq}`)
133
+ * carries a colon; it is sanitized to a filesystem/object-store-safe segment so
134
+ * the key is portable across the local FS and R2 backends.
135
+ */
136
+ export function casManifestKey(executionId: string, changeSetId: string): string {
137
+ const safe = changeSetId.replace(/[^A-Za-z0-9._-]/g, "_");
138
+ return `artifacts/${executionId}/filereview/cas/${safe}.manifest.json`;
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Capture
143
+ // ---------------------------------------------------------------------------
144
+
145
+ /**
146
+ * Capture a CAS change set: store every distinct before/after body as a
147
+ * content-addressed blob (deduped, idempotent), assemble the per-file manifest,
148
+ * persist it, and return the manifest plus its ref for the ledger's SnapshotRef.
149
+ *
150
+ * Captures whose before and after bytes are byte-identical (a path touched but
151
+ * left unchanged) are dropped — they are not a reviewable change, exactly as the
152
+ * git substrate's diff omits unchanged files.
153
+ */
154
+ export async function snapshotCasChangeSet(opts: {
155
+ readonly storage: ArtifactStorage;
156
+ readonly executionId: string;
157
+ readonly changeSetId: string;
158
+ readonly captures: readonly CasPathCapture[];
159
+ }): Promise<{ manifest: CasManifest; ref: CasSnapshotRef }> {
160
+ const { storage, executionId, changeSetId, captures } = opts;
161
+
162
+ const files: CasCapturedFile[] = [];
163
+ for (const capture of captures) {
164
+ const file = await buildCasCapturedFile(storage, executionId, capture);
165
+ if (file) files.push(file);
166
+ }
167
+ // Deterministic order (by after path, then before path) so the manifest digest
168
+ // is stable and, later, composes with the git manifest into one aggregate
169
+ // digest that is byte-identical across editions (design doc 08 D3).
170
+ files.sort(compareByPath);
171
+
172
+ const manifest: CasManifest = { changeSetId, files };
173
+ const canonical = canonicalManifestJson(manifest);
174
+ const manifestKey = casManifestKey(executionId, changeSetId);
175
+ await storage.upload(manifestKey, Buffer.from(canonical, "utf8"), "application/json");
176
+
177
+ return {
178
+ manifest,
179
+ ref: { manifestDigest: sha256Hex(canonical), artifactUri: manifestKey },
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Store one path's before/after blobs and classify the change. Returns
185
+ * `undefined` for a no-op (both sides absent, or an unchanged touch).
186
+ */
187
+ async function buildCasCapturedFile(
188
+ storage: ArtifactStorage,
189
+ executionId: string,
190
+ capture: CasPathCapture,
191
+ ): Promise<CasCapturedFile | undefined> {
192
+ const { path, before, after, captureClass } = capture;
193
+ if (before === null && after === null) return undefined;
194
+
195
+ const beforeBuf = before === null ? null : Buffer.from(before);
196
+ const afterBuf = after === null ? null : Buffer.from(after);
197
+
198
+ // A touch that did not change the bytes is not a reviewable change.
199
+ if (beforeBuf && afterBuf && beforeBuf.equals(afterBuf)) return undefined;
200
+
201
+ const beforeRef = beforeBuf ? await storeBlob(storage, executionId, beforeBuf) : undefined;
202
+ const afterRef = afterBuf ? await storeBlob(storage, executionId, afterBuf) : undefined;
203
+
204
+ const kind =
205
+ beforeRef === undefined
206
+ ? FileChangeKind.ADD
207
+ : afterRef === undefined
208
+ ? FileChangeKind.DELETE
209
+ : FileChangeKind.MODIFY;
210
+
211
+ const isCreate = kind === FileChangeKind.ADD;
212
+ const isDelete = kind === FileChangeKind.DELETE;
213
+
214
+ return {
215
+ pathBefore: isCreate ? "" : path,
216
+ pathAfter: isDelete ? "" : path,
217
+ kind,
218
+ captureClass,
219
+ before: beforeRef,
220
+ after: afterRef,
221
+ // Binary on either side means the diff cannot render as text; the change set
222
+ // then cannot be approved as complete (parity with the git substrate).
223
+ diffComplete: !(beforeRef?.isBinary || afterRef?.isBinary),
224
+ };
225
+ }
226
+
227
+ /** Upload bytes under their content-address, skipping the write when present. */
228
+ async function storeBlob(
229
+ storage: ArtifactStorage,
230
+ executionId: string,
231
+ bytes: Buffer,
232
+ ): Promise<CasBlobRef> {
233
+ const sha256 = sha256Bytes(bytes);
234
+ const storageKey = casBlobKey(executionId, sha256);
235
+ // Content-addressed: identical bytes yield the same key, so an existing blob
236
+ // is never re-uploaded (idempotent under Temporal retries + intra-turn dedup).
237
+ if (!(await storage.exists(storageKey))) {
238
+ await storage.upload(storageKey, bytes, "application/octet-stream");
239
+ }
240
+ return { sha256, storageKey, sizeBytes: bytes.length, isBinary: bytesLookBinary(bytes) };
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Resume: load + reconcile
245
+ // ---------------------------------------------------------------------------
246
+
247
+ /**
248
+ * Load a change set's CAS manifest for the resume-time reconcile, digest-verified
249
+ * against the ledger's {@link CasSnapshotRef}.
250
+ *
251
+ * Called ONLY when the caller has already determined this turn captured CAS files
252
+ * — from the change set's CANDIDATE snapshot (the ledger's record of the
253
+ * substrate), never by probing storage. The ref is the whole contract: the
254
+ * manifest is fetched from the exact `artifactUri` the CANDIDATE event recorded
255
+ * (not a re-derived key, so a pending change set survives any future key-scheme
256
+ * change), and the downloaded bytes must hash to `manifestDigest` before they are
257
+ * trusted. Per-blob content addresses protect blob BODIES, but only this check
258
+ * protects the path→blob mapping itself — a corrupted or swapped manifest could
259
+ * otherwise direct a valid blob to the wrong path.
260
+ *
261
+ * Fail-closed: a missing manifest (surfaced by the reader) or a digest mismatch
262
+ * is a genuine integrity error, never a "this was a git-only turn" signal.
263
+ * (Deciding the substrate from storage existence was unsafe: a presigned/proxy
264
+ * backend cannot answer "does this key exist?" reliably, and a git-only turn —
265
+ * which writes no manifest — would then attempt a doomed download.)
266
+ */
267
+ export async function loadCasManifest(opts: {
268
+ readonly readBlob: BlobReader;
269
+ readonly ref: CasSnapshotRef;
270
+ }): Promise<CasManifest> {
271
+ const { readBlob, ref } = opts;
272
+ const bytes = await readBlob(ref.artifactUri);
273
+ const actual = sha256Bytes(bytes);
274
+ if (actual !== ref.manifestDigest) {
275
+ throw new Error(
276
+ `CAS manifest integrity check failed for '${ref.artifactUri}': ` +
277
+ `expected ${ref.manifestDigest}, got ${actual}`,
278
+ );
279
+ }
280
+ return JSON.parse(bytes.toString("utf8")) as CasManifest;
281
+ }
282
+
283
+ /**
284
+ * Ensure the APPROVED files hold their captured "after" bytes on disk. An
285
+ * approved CREATE/MODIFY writes the after blob (integrity-checked against its
286
+ * content address); an approved DELETE removes the file. Idempotent — re-running
287
+ * converges (the after blob is immutable), so a Temporal retry is safe.
288
+ *
289
+ * Enforcement ("what you approve is what applies"): the downloaded bytes must
290
+ * still hash to the blob's content address; a mismatch (storage corruption)
291
+ * throws rather than writing wrong bytes.
292
+ */
293
+ export async function applyCasApproved(opts: {
294
+ readonly readBlob: BlobReader;
295
+ readonly workspaceRoot: string;
296
+ readonly files: readonly CasCapturedFile[];
297
+ }): Promise<void> {
298
+ const { readBlob, workspaceRoot, files } = opts;
299
+ for (const file of files) {
300
+ if (file.kind === FileChangeKind.DELETE) {
301
+ await rm(join(workspaceRoot, file.pathBefore), { force: true });
302
+ continue;
303
+ }
304
+ // CREATE or MODIFY: write the approved after bytes.
305
+ if (!file.after) continue;
306
+ await writeVerifiedBlob(readBlob, join(workspaceRoot, file.pathAfter), file.after);
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Snap the REJECTED/undecided files back to their captured "before" bytes: a
312
+ * rejected CREATE is removed; a rejected MODIFY/DELETE is rewritten with its
313
+ * before blob. Idempotent, and the byte-exact inverse of {@link applyCasApproved}.
314
+ */
315
+ export async function restoreCasToBaseline(opts: {
316
+ readonly readBlob: BlobReader;
317
+ readonly workspaceRoot: string;
318
+ readonly files: readonly CasCapturedFile[];
319
+ }): Promise<void> {
320
+ const { readBlob, workspaceRoot, files } = opts;
321
+ for (const file of files) {
322
+ if (file.kind === FileChangeKind.ADD) {
323
+ // The file did not exist before this turn — remove the agent's creation.
324
+ await rm(join(workspaceRoot, file.pathAfter), { force: true });
325
+ continue;
326
+ }
327
+ // MODIFY or DELETE: the baseline holds the file — restore its exact bytes.
328
+ if (!file.before) continue;
329
+ await writeVerifiedBlob(readBlob, join(workspaceRoot, file.pathBefore), file.before);
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Adapt the artifact storage port's {@link ArtifactStorage.download} into a
335
+ * {@link BlobReader}. The seam is retained so the reconcile functions
336
+ * ({@link loadCasManifest}, {@link applyCasApproved}, {@link restoreCasToBaseline})
337
+ * depend only on "read one blob by key", not on the whole storage interface.
338
+ * `download` already throws a descriptive, key-scoped error on a missing blob or
339
+ * transport failure, so no extra wrapping is needed here.
340
+ */
341
+ export function casBlobReader(storage: ArtifactStorage): BlobReader {
342
+ return (storageKey: string): Promise<Buffer> => storage.download(storageKey);
343
+ }
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // Internals
347
+ // ---------------------------------------------------------------------------
348
+
349
+ /** Download a blob, verify its content address, and write it to `abs`. */
350
+ async function writeVerifiedBlob(
351
+ readBlob: BlobReader,
352
+ abs: string,
353
+ ref: CasBlobRef,
354
+ ): Promise<void> {
355
+ const bytes = await readBlob(ref.storageKey);
356
+ const actual = sha256Bytes(bytes);
357
+ if (actual !== ref.sha256) {
358
+ throw new Error(
359
+ `CAS blob integrity check failed for '${ref.storageKey}': ` +
360
+ `expected ${ref.sha256}, got ${actual}`,
361
+ );
362
+ }
363
+ await mkdir(dirname(abs), { recursive: true });
364
+ await writeFile(abs, bytes);
365
+ }
366
+
367
+ /** Stable order: by after path, then before path (substrate-agnostic, doc 08 D3). */
368
+ function compareByPath(a: CasCapturedFile, b: CasCapturedFile): number {
369
+ const ka = a.pathAfter || a.pathBefore;
370
+ const kb = b.pathAfter || b.pathBefore;
371
+ if (ka < kb) return -1;
372
+ if (ka > kb) return 1;
373
+ return a.pathBefore < b.pathBefore ? -1 : a.pathBefore > b.pathBefore ? 1 : 0;
374
+ }
375
+
376
+ /** Serialize a blob ref fully so the persisted manifest round-trips faithfully. */
377
+ function serializeBlob(b: CasBlobRef | undefined): object | null {
378
+ return b
379
+ ? { sha256: b.sha256, storageKey: b.storageKey, sizeBytes: b.sizeBytes, isBinary: b.isBinary }
380
+ : null;
381
+ }
382
+
383
+ /**
384
+ * The canonical JSON the manifest is BOTH persisted as and digested over: files
385
+ * in sorted order with a fixed key order, blob refs serialized in full (incl.
386
+ * `storageKey`) so a reloaded manifest is a faithful {@link CasManifest} the
387
+ * resume reconcile can read directly. Every field is deterministic, so the digest
388
+ * is a stable integrity check over the exact stored bytes.
389
+ */
390
+ function canonicalManifestJson(manifest: CasManifest): string {
391
+ const files = manifest.files.map((f) => ({
392
+ pathBefore: f.pathBefore,
393
+ pathAfter: f.pathAfter,
394
+ kind: f.kind,
395
+ captureClass: f.captureClass,
396
+ before: serializeBlob(f.before),
397
+ after: serializeBlob(f.after),
398
+ diffComplete: f.diffComplete,
399
+ }));
400
+ return JSON.stringify({ changeSetId: manifest.changeSetId, files });
401
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Canonical file-review digests — the runner-side mirror of the Go
3
+ * {@code filereview/digest.go} and the Java {@code FileDigest}, locked
4
+ * byte-for-byte by `apis/testdata/hitl/file-digest/vectors.json`.
5
+ *
6
+ * These are ENFORCEMENT digests, never correlation keys: the reconcile refuses
7
+ * to apply content whose digest differs from what the user approved. The runner
8
+ * is the sole producer of `before_sha256`/`after_sha256` (over the exact captured
9
+ * bytes) and of the derived `file_digest`/`aggregate_digest`; the backends only
10
+ * carry them. Keeping the formula identical across all three editions is what
11
+ * lets the cross-edition corpus prove parity.
12
+ */
13
+
14
+ import { createHash } from "node:crypto";
15
+ import { FileChangeKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
16
+
17
+ /**
18
+ * The proto enum value NAME for a {@link FileChangeKind}, e.g.
19
+ * `FILE_CHANGE_KIND_MODIFY`. The digest is computed over this name (matching Go's
20
+ * `kind.String()` and Java's `kind.name()`), NOT the TS member name, so all three
21
+ * editions hash an identical string. The cross-edition digest corpus validates
22
+ * this map; a missing or wrong entry fails the parity test.
23
+ */
24
+ const FILE_CHANGE_KIND_NAME: Readonly<Record<FileChangeKind, string>> = {
25
+ [FileChangeKind.UNSPECIFIED]: "FILE_CHANGE_KIND_UNSPECIFIED",
26
+ [FileChangeKind.ADD]: "FILE_CHANGE_KIND_ADD",
27
+ [FileChangeKind.MODIFY]: "FILE_CHANGE_KIND_MODIFY",
28
+ [FileChangeKind.DELETE]: "FILE_CHANGE_KIND_DELETE",
29
+ [FileChangeKind.RENAME]: "FILE_CHANGE_KIND_RENAME",
30
+ [FileChangeKind.BINARY_CHANGE]: "FILE_CHANGE_KIND_BINARY_CHANGE",
31
+ };
32
+
33
+ /** The proto enum value name for a FileChangeKind (for the digest canonical form). */
34
+ export function fileChangeKindName(kind: FileChangeKind): string {
35
+ return FILE_CHANGE_KIND_NAME[kind] ?? "FILE_CHANGE_KIND_UNSPECIFIED";
36
+ }
37
+
38
+ /** Lowercase-hex SHA-256 of a UTF-8 string. */
39
+ export function sha256Hex(s: string): string {
40
+ return createHash("sha256").update(s, "utf8").digest("hex");
41
+ }
42
+
43
+ /** Lowercase-hex SHA-256 of raw bytes — used for `before_sha256`/`after_sha256`. */
44
+ export function sha256Bytes(bytes: Uint8Array): string {
45
+ return createHash("sha256").update(bytes).digest("hex");
46
+ }
47
+
48
+ /** The identity+content fields a {@link fileDigest} is computed over. */
49
+ export interface FileDigestInput {
50
+ readonly pathBefore: string;
51
+ readonly pathAfter: string;
52
+ readonly kind: FileChangeKind;
53
+ readonly beforeSha256: string;
54
+ readonly afterSha256: string;
55
+ }
56
+
57
+ /**
58
+ * The canonical per-file digest:
59
+ * `sha256(join("\n", [path_before, path_after, kind_name, before_sha256, after_sha256]))`.
60
+ * Digesting the content HASHES (not raw bytes) keeps the string pure ASCII, free
61
+ * of any line-ending or encoding ambiguity across editions.
62
+ */
63
+ export function fileDigest(input: FileDigestInput): string {
64
+ return sha256Hex(
65
+ [
66
+ input.pathBefore,
67
+ input.pathAfter,
68
+ fileChangeKindName(input.kind),
69
+ input.beforeSha256,
70
+ input.afterSha256,
71
+ ].join("\n"),
72
+ );
73
+ }
74
+
75
+ /**
76
+ * The canonical change-set digest: `sha256(join("\n", sort_ascending(file_digests)))`.
77
+ * Sorting makes it independent of file order (a change set is an unordered set of
78
+ * files for identity). The empty set hashes the empty string.
79
+ */
80
+ export function aggregateDigest(inputs: readonly FileDigestInput[]): string {
81
+ const digests = inputs.map(fileDigest).sort();
82
+ return sha256Hex(digests.join("\n"));
83
+ }