@stigmer/runner 3.15.1 → 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
@@ -44,9 +44,9 @@ describe("costCapExceeded", () => {
44
44
 
45
45
  describe("cost-limit terminal copy", () => {
46
46
  it("pins the error prefix consumers match on (no structured termination reason exists)", () => {
47
- // Mirrors the TOOL_CALL_LIMIT_ERROR_PREFIX guard in
48
- // streaming-terminal.test.ts: a reword silently downgrades any consumer
49
- // matching this prefix to generic error copy.
47
+ // The twin of TOOL_CALL_LIMIT_ERROR_PREFIX's pin (`shared/tool-rounds.ts`,
48
+ // matched by the cloud's reply-extractor): a reword silently downgrades
49
+ // any consumer matching this prefix to generic error copy.
50
50
  expect(COST_LIMIT_ERROR_PREFIX).toBe("Agent reached the cost limit");
51
51
  });
52
52
 
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The shared prompt glue (`shared/prompt-sections.ts`): the standing order,
3
+ * the input-files lines, the skill selection and the also-available sentence
4
+ * (the sentence's arms came from `skill-writer.test.ts`, retired with its
5
+ * module at S3 M5). What each harness renders AROUND these is pinned whole by
6
+ * the two builders' prompt goldens (`execute-deep-agent/__tests__/
7
+ * prompt-goldens.test.ts`, `execute-cursor/__tests__/prompt-goldens.test.ts`);
8
+ * this file pins the glue's own contract, harness-free.
9
+ */
10
+
11
+ import { describe, it, expect } from "vitest";
12
+ import {
13
+ alsoAvailableSkillsNote,
14
+ inputFileLines,
15
+ selectSkillsForPrompt,
16
+ standingContextSections,
17
+ visionPromptInfoOf,
18
+ type StandingSectionKind,
19
+ } from "../prompt-sections.js";
20
+ import type { ResolvedAttachment } from "../attachment-resolver.js";
21
+ import type { SkillMetadata } from "../skill-resolver.js";
22
+ import { SKILL_COUNT_THRESHOLD } from "../skill-relevance.js";
23
+ import { formatDeclaredPreferencesText } from "../declared-preferences.js";
24
+
25
+ describe("standingContextSections", () => {
26
+ const ORDER: readonly StandingSectionKind[] = [
27
+ "conversation-sender",
28
+ "declared-preferences",
29
+ "recalled-memories",
30
+ "session-context",
31
+ "previous-conversation",
32
+ ];
33
+
34
+ it("renders every present section in the one ruled order, whatever order the fields arrive in", () => {
35
+ const sections = standingContextSections({
36
+ contextBridge: "bridge",
37
+ sessionContext: "session",
38
+ recalledMemories: { facts: ["fact"] },
39
+ declaredPreferences: { orgContext: "org" },
40
+ senderIdentity: { value: "15550001111", kind: "whatsapp_phone" },
41
+ });
42
+ expect(sections.map((s) => s.kind)).toEqual(ORDER);
43
+ });
44
+
45
+ it("omits an absent section and keeps the relative order of the rest", () => {
46
+ const sections = standingContextSections({ contextBridge: "bridge", senderIdentity: { value: "U1", kind: "slack_user_id" } });
47
+ expect(sections.map((s) => s.kind)).toEqual(["conversation-sender", "previous-conversation"]);
48
+ });
49
+
50
+ it("renders nothing for an empty context", () => {
51
+ expect(standingContextSections({})).toEqual([]);
52
+ });
53
+
54
+ it("carries each body from the fact's own module, unframed", () => {
55
+ const [section] = standingContextSections({ declaredPreferences: { userContext: "Keep answers terse." } });
56
+ expect(section?.body).toBe(formatDeclaredPreferencesText({ userContext: "Keep answers terse." }));
57
+ expect(section?.body.startsWith("<")).toBe(false);
58
+ expect(section?.body.startsWith("#")).toBe(false);
59
+ });
60
+ });
61
+
62
+ describe("inputFileLines", () => {
63
+ const file = (relativePath: string, extra: Partial<ResolvedAttachment> = {}): ResolvedAttachment => ({
64
+ filename: relativePath.split("/").pop()!,
65
+ relativePath,
66
+ sizeBytes: 1024,
67
+ ...extra,
68
+ });
69
+
70
+ it("one bullet per file with the size, the rename disclosure and the download URL", () => {
71
+ const { entries } = inputFileLines(
72
+ [
73
+ file(".stigmer/inputs/spec.pdf"),
74
+ file(".stigmer/inputs/report-2.pdf", { renamedFrom: "report.pdf" }),
75
+ file(".stigmer/inputs/diagram.png", { downloadUrl: "https://r2.example/d?sig=1" }),
76
+ ],
77
+ undefined,
78
+ undefined,
79
+ );
80
+ expect(entries).toEqual([
81
+ "- `.stigmer/inputs/spec.pdf` (1024 bytes)",
82
+ "- `.stigmer/inputs/report-2.pdf` (1024 bytes) (renamed from duplicate 'report.pdf')",
83
+ "- `.stigmer/inputs/diagram.png` (1024 bytes) — download URL: https://r2.example/d?sig=1",
84
+ ]);
85
+ });
86
+
87
+ it("renders the URL hand-off line only when a listed file carries a URL AND the storage's kind is known", () => {
88
+ const withUrl = [file("a.pdf", { downloadUrl: "https://r2.example/a" })];
89
+ expect(inputFileLines(withUrl, undefined, "presigned").urlHandoff).toBeDefined();
90
+ expect(inputFileLines(withUrl, undefined, undefined).urlHandoff).toBeUndefined();
91
+ expect(inputFileLines([file("a.pdf")], undefined, "presigned").urlHandoff).toBeUndefined();
92
+ });
93
+
94
+ it("renders the vision lines only when the turn carries vision facts", () => {
95
+ expect(inputFileLines([file("a.png")], undefined, undefined).vision).toEqual([]);
96
+ const { vision } = inputFileLines(
97
+ [file("a.png")],
98
+ { inlineFilenames: ["a.png"], notViewable: [{ path: ".stigmer/inputs/big.png", reason: "too_large" }] },
99
+ undefined,
100
+ );
101
+ expect(vision.some((l) => l.includes("a.png"))).toBe(true);
102
+ expect(vision.some((l) => l.includes("big.png"))).toBe(true);
103
+ });
104
+ });
105
+
106
+ describe("visionPromptInfoOf", () => {
107
+ it("is undefined when the turn has neither inline images nor degraded ones", () => {
108
+ expect(visionPromptInfoOf({ visionImages: [], visionNotViewable: [] })).toBeUndefined();
109
+ });
110
+
111
+ it("names the inline images in send order and carries the degraded entries", () => {
112
+ const info = visionPromptInfoOf({
113
+ visionImages: [{ filename: "b.png" }, { filename: "a.jpg" }],
114
+ visionNotViewable: [{ path: ".stigmer/inputs/big.png", reason: "too_large" }],
115
+ });
116
+ expect(info).toEqual({
117
+ inlineFilenames: ["b.png", "a.jpg"],
118
+ notViewable: [{ path: ".stigmer/inputs/big.png", reason: "too_large" }],
119
+ });
120
+ });
121
+ });
122
+
123
+ describe("selectSkillsForPrompt", () => {
124
+ const skill = (name: string, description: string): SkillMetadata => ({ name, description, path: `.stigmer/skills/${name}/SKILL.md` });
125
+
126
+ it("highlights every skill below the threshold, in mount order, and names none as also-available", () => {
127
+ const skills = [skill("alpha", "Deploy to kubernetes"), skill("beta", "Reshape CSV exports")];
128
+ const selection = selectSkillsForPrompt("deploy the service", skills);
129
+ expect(selection.highlighted).toEqual(skills);
130
+ expect(selection.alsoAvailable).toEqual([]);
131
+ });
132
+
133
+ it("at the threshold, highlights the skills the message reaches and names the rest", () => {
134
+ const skills = Array.from({ length: SKILL_COUNT_THRESHOLD }, (_, i) =>
135
+ i === 0 ? skill("k8s-deploy", "Deploy services to kubernetes clusters") : skill(`other-${i}`, `Unrelated thing number ${i}`),
136
+ );
137
+ const selection = selectSkillsForPrompt("Deploy the payments service to kubernetes", skills);
138
+ expect(selection.highlighted.map((s) => s.name)).toContain("k8s-deploy");
139
+ expect(selection.alsoAvailable.length).toBeGreaterThan(0);
140
+ expect(selection.highlighted.length + selection.alsoAvailable.length).toBe(SKILL_COUNT_THRESHOLD);
141
+ });
142
+
143
+ it("selects nothing from nothing", () => {
144
+ expect(selectSkillsForPrompt("anything", [])).toEqual({ highlighted: [], alsoAvailable: [] });
145
+ });
146
+ });
147
+
148
+ describe("alsoAvailableSkillsNote", () => {
149
+ it("returns empty for no excluded names", () => {
150
+ expect(alsoAvailableSkillsNote([])).toBe("");
151
+ });
152
+
153
+ it("lists excluded skills with backtick formatting", () => {
154
+ const note = alsoAvailableSkillsNote(["alpha", "beta", "gamma"]);
155
+ expect(note).toContain("`alpha`");
156
+ expect(note).toContain("`beta`");
157
+ expect(note).toContain("`gamma`");
158
+ });
159
+
160
+ it("includes activation instructions that name no tool, so any harness can carry it", () => {
161
+ const note = alsoAvailableSkillsNote(["some-skill"]);
162
+ expect(note).toContain(".stigmer/skills/<name>/SKILL.md");
163
+ expect(note).toContain("relevant to your task");
164
+ expect(note).not.toMatch(/\b(read|Read) tool\b/);
165
+ expect(note.startsWith("#")).toBe(false);
166
+ });
167
+ });
@@ -10,8 +10,8 @@
10
10
  * An offline end-to-end test is structurally infeasible for this path (DD-23 /
11
11
  * DD-26 F3: the offline harness always runs a git workspace with LocalArtifactDir,
12
12
  * so deriveCaptureMode is always true and the no-storage deny-gate is unreachable).
13
- * This test exercises the exact function both `execute-deep-agent/index.ts` and the
14
- * Cursor `persist` wrapper invoke, over a full AgentExecutionStatus.
13
+ * This test exercises the exact function the turn runtime's persist chokepoint
14
+ * invokes for both harnesses, over a full AgentExecutionStatus.
15
15
  */
16
16
 
17
17
  import { describe, it, expect } from "vitest";
@@ -1,9 +1,12 @@
1
1
  /**
2
- * Unit tests for the deep-agent turn-boundary flowed-row stamping
3
- * (`stamp-flowed-rows.ts`), with emphasis on the sub-agent path added by the
4
- * DD-24 follow-up: sub-agent edit rows fold into the parent turn's change set,
5
- * so they carry the parent change set id, scoped to this turn by tool-call-id
6
- * novelty (they lack the already-stamped/hidden shields the top-level pass has).
2
+ * Unit tests for the shared turn-boundary flowed-row stamping
3
+ * (`shared/tool-row.ts` `stampFlowedFileEditRows` /
4
+ * `stampFlowedSubAgentFileEditRows`; the deep-agent's copy until S3 M4), with
5
+ * emphasis on the sub-agent path added by the DD-24 follow-up: sub-agent edit
6
+ * rows fold into the parent turn's change set, so they carry the parent change
7
+ * set id, scoped to this turn by tool-call-id novelty (they lack the
8
+ * already-stamped/hidden shields the top-level pass has). The `flowed`
9
+ * predicate is the one seam a deny-gate harness narrows the pass with.
7
10
  */
8
11
 
9
12
  import { describe, it, expect } from "vitest";
@@ -15,11 +18,12 @@ import {
15
18
  import type { AgentMessage } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
16
19
  import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
17
20
  import { ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
18
- import { collectSubAgentToolCallIds } from "../../../shared/tool-row.js";
19
21
  import {
22
+ collectSubAgentToolCallIds,
23
+ hideToolCallRow,
20
24
  stampFlowedFileEditRows,
21
25
  stampFlowedSubAgentFileEditRows,
22
- } from "../stamp-flowed-rows.js";
26
+ } from "../tool-row.js";
23
27
 
24
28
  const CHANGE_SET_ID = "exec-1:0";
25
29
 
@@ -42,7 +46,7 @@ function subAgent(subId: string, ...messages: AgentMessage[]) {
42
46
  return create(SubAgentExecutionSchema, { id: subId, name: "code_editor", messages });
43
47
  }
44
48
 
45
- describe("stampFlowedFileEditRows (deep-agent)", () => {
49
+ describe("stampFlowedFileEditRows", () => {
46
50
  it("stamps a flowed write/delete row and leaves a non-file row untouched", () => {
47
51
  const write = editMessage("tc-1", "src/a.ts", "write_file");
48
52
  const shell = create(AgentMessageSchema, {
@@ -60,7 +64,7 @@ describe("stampFlowedFileEditRows (deep-agent)", () => {
60
64
  const prior = editMessage("tc-prior", "src/old.ts");
61
65
  const current = editMessage("tc-current", "src/new.ts");
62
66
 
63
- stampFlowedFileEditRows([prior, current], CHANGE_SET_ID, new Set(["tc-prior"]));
67
+ stampFlowedFileEditRows([prior, current], CHANGE_SET_ID, { skipToolCallIds: new Set(["tc-prior"]) });
64
68
 
65
69
  expect(prior.toolCalls[0].fileChangeSetId).toBe(""); // skipped
66
70
  expect(current.toolCalls[0].fileChangeSetId).toBe(CHANGE_SET_ID);
@@ -74,9 +78,28 @@ describe("stampFlowedFileEditRows (deep-agent)", () => {
74
78
 
75
79
  expect(row.toolCalls[0].fileChangeSetId).toBe("exec-1:prior");
76
80
  });
81
+
82
+ it("skips a legacy hidden row (a pre-stamping session's collapsed edit): it belongs to an earlier turn's set", () => {
83
+ const legacy = editMessage("tc-legacy", "src/old.ts");
84
+ hideToolCallRow(legacy.toolCalls[0]!);
85
+
86
+ stampFlowedFileEditRows([legacy], CHANGE_SET_ID);
87
+
88
+ expect(legacy.toolCalls[0]!.fileChangeSetId).toBe("");
89
+ });
90
+
91
+ it("leaves a row the caller's `flowed` predicate refuses (a denied identity, a non-terminal status) unstamped", () => {
92
+ const flowed = editMessage("tc-flowed", "src/a.ts");
93
+ const denied = editMessage("tc-denied", "src/b.ts");
94
+
95
+ stampFlowedFileEditRows([flowed, denied], CHANGE_SET_ID, { flowed: (tc) => tc.id !== "tc-denied" });
96
+
97
+ expect(flowed.toolCalls[0].fileChangeSetId).toBe(CHANGE_SET_ID);
98
+ expect(denied.toolCalls[0].fileChangeSetId).toBe("");
99
+ });
77
100
  });
78
101
 
79
- describe("stampFlowedSubAgentFileEditRows (deep-agent)", () => {
102
+ describe("stampFlowedSubAgentFileEditRows", () => {
80
103
  it("stamps a current-turn sub-agent's edit row with the parent change set id", () => {
81
104
  const sub = subAgent("sa-1", editMessage("sa-tc-1", "src/sub.ts"));
82
105
 
@@ -119,7 +119,7 @@ describe("buildChatModel vertex adapter", () => {
119
119
  });
120
120
 
121
121
  it("translated ids inherit the same default maxTokens as canonical ids", () => {
122
- // setup.ts deliberately omits maxTokens, so the LangChain per-model
122
+ // turn-setup.ts deliberately omits maxTokens, so the LangChain per-model
123
123
  // default applies. That default prefix-matches the model string — and the
124
124
  // vertex branch hands ChatAnthropic the TRANSLATED string. This pins the
125
125
  // equality the zero-behavior-change criterion rests on (verified 16384 ==
@@ -14,34 +14,56 @@
14
14
  * the backend's create pipeline requires a storage key on every attachment
15
15
  * it accepts, and in local mode the storage reads directly off disk.
16
16
  *
17
- * Placement is always `inputs/{filename}` — the platform namespace this
18
- * harness can surface in the workspace. An attachment's `mountPath` is
19
- * honored by convention, not mechanism: the standard mounts (e.g. the
20
- * approved plan at `.stigmer/inputs/<slug>_<id>.plan.md`) resolve to exactly this
21
- * placement, and the prompt's `<input_files>` section plus any path-derived
22
- * directives are built from the RESOLVED paths, so prompt and filesystem can
23
- * never disagree.
17
+ * Placement is the `inputs/` namespace — the platform directory every
18
+ * harness surfaces in the workspace through the `.stigmer` link, and the
19
+ * only place an attachment may land. An attachment names its place INSIDE
20
+ * that namespace with `mountPath` (`inputs/<name>`, `inputs/<dir>/`,
21
+ * `.stigmer/inputs/<name>`; a leading slash is stripped), or takes the
22
+ * default `inputs/{filename}`. A `mountPath` outside the namespace is
23
+ * refused with an actionable error, never relocated silently: a file
24
+ * written into the user's own tree would be committed to the session's
25
+ * write-back branch on completion (`git add -A`), and every real producer —
26
+ * the CLI's directory attachments, the approved plan the console mounts —
27
+ * already stays inside `inputs/` (S3 M1, Q-M1-2; the proto's `/workspace/…`
28
+ * example is a doc issue). The prompt's `<input_files>` section and every
29
+ * path-derived directive are built from the RESOLVED paths, so prompt and
30
+ * filesystem can never disagree.
24
31
  *
25
- * Duplicate filenames are renamed, never overwritten (issue #364): because
26
- * placement keys purely on the filename, two attachments with the same name
27
- * contend for one path — the later one takes the platform's `stem-2.ext`
28
- * rename (shared/attachment-naming.ts, same semantics as the deep-agent
29
- * injector and the React composer) and the rename is disclosed in the
30
- * prompt's `<input_files>` section via {@link ResolvedAttachment.renamedFrom}.
32
+ * Archives: an attachment marked `extract` is a ZIP whose entries land under
33
+ * its mount as a DIRECTORY (`inputs/<mount>/<entry>`), validated and
34
+ * decompressed by `attachment-zip.ts` (the #567 guards). Each entry is its
35
+ * own {@link ResolvedAttachment} with no vision, no `renamedFrom` and no
36
+ * `downloadUrl`: an image inside a ZIP has no attachment-level bytes, a
37
+ * renamed mount directory is visible through every entry path, and the
38
+ * stored object is the ZIP, not any listed file.
31
39
  *
32
- * Error model: fail-hard, matching the native harness's attachment injector.
33
- * Attachments are explicit user inputs an execution that silently runs
34
- * without one produces silently incorrect results (the "plan file wasn't
35
- * found" class of failure). Any attachment that cannot be materialized aborts
36
- * the execution with an actionable error.
40
+ * Duplicate names are renamed, never overwritten (issue #364), in two passes:
41
+ * explicit mount paths claim their exact targets first two attachments
42
+ * pinning the SAME path is a user contradiction no rename can honestly
43
+ * resolve, so that alone still refuses and default-derived names then
44
+ * uniquify around everything already taken with the platform's `stem-2.ext`
45
+ * rename (shared/attachment-naming.ts, the React composer's semantics),
46
+ * disclosed in the prompt via {@link ResolvedAttachment.renamedFrom}.
47
+ *
48
+ * Error model: fail-hard. Attachments are explicit user inputs — an
49
+ * execution that silently runs without one produces silently incorrect
50
+ * results (the "plan file wasn't found" class of failure). Any attachment
51
+ * that cannot be materialized aborts the execution with an actionable
52
+ * {@link AttachmentResolutionError}; an archive that fails its guards
53
+ * aborts with the guard's own `AttachmentValidationError`.
54
+ *
55
+ * Until S3 M1 this resolver ignored `mountPath` and could not extract; the
56
+ * native harness's `attachment-injector.ts` did both and was retired at S3
57
+ * M2b for this one pipeline (Q-S3-14).
37
58
  */
38
59
 
39
60
  import { mkdir, copyFile, readFile, stat, writeFile } from "node:fs/promises";
40
- import { join, basename } from "node:path";
61
+ import { join, basename, dirname, posix } from "node:path";
41
62
  import type { Attachment } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
42
63
  import type { ArtifactStorage } from "./artifact-storage.js";
43
64
  import { mintAttachmentDownloadUrl } from "./attachment-download-urls.js";
44
65
  import { allocateUniqueName } from "./attachment-naming.js";
66
+ import { decompressEntry, parseAndValidateZip } from "./attachment-zip.js";
45
67
  import {
46
68
  isVisionCandidate,
47
69
  type VisionBudget,
@@ -59,6 +81,8 @@ export interface ResolvedAttachment {
59
81
  filename: string;
60
82
  /** Workspace-relative path the agent reads (`.stigmer/inputs/{filename}`). */
61
83
  relativePath: string;
84
+ /** The bytes on disk; a prompt discloses it beside the path so the agent can weigh a read. */
85
+ sizeBytes: number;
62
86
  /**
63
87
  * The attachment's original filename, present only when a duplicate name
64
88
  * was renamed (shared/attachment-naming.ts) — rendered as disclosure in
@@ -138,13 +162,18 @@ export async function resolveAttachments(
138
162
  // it, but only when the agent has skills).
139
163
  await ensureStigmerSymlink(options.primaryWorkspaceDir, platformDir);
140
164
 
141
- // Placement keys purely on the filename, so this set is the whole
142
- // collision domain sequential resolution means each attachment sees
143
- // every name claimed before it (see module doc on duplicate handling).
144
- const takenNames = new Set<string>();
165
+ // Every place is decided before any bytes move: an explicit-path
166
+ // contradiction refuses up front, and a default name never lands on a path
167
+ // an explicit one claims (see module doc on duplicate handling).
168
+ const places = resolvePlacements(attachments);
145
169
  const results: ResolvedAttachment[] = [];
146
170
  for (const attachment of attachments) {
147
- results.push(await resolveAttachment(attachment, inputsDir, takenNames, options));
171
+ const place = places.get(attachment)!;
172
+ if (attachment.extract) {
173
+ results.push(...(await extractArchive(attachment, place, inputsDir, options)));
174
+ } else {
175
+ results.push(await resolveAttachment(attachment, place, inputsDir, options));
176
+ }
148
177
  }
149
178
 
150
179
  console.log(
@@ -157,19 +186,22 @@ export async function resolveAttachments(
157
186
 
158
187
  async function resolveAttachment(
159
188
  attachment: Attachment,
189
+ place: Placement,
160
190
  inputsDir: string,
161
- takenNames: Set<string>,
162
191
  options: AttachmentResolverOptions,
163
192
  ): Promise<ResolvedAttachment> {
193
+ const { relative, renamedFrom } = place;
194
+ // The final basename is the canonical filename: after a duplicate rename
195
+ // the original attachment.filename no longer names the file on disk, and
196
+ // the vision label must match what the prompt lists.
197
+ const filename = posix.basename(relative);
198
+ await mkdir(dirname(join(inputsDir, relative)), { recursive: true });
199
+
164
200
  // Local-mode fast path: the file is already on this machine's disk.
165
201
  if (options.mode === "local" && attachment.localPath) {
166
- const { name: filename, renamedFrom } = allocateUniqueName(
167
- safeInputName(attachment.filename || attachment.localPath),
168
- takenNames,
169
- );
170
202
  let vision: VisionOutcome | undefined;
171
203
  try {
172
- vision = await materializeLocalFile(attachment, filename, inputsDir, options.visionBudget);
204
+ vision = await materializeLocalFile(attachment, filename, join(inputsDir, relative), options.visionBudget);
173
205
  } catch (err) {
174
206
  throw new AttachmentResolutionError(
175
207
  attachment.filename,
@@ -185,7 +217,8 @@ async function resolveAttachment(
185
217
  );
186
218
  return {
187
219
  filename,
188
- relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
220
+ relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, relative),
221
+ sizeBytes: (await stat(join(inputsDir, relative))).size,
189
222
  ...(renamedFrom !== undefined ? { renamedFrom } : {}),
190
223
  ...visionOutcomeFields(vision),
191
224
  ...(downloadUrl !== undefined ? { downloadUrl } : {}),
@@ -193,6 +226,77 @@ async function resolveAttachment(
193
226
  }
194
227
 
195
228
  // Universal path: download the uploaded content by storage key.
229
+ const content = await downloadFromStorage(attachment, options);
230
+ await writeFile(join(inputsDir, relative), content);
231
+
232
+ // The bytes are already in hand for the file write — offer them to the
233
+ // vision budget before they go out of scope (the sniff decides eligibility;
234
+ // no pre-filter needed on this branch).
235
+ const vision = options.visionBudget?.offer(filename, attachment.contentType, content);
236
+
237
+ const downloadUrl = await mintAttachmentDownloadUrl(
238
+ options.storage, attachment.storageKey, filename,
239
+ );
240
+
241
+ return {
242
+ filename,
243
+ relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, relative),
244
+ sizeBytes: content.byteLength,
245
+ ...(renamedFrom !== undefined ? { renamedFrom } : {}),
246
+ ...visionOutcomeFields(vision),
247
+ ...(downloadUrl !== undefined ? { downloadUrl } : {}),
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Extract an `extract` attachment's archive under its mount directory. The
253
+ * bytes are read whole (an archive is never a vision candidate, so the local
254
+ * fast path's lazy read has nothing to save), validated and decompressed by
255
+ * the shared guards, and written entry by entry; the entries are the
256
+ * resolved attachments (see the module doc for what they do not carry).
257
+ */
258
+ async function extractArchive(
259
+ attachment: Attachment,
260
+ place: Placement,
261
+ inputsDir: string,
262
+ options: AttachmentResolverOptions,
263
+ ): Promise<ResolvedAttachment[]> {
264
+ const content = await bytesOf(attachment, options);
265
+ const entries = parseAndValidateZip(content, attachment.filename);
266
+ const mountDir = place.relative.replace(/\/+$/, "");
267
+ const results: ResolvedAttachment[] = [];
268
+ for (const entry of entries) {
269
+ const bytes = await decompressEntry(entry, attachment.filename);
270
+ const relative = posix.join(mountDir, entry.relativePath);
271
+ const dest = join(inputsDir, relative);
272
+ await mkdir(dirname(dest), { recursive: true });
273
+ await writeFile(dest, bytes);
274
+ results.push({
275
+ filename: posix.basename(entry.relativePath),
276
+ relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, relative),
277
+ sizeBytes: bytes.byteLength,
278
+ });
279
+ }
280
+ return results;
281
+ }
282
+
283
+ /** The attachment's bytes, from the local fast path or from storage. */
284
+ async function bytesOf(attachment: Attachment, options: AttachmentResolverOptions): Promise<Buffer> {
285
+ if (options.mode === "local" && attachment.localPath) {
286
+ try {
287
+ return await readFile(attachment.localPath);
288
+ } catch (err) {
289
+ throw new AttachmentResolutionError(
290
+ attachment.filename,
291
+ `failed to read local file '${attachment.localPath}': ` +
292
+ `${err instanceof Error ? err.message : String(err)}`,
293
+ );
294
+ }
295
+ }
296
+ return downloadFromStorage(attachment, options);
297
+ }
298
+
299
+ async function downloadFromStorage(attachment: Attachment, options: AttachmentResolverOptions): Promise<Buffer> {
196
300
  if (!attachment.storageKey) {
197
301
  throw new AttachmentResolutionError(
198
302
  attachment.filename,
@@ -206,14 +310,8 @@ async function resolveAttachment(
206
310
  `(key: ${attachment.storageKey}) cannot be downloaded`,
207
311
  );
208
312
  }
209
-
210
- const { name: filename, renamedFrom } = allocateUniqueName(
211
- safeInputName(attachment.filename || attachment.storageKey),
212
- takenNames,
213
- );
214
- let content: Buffer;
215
313
  try {
216
- content = await options.storage.download(attachment.storageKey);
314
+ return await options.storage.download(attachment.storageKey);
217
315
  } catch (err) {
218
316
  throw new AttachmentResolutionError(
219
317
  attachment.filename,
@@ -221,24 +319,90 @@ async function resolveAttachment(
221
319
  `${err instanceof Error ? err.message : String(err)}`,
222
320
  );
223
321
  }
224
- await writeFile(join(inputsDir, filename), content);
322
+ }
225
323
 
226
- // The bytes are already in hand for the file write — offer them to the
227
- // vision budget before they go out of scope (the sniff decides eligibility;
228
- // no pre-filter needed on this branch).
229
- const vision = options.visionBudget?.offer(filename, attachment.contentType, content);
324
+ // ── Placement ────────────────────────────────────────────────────────
230
325
 
231
- const downloadUrl = await mintAttachmentDownloadUrl(
232
- options.storage, attachment.storageKey, filename,
233
- );
326
+ /** Where an attachment lands, relative to the `inputs/` directory. */
327
+ interface Placement {
328
+ readonly relative: string;
329
+ /** Present when a default-derived name was uniquified (issue #364). */
330
+ readonly renamedFrom?: string;
331
+ }
234
332
 
235
- return {
236
- filename,
237
- relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
238
- ...(renamedFrom !== undefined ? { renamedFrom } : {}),
239
- ...visionOutcomeFields(vision),
240
- ...(downloadUrl !== undefined ? { downloadUrl } : {}),
241
- };
333
+ /**
334
+ * The two-pass placement (see module doc): explicit mount paths first, each
335
+ * resolved into the namespace and refused on a contradiction; then the
336
+ * default-derived names, uniquified around every name already taken directly
337
+ * under `inputs/`.
338
+ */
339
+ function resolvePlacements(attachments: readonly Attachment[]): Map<Attachment, Placement> {
340
+ const places = new Map<Attachment, Placement>();
341
+ const claimed = new Map<string, Attachment>();
342
+
343
+ for (const attachment of attachments) {
344
+ if (!attachment.mountPath) continue;
345
+ const relative = explicitPlacement(attachment);
346
+ const existing = claimed.get(relative);
347
+ if (existing) {
348
+ throw new AttachmentResolutionError(
349
+ attachment.filename,
350
+ `mount path '${attachment.mountPath}' collides with attachment '${existing.filename}'. ` +
351
+ "Set distinct mountPath values on the attachments to resolve this conflict.",
352
+ );
353
+ }
354
+ claimed.set(relative, attachment);
355
+ places.set(attachment, { relative });
356
+ }
357
+
358
+ const takenNames = new Set<string>();
359
+ for (const relative of claimed.keys()) {
360
+ if (!relative.includes("/")) takenNames.add(relative);
361
+ }
362
+ for (const attachment of attachments) {
363
+ if (attachment.mountPath) continue;
364
+ const { name, renamedFrom } = allocateUniqueName(
365
+ safeInputName(attachment.filename || attachment.localPath || attachment.storageKey),
366
+ takenNames,
367
+ );
368
+ places.set(attachment, renamedFrom !== undefined ? { relative: name, renamedFrom } : { relative: name });
369
+ }
370
+ return places;
371
+ }
372
+
373
+ /** The namespace prefixes an explicit `mountPath` may name, longest first. */
374
+ const INPUTS_NAMESPACE_PREFIXES = [`${STIGMER_LOCAL_STATE_DIR}/${INPUTS_SUBDIR}`, INPUTS_SUBDIR] as const;
375
+
376
+ /**
377
+ * Resolve an explicit `mountPath` into a path relative to `inputs/`, or
378
+ * refuse: empty after cleaning, outside the namespace, or climbing out of it.
379
+ * A caller-supplied path is untrusted, so `..` is judged after normalization.
380
+ */
381
+ function explicitPlacement(attachment: Attachment): string {
382
+ const cleaned = attachment.mountPath.replace(/^\/+/, "").replace(/\/+$/, "");
383
+ const prefix = INPUTS_NAMESPACE_PREFIXES.find((p) => cleaned === p || cleaned.startsWith(`${p}/`));
384
+ if (prefix === undefined) {
385
+ throw new AttachmentResolutionError(
386
+ attachment.filename,
387
+ `mount path '${attachment.mountPath}' is outside the attachment namespace. ` +
388
+ `Attachments land under '${INPUTS_SUBDIR}/' (surfaced as '${STIGMER_LOCAL_STATE_DIR}/${INPUTS_SUBDIR}/'); ` +
389
+ `set mountPath to '${INPUTS_SUBDIR}/<name>' or leave it empty for the default placement.`,
390
+ );
391
+ }
392
+ const relative = posix.normalize(cleaned.slice(prefix.length).replace(/^\/+/, ""));
393
+ if (relative === "" || relative === ".") {
394
+ throw new AttachmentResolutionError(
395
+ attachment.filename,
396
+ `mount path '${attachment.mountPath}' names the inputs directory itself, not a place inside it`,
397
+ );
398
+ }
399
+ if (relative === ".." || relative.startsWith("../") || posix.isAbsolute(relative)) {
400
+ throw new AttachmentResolutionError(
401
+ attachment.filename,
402
+ `mount path '${attachment.mountPath}' escapes the attachment namespace`,
403
+ );
404
+ }
405
+ return relative;
242
406
  }
243
407
 
244
408
  /**
@@ -251,10 +415,9 @@ async function resolveAttachment(
251
415
  async function materializeLocalFile(
252
416
  attachment: Attachment,
253
417
  filename: string,
254
- inputsDir: string,
418
+ dest: string,
255
419
  visionBudget: VisionBudget | undefined,
256
420
  ): Promise<VisionOutcome | undefined> {
257
- const dest = join(inputsDir, filename);
258
421
  if (!visionBudget || !isVisionCandidate(attachment.contentType, filename)) {
259
422
  await copyFile(attachment.localPath, dest);
260
423
  return undefined;