@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
@@ -36,12 +36,20 @@
36
36
  * `awaiting_approval` resolves, and never executes in that turn.
37
37
  * 3. Reinvoked with APPROVE for that id → executes exactly once and the row
38
38
  * is carried to COMPLETED; with REJECT or SKIP → never executes and the
39
- * turn continues (the row's terminal status is the runtime's to write,
40
- * not the adapter's Q-M4-1); reinvoked again after the approval → still
41
- * once, never re-gated.
39
+ * turn continues (the row's SKIPPED status is the runtime's to write,
40
+ * `harness/approval-decisions.ts`, proven in the runtime half the
41
+ * driver stands in for that write between turns, Q-M1-3); reinvoked again
42
+ * after the approval → still once, never re-gated.
42
43
  * 4. Aborting `stopSignal` mid-hang settles `runTurn` as `interrupted` within
43
- * {@link INTERRUPT_SETTLE_BOUND_MS} and nothing after the stop executes; a
44
- * signal aborted BEFORE `runTurn` yields `interrupted` with nothing done.
44
+ * {@link INTERRUPT_SETTLE_BOUND_MS} and nothing after the stop executes or
45
+ * is even gated (a proposal after the hang has no row); a signal aborted
46
+ * BEFORE `runTurn` yields `interrupted` with nothing done. Stated over a
47
+ * FRESH proposal on purpose: WHEN an already-approved pending action
48
+ * executes on the resumed turn is the pause primitive's business — an
49
+ * interrupt engine completes the pending task before its model speaks,
50
+ * a deny-and-retry engine re-reaches it whenever its model does — so it
51
+ * is below the contract line (S3 M3 Q-M3-2; each harness's own kit file
52
+ * observes its order).
45
53
  * 5. Usage reaches the sink as non-negative deltas summing to what the engine
46
54
  * emitted.
47
55
  * 6. Capability and behaviour agree on the state id: an `engine-minted`
@@ -74,7 +82,7 @@ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/a
74
82
  import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
75
83
  import { ApprovalAction, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
76
84
 
77
- import { approvalDecisionsOf } from "../../harness/turn-context.js";
85
+ import { approvalDecisionsOf, terminalizeNonExecutingDecisions } from "../../harness/approval-decisions.js";
78
86
  import type { TurnInput, TurnOutcome, UsageDelta } from "../../harness/types.js";
79
87
  import type { ProposedAction } from "../approval-contract/types.js";
80
88
  import { emptyStatus, findToolCallRow } from "../proto-helpers.js";
@@ -92,7 +100,7 @@ import type { EngineView, HarnessContractSubject, TurnScenario } from "./types.j
92
100
  */
93
101
  export const INTERRUPT_SETTLE_BOUND_MS = 5_000;
94
102
 
95
- const OUTCOME_KINDS = ["completed", "cancelled", "awaiting_approval", "failed", "interrupted"] as const satisfies readonly TurnOutcome["kind"][];
103
+ const OUTCOME_KINDS = ["completed", "cancelled", "awaiting_approval", "tool_call_limit", "failed", "interrupted"] as const satisfies readonly TurnOutcome["kind"][];
96
104
 
97
105
  /** Representative gated action shared by the invariants. */
98
106
  const WRITE_ALPHA: ProposedAction = { kind: "write", resource: "/work/alpha.txt" };
@@ -185,8 +193,14 @@ export class ExecutionDriver {
185
193
  return { input, sink, outcome };
186
194
  }
187
195
 
188
- /** The runtime's post-turn bookkeeping: persist the status, adopt a bound id, advance the cycle. */
196
+ /**
197
+ * The runtime's post-turn bookkeeping: settle the SKIP and REJECT rows the
198
+ * adapter never executes (the runtime's write, `terminalizeNonExecutingDecisions`,
199
+ * so a decided row is not re-read as WAITING on the next turn), persist the
200
+ * status, adopt a bound id, advance the cycle.
201
+ */
189
202
  recordTurn(sink: RecordingTurnSink): void {
203
+ terminalizeNonExecutingDecisions(sink.status);
190
204
  this.persisted = sink.status;
191
205
  const bound = sink.boundStateIds.at(-1);
192
206
  if (bound !== undefined) this.threadId = bound;
@@ -321,9 +335,10 @@ export async function assertProposalIsWaitingAndUnexecuted(subject: HarnessContr
321
335
  * turn continues — the row's terminal status is NOT asserted here because
322
336
  * it is not the adapter's to write (the decision is the server's field and
323
337
  * the WAITING → SKIPPED transition follows from it with no engine knowledge,
324
- * so it is the runtime's; S2 M4 Q-M4-1, the runtime's arm lands in S3). The
325
- * reinvocation sees a CLONE of the persisted status, as it would from the
326
- * server.
338
+ * so it is the runtime's `terminalizeNonExecutingDecisions`, landed at
339
+ * S3 M1 and pinned by the runtime half's REJECT / SKIP arms; S2 M4 Q-M4-1).
340
+ * The reinvocation sees a CLONE of the persisted status, as it would from
341
+ * the server.
327
342
  */
328
343
  export async function assertDecisionsExecuteExactlyOnce(subject: HarnessContractSubject): Promise<void> {
329
344
  const driver = new ExecutionDriver(subject, "inv3");
@@ -360,22 +375,20 @@ export async function assertDecisionsExecuteExactlyOnce(subject: HarnessContract
360
375
  * Invariant 4: the stop signal is honoured promptly and completely. A turn
361
376
  * stopped WHILE HANGING (the engine has reported it is parked, so the stop
362
377
  * lands in the hang and not in the adapter's setup) settles `interrupted`
363
- * within the bound and executes nothing that came after the hang, even an
364
- * already-approved action; a turn entered under an aborted signal does
365
- * nothing at all.
378
+ * within the bound, and the proposal after the hang neither executes nor
379
+ * reaches the gate (no row of any status); a turn entered under an aborted
380
+ * signal does nothing at all. The proposal is FRESH — see the catalog on why
381
+ * an approved one would assert the pause primitive's order, not the contract.
366
382
  */
367
383
  export async function assertStopSignalInterrupts(subject: HarnessContractSubject, boundMs = INTERRUPT_SETTLE_BOUND_MS): Promise<void> {
368
384
  const driver = new ExecutionDriver(subject, "inv4");
369
- const id = "inv4-approved-after-hang";
370
-
371
- const proposed = await driver.turn([scenario.propose(id, WRITE_ALPHA)]);
372
- expect(proposed.outcome.kind, `${subject.name}: proposal must end awaiting_approval`).toBe("awaiting_approval");
373
- driver.decide(id, ApprovalAction.APPROVE);
385
+ const id = "inv4-proposed-after-hang";
374
386
 
375
387
  const hanging = driver.begin([scenario.say("working"), scenario.hang(), scenario.propose(id, WRITE_ALPHA)], { stopWhenHanging: "kit: user pause" });
376
388
  const outcome = await settleWithinBound(subject, hanging.settled, "after stopSignal aborted mid-hang", boundMs);
377
389
  expect(outcome.kind, `${subject.name}: a turn stopped mid-hang must settle interrupted`).toBe("interrupted");
378
- expect(subject.executionCount(id), `${subject.name}: nothing after the stop may execute, even an approved action`).toBe(0);
390
+ expect(subject.executionCount(id), `${subject.name}: nothing after the stop may execute`).toBe(0);
391
+ expect(findToolCallRow(hanging.sink.status, id), `${subject.name}: nothing after the stop may even reach the gate — the proposal after the hang has no row`).toBeUndefined();
379
392
  driver.recordTurn(hanging.sink);
380
393
 
381
394
  const preAborted = new RecordingTurnSink({ status: driver.seedStatus() });
@@ -21,6 +21,7 @@
21
21
  import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
22
22
 
23
23
  import type { TurnSink, UsageDelta } from "../../harness/types.js";
24
+ import type { CasTouchedReader } from "../../shared/filereview/cas-touched.js";
24
25
  import { TimingRecorder } from "../../shared/cold-start-timing.js";
25
26
  import { emptyStatus } from "../proto-helpers.js";
26
27
 
@@ -30,7 +31,8 @@ export type SinkEvent =
30
31
  | { readonly kind: "activity"; readonly detail: string | undefined }
31
32
  | { readonly kind: "usage"; readonly delta: UsageDelta }
32
33
  | { readonly kind: "progress"; readonly label: string }
33
- | { readonly kind: "bind"; readonly harnessStateId: string };
34
+ | { readonly kind: "bind"; readonly harnessStateId: string }
35
+ | { readonly kind: "cas-observations" };
34
36
 
35
37
  export interface RecordingTurnSinkOptions {
36
38
  /** The status this turn folds into; a fresh empty one when omitted. */
@@ -80,6 +82,14 @@ export class RecordingTurnSink implements TurnSink {
80
82
  this.log.push({ kind: "bind", harnessStateId });
81
83
  }
82
84
 
85
+ bindCasObservations(read: CasTouchedReader): void {
86
+ this.casObservations = read;
87
+ this.log.push({ kind: "cas-observations" });
88
+ }
89
+
90
+ /** The reader the adapter bound, for a test that wants to pull what the engine observed. */
91
+ casObservations: CasTouchedReader | undefined;
92
+
83
93
  /** Every call, in order. */
84
94
  get events(): readonly SinkEvent[] {
85
95
  return this.log;
@@ -44,21 +44,29 @@
44
44
  */
45
45
 
46
46
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
47
- import { mkdirSync } from "node:fs";
47
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
48
48
  import { realpath } from "node:fs/promises";
49
49
  import { join } from "node:path";
50
- import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
51
- import { ApprovalAction, ExecutionControlSignal, ExecutionPhase, MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
50
+ import { ApprovalAction, ExecutionControlSignal, ExecutionPhase, MessageType, SubAgentStatus, TodoStatus, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
51
+ import { create, type JsonObject } from "@bufbuild/protobuf";
52
+ import { RecalledMemoriesReportSchema, type AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
53
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
54
+ import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
55
+ import { TodoItemSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/todo_pb";
56
+ import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
57
+ import { WorkspaceWriteBackPhase, WorkspaceWriteBackSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
52
58
 
53
59
  import type { StigmerClient } from "../../client/stigmer-client.js";
54
60
  import type { Config } from "../../config.js";
55
61
  import { HARNESS_ACTIVITY_NAMES, createHarnessActivities } from "../../harness/registry.js";
56
62
  import { TERMINAL_COPY } from "../../harness/terminal-table.js";
57
- import { approvalDecisionsOf } from "../../harness/turn-context.js";
63
+ import { REJECTED_BY_USER_ERROR, approvalDecisionsOf } from "../../harness/approval-decisions.js";
64
+ import { MIN_TOOL_ROUNDS, TOOL_CALL_LIMIT_ERROR_PREFIX, TOOL_CALL_LIMIT_USER_COPY, formatToolCallLimitError } from "../../shared/tool-rounds.js";
58
65
  import type { FailureSurface } from "../../harness/types.js";
59
66
  import type { ExecuteActivityInput } from "../../shared/activity-input.js";
60
67
  import { acquireWorkspaceLock } from "../../shared/workspace/workspace-lock.js";
61
68
  import { executionRecordFixture, type ExecutionRecordOptions } from "../execution-record-fixture.js";
69
+ import { initGitWorkspace } from "../git-workspace-fixture.js";
62
70
  import {
63
71
  ExecutionRecord,
64
72
  ScriptedClock,
@@ -292,6 +300,256 @@ export async function assertApprovalRoundTrip(harness: RuntimeContractHarness, l
292
300
  return { driver, invocations: [first, second], final: finalStatusOf(harness, driver) };
293
301
  }
294
302
 
303
+ // ── Arm: file review — capture, decide, reconcile ───────────────────────────
304
+
305
+ /**
306
+ * Apply-then-review across two invocations — the runtime's own act for every
307
+ * harness (`harness/capture.ts`; `turn-context.ts` `reconcileReinvocation`),
308
+ * proven on a REAL git work tree (the session's workspace directory IS the
309
+ * tree, so no harness needs a workspace entry to reach it).
310
+ *
311
+ * Turn 1: two writes FLOW (no gate row, each executed exactly once), and the
312
+ * runtime — not the adapter — pins the baseline before the engine, captures
313
+ * the candidate after it, badges the two flowed rows with the change set,
314
+ * carries the strip at the WAITING write, and pauses `WAITING_FOR_APPROVAL`
315
+ * with the ledger reading `[baseline, candidate]` under the harness's own
316
+ * file-review identity. Between turns the reviewer keeps one file and
317
+ * discards the other (the record stands in for the server's decision and
318
+ * projection). Turn 2: the reinvocation reconciles WITHOUT asking the engine
319
+ * — the kept file holds its bytes, the discarded one is back at its baseline,
320
+ * RECONCILED is authored, the execution settles COMPLETED with the runtime's
321
+ * discard row, and the engine's execution counts are unchanged.
322
+ *
323
+ * Content is the engine's own (a real `edit_file`, a real hook-allowed edit,
324
+ * the fake's one line), so the arm asserts PATHS and the reconcile's effect
325
+ * on disk, never bytes; the change kinds differ per engine (an engine that
326
+ * seeds its ledger file before the turn produces a MODIFY where another
327
+ * produces an ADD) and are not asserted either.
328
+ */
329
+ export async function assertFileReviewRoundTrip(harness: RuntimeContractHarness, label = "rt-file-review"): Promise<RuntimeArmResult> {
330
+ const { subject } = harness;
331
+ const driver = new RuntimeExecutionDriver(harness, label);
332
+ initGitWorkspace(driver.workspaceDir, { "README.md": "# the kit's tree\n" });
333
+ const keepId = `${label}-keep`;
334
+ const dropId = `${label}-drop`;
335
+ const KEPT = "kept.md";
336
+ const DROPPED = "dropped.md";
337
+ const changeSetId = `${driver.record.executionId}:0`;
338
+
339
+ const first = await driver.turn([scenario.flowingWrite(keepId, KEPT), scenario.flowingWrite(dropId, DROPPED), scenario.say("Edited two files.")]);
340
+ expect(slimOf(subject, first).phase, `${subject.name}: a captured change pauses the turn for review`).toBe("EXECUTION_WAITING_FOR_APPROVAL");
341
+ expect(driver.record.waitingToolCalls(), `${subject.name}: a flowing write opens no gate row`).toHaveLength(0);
342
+ expect(subject.executionCount(keepId), `${subject.name}: a flowing write executes at once`).toBe(1);
343
+ expect(subject.executionCount(dropId)).toBe(1);
344
+ expect(existsSync(join(driver.workspaceDir, KEPT)) && existsSync(join(driver.workspaceDir, DROPPED)), `${subject.name}: both writes landed on the tree`).toBe(true);
345
+
346
+ const afterCapture = finalStatusOf(harness, driver);
347
+ const events = afterCapture.fileReviewEventStream?.events ?? [];
348
+ expect(events.map((e) => e.payload.case), `${subject.name}: the runtime authored the baseline and the candidate`).toEqual(["baselineCaptured", "candidateCaptured"]);
349
+ const candidate = events[1]!.payload;
350
+ if (candidate.case !== "candidateCaptured") throw new Error("unreachable");
351
+ expect(candidate.value.changeSetId).toBe(changeSetId);
352
+ expect([...candidate.value.changes.map((c) => c.pathAfter)].sort(), `${subject.name}: exactly the two written paths are reviewable`).toEqual([DROPPED, KEPT]);
353
+ const baseline = events[0]!.payload;
354
+ if (baseline.case !== "baselineCaptured") throw new Error("unreachable");
355
+ expect(baseline.value.harnessId, `${subject.name}: the ledger carries this harness's file-review identity`).toBe(subject.adapter.capabilities.fileReview.harnessId);
356
+ const rows = new Map(afterCapture.messages.flatMap((m) => m.toolCalls).map((tc) => [tc.id, tc]));
357
+ expect(rows.get(keepId)?.fileChangeSetId, `${subject.name}: a flowed row is badged with the change set`).toBe(changeSetId);
358
+ expect(rows.get(dropId)?.fileChangeSetId).toBe(changeSetId);
359
+ expect(rows.get(keepId)?.status, `${subject.name}: a flowed row is COMPLETED, never gated`).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
360
+ // The strip is the runtime's, attached on its writes (Q-M4-7). Its COUNT is
361
+ // the capture floor's: a real engine's first persist precedes its first
362
+ // write and both writes land within one floor interval, so the pause can
363
+ // honestly read "0 files" beside a two-file candidate (F-M4-3, recorded
364
+ // for S5: whether the terminal write should bypass the floor). The arm
365
+ // asserts the ownership, not the floor.
366
+ expect(afterCapture.fileChangeProgress?.changeSetId, `${subject.name}: the runtime carries this turn's strip`).toBe(changeSetId);
367
+ expect(afterCapture.completedAt, `${subject.name}: a review pause is not complete`).toBe("");
368
+
369
+ const keptBytes = readFileSync(join(driver.workspaceDir, KEPT), "utf-8");
370
+ expect(keptBytes).toContain(keepId);
371
+ harness.clock.tick();
372
+ expect(driver.record.decideCapturedFileChanges({ approve: [KEPT], reject: [DROPPED] }, new Date().toISOString()), `${subject.name}: two files to decide`).toBe(2);
373
+
374
+ const second = await driver.turn([]);
375
+ expect(slimOf(subject, second).phase, `${subject.name}: a pure file-review resume completes without the engine`).toBe("EXECUTION_COMPLETED");
376
+ // The kept file is the engine-not-re-run witness: a subject that counts
377
+ // executions by reading its ledger off disk (the native one, Q-M3-4) reads
378
+ // the DISCARDED file's count as 0 now, because the reconcile just restored
379
+ // that file to its baseline — which is the reconcile working, not the
380
+ // engine forgetting.
381
+ expect(subject.executionCount(keepId), `${subject.name}: the engine was not asked again`).toBe(1);
382
+ expect(readFileSync(join(driver.workspaceDir, KEPT), "utf-8"), `${subject.name}: the kept file holds its reviewed bytes`).toBe(keptBytes);
383
+ const droppedPath = join(driver.workspaceDir, DROPPED);
384
+ expect(!existsSync(droppedPath) || !readFileSync(droppedPath, "utf-8").includes(dropId), `${subject.name}: the discarded file is back at its baseline`).toBe(true);
385
+ const final = finalStatusOf(harness, driver);
386
+ expect(systemMessages(final), `${subject.name}: the discard row names the discarded path`).toContain(`${TERMINAL_COPY.fileReview.discardedPrefix}${DROPPED}.`);
387
+ expect(final.fileReviewEventStream?.events.map((e) => e.payload.case), `${subject.name}: the reconcile is authored`).toContain("reconciled");
388
+ expect(final.completedAt).not.toBe("");
389
+ return { driver, invocations: [first, second], final };
390
+ }
391
+
392
+ // ── Arm: awaiting_approval then REJECT or SKIP ──────────────────────────────
393
+
394
+ /**
395
+ * A decision that never runs the tool: the user REJECTs (or SKIPs) the
396
+ * proposal; the reinvocation continues WITHOUT it — the adapter's next step
397
+ * runs, the turn COMPLETES, the tool executes zero times — and the runtime
398
+ * settles the row SKIPPED (a REJECT row carries `error: "Rejected by user"`),
399
+ * so no WAITING row survives on a finished execution. This is the proto
400
+ * contract as of stigmer#197 and the runtime's own write
401
+ * (`harness/approval-decisions.ts`); before S3 M1 the runtime FAILED a
402
+ * REJECTed reinvocation before any engine ran.
403
+ */
404
+ export async function assertNonExecutingDecisionSettlesSkipped(
405
+ harness: RuntimeContractHarness,
406
+ action: ApprovalAction.REJECT | ApprovalAction.SKIP,
407
+ label = `rt-${ApprovalAction[action].toLowerCase()}`,
408
+ ): Promise<RuntimeArmResult> {
409
+ const { subject } = harness;
410
+ const driver = new RuntimeExecutionDriver(harness, label);
411
+ const id = `${label}-shell`;
412
+ const propose = scenario.propose(id, { kind: "shell", resource: `rm -rf build # ${label}` });
413
+ const verdict = ApprovalAction[action];
414
+
415
+ const first = await driver.turn([propose]);
416
+ expect(slimOf(subject, first).phase, `${subject.name}: a proposal returns WAITING_FOR_APPROVAL`).toBe("EXECUTION_WAITING_FOR_APPROVAL");
417
+ expect(driver.decide(action), `${subject.name}: one row to decide`).toBe(1);
418
+
419
+ const second = await driver.turn([propose, scenario.say("Moving on.")]);
420
+ expect(slimOf(subject, second).phase, `${subject.name}: after ${verdict} the run continues to COMPLETED — it does not fail`).toBe("EXECUTION_COMPLETED");
421
+ expect(slimOf(subject, second).final_text, `${subject.name}: the step after the ${verdict}ed action ran`).toBe("Moving on.");
422
+ expect(subject.executionCount(id), `${subject.name}: a ${verdict}ed action never executes`).toBe(0);
423
+
424
+ const final = finalStatusOf(harness, driver);
425
+ const row = final.messages.flatMap((m) => m.toolCalls).find((tc) => tc.id === id);
426
+ expect(row, `${subject.name}: the decided row is carried on the transcript`).toBeDefined();
427
+ expect(row!.status, `${subject.name}: the runtime settles a ${verdict}ed row SKIPPED`).toBe(ToolCallStatus.TOOL_CALL_SKIPPED);
428
+ expect(row!.approvalAction, `${subject.name}: the decision stays legible on the settled row`).toBe(action);
429
+ expect(row!.error, `${subject.name}: a REJECT carries its reason, a SKIP carries none`).toBe(action === ApprovalAction.REJECT ? REJECTED_BY_USER_ERROR : "");
430
+ expect(driver.record.waitingToolCalls(), `${subject.name}: nothing waits on a finished execution`).toHaveLength(0);
431
+ expect(final.error, `${subject.name}: the execution carries no error`).toBe("");
432
+ return { driver, invocations: [first, second], final };
433
+ }
434
+
435
+ // ── Arm: the tool-call limit ────────────────────────────────────────────────
436
+
437
+ /**
438
+ * The engine exhausted its tool-round budget: TERMINATED (not FAILED — the
439
+ * platform deliberately stopped the run and the conversation continues on
440
+ * the next message), `error` starting with the cross-repo prefix Stigmer
441
+ * Cloud's channel delivery matches on, the user copy as the one system row,
442
+ * `completedAt` stamped, RETURNED (a retry would spend the same budget), and
443
+ * the step after the limit never processed. Before S3 M1 the native harness
444
+ * RETURNED this terminal without persisting it (M0 finding F-M0-4); the
445
+ * runtime's arm is the one persisted terminal.
446
+ *
447
+ * The execution SETS the budget it exhausts: the record carries the smallest
448
+ * valid `max_tool_rounds` (an unbounded execution has no limit to reach — on
449
+ * native `resolveRecursionLimit(0)` is unlimited, and the first run of this
450
+ * arm against it spun until the test timed out; S3 M3 F-M3-3). The fake
451
+ * exhausts whatever budget it is given; a real engine spins its cheapest
452
+ * ungated tool until the graph stops it.
453
+ */
454
+ export async function assertToolCallLimitTerminates(harness: RuntimeContractHarness, label = "rt-tool-call-limit"): Promise<RuntimeArmResult> {
455
+ const { subject } = harness;
456
+ const driver = new RuntimeExecutionDriver(harness, label, { maxToolRounds: MIN_TOOL_ROUNDS });
457
+ const invocation = await driver.turn([scenario.say("Working…"), scenario.limit(), scenario.say(NEVER_SEEN)]);
458
+ // `slimOf` is the RETURN assertion (a retry would spend the same budget).
459
+ expect(slimOf(subject, invocation).phase, `${subject.name}: the tool-call limit is TERMINATED, not FAILED`).toBe("EXECUTION_TERMINATED");
460
+ const final = finalStatusOf(harness, driver);
461
+ expect(final.phase).toBe(ExecutionPhase.EXECUTION_TERMINATED);
462
+ expect(final.error.startsWith(TOOL_CALL_LIMIT_ERROR_PREFIX), `${subject.name}: the error carries the cross-repo prefix; got "${final.error}"`).toBe(true);
463
+ expect(final.error, `${subject.name}: the copy is the table's, byte for byte`).toBe(formatToolCallLimitError());
464
+ expect(systemMessages(final), `${subject.name}: the user copy is the one system row`).toEqual([TOOL_CALL_LIMIT_USER_COPY]);
465
+ expect(final.completedAt, `${subject.name}: a TERMINATED turn completes`).not.toBe("");
466
+ expect(aiMessages(final), `${subject.name}: the step after the limit is never processed`).not.toContain(NEVER_SEEN);
467
+ expect(driver.record.persistedPhases.at(-1), `${subject.name}: the terminal is persisted, not merely returned`).toBe(ExecutionPhase.EXECUTION_TERMINATED);
468
+ return { driver, invocations: [invocation], final };
469
+ }
470
+
471
+ // ── Arm: the seed's breadth on a reinvocation ───────────────────────────────
472
+
473
+ /**
474
+ * What a reinvocation carries forward. Every runner-owned collection and
475
+ * singleton the prior invocation persisted — the sub-agent rows, the to-dos,
476
+ * the artifacts, the write-back rows, the usage summary, the recalled-memories
477
+ * report, the structured output — is on the completed status of the resumed
478
+ * turn, because the server REPLACES each of them wholesale when the runner's
479
+ * write carries any (`update-status.ts`), so a write holding only the resumed
480
+ * turn's own would erase the prior turn's. A first turn (no transcript)
481
+ * carries none of them, whatever the record holds: the seed is gated on the
482
+ * persisted transcript, the one signal every harness shares.
483
+ *
484
+ * The record here REPLACES its held status with every full write (stricter
485
+ * than the server's presence guard), so a dropped collection is a failing
486
+ * assertion, not a masked one. The planted rows are the prior turn's own
487
+ * facts, written onto the record between invocations as the prior turn would
488
+ * have persisted them.
489
+ */
490
+ export async function assertReinvocationCarriesThePersistedStatus(harness: RuntimeContractHarness, label = "rt-seed"): Promise<RuntimeArmResult> {
491
+ const { subject } = harness;
492
+ const driver = new RuntimeExecutionDriver(harness, label);
493
+ const id = `${label}-shell`;
494
+ const propose = scenario.propose(id, { kind: "shell", resource: `make release # ${label}` });
495
+
496
+ const first = await driver.turn([propose]);
497
+ expect(slimOf(subject, first).phase).toBe("EXECUTION_WAITING_FOR_APPROVAL");
498
+ const planted = plantPriorTurnFacts(driver.record.status!, label);
499
+ expect(driver.decide(ApprovalAction.APPROVE)).toBe(1);
500
+
501
+ const second = await driver.turn([propose, scenario.say("Done.")]);
502
+ expect(slimOf(subject, second).phase, `${subject.name}: the resumed turn completes`).toBe("EXECUTION_COMPLETED");
503
+ const final = finalStatusOf(harness, driver);
504
+ expect(final.subAgentExecutions.map((s) => s.id), `${subject.name}: the prior turn's sub-agent row survives the resume`).toContain(planted.subAgentId);
505
+ expect(Object.keys(final.todos), `${subject.name}: the prior turn's to-dos survive the resume`).toContain(planted.todoId);
506
+ expect(final.artifacts.map((a) => a.name), `${subject.name}: the prior turn's artifacts survive the resume`).toContain(planted.artifactName);
507
+ expect(final.workspaceWriteBacks.map((w) => w.workspaceEntryName), `${subject.name}: the prior turn's write-back rows survive the resume`).toContain(planted.writeBackEntry);
508
+ expect(final.recalledMemoriesReport?.injectedMemoryIds, `${subject.name}: the recalled-memories report survives the resume`).toEqual([planted.memoryId]);
509
+ expect(final.structuredOutput, `${subject.name}: a structured output already on the record survives the resume`).toEqual(planted.structuredOutput);
510
+ expect(final.streamingUsage?.inputTokens, `${subject.name}: the usage summary carries until the resumed turn reports its own`).toBe(planted.inputTokens);
511
+ expect(final.messages.flatMap((m) => m.toolCalls).filter((tc) => tc.id === id), `${subject.name}: the gated row is carried once, not duplicated`).toHaveLength(1);
512
+ return { driver, invocations: [first, second], final };
513
+ }
514
+
515
+ /** The same record with the same planted facts but NO transcript: a first turn seeds nothing. */
516
+ export async function assertFirstTurnSeedsNothing(harness: RuntimeContractHarness, label = "rt-seed-first"): Promise<RuntimeArmResult> {
517
+ const { subject } = harness;
518
+ const driver = new RuntimeExecutionDriver(harness, label);
519
+ const planted = plantPriorTurnFacts(driver.record.status ?? (driver.record.execution.status = emptyStatus()), label);
520
+ const invocation = await driver.turn([scenario.say("Fresh start.")]);
521
+ expect(slimOf(subject, invocation).phase).toBe("EXECUTION_COMPLETED");
522
+ const final = finalStatusOf(harness, driver);
523
+ expect(final.subAgentExecutions.map((s) => s.id), `${subject.name}: a first turn seeds no sub-agent rows`).not.toContain(planted.subAgentId);
524
+ expect(Object.keys(final.todos), `${subject.name}: a first turn seeds no to-dos`).not.toContain(planted.todoId);
525
+ expect(final.artifacts.map((a) => a.name), `${subject.name}: a first turn seeds no artifacts`).not.toContain(planted.artifactName);
526
+ expect(final.workspaceWriteBacks, `${subject.name}: a first turn seeds no write-back rows`).toHaveLength(0);
527
+ expect(final.recalledMemoriesReport, `${subject.name}: a first turn seeds no report`).toBeUndefined();
528
+ expect(final.structuredOutput, `${subject.name}: a first turn seeds no structured output`).toBeUndefined();
529
+ return { driver, invocations: [invocation], final };
530
+ }
531
+
532
+ /** Write the prior turn's facts onto the server-held status, as its writes would have left them. */
533
+ function plantPriorTurnFacts(held: AgentExecutionStatus, label: string) {
534
+ const planted = {
535
+ subAgentId: `${label}-sub`,
536
+ todoId: `${label}-todo`,
537
+ artifactName: `${label}-report.md`,
538
+ writeBackEntry: `${label}-repo`,
539
+ memoryId: `${label}-memory`,
540
+ structuredOutput: { verdict: "carried" } as JsonObject,
541
+ inputTokens: 4_242n,
542
+ };
543
+ held.subAgentExecutions.push(create(SubAgentExecutionSchema, { id: planted.subAgentId, name: "researcher", status: SubAgentStatus.SUB_AGENT_COMPLETED }));
544
+ held.todos[planted.todoId] = create(TodoItemSchema, { id: planted.todoId, content: "carry me", status: TodoStatus.TODO_PENDING });
545
+ held.artifacts.push(create(ExecutionArtifactSchema, { name: planted.artifactName }));
546
+ held.workspaceWriteBacks.push(create(WorkspaceWriteBackSchema, { workspaceEntryName: planted.writeBackEntry, phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_COMMITTED }));
547
+ held.recalledMemoriesReport = create(RecalledMemoriesReportSchema, { selectionActive: true, injectedMemoryIds: [planted.memoryId] });
548
+ held.structuredOutput = planted.structuredOutput;
549
+ held.streamingUsage = create(StreamingUsageSummarySchema, { inputTokens: planted.inputTokens, totalTokens: planted.inputTokens });
550
+ return planted;
551
+ }
552
+
295
553
  // ── Arms: the stop controller's producers ───────────────────────────────────
296
554
 
297
555
  /** A hanging scenario with the never-seen text after the hang; the arms below stop it in three different ways. */
@@ -353,17 +611,42 @@ export async function assertWorkerShutdownPersistsAndThrows(harness: RuntimeCont
353
611
  return { driver, invocations: [invocation], final };
354
612
  }
355
613
 
614
+ /** How often the stall arm's clock advances once the engine is parked; real time between scripted windows. */
615
+ const STALL_CLOCK_INTERVAL_MS = 25;
616
+
356
617
  /**
357
- * A stall: the watchdog measures idle time on the scripted clock; one tick
358
- * past the window while the engine hangs fails the turn (RETURNED — a retry
359
- * would wedge again) with the watchdog's copy naming the idle seconds.
618
+ * A stall: the watchdog measures idle time on the scripted clock; while the
619
+ * engine hangs the clock KEEPS RUNNING a stall window per interval until
620
+ * the turn settles and the first window with no activity fails the turn
621
+ * (RETURNED — a retry would wedge again) with the watchdog's copy naming the
622
+ * idle seconds.
623
+ *
624
+ * Why the clock runs rather than ticks once: on a push-based engine the
625
+ * park is reported while the chunks the engine produced before parking are
626
+ * still in its event pipeline ahead of the adapter's loop, so activity marks
627
+ * land AFTER a single tick and a frozen clock reads idle 0 from then on
628
+ * (S3 M3 F-M3-5, measured on the native adapter: 35 marks at the park, 37
629
+ * within 10 ms). A pull-based double's park implies everything before it
630
+ * was seen; a real stall is wall time passing while the engine is silent,
631
+ * whichever shape the engine has. The interval advances a SCRIPTED clock —
632
+ * the kit's rule against timers is about stopping a turn on a guess, which
633
+ * this never does (Q-M3-7).
360
634
  */
361
635
  export async function assertStallFailsTheTurn(harness: RuntimeContractHarness, label = "rt-stall"): Promise<RuntimeArmResult> {
362
636
  const { subject, clock } = harness;
363
637
  const driver = new RuntimeExecutionDriver(harness, label);
364
- void subject.whenHanging().then(() => clock.tick(STALL_TIMEOUT_MS));
638
+ let running: NodeJS.Timeout | undefined;
639
+ void subject.whenHanging().then(() => {
640
+ clock.tick(STALL_TIMEOUT_MS);
641
+ running = setInterval(() => clock.tick(STALL_TIMEOUT_MS), STALL_CLOCK_INTERVAL_MS);
642
+ });
365
643
 
366
- const invocation = await driver.turn(hangingTurn("Running the suite."), { config: { cursorStreamStallTimeoutMs: STALL_TIMEOUT_MS } });
644
+ let invocation: ActivityInvocation;
645
+ try {
646
+ invocation = await driver.turn(hangingTurn("Running the suite."), { config: { cursorStreamStallTimeoutMs: STALL_TIMEOUT_MS } });
647
+ } finally {
648
+ if (running) clearInterval(running);
649
+ }
367
650
 
368
651
  expect(slimOf(subject, invocation).phase, `${subject.name}: a stall RETURNS FAILED`).toBe("EXECUTION_FAILED");
369
652
  expect(driver.record.persistedPhases.at(-1)).toBe(ExecutionPhase.EXECUTION_FAILED);
@@ -589,6 +872,21 @@ export interface RuntimeContractOptions {
589
872
  * pass). Every surface by default.
590
873
  */
591
874
  readonly failureSurfaces?: readonly FailureSurface[];
875
+ /**
876
+ * Whether this subject's engine can exhaust a tool-round budget and end a
877
+ * turn `tool_call_limit` (`scenario.limit()`); the arm is registered
878
+ * SKIPPED otherwise. On by default; the Cursor subject turns it off (its
879
+ * SDK has no such budget).
880
+ */
881
+ readonly toolCallLimit?: boolean;
882
+ /**
883
+ * Whether this subject's engine can end its OWN run cancelled with nothing
884
+ * to wait for (`scenario.cancelled()`, an SDK-side cancel); the arm is
885
+ * registered SKIPPED otherwise. On by default; the native subject turns it
886
+ * off (no LangGraph path ends a turn `cancelled` — a stop is the runtime's
887
+ * and settles `interrupted`; S3 M3 F-M3-2).
888
+ */
889
+ readonly engineCancel?: boolean;
592
890
  }
593
891
 
594
892
  const ALL_SURFACES: readonly FailureSurface[] = ["engine", "actionable", "internal"];
@@ -620,6 +918,22 @@ export function describeHarnessRuntimeContract(harness: RuntimeContractHarness,
620
918
  clock.reset();
621
919
  await assertApprovalRoundTrip(harness);
622
920
  });
921
+ it("a reinvocation carries every runner-owned collection and singleton the prior turn persisted; a first turn seeds nothing", async () => {
922
+ clock.reset();
923
+ await assertReinvocationCarriesThePersistedStatus(harness);
924
+ clock.reset();
925
+ await assertFirstTurnSeedsNothing(harness);
926
+ });
927
+ it("file review: two writes flow, the runtime captures and pauses, the reviewer keeps one and discards one, the resume reconciles and completes", async () => {
928
+ clock.reset();
929
+ await assertFileReviewRoundTrip(harness);
930
+ });
931
+ for (const action of [ApprovalAction.REJECT, ApprovalAction.SKIP] as const) {
932
+ it(`awaiting_approval then ${ApprovalAction[action]}: the run continues without the tool and the runtime settles the row SKIPPED`, async () => {
933
+ clock.reset();
934
+ await assertNonExecutingDecisionSettlesSkipped(harness, action);
935
+ });
936
+ }
623
937
  it("a user pause persists PAUSED with the pause row once and throws the pause CancelledFailure", async () => {
624
938
  clock.reset();
625
939
  await assertPausePersistsAndThrows(harness);
@@ -640,10 +954,14 @@ export function describeHarnessRuntimeContract(harness: RuntimeContractHarness,
640
954
  clock.reset();
641
955
  await assertPlatformStopCompletesEarly(harness);
642
956
  });
643
- it("an engine-cancelled run ends CANCELLED with no error and no copy", async () => {
957
+ it.skipIf(options.engineCancel === false)("an engine-cancelled run ends CANCELLED with no error and no copy", async () => {
644
958
  clock.reset();
645
959
  await assertEngineCancelledEndsCancelled(harness);
646
960
  });
961
+ it.skipIf(options.toolCallLimit === false)("a tool-call limit terminates the turn with the cross-repo prefix and the user copy, persisted and returned", async () => {
962
+ clock.reset();
963
+ await assertToolCallLimitTerminates(harness);
964
+ });
647
965
  for (const surface of ALL_SURFACES) {
648
966
  it.skipIf(!surfaces.includes(surface))(`a failed turn on the ${surface} surface writes that surface's copy and returns`, async () => {
649
967
  clock.reset();
@@ -33,9 +33,10 @@
33
33
  * job: carry a REJECTed or SKIPped row to its terminal status. The decision
34
34
  * is on the row (the server's field) and the transition follows from it with
35
35
  * no engine knowledge, so it belongs to the runtime — one writer per field,
36
- * as `approvalDecisionsOf` is the runtime's one reader (S2 M4, Q-M4-1; the
37
- * runtime's arm lands in S3). An adapter's whole duty for a non-executing
38
- * decision is to not execute.
36
+ * as `approvalDecisionsOf` is the runtime's one reader and
37
+ * `terminalizeNonExecutingDecisions` its one writer (S2 M4, Q-M4-1; landed at
38
+ * S3 M1, `harness/approval-decisions.ts`). An adapter's whole duty for a
39
+ * non-executing decision is to not execute.
39
40
  *
40
41
  * Scenarios are arranged PER SESSION (the engine is per session in every real
41
42
  * harness; a subject arranges that session's engine), and arranging replaces
@@ -45,6 +46,8 @@
45
46
  * `TurnOutcome`.
46
47
  */
47
48
 
49
+ import { mkdir, writeFile } from "node:fs/promises";
50
+ import { dirname, join } from "node:path";
48
51
  import { ApprovalAction, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
49
52
  import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
50
53
 
@@ -217,6 +220,7 @@ export class ScriptedHarnessAdapter implements HarnessAdapter {
217
220
  return undefined;
218
221
  }
219
222
  case "propose":
223
+ if (step.flows) return this.flowingWrite(step.toolCallId, step.action, input, sink);
220
224
  return this.propose(step.toolCallId, step.action, input, sink);
221
225
  case "read": {
222
226
  // Ungated: the row lands COMPLETED at once, the effect counts as run,
@@ -246,6 +250,8 @@ export class ScriptedHarnessAdapter implements HarnessAdapter {
246
250
  return { kind: "failed", surface: step.surface, message: step.message };
247
251
  case "cancelled":
248
252
  return { kind: "cancelled" };
253
+ case "limit":
254
+ return { kind: "tool_call_limit" };
249
255
  default: {
250
256
  const exhaustive: never = step;
251
257
  throw new Error(`${this.name}: unknown scenario step ${JSON.stringify(exhaustive)}`);
@@ -303,6 +309,29 @@ export class ScriptedHarnessAdapter implements HarnessAdapter {
303
309
  }
304
310
  }
305
311
 
312
+ /**
313
+ * A write under apply-then-review capture: no gate — the engine writes the
314
+ * bytes into the workspace and reports a COMPLETED `write` row, and the
315
+ * RUNTIME captures the change for review after the turn. The bytes are the
316
+ * fake's own (the tool-call id, one line); an already-settled row on a
317
+ * reinvocation is a no-op, as `propose`'s is.
318
+ */
319
+ private async flowingWrite(toolCallId: string, action: ProposedAction, input: TurnInput, sink: TurnSink): Promise<TurnOutcome | undefined> {
320
+ if (isSettled(findToolCallRow(sink.status, toolCallId))) return undefined;
321
+ const file = join(input.workspace.primaryDir, action.resource);
322
+ await mkdir(dirname(file), { recursive: true });
323
+ await writeFile(file, `${toolCallId}\n`);
324
+ const message = aiMessage("");
325
+ const row = toolCall(toolCallId, "write", ToolCallStatus.TOOL_CALL_COMPLETED);
326
+ row.args = { path: action.resource };
327
+ message.toolCalls.push(row);
328
+ sink.status.messages.push(message);
329
+ this.executions.set(toolCallId, this.executionCount(toolCallId) + 1);
330
+ sink.recordActivity("write");
331
+ await sink.requestPersist();
332
+ return undefined;
333
+ }
334
+
306
335
  /**
307
336
  * Report the executed action on its row, as a real engine's completion
308
337
  * event does. The row normally exists (the runtime seeded the status with
@@ -331,9 +360,12 @@ export interface ScriptedSubject extends HarnessContractSubject {
331
360
  }
332
361
 
333
362
  /**
334
- * The fake fills the `deep-agent` row in S2 the one row the runtime does
335
- * not yet serve for real (the native adapter lands in S3, and takes the row
336
- * over from the fake in the runtime half then).
363
+ * The fake declares the `deep-agent` row: in S2 the runtime served that row
364
+ * through this fake alone; since S3 M3 the real native adapter takes it over
365
+ * in the runtime half
366
+ * (`execute-deep-agent/__test-utils__/contract-subject.ts`), and the fake
367
+ * keeps declaring it so the kit's self-check runs a full subject under both
368
+ * pause primitives without an engine.
337
369
  */
338
370
  export function scriptedSubject(options: ScriptedHarnessOptions): ScriptedSubject {
339
371
  const adapter = new ScriptedHarnessAdapter(options);