@stigmer/runner 3.15.0 → 3.15.2-dev.20260913223433

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 (518) hide show
  1. package/README.md +2 -4
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +14 -0
  4. package/dist/__test-utils__/execution-record-fixture.js +4 -0
  5. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  6. package/dist/__test-utils__/file-review-projection.d.ts +40 -0
  7. package/dist/__test-utils__/file-review-projection.js +136 -0
  8. package/dist/__test-utils__/file-review-projection.js.map +1 -0
  9. package/dist/__test-utils__/harness-contract/types.d.ts +23 -1
  10. package/dist/__test-utils__/harness-contract/types.js +15 -0
  11. package/dist/__test-utils__/harness-contract/types.js.map +1 -1
  12. package/dist/__test-utils__/hermetic-activity.d.ts +44 -12
  13. package/dist/__test-utils__/hermetic-activity.js +52 -14
  14. package/dist/__test-utils__/hermetic-activity.js.map +1 -1
  15. package/dist/__test-utils__/model-registry-fixture.d.ts +32 -7
  16. package/dist/__test-utils__/model-registry-fixture.js +31 -7
  17. package/dist/__test-utils__/model-registry-fixture.js.map +1 -1
  18. package/dist/__test-utils__/turn-input-fixture.d.ts +1 -1
  19. package/dist/__test-utils__/turn-input-fixture.js +1 -1
  20. package/dist/__test-utils__/turn-input-fixture.js.map +1 -1
  21. package/dist/activities/call-agent.js +2 -2
  22. package/dist/activities/call-agent.js.map +1 -1
  23. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +45 -3
  24. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -1
  25. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +0 -9
  26. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +2 -41
  27. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  28. package/dist/activities/execute-cursor/adapter.d.ts +7 -5
  29. package/dist/activities/execute-cursor/adapter.js +7 -5
  30. package/dist/activities/execute-cursor/adapter.js.map +1 -1
  31. package/dist/activities/execute-cursor/approval-policy.d.ts +5 -2
  32. package/dist/activities/execute-cursor/approval-policy.js +5 -2
  33. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  34. package/dist/activities/execute-cursor/approval-state.d.ts +16 -8
  35. package/dist/activities/execute-cursor/approval-state.js +6 -3
  36. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  37. package/dist/activities/execute-cursor/cas-observations.d.ts +14 -2
  38. package/dist/activities/execute-cursor/cas-observations.js +19 -2
  39. package/dist/activities/execute-cursor/cas-observations.js.map +1 -1
  40. package/dist/activities/execute-cursor/cursor-capabilities.d.ts +8 -4
  41. package/dist/activities/execute-cursor/cursor-capabilities.js +21 -4
  42. package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -1
  43. package/dist/activities/execute-cursor/message-translator.d.ts +12 -20
  44. package/dist/activities/execute-cursor/message-translator.js +19 -32
  45. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  46. package/dist/activities/execute-cursor/prompt-builder.d.ts +48 -58
  47. package/dist/activities/execute-cursor/prompt-builder.js +76 -95
  48. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  49. package/dist/activities/execute-cursor/turn-boundary.d.ts +27 -55
  50. package/dist/activities/execute-cursor/turn-boundary.js +30 -90
  51. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
  52. package/dist/activities/execute-cursor/turn-settle.d.ts +11 -7
  53. package/dist/activities/execute-cursor/turn-settle.js +37 -49
  54. package/dist/activities/execute-cursor/turn-settle.js.map +1 -1
  55. package/dist/activities/execute-cursor/turn-setup.d.ts +16 -24
  56. package/dist/activities/execute-cursor/turn-setup.js +29 -59
  57. package/dist/activities/execute-cursor/turn-setup.js.map +1 -1
  58. package/dist/activities/execute-cursor/turn-stream.d.ts +3 -8
  59. package/dist/activities/execute-cursor/turn-stream.js +9 -22
  60. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  61. package/dist/activities/execute-cursor/turn.js +5 -1
  62. package/dist/activities/execute-cursor/turn.js.map +1 -1
  63. package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.d.ts +93 -0
  64. package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.js +348 -0
  65. package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.js.map +1 -0
  66. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +170 -0
  67. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +225 -0
  68. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -0
  69. package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.d.ts +24 -0
  70. package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.js +41 -0
  71. package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.js.map +1 -0
  72. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.d.ts +54 -0
  73. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.js +72 -0
  74. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.js.map +1 -0
  75. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +187 -23
  76. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +238 -27
  77. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -1
  78. package/dist/activities/execute-deep-agent/adapter.d.ts +41 -0
  79. package/dist/activities/execute-deep-agent/adapter.js +78 -0
  80. package/dist/activities/execute-deep-agent/adapter.js.map +1 -0
  81. package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +8 -0
  82. package/dist/activities/execute-deep-agent/cas-capture-observer.js +9 -0
  83. package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -1
  84. package/dist/activities/execute-deep-agent/deep-agent-capabilities.d.ts +31 -0
  85. package/dist/activities/execute-deep-agent/deep-agent-capabilities.js +40 -0
  86. package/dist/activities/execute-deep-agent/deep-agent-capabilities.js.map +1 -0
  87. package/dist/activities/execute-deep-agent/execution-state.d.ts +4 -11
  88. package/dist/activities/execute-deep-agent/execution-state.js +4 -12
  89. package/dist/activities/execute-deep-agent/execution-state.js.map +1 -1
  90. package/dist/activities/execute-deep-agent/hitl.d.ts +49 -47
  91. package/dist/activities/execute-deep-agent/hitl.js +77 -103
  92. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  93. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +51 -23
  94. package/dist/activities/execute-deep-agent/prompt-builder.js +115 -76
  95. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  96. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +18 -32
  97. package/dist/activities/execute-deep-agent/status-builder-shared.js +10 -65
  98. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  99. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +10 -8
  100. package/dist/activities/execute-deep-agent/streaming-side-effects.js +12 -14
  101. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  102. package/dist/activities/execute-deep-agent/subagent-tracker.d.ts +36 -13
  103. package/dist/activities/execute-deep-agent/subagent-tracker.js +76 -39
  104. package/dist/activities/execute-deep-agent/subagent-tracker.js.map +1 -1
  105. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -28
  106. package/dist/activities/execute-deep-agent/subagent-transformer.js +29 -82
  107. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  108. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -2
  109. package/dist/activities/execute-deep-agent/subagent-wiring.js +2 -2
  110. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  111. package/dist/activities/execute-deep-agent/turn-settle.d.ts +42 -0
  112. package/dist/activities/execute-deep-agent/turn-settle.js +146 -0
  113. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -0
  114. package/dist/activities/execute-deep-agent/turn-setup.d.ts +212 -0
  115. package/dist/activities/execute-deep-agent/turn-setup.js +430 -0
  116. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -0
  117. package/dist/activities/execute-deep-agent/turn-stream.d.ts +97 -0
  118. package/dist/activities/execute-deep-agent/turn-stream.js +242 -0
  119. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -0
  120. package/dist/activities/execute-deep-agent/turn.d.ts +42 -0
  121. package/dist/activities/execute-deep-agent/turn.js +116 -0
  122. package/dist/activities/execute-deep-agent/turn.js.map +1 -0
  123. package/dist/activities/execute-deep-agent/v3-event-recorder.d.ts +6 -5
  124. package/dist/activities/execute-deep-agent/v3-event-recorder.js +6 -5
  125. package/dist/activities/execute-deep-agent/v3-event-recorder.js.map +1 -1
  126. package/dist/activities/execute-deep-agent/v3-events.d.ts +1 -1
  127. package/dist/activities/execute-deep-agent/v3-events.js +1 -1
  128. package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +60 -17
  129. package/dist/activities/execute-deep-agent/v3-status-builder.js +72 -57
  130. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  131. package/dist/harness/approval-decisions.d.ts +61 -0
  132. package/dist/harness/approval-decisions.js +102 -0
  133. package/dist/harness/approval-decisions.js.map +1 -0
  134. package/dist/harness/capabilities.d.ts +16 -5
  135. package/dist/harness/capabilities.js +9 -0
  136. package/dist/harness/capabilities.js.map +1 -1
  137. package/dist/harness/capture.d.ts +144 -0
  138. package/dist/harness/capture.js +219 -0
  139. package/dist/harness/capture.js.map +1 -0
  140. package/dist/harness/persist-chokepoint.d.ts +19 -4
  141. package/dist/harness/persist-chokepoint.js +17 -4
  142. package/dist/harness/persist-chokepoint.js.map +1 -1
  143. package/dist/harness/run-turn.js +115 -29
  144. package/dist/harness/run-turn.js.map +1 -1
  145. package/dist/harness/terminal-table.d.ts +40 -11
  146. package/dist/harness/terminal-table.js +58 -19
  147. package/dist/harness/terminal-table.js.map +1 -1
  148. package/dist/harness/turn-context.d.ts +141 -76
  149. package/dist/harness/turn-context.js +194 -99
  150. package/dist/harness/turn-context.js.map +1 -1
  151. package/dist/harness/types.d.ts +65 -12
  152. package/dist/harness/types.js +8 -2
  153. package/dist/harness/types.js.map +1 -1
  154. package/dist/harness-adapters.d.ts +12 -8
  155. package/dist/harness-adapters.js +17 -9
  156. package/dist/harness-adapters.js.map +1 -1
  157. package/dist/middleware/approval-gate.d.ts +1 -1
  158. package/dist/middleware/approval-gate.js +1 -1
  159. package/dist/middleware/approval-gate.js.map +1 -1
  160. package/dist/middleware/cost-advisory.d.ts +39 -0
  161. package/dist/middleware/{cost-cap.js → cost-advisory.js} +37 -55
  162. package/dist/middleware/cost-advisory.js.map +1 -0
  163. package/dist/middleware/index.d.ts +23 -12
  164. package/dist/middleware/index.js +26 -18
  165. package/dist/middleware/index.js.map +1 -1
  166. package/dist/middleware/types.d.ts +3 -2
  167. package/dist/runner-manager.js +1 -5
  168. package/dist/runner-manager.js.map +1 -1
  169. package/dist/runner.js +1 -5
  170. package/dist/runner.js.map +1 -1
  171. package/dist/shared/attachment-resolver.d.ts +41 -18
  172. package/dist/shared/attachment-resolver.js +178 -46
  173. package/dist/shared/attachment-resolver.js.map +1 -1
  174. package/dist/shared/attachment-zip.d.ts +85 -0
  175. package/dist/shared/attachment-zip.js +185 -0
  176. package/dist/shared/attachment-zip.js.map +1 -0
  177. package/dist/shared/connect-backfill.js +5 -4
  178. package/dist/shared/connect-backfill.js.map +1 -1
  179. package/dist/shared/cost-guard.d.ts +3 -3
  180. package/dist/shared/cost-guard.js +3 -3
  181. package/dist/shared/execution-status-writer.d.ts +5 -5
  182. package/dist/shared/execution-status-writer.js +5 -5
  183. package/dist/shared/extract-structured-output.d.ts +8 -7
  184. package/dist/shared/extract-structured-output.js +8 -7
  185. package/dist/shared/extract-structured-output.js.map +1 -1
  186. package/dist/shared/filereview/cas-progress.d.ts +1 -18
  187. package/dist/shared/filereview/cas-progress.js +4 -0
  188. package/dist/shared/filereview/cas-progress.js.map +1 -1
  189. package/dist/shared/filereview/cas-touched.d.ts +56 -0
  190. package/dist/shared/filereview/cas-touched.js +63 -0
  191. package/dist/shared/filereview/cas-touched.js.map +1 -0
  192. package/dist/shared/filereview/index.d.ts +2 -1
  193. package/dist/shared/filereview/index.js +2 -1
  194. package/dist/shared/filereview/index.js.map +1 -1
  195. package/dist/shared/mcp-resolver.d.ts +4 -5
  196. package/dist/shared/mcp-resolver.js +4 -5
  197. package/dist/shared/mcp-resolver.js.map +1 -1
  198. package/dist/shared/persist-decision.d.ts +3 -4
  199. package/dist/shared/persist-decision.js +3 -4
  200. package/dist/shared/persist-decision.js.map +1 -1
  201. package/dist/shared/plan-mode-permissions.d.ts +2 -2
  202. package/dist/shared/plan-mode-permissions.js +2 -2
  203. package/dist/shared/plan-mode-prompt.d.ts +1 -1
  204. package/dist/shared/plan-mode-prompt.js +1 -1
  205. package/dist/shared/prompt-sections.d.ts +163 -0
  206. package/dist/shared/prompt-sections.js +164 -0
  207. package/dist/shared/prompt-sections.js.map +1 -0
  208. package/dist/shared/skill-resolver.d.ts +4 -2
  209. package/dist/shared/skill-resolver.js +4 -2
  210. package/dist/shared/skill-resolver.js.map +1 -1
  211. package/dist/shared/tool-rounds.d.ts +32 -0
  212. package/dist/shared/tool-rounds.js +43 -0
  213. package/dist/shared/tool-rounds.js.map +1 -1
  214. package/dist/shared/tool-row.d.ts +34 -0
  215. package/dist/shared/tool-row.js +52 -0
  216. package/dist/shared/tool-row.js.map +1 -1
  217. package/dist/shared/workspace/session-provision.d.ts +3 -3
  218. package/dist/shared/workspace/session-provision.js +3 -3
  219. package/dist/shared/workspace/stigmer-link.d.ts +3 -3
  220. package/dist/shared/workspace/stigmer-link.js +3 -3
  221. package/dist/shared/workspace/writeback-coordinator.d.ts +15 -22
  222. package/dist/shared/workspace/writeback-coordinator.js +16 -53
  223. package/dist/shared/workspace/writeback-coordinator.js.map +1 -1
  224. package/dist/tools/index.d.ts +1 -1
  225. package/dist/tools/index.js +1 -1
  226. package/package.json +4 -4
  227. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +1 -0
  228. package/src/__test-utils__/execution-record-fixture.ts +18 -2
  229. package/src/__test-utils__/file-review-projection.ts +156 -0
  230. package/src/__test-utils__/git-workspace-fixture.ts +58 -0
  231. package/src/__test-utils__/harness-boot-order-child.ts +7 -2
  232. package/src/__test-utils__/harness-contract/contract.ts +33 -20
  233. package/src/__test-utils__/harness-contract/recording-sink.ts +11 -1
  234. package/src/__test-utils__/harness-contract/runtime-contract.ts +328 -10
  235. package/src/__test-utils__/harness-contract/scripted-adapter.ts +38 -6
  236. package/src/__test-utils__/harness-contract/types.ts +27 -3
  237. package/src/__test-utils__/hermetic-activity.ts +52 -14
  238. package/src/__test-utils__/model-registry-fixture.ts +32 -7
  239. package/src/__test-utils__/module-specifiers.ts +24 -0
  240. package/src/__test-utils__/turn-input-fixture.ts +2 -2
  241. package/src/__tests__/harness-boot-order.test.ts +9 -2
  242. package/src/activities/call-agent.ts +2 -2
  243. package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +47 -3
  244. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +2 -48
  245. package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +1 -1
  246. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +6 -1
  247. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +52 -27
  248. package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +25 -0
  249. package/src/activities/execute-cursor/__tests__/goldens/prompt.enhanced.everything.prompt.md +166 -0
  250. package/src/activities/execute-cursor/__tests__/goldens/prompt.enhanced.plan-mode.prompt.md +186 -0
  251. package/src/activities/execute-cursor/__tests__/goldens/prompt.hitl.recovery.prompt.md +191 -0
  252. package/src/activities/execute-cursor/__tests__/goldens/prompt.hitl.reinvocation.prompt.md +13 -0
  253. package/src/activities/execute-cursor/__tests__/goldens/prompt.resumed.prefixed.prompt.md +32 -0
  254. package/src/activities/execute-cursor/__tests__/goldens/prompt.resumed.raw.prompt.md +1 -0
  255. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +1 -2
  256. package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +4 -0
  257. package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +24 -4
  258. package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +6 -5
  259. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +9 -0
  260. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +11 -8
  261. package/src/activities/execute-cursor/__tests__/prompt-goldens.test.ts +229 -0
  262. package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +3 -3
  263. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +2 -2
  264. package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +78 -234
  265. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +1 -26
  266. package/src/activities/execute-cursor/adapter.ts +7 -5
  267. package/src/activities/execute-cursor/approval-policy.ts +5 -2
  268. package/src/activities/execute-cursor/approval-state.ts +16 -8
  269. package/src/activities/execute-cursor/cas-observations.ts +20 -2
  270. package/src/activities/execute-cursor/cursor-capabilities.ts +24 -5
  271. package/src/activities/execute-cursor/message-translator.ts +29 -41
  272. package/src/activities/execute-cursor/prompt-builder.ts +99 -157
  273. package/src/activities/execute-cursor/turn-boundary.ts +34 -140
  274. package/src/activities/execute-cursor/turn-settle.ts +37 -51
  275. package/src/activities/execute-cursor/turn-setup.ts +30 -70
  276. package/src/activities/execute-cursor/turn-stream.ts +8 -31
  277. package/src/activities/execute-cursor/turn.ts +5 -1
  278. package/src/activities/execute-deep-agent/__test-utils__/contract-subject.ts +391 -0
  279. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +313 -0
  280. package/src/activities/execute-deep-agent/__test-utils__/hitl-script.ts +48 -0
  281. package/src/activities/execute-deep-agent/__test-utils__/scripted-model-module.ts +85 -0
  282. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +383 -36
  283. package/src/activities/execute-deep-agent/__tests__/adapter-graph-is-sdk-free.test.ts +76 -0
  284. package/src/activities/execute-deep-agent/__tests__/adapter-is-temporal-free.test.ts +47 -0
  285. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +80 -0
  286. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +1 -1
  287. package/src/activities/execute-deep-agent/__tests__/execution-state-extended.test.ts +0 -2
  288. package/src/activities/execute-deep-agent/__tests__/execution-state.test.ts +0 -3
  289. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +172 -0
  290. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +167 -0
  291. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.minimal.prompt.md +36 -0
  292. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +178 -0
  293. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +158 -0
  294. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +210 -0
  295. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve-all-lease.turn1.status.json +74 -0
  296. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve-all-lease.turn2.status.json +104 -0
  297. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve.turn2.status.json +78 -0
  298. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +138 -0
  299. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/gate.turn1.status.json +68 -0
  300. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +154 -0
  301. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +116 -0
  302. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.after-tool.status.json +69 -0
  303. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.mid-tool.status.json +67 -0
  304. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/plain-turn.status.json +47 -0
  305. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/platform-stop.status.json +68 -0
  306. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +280 -0
  307. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/reject.turn2.status.json +77 -0
  308. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
  309. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sequential-gates.turn2.status.json +99 -0
  310. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sequential-gates.turn3.status.json +109 -0
  311. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/skip.turn2.status.json +76 -0
  312. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/structured-output.text-fallback.status.json +51 -0
  313. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/structured-output.tier2.status.json +51 -0
  314. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/structured-output.tool-strategy.status.json +56 -0
  315. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sub-agent-delegation.status.json +90 -0
  316. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/tool-call.status.json +70 -0
  317. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/worker-shutdown.status.json +71 -0
  318. package/src/activities/execute-deep-agent/__tests__/hermetic/harness-contract.test.ts +144 -0
  319. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-approve-all-lease.test.ts +184 -0
  320. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-round-trips.test.ts +203 -0
  321. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-sequential-gates.test.ts +170 -0
  322. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +175 -0
  323. package/src/activities/execute-deep-agent/__tests__/hermetic/pause-vs-shutdown.test.ts +206 -0
  324. package/src/activities/execute-deep-agent/__tests__/hermetic/plain-turn.test.ts +142 -0
  325. package/src/activities/execute-deep-agent/__tests__/hermetic/platform-stop.test.ts +126 -0
  326. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +139 -0
  327. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +122 -0
  328. package/src/activities/execute-deep-agent/__tests__/hermetic/structured-output.test.ts +236 -0
  329. package/src/activities/execute-deep-agent/__tests__/hermetic/sub-agent-delegation.test.ts +161 -0
  330. package/src/activities/execute-deep-agent/__tests__/hermetic/tool-call.test.ts +157 -0
  331. package/src/activities/execute-deep-agent/__tests__/hermetic/workspace-lock-timeout.test.ts +152 -0
  332. package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +50 -122
  333. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +8 -6
  334. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +2 -2
  335. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +35 -35
  336. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +247 -0
  337. package/src/activities/execute-deep-agent/__tests__/scripted-model.test.ts +329 -0
  338. package/src/activities/execute-deep-agent/__tests__/status-builder-shared.test.ts +1 -20
  339. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +78 -0
  340. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +1 -1
  341. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +2 -2
  342. package/src/activities/execute-deep-agent/__tests__/subagent-tracker.test.ts +57 -31
  343. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +19 -79
  344. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +15 -15
  345. package/src/activities/execute-deep-agent/__tests__/summarization-verification.test.ts +1 -1
  346. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +391 -0
  347. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +343 -38
  348. package/src/activities/execute-deep-agent/__tests__/vision-input.test.ts +5 -5
  349. package/src/activities/execute-deep-agent/adapter.ts +94 -0
  350. package/src/activities/execute-deep-agent/cas-capture-observer.ts +11 -0
  351. package/src/activities/execute-deep-agent/deep-agent-capabilities.ts +43 -0
  352. package/src/activities/execute-deep-agent/execution-state.ts +4 -13
  353. package/src/activities/execute-deep-agent/hitl.ts +100 -123
  354. package/src/activities/execute-deep-agent/prompt-builder.ts +141 -115
  355. package/src/activities/execute-deep-agent/status-builder-shared.ts +21 -84
  356. package/src/activities/execute-deep-agent/streaming-side-effects.ts +13 -19
  357. package/src/activities/execute-deep-agent/subagent-tracker.ts +77 -42
  358. package/src/activities/execute-deep-agent/subagent-transformer.ts +46 -105
  359. package/src/activities/execute-deep-agent/subagent-wiring.ts +5 -4
  360. package/src/activities/execute-deep-agent/turn-settle.ts +164 -0
  361. package/src/activities/execute-deep-agent/turn-setup.ts +541 -0
  362. package/src/activities/execute-deep-agent/turn-stream.ts +313 -0
  363. package/src/activities/execute-deep-agent/turn.ts +131 -0
  364. package/src/activities/execute-deep-agent/v3-event-recorder.ts +6 -5
  365. package/src/activities/execute-deep-agent/v3-events.ts +1 -1
  366. package/src/activities/execute-deep-agent/v3-status-builder.ts +84 -63
  367. package/src/harness/__tests__/approval-decisions.test.ts +116 -0
  368. package/src/harness/__tests__/capture.test.ts +268 -0
  369. package/src/harness/__tests__/mount-skills.test.ts +169 -0
  370. package/src/harness/__tests__/persist-chokepoint.test.ts +38 -2
  371. package/src/harness/__tests__/run-turn-deterministic.test.ts +94 -0
  372. package/src/harness/__tests__/run-turn.test.ts +39 -23
  373. package/src/harness/__tests__/turn-attachments-vision.test.ts +80 -0
  374. package/src/harness/__tests__/turn-context.test.ts +73 -26
  375. package/src/harness/approval-decisions.ts +104 -0
  376. package/src/harness/capabilities.ts +16 -5
  377. package/src/harness/capture.ts +301 -0
  378. package/src/harness/persist-chokepoint.ts +24 -4
  379. package/src/harness/run-turn.ts +120 -29
  380. package/src/harness/terminal-table.ts +61 -20
  381. package/src/harness/turn-context.ts +208 -111
  382. package/src/harness/types.ts +66 -12
  383. package/src/harness-adapters.ts +17 -9
  384. package/src/middleware/__tests__/approval-gate.test.ts +1 -1
  385. package/src/middleware/__tests__/cost-advisory.test.ts +113 -0
  386. package/src/middleware/approval-gate.ts +2 -2
  387. package/src/middleware/{cost-cap.ts → cost-advisory.ts} +40 -70
  388. package/src/middleware/index.ts +29 -29
  389. package/src/middleware/types.ts +3 -2
  390. package/src/runner-manager.ts +0 -8
  391. package/src/runner.ts +0 -8
  392. package/src/shared/__tests__/attachment-naming.test.ts +6 -19
  393. package/src/shared/__tests__/attachment-resolver.test.ts +158 -8
  394. package/src/shared/__tests__/attachment-zip.test.ts +236 -0
  395. package/src/shared/__tests__/bedrock-adapter.test.ts +1 -1
  396. package/src/shared/__tests__/blueprint-resolver.test.ts +43 -0
  397. package/src/shared/__tests__/cost-guard.test.ts +3 -3
  398. package/src/shared/__tests__/prompt-sections.test.ts +167 -0
  399. package/src/shared/__tests__/secret-leak-scan.test.ts +2 -2
  400. package/src/{activities/execute-deep-agent → shared}/__tests__/stamp-flowed-rows.test.ts +33 -10
  401. package/src/shared/__tests__/vertex-adapter.test.ts +1 -1
  402. package/src/shared/attachment-resolver.ts +218 -55
  403. package/src/shared/attachment-zip.ts +281 -0
  404. package/src/shared/connect-backfill.ts +9 -11
  405. package/src/shared/cost-guard.ts +3 -3
  406. package/src/shared/execution-status-writer.ts +5 -5
  407. package/src/shared/extract-structured-output.ts +8 -7
  408. package/src/shared/filereview/__tests__/capture.test.ts +86 -4
  409. package/src/shared/filereview/__tests__/cas-progress.test.ts +2 -1
  410. package/src/shared/filereview/cas-progress.ts +5 -19
  411. package/src/shared/filereview/cas-touched.ts +89 -0
  412. package/src/shared/filereview/index.ts +4 -2
  413. package/src/shared/mcp-resolver.ts +4 -5
  414. package/src/shared/persist-decision.ts +3 -4
  415. package/src/shared/plan-mode-permissions.ts +2 -2
  416. package/src/shared/plan-mode-prompt.ts +1 -1
  417. package/src/shared/prompt-sections.ts +256 -0
  418. package/src/shared/skill-resolver.ts +4 -2
  419. package/src/shared/tool-rounds.ts +48 -0
  420. package/src/shared/tool-row.ts +61 -0
  421. package/src/shared/workspace/__tests__/stigmer-link.test.ts +7 -22
  422. package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +20 -61
  423. package/src/shared/workspace/session-provision.ts +3 -3
  424. package/src/shared/workspace/stigmer-link.ts +3 -3
  425. package/src/shared/workspace/writeback-coordinator.ts +16 -64
  426. package/src/tools/index.ts +1 -1
  427. package/dist/activities/execute-cursor/capture-flow.d.ts +0 -142
  428. package/dist/activities/execute-cursor/capture-flow.js +0 -285
  429. package/dist/activities/execute-cursor/capture-flow.js.map +0 -1
  430. package/dist/activities/execute-cursor/command-provenance.d.ts +0 -48
  431. package/dist/activities/execute-cursor/command-provenance.js +0 -38
  432. package/dist/activities/execute-cursor/command-provenance.js.map +0 -1
  433. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +0 -134
  434. package/dist/activities/execute-deep-agent/attachment-injector.js +0 -386
  435. package/dist/activities/execute-deep-agent/attachment-injector.js.map +0 -1
  436. package/dist/activities/execute-deep-agent/command-provenance.d.ts +0 -61
  437. package/dist/activities/execute-deep-agent/command-provenance.js +0 -72
  438. package/dist/activities/execute-deep-agent/command-provenance.js.map +0 -1
  439. package/dist/activities/execute-deep-agent/environment.d.ts +0 -24
  440. package/dist/activities/execute-deep-agent/environment.js +0 -58
  441. package/dist/activities/execute-deep-agent/environment.js.map +0 -1
  442. package/dist/activities/execute-deep-agent/event-recorder.d.ts +0 -21
  443. package/dist/activities/execute-deep-agent/event-recorder.js +0 -67
  444. package/dist/activities/execute-deep-agent/event-recorder.js.map +0 -1
  445. package/dist/activities/execute-deep-agent/index.d.ts +0 -15
  446. package/dist/activities/execute-deep-agent/index.js +0 -918
  447. package/dist/activities/execute-deep-agent/index.js.map +0 -1
  448. package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -28
  449. package/dist/activities/execute-deep-agent/mcp-gate.js +0 -22
  450. package/dist/activities/execute-deep-agent/mcp-gate.js.map +0 -1
  451. package/dist/activities/execute-deep-agent/post-stream.d.ts +0 -23
  452. package/dist/activities/execute-deep-agent/post-stream.js +0 -71
  453. package/dist/activities/execute-deep-agent/post-stream.js.map +0 -1
  454. package/dist/activities/execute-deep-agent/setup.d.ts +0 -136
  455. package/dist/activities/execute-deep-agent/setup.js +0 -767
  456. package/dist/activities/execute-deep-agent/setup.js.map +0 -1
  457. package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +0 -36
  458. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +0 -56
  459. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +0 -1
  460. package/dist/activities/execute-deep-agent/status-builder.d.ts +0 -93
  461. package/dist/activities/execute-deep-agent/status-builder.js +0 -365
  462. package/dist/activities/execute-deep-agent/status-builder.js.map +0 -1
  463. package/dist/activities/execute-deep-agent/streaming-terminal.d.ts +0 -21
  464. package/dist/activities/execute-deep-agent/streaming-terminal.js +0 -80
  465. package/dist/activities/execute-deep-agent/streaming-terminal.js.map +0 -1
  466. package/dist/activities/execute-deep-agent/streaming-v3.d.ts +0 -13
  467. package/dist/activities/execute-deep-agent/streaming-v3.js +0 -174
  468. package/dist/activities/execute-deep-agent/streaming-v3.js.map +0 -1
  469. package/dist/activities/execute-deep-agent/streaming.d.ts +0 -81
  470. package/dist/activities/execute-deep-agent/streaming.js +0 -160
  471. package/dist/activities/execute-deep-agent/streaming.js.map +0 -1
  472. package/dist/middleware/cost-cap.d.ts +0 -22
  473. package/dist/middleware/cost-cap.js.map +0 -1
  474. package/dist/middleware/graceful-stop.d.ts +0 -17
  475. package/dist/middleware/graceful-stop.js +0 -63
  476. package/dist/middleware/graceful-stop.js.map +0 -1
  477. package/dist/shared/skill-writer.d.ts +0 -75
  478. package/dist/shared/skill-writer.js +0 -207
  479. package/dist/shared/skill-writer.js.map +0 -1
  480. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +0 -1032
  481. package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +0 -240
  482. package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +0 -169
  483. package/src/activities/execute-cursor/capture-flow.ts +0 -379
  484. package/src/activities/execute-cursor/command-provenance.ts +0 -73
  485. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +0 -1161
  486. package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +0 -252
  487. package/src/activities/execute-deep-agent/__tests__/environment.test.ts +0 -108
  488. package/src/activities/execute-deep-agent/__tests__/event-recorder.test.ts +0 -150
  489. package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +0 -226
  490. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -318
  491. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -351
  492. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -293
  493. package/src/activities/execute-deep-agent/__tests__/index.test.ts +0 -99
  494. package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +0 -44
  495. package/src/activities/execute-deep-agent/__tests__/post-stream.test.ts +0 -112
  496. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -358
  497. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +0 -1933
  498. package/src/activities/execute-deep-agent/__tests__/streaming-terminal.test.ts +0 -40
  499. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +0 -597
  500. package/src/activities/execute-deep-agent/__tests__/streaming.test.ts +0 -508
  501. package/src/activities/execute-deep-agent/attachment-injector.ts +0 -627
  502. package/src/activities/execute-deep-agent/command-provenance.ts +0 -102
  503. package/src/activities/execute-deep-agent/environment.ts +0 -76
  504. package/src/activities/execute-deep-agent/event-recorder.ts +0 -95
  505. package/src/activities/execute-deep-agent/index.ts +0 -1072
  506. package/src/activities/execute-deep-agent/mcp-gate.ts +0 -37
  507. package/src/activities/execute-deep-agent/post-stream.ts +0 -109
  508. package/src/activities/execute-deep-agent/setup.ts +0 -1081
  509. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +0 -64
  510. package/src/activities/execute-deep-agent/status-builder.ts +0 -481
  511. package/src/activities/execute-deep-agent/streaming-terminal.ts +0 -106
  512. package/src/activities/execute-deep-agent/streaming-v3.ts +0 -277
  513. package/src/activities/execute-deep-agent/streaming.ts +0 -313
  514. package/src/middleware/__tests__/cost-cap.test.ts +0 -192
  515. package/src/middleware/__tests__/graceful-stop.test.ts +0 -105
  516. package/src/middleware/graceful-stop.ts +0 -86
  517. package/src/shared/__tests__/skill-writer.test.ts +0 -372
  518. package/src/shared/skill-writer.ts +0 -266
@@ -1,1161 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { writeFile, mkdir } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { mkdtemp, rm } from "node:fs/promises";
6
-
7
- import {
8
- validateZipForExtraction,
9
- injectAttachments,
10
- AttachmentValidationError,
11
- AttachmentInjectionError,
12
- MAX_ZIP_FILES,
13
- MAX_ZIP_EXTRACTED_SIZE,
14
- } from "../attachment-injector.js";
15
- import { mockWorkspaceBackend } from "../../../__test-utils__/mock-workspace.js";
16
- import { makeInMemoryArtifactStorage } from "../../../__test-utils__/fake-artifact-storage.js";
17
- import { buildZip, type ZipFixtureFile } from "@stigmer/zip-structure/testing";
18
- import {
19
- DEEP_AGENT_VISION_PROFILE,
20
- VisionBudget,
21
- } from "../../../shared/attachment-vision.js";
22
-
23
- // ── ZIP Construction Helpers ─────────────────────────────────────────
24
- //
25
- // All archives come from the shared real-shape builder
26
- // (@stigmer/zip-structure/testing):
27
- // local headers, payloads, central directory, EOCD — the only shape real
28
- // ZIP writers produce and the shape central-directory parsing requires.
29
- // The record-form helpers below keep ordinary call sites terse; tests that
30
- // need streaming or declared-size shapes call buildZip directly.
31
-
32
- function makeZip(entries: Record<string, string | Buffer>): Buffer {
33
- return makeZipWith(entries, { method: "deflated" });
34
- }
35
-
36
- function makeStoredZip(entries: Record<string, Buffer>): Buffer {
37
- return makeZipWith(entries, { method: "stored" });
38
- }
39
-
40
- function makeZipWith(
41
- entries: Record<string, string | Buffer>,
42
- shape: Pick<ZipFixtureFile, "method" | "streaming">,
43
- ): Buffer {
44
- const files: ZipFixtureFile[] = Object.entries(entries).map(([name, content]) => ({
45
- name,
46
- content: typeof content === "string" ? content : new Uint8Array(content),
47
- ...shape,
48
- }));
49
- return Buffer.from(buildZip(files));
50
- }
51
-
52
- function makeDirectoryOnlyZip(): Buffer {
53
- return Buffer.from(buildZip([{ name: "empty_dir/", content: "" }]));
54
- }
55
-
56
- function makeAttachment(overrides: Partial<{
57
- filename: string;
58
- storageKey: string;
59
- mountPath: string;
60
- contentType: string;
61
- extract: boolean;
62
- localPath: string;
63
- }> = {}) {
64
- return {
65
- filename: overrides.filename ?? "data.zip",
66
- storageKey: overrides.storageKey ?? "attachments/abc/data.zip",
67
- mountPath: overrides.mountPath ?? "",
68
- contentType: overrides.contentType ?? "application/zip",
69
- extract: overrides.extract ?? false,
70
- localPath: overrides.localPath ?? "",
71
- $typeName: "ai.stigmer.agentic.agentexecution.v1.Attachment" as const,
72
- $unknown: undefined,
73
- } as any;
74
- }
75
-
76
- function makeMockStorage() {
77
- // The canonical in-memory double; cloud downloads read from what was uploaded,
78
- // so tests seed a blob via `storage.upload` and assert on `storage.download`.
79
- const { storage } = makeInMemoryArtifactStorage();
80
- return storage;
81
- }
82
-
83
- // ═══════════════════════════════════════════════════════════════════════
84
- // validateZipForExtraction
85
- // ═══════════════════════════════════════════════════════════════════════
86
-
87
- describe("validateZipForExtraction", () => {
88
- it("returns sorted manifest for valid ZIP with multiple files", () => {
89
- const zip = makeZip({ "b.txt": "beta", "a.txt": "alpha", "c.txt": "charlie" });
90
- const result = validateZipForExtraction(zip, "test.zip");
91
-
92
- expect(result).toHaveLength(3);
93
- expect(result.map(e => e.relativePath)).toEqual(["a.txt", "b.txt", "c.txt"]);
94
- });
95
-
96
- it("preserves nested directory paths in entries", () => {
97
- const zip = makeZip({
98
- "src/main.py": "print('hi')",
99
- "README.md": "# Hello",
100
- "src/lib/util.py": "pass",
101
- });
102
- const result = validateZipForExtraction(zip, "project.zip");
103
- const paths = result.map(e => e.relativePath);
104
-
105
- expect(paths).toContain("src/main.py");
106
- expect(paths).toContain("src/lib/util.py");
107
- expect(paths).toContain("README.md");
108
- });
109
-
110
- it("reports correct uncompressed sizes", () => {
111
- const contentA = "hello world";
112
- const contentB = "x".repeat(500);
113
- const zip = makeZip({ "a.txt": contentA, "b.txt": contentB });
114
-
115
- const result = validateZipForExtraction(zip, "test.zip");
116
- const sizeMap = new Map(result.map(e => [e.relativePath, e.uncompressedSize]));
117
-
118
- expect(sizeMap.get("a.txt")).toBe(Buffer.from(contentA).length);
119
- expect(sizeMap.get("b.txt")).toBe(Buffer.from(contentB).length);
120
- });
121
-
122
- it("excludes directory-only entries", () => {
123
- const zip = makeDirectoryOnlyZip();
124
- expect(() => validateZipForExtraction(zip, "dirs.zip")).toThrow(
125
- AttachmentValidationError,
126
- );
127
- });
128
-
129
- it("rejects invalid ZIP format (random bytes)", () => {
130
- expect(() => validateZipForExtraction(Buffer.from("not-a-zip"), "bad.zip"))
131
- .toThrow(AttachmentValidationError);
132
- expect(() => validateZipForExtraction(Buffer.from("not-a-zip"), "bad.zip"))
133
- .toThrow(/not a valid ZIP archive/);
134
- });
135
-
136
- it("rejects file too small to be a ZIP", () => {
137
- expect(() => validateZipForExtraction(Buffer.from([0x50, 0x4b]), "tiny.zip"))
138
- .toThrow(/too small/);
139
- });
140
-
141
- it("rejects absolute path entries (forward slash)", () => {
142
- const zip = makeZip({ "/etc/passwd": "root:x:0:0" });
143
- expect(() => validateZipForExtraction(zip, "evil.zip"))
144
- .toThrow(/absolute path/);
145
- });
146
-
147
- it("rejects absolute path entries (backslash)", () => {
148
- const zip = makeZip({ "\\windows\\system32\\evil.dll": "payload" });
149
- expect(() => validateZipForExtraction(zip, "evil.zip"))
150
- .toThrow(/absolute path/);
151
- });
152
-
153
- it("rejects path traversal with leading ..", () => {
154
- const zip = makeZip({ "../../etc/passwd": "root:x:0:0" });
155
- expect(() => validateZipForExtraction(zip, "evil.zip"))
156
- .toThrow(/path traversal/);
157
- });
158
-
159
- it("rejects path traversal with embedded ..", () => {
160
- const zip = makeZip({ "foo/../../etc/passwd": "root:x:0:0" });
161
- expect(() => validateZipForExtraction(zip, "evil.zip"))
162
- .toThrow(/path traversal/);
163
- });
164
-
165
- it("rejects null bytes in filenames", () => {
166
- const zip = makeZip({ "file\u0000.txt": "test" });
167
- expect(() => validateZipForExtraction(zip, "null.zip"))
168
- .toThrow(/null bytes/);
169
- });
170
-
171
- it("rejects empty archive (valid ZIP, zero file entries)", () => {
172
- const zip = makeDirectoryOnlyZip();
173
- expect(() => validateZipForExtraction(zip, "empty.zip"))
174
- .toThrow(/empty ZIP archive/);
175
- });
176
-
177
- it("rejects file count exceeding limit", () => {
178
- const entries: Record<string, Buffer> = {};
179
- for (let i = 0; i < MAX_ZIP_FILES + 1; i++) {
180
- entries[`file_${String(i).padStart(4, "0")}.txt`] = Buffer.from("x");
181
- }
182
- const zip = makeStoredZip(entries);
183
- expect(() => validateZipForExtraction(zip, "bomb.zip"))
184
- .toThrow(new RegExp(`limit: ${MAX_ZIP_FILES}`));
185
- });
186
-
187
- it("rejects total uncompressed size exceeding limit", () => {
188
- const overLimit = MAX_ZIP_EXTRACTED_SIZE + 1;
189
- const zip = makeStoredZip({ "big.bin": Buffer.alloc(overLimit, 0) });
190
- expect(() => validateZipForExtraction(zip, "bomb.zip"))
191
- .toThrow(/limit: 100 MB/);
192
- });
193
-
194
- it("accepts ZIP at exactly the file count limit", () => {
195
- const entries: Record<string, Buffer> = {};
196
- for (let i = 0; i < MAX_ZIP_FILES; i++) {
197
- entries[`f${i}.txt`] = Buffer.from("x");
198
- }
199
- const zip = makeStoredZip(entries);
200
- const result = validateZipForExtraction(zip, "ok.zip");
201
- expect(result).toHaveLength(MAX_ZIP_FILES);
202
- });
203
- });
204
-
205
- // ═══════════════════════════════════════════════════════════════════════
206
- // Central-directory parsing (issue #567)
207
- // ═══════════════════════════════════════════════════════════════════════
208
- // The archive shapes the old local-header walk corrupted or rejected:
209
- // stored streaming entries (silent manifest truncation) and Go-default
210
- // deflated streaming entries (rejected outright). Sizes come from the
211
- // central directory — the format's authoritative index — and structural
212
- // failures are fail-hard, unlike the skill-artifact reader's non-fatal
213
- // empty return: nothing upstream vouches for an attachment.
214
-
215
- describe("validateZipForExtraction — central-directory parsing (issue #567)", () => {
216
- it("validates every entry of a stored streaming archive (no silent truncation)", () => {
217
- // Method 0 + flag bit 3 + zeroed local sizes: the old walk admitted the
218
- // first entry with size 0, landed mid-payload, and quietly dropped the
219
- // rest of the manifest.
220
- const zip = Buffer.from(buildZip([
221
- { name: "first.txt", content: "first file content", streaming: true },
222
- { name: "second.txt", content: "second file content", streaming: true },
223
- ]));
224
-
225
- const result = validateZipForExtraction(zip, "streamed.zip");
226
- expect(result.map((e) => e.relativePath)).toEqual(["first.txt", "second.txt"]);
227
- expect(result.map((e) => e.uncompressedSize)).toEqual([
228
- "first file content".length,
229
- "second file content".length,
230
- ]);
231
- });
232
-
233
- it("accepts a Go-default archive (deflated streaming entries)", () => {
234
- const zip = Buffer.from(buildZip([
235
- { name: "main.go", content: "package main", method: "deflated", streaming: true },
236
- { name: "go.mod", content: "module example", method: "deflated", streaming: true },
237
- ]));
238
-
239
- const result = validateZipForExtraction(zip, "go-built.zip");
240
- expect(result.map((e) => e.relativePath)).toEqual(["go.mod", "main.go"]);
241
- });
242
-
243
- it("rejects an archive with no central directory (fail-hard, unlike skill extraction)", () => {
244
- const zip = Buffer.from(buildZip(
245
- [{ name: "a.txt", content: "aaa" }],
246
- { omitCentralDirectory: true },
247
- ));
248
-
249
- expect(() => validateZipForExtraction(zip, "truncated.zip"))
250
- .toThrow(AttachmentValidationError);
251
- expect(() => validateZipForExtraction(zip, "truncated.zip"))
252
- .toThrow(/not a valid ZIP archive/);
253
- });
254
-
255
- it("rejects duplicate entry paths (a contradictory manifest)", () => {
256
- const zip = Buffer.from(buildZip([
257
- { name: "dup.txt", content: "one" },
258
- { name: "dup.txt", content: "two" },
259
- ]));
260
-
261
- expect(() => validateZipForExtraction(zip, "dup.zip"))
262
- .toThrow(/duplicate entry/);
263
- });
264
-
265
- it("locates the central directory behind a trailing archive comment", () => {
266
- const zip = Buffer.from(buildZip(
267
- [{ name: "a.txt", content: "aaa" }],
268
- { comment: "release archive — built by tooling" },
269
- ));
270
-
271
- const result = validateZipForExtraction(zip, "commented.zip");
272
- expect(result.map((e) => e.relativePath)).toEqual(["a.txt"]);
273
- });
274
- });
275
-
276
- describe("injectAttachments — central-directory extraction (issue #567)", () => {
277
- let tempDir: string;
278
-
279
- beforeEach(async () => {
280
- tempDir = await mkdtemp(join(tmpdir(), "attachment-cd-"));
281
- });
282
-
283
- async function extractArchive(zip: Uint8Array) {
284
- const localFile = join(tempDir, "archive.zip");
285
- await writeFile(localFile, zip);
286
- const backend = mockWorkspaceBackend();
287
- const result = await injectAttachments({
288
- backend,
289
- attachments: [makeAttachment({
290
- filename: "archive.zip",
291
- mountPath: ".stigmer/inputs/archive",
292
- extract: true,
293
- localPath: localFile,
294
- })],
295
- storage: makeMockStorage(),
296
- isLocalMode: true,
297
- });
298
- return { backend, result };
299
- }
300
-
301
- it("extracts a stored streaming archive completely, contents intact", async () => {
302
- const { backend, result } = await extractArchive(buildZip([
303
- { name: "first.txt", content: "first file content", streaming: true },
304
- { name: "second.txt", content: "second file content", streaming: true },
305
- ]));
306
-
307
- expect(result.map((f) => f.path).sort()).toEqual([
308
- ".stigmer/inputs/archive/first.txt",
309
- ".stigmer/inputs/archive/second.txt",
310
- ]);
311
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(
312
- ".stigmer/inputs/archive/first.txt", Buffer.from("first file content"),
313
- );
314
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(
315
- ".stigmer/inputs/archive/second.txt", Buffer.from("second file content"),
316
- );
317
- });
318
-
319
- it("extracts a Go-default deflated streaming archive", async () => {
320
- const { backend, result } = await extractArchive(buildZip([
321
- { name: "src/main.go", content: "package main\n", method: "deflated", streaming: true },
322
- ]));
323
-
324
- expect(result).toHaveLength(1);
325
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(
326
- ".stigmer/inputs/archive/src/main.go", Buffer.from("package main\n"),
327
- );
328
- });
329
-
330
- it("aborts when an entry inflates past its declared size (crafted archive)", async () => {
331
- await expect(extractArchive(buildZip([
332
- {
333
- name: "bomb.txt",
334
- content: "x".repeat(4096),
335
- method: "deflated",
336
- declaredUncompressedSize: 16,
337
- },
338
- ]))).rejects.toThrow(AttachmentValidationError);
339
- });
340
-
341
- it("aborts when a stored entry's payload disagrees with its declared size", async () => {
342
- await expect(extractArchive(buildZip([
343
- { name: "short.txt", content: "eleven byte", declaredUncompressedSize: 4096 },
344
- ]))).rejects.toThrow(/declare/);
345
- });
346
- });
347
-
348
- // ═══════════════════════════════════════════════════════════════════════
349
- // injectAttachments
350
- // ═══════════════════════════════════════════════════════════════════════
351
-
352
- describe("injectAttachments", () => {
353
- let tempDir: string;
354
-
355
- beforeEach(async () => {
356
- tempDir = await mkdtemp(join(tmpdir(), "attachment-test-"));
357
- });
358
-
359
- it("returns empty array for empty attachments list", async () => {
360
- const backend = mockWorkspaceBackend();
361
- const storage = makeMockStorage();
362
-
363
- const result = await injectAttachments({
364
- backend,
365
- attachments: [],
366
- storage,
367
- isLocalMode: false,
368
- });
369
-
370
- expect(result).toEqual([]);
371
- });
372
-
373
- it("injects single non-ZIP attachment in local mode", async () => {
374
- const content = Buffer.from("hello world");
375
- const localFile = join(tempDir, "input.txt");
376
- await writeFile(localFile, content);
377
-
378
- const backend = mockWorkspaceBackend();
379
- const storage = makeMockStorage();
380
-
381
- const result = await injectAttachments({
382
- backend,
383
- attachments: [makeAttachment({
384
- filename: "input.txt",
385
- localPath: localFile,
386
- })],
387
- storage,
388
- isLocalMode: true,
389
- });
390
-
391
- expect(result).toHaveLength(1);
392
- expect(result[0].filename).toBe("input.txt");
393
- expect(result[0].path).toBe(".stigmer/inputs/input.txt");
394
- expect(result[0].sizeBytes).toBe(content.length);
395
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(
396
- ".stigmer/inputs/input.txt",
397
- content,
398
- );
399
- });
400
-
401
- it("injects single non-ZIP attachment in cloud mode", async () => {
402
- const content = Buffer.from("cloud data");
403
- const storage = makeMockStorage();
404
- await storage.upload("attachments/xyz/data.csv", content);
405
-
406
- const backend = mockWorkspaceBackend();
407
-
408
- const result = await injectAttachments({
409
- backend,
410
- attachments: [makeAttachment({
411
- filename: "data.csv",
412
- storageKey: "attachments/xyz/data.csv",
413
- })],
414
- storage,
415
- isLocalMode: false,
416
- });
417
-
418
- expect(result).toHaveLength(1);
419
- expect(result[0].filename).toBe("data.csv");
420
- expect(result[0].path).toBe(".stigmer/inputs/data.csv");
421
- expect(result[0].sizeBytes).toBe(content.length);
422
- expect(storage.download).toHaveBeenCalledWith("attachments/xyz/data.csv");
423
- });
424
-
425
- it("extracts ZIP when extract=true", async () => {
426
- const zip = makeZip({ "main.py": "print('hi')", "lib/util.py": "pass" });
427
- const localFile = join(tempDir, "project.zip");
428
- await writeFile(localFile, zip);
429
-
430
- const backend = mockWorkspaceBackend();
431
- const storage = makeMockStorage();
432
-
433
- const result = await injectAttachments({
434
- backend,
435
- attachments: [makeAttachment({
436
- filename: "project.zip",
437
- mountPath: ".stigmer/inputs/project",
438
- extract: true,
439
- localPath: localFile,
440
- })],
441
- storage,
442
- isLocalMode: true,
443
- });
444
-
445
- expect(result).toHaveLength(2);
446
- const paths = result.map(f => f.path);
447
- expect(paths).toContain(".stigmer/inputs/project/lib/util.py");
448
- expect(paths).toContain(".stigmer/inputs/project/main.py");
449
- });
450
-
451
- it("writes ZIP as single file when extract=false", async () => {
452
- const zip = makeZip({ "a.txt": "hello" });
453
- const localFile = join(tempDir, "archive.zip");
454
- await writeFile(localFile, zip);
455
-
456
- const backend = mockWorkspaceBackend();
457
- const storage = makeMockStorage();
458
-
459
- const result = await injectAttachments({
460
- backend,
461
- attachments: [makeAttachment({
462
- filename: "archive.zip",
463
- localPath: localFile,
464
- extract: false,
465
- })],
466
- storage,
467
- isLocalMode: true,
468
- });
469
-
470
- expect(result).toHaveLength(1);
471
- expect(result[0].filename).toBe("archive.zip");
472
- expect(result[0].path).toBe(".stigmer/inputs/archive.zip");
473
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(
474
- ".stigmer/inputs/archive.zip",
475
- expect.any(Buffer),
476
- );
477
- });
478
-
479
- it("honors custom mountPath", async () => {
480
- const content = Buffer.from("custom path");
481
- const localFile = join(tempDir, "file.txt");
482
- await writeFile(localFile, content);
483
-
484
- const backend = mockWorkspaceBackend();
485
- const storage = makeMockStorage();
486
-
487
- const result = await injectAttachments({
488
- backend,
489
- attachments: [makeAttachment({
490
- filename: "file.txt",
491
- mountPath: "workspace/custom/location.txt",
492
- localPath: localFile,
493
- })],
494
- storage,
495
- isLocalMode: true,
496
- });
497
-
498
- expect(result[0].path).toBe("workspace/custom/location.txt");
499
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(
500
- "workspace/custom/location.txt",
501
- content,
502
- );
503
- });
504
-
505
- it("uses default mountPath .stigmer/inputs/{filename}", async () => {
506
- const content = Buffer.from("test");
507
- const localFile = join(tempDir, "readme.md");
508
- await writeFile(localFile, content);
509
-
510
- const backend = mockWorkspaceBackend();
511
- const storage = makeMockStorage();
512
-
513
- const result = await injectAttachments({
514
- backend,
515
- attachments: [makeAttachment({
516
- filename: "readme.md",
517
- mountPath: "",
518
- localPath: localFile,
519
- })],
520
- storage,
521
- isLocalMode: true,
522
- });
523
-
524
- expect(result[0].path).toBe(".stigmer/inputs/readme.md");
525
- });
526
-
527
- it("processes multiple attachments in order", async () => {
528
- const fileA = join(tempDir, "a.txt");
529
- const fileB = join(tempDir, "b.txt");
530
- await writeFile(fileA, "alpha");
531
- await writeFile(fileB, "beta");
532
-
533
- const backend = mockWorkspaceBackend();
534
- const storage = makeMockStorage();
535
-
536
- const result = await injectAttachments({
537
- backend,
538
- attachments: [
539
- makeAttachment({ filename: "a.txt", localPath: fileA }),
540
- makeAttachment({ filename: "b.txt", localPath: fileB }),
541
- ],
542
- storage,
543
- isLocalMode: true,
544
- });
545
-
546
- expect(result).toHaveLength(2);
547
- expect(result[0].filename).toBe("a.txt");
548
- expect(result[1].filename).toBe("b.txt");
549
- });
550
-
551
- it("propagates error when local file not found", async () => {
552
- const backend = mockWorkspaceBackend();
553
- const storage = makeMockStorage();
554
-
555
- await expect(injectAttachments({
556
- backend,
557
- attachments: [makeAttachment({
558
- filename: "missing.txt",
559
- localPath: "/nonexistent/path/missing.txt",
560
- })],
561
- storage,
562
- isLocalMode: true,
563
- })).rejects.toThrow(AttachmentInjectionError);
564
- });
565
-
566
- it("propagates error when storage download fails", async () => {
567
- const storage = makeMockStorage();
568
- storage.download.mockRejectedValue(new Error("network timeout"));
569
-
570
- const backend = mockWorkspaceBackend();
571
-
572
- await expect(injectAttachments({
573
- backend,
574
- attachments: [makeAttachment({
575
- filename: "data.csv",
576
- storageKey: "attachments/xyz/data.csv",
577
- })],
578
- storage,
579
- isLocalMode: false,
580
- })).rejects.toThrow(AttachmentInjectionError);
581
- });
582
-
583
- it("propagates error when storageKey is missing in cloud mode", async () => {
584
- const backend = mockWorkspaceBackend();
585
- const storage = makeMockStorage();
586
-
587
- await expect(injectAttachments({
588
- backend,
589
- attachments: [makeAttachment({
590
- filename: "file.txt",
591
- storageKey: "",
592
- localPath: "",
593
- })],
594
- storage,
595
- isLocalMode: false,
596
- })).rejects.toThrow(/missing storageKey/);
597
- });
598
-
599
- it("rejects a caller-supplied mountPath that escapes the workspace root", async () => {
600
- // `resolveMountPath` only stripped leading slashes, so `..` segments on a
601
- // non-`.stigmer/` mount path reached the unchecked join(rootDir, path).
602
- const storage = makeMockStorage();
603
- storage.download.mockResolvedValue(Buffer.from("owned"));
604
- const backend = mockWorkspaceBackend();
605
-
606
- const escapes = ["../../escape.txt", "../etc/evil", "foo/../../bar"];
607
- for (const mountPath of escapes) {
608
- await expect(injectAttachments({
609
- backend,
610
- attachments: [makeAttachment({
611
- filename: "data.txt",
612
- storageKey: "attachments/xyz/data.txt",
613
- mountPath,
614
- })],
615
- storage,
616
- isLocalMode: false,
617
- })).rejects.toThrow(/mount path .* escapes the workspace root|traversal/i);
618
- }
619
- });
620
-
621
- it("preserves binary content via writeFileBuffer", async () => {
622
- const binaryContent = Buffer.from([0x00, 0x01, 0xFF, 0xFE, 0x89, 0x50, 0x4E, 0x47]);
623
- const localFile = join(tempDir, "image.png");
624
- await writeFile(localFile, binaryContent);
625
-
626
- const backend = mockWorkspaceBackend();
627
- const storage = makeMockStorage();
628
-
629
- await injectAttachments({
630
- backend,
631
- attachments: [makeAttachment({
632
- filename: "image.png",
633
- localPath: localFile,
634
- })],
635
- storage,
636
- isLocalMode: true,
637
- });
638
-
639
- const writtenBuffer = (backend.writeFileBuffer as any).mock.calls[0][1] as Buffer;
640
- expect(Buffer.compare(writtenBuffer, binaryContent)).toBe(0);
641
- });
642
-
643
- it("uniquifies duplicate default-derived filenames instead of failing (issue #364)", async () => {
644
- const contentA = Buffer.from("first");
645
- const contentB = Buffer.from("second");
646
- const storage = makeMockStorage();
647
- await storage.upload("attachments/a/data.csv", contentA);
648
- await storage.upload("attachments/b/data.csv", contentB);
649
-
650
- const backend = mockWorkspaceBackend();
651
-
652
- const result = await injectAttachments({
653
- backend,
654
- attachments: [
655
- makeAttachment({ filename: "data.csv", storageKey: "attachments/a/data.csv" }),
656
- makeAttachment({ filename: "data.csv", storageKey: "attachments/b/data.csv" }),
657
- ],
658
- storage,
659
- isLocalMode: false,
660
- });
661
-
662
- // Both files land, both downloads run — the execution is never burned
663
- // over a mechanically resolvable name.
664
- expect(result).toHaveLength(2);
665
- expect(result[0]).toMatchObject({
666
- filename: "data.csv",
667
- path: ".stigmer/inputs/data.csv",
668
- });
669
- expect(result[0].renamedFrom).toBeUndefined();
670
- // The rename follows the platform's stem-2.ext semantics and carries the
671
- // disclosure payload for the prompt.
672
- expect(result[1]).toMatchObject({
673
- filename: "data-2.csv",
674
- path: ".stigmer/inputs/data-2.csv",
675
- renamedFrom: "data.csv",
676
- });
677
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(
678
- ".stigmer/inputs/data.csv", contentA,
679
- );
680
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(
681
- ".stigmer/inputs/data-2.csv", contentB,
682
- );
683
- });
684
-
685
- it("rejects two attachments explicitly pinning the same mountPath (a user contradiction)", async () => {
686
- const backend = mockWorkspaceBackend();
687
- const storage = makeMockStorage();
688
-
689
- await expect(injectAttachments({
690
- backend,
691
- attachments: [
692
- makeAttachment({ filename: "a.csv", storageKey: "key1", mountPath: "inputs/data.csv" }),
693
- makeAttachment({ filename: "b.csv", storageKey: "key2", mountPath: "inputs/data.csv" }),
694
- ],
695
- storage,
696
- isLocalMode: false,
697
- })).rejects.toThrow(/collides with/);
698
-
699
- // The contradiction is detected before any downloads
700
- expect(storage.download).not.toHaveBeenCalled();
701
- });
702
-
703
- it("default-derived names dodge an explicit mountPath already inside the inputs prefix", async () => {
704
- const storage = makeMockStorage();
705
- await storage.upload("attachments/a/pinned", Buffer.from("pinned"));
706
- await storage.upload("attachments/b/report.pdf", Buffer.from("derived"));
707
-
708
- const backend = mockWorkspaceBackend();
709
-
710
- const result = await injectAttachments({
711
- backend,
712
- attachments: [
713
- // Listed AFTER the default-derived attachment: explicit paths claim
714
- // first regardless of order, so the default still dodges.
715
- makeAttachment({ filename: "report.pdf", storageKey: "attachments/b/report.pdf" }),
716
- makeAttachment({
717
- filename: "report.pdf",
718
- storageKey: "attachments/a/pinned",
719
- mountPath: ".stigmer/inputs/report.pdf",
720
- }),
721
- ],
722
- storage,
723
- isLocalMode: false,
724
- });
725
-
726
- const byPath = new Map(result.map((f) => [f.path, f]));
727
- expect(byPath.get(".stigmer/inputs/report.pdf")?.renamedFrom).toBeUndefined();
728
- expect(byPath.get(".stigmer/inputs/report-2.pdf")).toMatchObject({
729
- filename: "report-2.pdf",
730
- renamedFrom: "report.pdf",
731
- });
732
- });
733
-
734
- it("derives filename from storageKey when filename is empty", async () => {
735
- const content = Buffer.from("fallback");
736
- const localFile = join(tempDir, "fallback.txt");
737
- await writeFile(localFile, content);
738
-
739
- const backend = mockWorkspaceBackend();
740
- const storage = makeMockStorage();
741
-
742
- const result = await injectAttachments({
743
- backend,
744
- attachments: [makeAttachment({
745
- filename: "",
746
- storageKey: "attachments/ulid123/fallback.txt",
747
- localPath: localFile,
748
- })],
749
- storage,
750
- isLocalMode: true,
751
- });
752
-
753
- expect(result[0].path).toBe(".stigmer/inputs/fallback.txt");
754
- });
755
-
756
- it("strips leading slashes from mountPath", async () => {
757
- const content = Buffer.from("data");
758
- const localFile = join(tempDir, "data.csv");
759
- await writeFile(localFile, content);
760
-
761
- const backend = mockWorkspaceBackend();
762
- const storage = makeMockStorage();
763
-
764
- const result = await injectAttachments({
765
- backend,
766
- attachments: [makeAttachment({
767
- filename: "data.csv",
768
- mountPath: "/workspace/input.csv",
769
- localPath: localFile,
770
- })],
771
- storage,
772
- isLocalMode: true,
773
- });
774
-
775
- expect(result[0].path).toBe("workspace/input.csv");
776
- });
777
-
778
- it("explicit-collision error names both attachments and stays actionable", async () => {
779
- const backend = mockWorkspaceBackend();
780
- const storage = makeMockStorage();
781
-
782
- try {
783
- await injectAttachments({
784
- backend,
785
- attachments: [
786
- makeAttachment({ filename: "first.csv", mountPath: "inputs/shared.csv" }),
787
- makeAttachment({ filename: "second.csv", storageKey: "key2", mountPath: "inputs/shared.csv" }),
788
- ],
789
- storage,
790
- isLocalMode: false,
791
- });
792
- expect.fail("should have thrown");
793
- } catch (err) {
794
- expect(err).toBeInstanceOf(AttachmentInjectionError);
795
- const injErr = err as AttachmentInjectionError;
796
- expect(injErr.attachmentFilename).toBe("second.csv");
797
- expect(injErr.message).toContain("first.csv");
798
- expect(injErr.message).toContain("mountPath");
799
- }
800
- });
801
-
802
- it("contentType does not affect extraction behavior", async () => {
803
- const zip = makeZip({ "data.txt": "content" });
804
- const localFile = join(tempDir, "data.zip");
805
- await writeFile(localFile, zip);
806
-
807
- const backend = mockWorkspaceBackend();
808
- const storage = makeMockStorage();
809
-
810
- // contentType says zip, but extract=false → writes as single file
811
- const result = await injectAttachments({
812
- backend,
813
- attachments: [makeAttachment({
814
- filename: "data.zip",
815
- contentType: "application/zip",
816
- extract: false,
817
- localPath: localFile,
818
- })],
819
- storage,
820
- isLocalMode: true,
821
- });
822
-
823
- expect(result).toHaveLength(1);
824
- expect(result[0].filename).toBe("data.zip");
825
- });
826
-
827
- it("ZIP with only directory entries produces validation error via extract", async () => {
828
- const zip = makeDirectoryOnlyZip();
829
- const localFile = join(tempDir, "dirs.zip");
830
- await writeFile(localFile, zip);
831
-
832
- const backend = mockWorkspaceBackend();
833
- const storage = makeMockStorage();
834
-
835
- await expect(injectAttachments({
836
- backend,
837
- attachments: [makeAttachment({
838
- filename: "dirs.zip",
839
- extract: true,
840
- localPath: localFile,
841
- })],
842
- storage,
843
- isLocalMode: true,
844
- })).rejects.toThrow(AttachmentValidationError);
845
- });
846
- });
847
-
848
- // ═══════════════════════════════════════════════════════════════════════
849
- // Vision selection during injection (T04)
850
- // ═══════════════════════════════════════════════════════════════════════
851
- // Vision is strictly additive: every case also asserts the file was written
852
- // exactly as it would be without a budget.
853
-
854
- describe("injectAttachments — vision selection", () => {
855
- const PNG_BYTES = Buffer.concat([
856
- Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
857
- Buffer.alloc(56, 0xab),
858
- ]);
859
- const WEBP_BYTES = Buffer.concat([
860
- Buffer.from("RIFF", "ascii"),
861
- Buffer.from([0x24, 0x00, 0x00, 0x00]),
862
- Buffer.from("WEBP", "ascii"),
863
- Buffer.alloc(52, 0xcd),
864
- ]);
865
-
866
- it("accepts a PNG into the vision payload (deep-agent profile) and still writes the file", async () => {
867
- const backend = mockWorkspaceBackend();
868
- const storage = makeMockStorage();
869
- await storage.upload("attachments/abc/photo.png", PNG_BYTES);
870
-
871
- const [injected] = await injectAttachments({
872
- backend,
873
- attachments: [makeAttachment({
874
- filename: "photo.png",
875
- storageKey: "attachments/abc/photo.png",
876
- contentType: "image/png",
877
- })],
878
- storage,
879
- isLocalMode: false,
880
- visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE),
881
- });
882
-
883
- expect(injected.vision).toMatchObject({
884
- filename: "photo.png",
885
- mimeType: "image/png",
886
- byteSize: PNG_BYTES.length,
887
- });
888
- expect(Buffer.from(injected.vision!.base64, "base64").equals(PNG_BYTES)).toBe(true);
889
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(".stigmer/inputs/photo.png", PNG_BYTES);
890
- });
891
-
892
- it("degrades an image with model_no_vision on a blind model, file written intact", async () => {
893
- const backend = mockWorkspaceBackend();
894
- const storage = makeMockStorage();
895
- await storage.upload("attachments/abc/photo.png", PNG_BYTES);
896
-
897
- const [injected] = await injectAttachments({
898
- backend,
899
- attachments: [makeAttachment({
900
- filename: "photo.png",
901
- storageKey: "attachments/abc/photo.png",
902
- contentType: "image/png",
903
- })],
904
- storage,
905
- isLocalMode: false,
906
- visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE, { modelVision: false }),
907
- });
908
-
909
- expect(injected.vision).toBeUndefined();
910
- expect(injected.visionDegraded).toBe("model_no_vision");
911
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(".stigmer/inputs/photo.png", PNG_BYTES);
912
- });
913
-
914
- it("accepts WebP on the deep-agent profile (unlike the Cursor harness)", async () => {
915
- const backend = mockWorkspaceBackend();
916
- const storage = makeMockStorage();
917
- await storage.upload("attachments/abc/pic.webp", WEBP_BYTES);
918
-
919
- const [injected] = await injectAttachments({
920
- backend,
921
- attachments: [makeAttachment({
922
- filename: "pic.webp",
923
- storageKey: "attachments/abc/pic.webp",
924
- contentType: "image/webp",
925
- })],
926
- storage,
927
- isLocalMode: false,
928
- visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE),
929
- });
930
-
931
- expect(injected.vision?.mimeType).toBe("image/webp");
932
- });
933
-
934
- it("carries no vision fields for a non-image attachment", async () => {
935
- const backend = mockWorkspaceBackend();
936
- const storage = makeMockStorage();
937
- await storage.upload("attachments/abc/doc.pdf", Buffer.from("%PDF-1.7"));
938
-
939
- const [injected] = await injectAttachments({
940
- backend,
941
- attachments: [makeAttachment({
942
- filename: "doc.pdf",
943
- storageKey: "attachments/abc/doc.pdf",
944
- contentType: "application/pdf",
945
- })],
946
- storage,
947
- isLocalMode: false,
948
- visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE),
949
- });
950
-
951
- expect(injected.vision).toBeUndefined();
952
- expect(injected.visionDegraded).toBeUndefined();
953
- });
954
-
955
- it("degrades a declared image whose bytes are not one (type_mismatch)", async () => {
956
- const backend = mockWorkspaceBackend();
957
- const storage = makeMockStorage();
958
- await storage.upload("attachments/abc/photo.jpg", Buffer.from("actually HEIC"));
959
-
960
- const [injected] = await injectAttachments({
961
- backend,
962
- attachments: [makeAttachment({
963
- filename: "photo.jpg",
964
- storageKey: "attachments/abc/photo.jpg",
965
- contentType: "image/jpeg",
966
- })],
967
- storage,
968
- isLocalMode: false,
969
- visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE),
970
- });
971
-
972
- expect(injected.vision).toBeUndefined();
973
- expect(injected.visionDegraded).toBe("type_mismatch");
974
- });
975
-
976
- it("labels a duplicate-renamed image's vision payload with the on-disk name", async () => {
977
- // Coherence pin: the prompt lists the renamed path and the vision
978
- // disclosure lists image filenames — if the offer used the original
979
- // attachment.filename, the agent would see two images with one
980
- // indistinguishable name.
981
- const backend = mockWorkspaceBackend();
982
- const storage = makeMockStorage();
983
- await storage.upload("attachments/a/photo.png", PNG_BYTES);
984
- await storage.upload("attachments/b/photo.png", PNG_BYTES);
985
-
986
- const injected = await injectAttachments({
987
- backend,
988
- attachments: [
989
- makeAttachment({ filename: "photo.png", storageKey: "attachments/a/photo.png", contentType: "image/png" }),
990
- makeAttachment({ filename: "photo.png", storageKey: "attachments/b/photo.png", contentType: "image/png" }),
991
- ],
992
- storage,
993
- isLocalMode: false,
994
- visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE),
995
- });
996
-
997
- expect(injected[0].vision?.filename).toBe("photo.png");
998
- expect(injected[1]).toMatchObject({
999
- filename: "photo-2.png",
1000
- renamedFrom: "photo.png",
1001
- });
1002
- expect(injected[1].vision?.filename).toBe("photo-2.png");
1003
- });
1004
-
1005
- it("never offers an extract archive to the budget — extracted files carry no vision fields", async () => {
1006
- // A PNG inside a ZIP has no attachment-level bytes; the archive rides the
1007
- // normal extraction story with no vision involvement or disclosure.
1008
- const zip = makeZip({ "inner.png": PNG_BYTES });
1009
- const backend = mockWorkspaceBackend();
1010
- const storage = makeMockStorage();
1011
- await storage.upload("attachments/abc/bundle.zip", zip);
1012
-
1013
- const injected = await injectAttachments({
1014
- backend,
1015
- attachments: [makeAttachment({
1016
- filename: "bundle.zip",
1017
- storageKey: "attachments/abc/bundle.zip",
1018
- extract: true,
1019
- })],
1020
- storage,
1021
- isLocalMode: false,
1022
- visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE),
1023
- });
1024
-
1025
- expect(injected.length).toBeGreaterThan(0);
1026
- for (const file of injected) {
1027
- expect(file.vision).toBeUndefined();
1028
- expect(file.visionDegraded).toBeUndefined();
1029
- }
1030
- });
1031
-
1032
- it("degrades over the total budget in attachment order (budget_exhausted)", async () => {
1033
- const backend = mockWorkspaceBackend();
1034
- const storage = makeMockStorage();
1035
- await storage.upload("attachments/a/a.png", PNG_BYTES);
1036
- await storage.upload("attachments/b/b.png", PNG_BYTES);
1037
-
1038
- const injected = await injectAttachments({
1039
- backend,
1040
- attachments: [
1041
- makeAttachment({ filename: "a.png", storageKey: "attachments/a/a.png", contentType: "image/png" }),
1042
- makeAttachment({ filename: "b.png", storageKey: "attachments/b/b.png", contentType: "image/png" }),
1043
- ],
1044
- storage,
1045
- isLocalMode: false,
1046
- visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE, {
1047
- maxImageBytes: PNG_BYTES.length,
1048
- maxTotalBytes: PNG_BYTES.length,
1049
- }),
1050
- });
1051
-
1052
- expect(injected[0].vision).toBeDefined();
1053
- expect(injected[1].vision).toBeUndefined();
1054
- expect(injected[1].visionDegraded).toBe("budget_exhausted");
1055
- });
1056
- });
1057
-
1058
- // ═══════════════════════════════════════════════════════════════════════
1059
- // injectAttachments — download-URL hand-off (issue #532)
1060
- // ═══════════════════════════════════════════════════════════════════════
1061
- // The URL is strictly additive (shared/attachment-download-urls.ts): every
1062
- // case also asserts the file injected exactly as it would without one.
1063
-
1064
- describe("injectAttachments — download-URL hand-off", () => {
1065
- it("mints a URL for a storage-key attachment", async () => {
1066
- const content = Buffer.from("cloud data");
1067
- const storage = makeMockStorage();
1068
- await storage.upload("attachments/xyz/data.csv", content);
1069
- const backend = mockWorkspaceBackend();
1070
-
1071
- const result = await injectAttachments({
1072
- backend,
1073
- attachments: [makeAttachment({ filename: "data.csv", storageKey: "attachments/xyz/data.csv" })],
1074
- storage,
1075
- isLocalMode: false,
1076
- });
1077
-
1078
- expect(result[0]).toMatchObject({
1079
- path: ".stigmer/inputs/data.csv",
1080
- downloadUrl: "mem://attachments/xyz/data.csv",
1081
- });
1082
- });
1083
-
1084
- it("mints a URL for an explicit-mountPath attachment (the rule ignores mount location)", async () => {
1085
- const storage = makeMockStorage();
1086
- await storage.upload("attachments/xyz/config.yaml", Buffer.from("a: 1"));
1087
- const backend = mockWorkspaceBackend();
1088
-
1089
- const result = await injectAttachments({
1090
- backend,
1091
- attachments: [makeAttachment({
1092
- filename: "config.yaml",
1093
- storageKey: "attachments/xyz/config.yaml",
1094
- mountPath: "config/app.yaml",
1095
- })],
1096
- storage,
1097
- isLocalMode: false,
1098
- });
1099
-
1100
- expect(result[0]).toMatchObject({
1101
- path: "config/app.yaml",
1102
- downloadUrl: "mem://attachments/xyz/config.yaml",
1103
- });
1104
- });
1105
-
1106
- it("carries no URL on extracted ZIP entries — the stored object is the ZIP, not any listed file", async () => {
1107
- const zip = makeZip({ "src/a.txt": "alpha", "src/b.txt": "beta" });
1108
- const storage = makeMockStorage();
1109
- await storage.upload("attachments/xyz/code.zip", zip);
1110
- const backend = mockWorkspaceBackend();
1111
-
1112
- const result = await injectAttachments({
1113
- backend,
1114
- attachments: [makeAttachment({
1115
- filename: "code.zip",
1116
- storageKey: "attachments/xyz/code.zip",
1117
- extract: true,
1118
- })],
1119
- storage,
1120
- isLocalMode: false,
1121
- });
1122
-
1123
- expect(result.length).toBeGreaterThan(0);
1124
- for (const file of result) {
1125
- expect(file.downloadUrl).toBeUndefined();
1126
- }
1127
- });
1128
-
1129
- it("carries no URL for a key-less local file", async () => {
1130
- const localFile = join(tmpdir(), `injector-url-${Date.now()}.txt`);
1131
- await writeFile(localFile, "local only");
1132
- const backend = mockWorkspaceBackend();
1133
-
1134
- const result = await injectAttachments({
1135
- backend,
1136
- attachments: [makeAttachment({ filename: "local.txt", storageKey: "", localPath: localFile })],
1137
- storage: makeMockStorage(),
1138
- isLocalMode: true,
1139
- });
1140
-
1141
- expect(result[0].downloadUrl).toBeUndefined();
1142
- });
1143
-
1144
- it("degrades to no URL when the mint fails — file still injected, no throw", async () => {
1145
- const content = Buffer.from("cloud data");
1146
- const storage = makeMockStorage();
1147
- await storage.upload("attachments/xyz/data.csv", content);
1148
- storage.getDownloadUrl.mockRejectedValueOnce(new Error("presign endpoint unreachable"));
1149
- const backend = mockWorkspaceBackend();
1150
-
1151
- const result = await injectAttachments({
1152
- backend,
1153
- attachments: [makeAttachment({ filename: "data.csv", storageKey: "attachments/xyz/data.csv" })],
1154
- storage,
1155
- isLocalMode: false,
1156
- });
1157
-
1158
- expect(result[0].downloadUrl).toBeUndefined();
1159
- expect(backend.writeFileBuffer).toHaveBeenCalledWith(".stigmer/inputs/data.csv", content);
1160
- });
1161
- });