humanish 0.104.3 → 0.106.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1407) hide show
  1. package/AGENTS.md +89 -94
  2. package/ARCHITECTURE.md +146 -0
  3. package/CHANGELOG.md +502 -0
  4. package/CONTEXT.md +51 -0
  5. package/CONTRIBUTING.md +111 -9
  6. package/README.md +213 -124
  7. package/SECURITY.md +1 -1
  8. package/TELEMETRY.md +112 -0
  9. package/dist/actors/affordance.d.ts +56 -0
  10. package/dist/actors/affordance.js +155 -0
  11. package/dist/actors/agent-session.js +42 -0
  12. package/dist/actors/codex/app-server-actor-trace.d.ts +11 -0
  13. package/dist/actors/codex/app-server-actor-trace.js +211 -0
  14. package/dist/actors/codex/app-server-client.d.ts +39 -0
  15. package/dist/actors/codex/app-server-client.js +116 -0
  16. package/dist/actors/codex/app-server-trace.d.ts +154 -0
  17. package/dist/actors/codex/app-server-trace.js +424 -0
  18. package/dist/actors/codex/app-server-ui.d.ts +40 -0
  19. package/dist/actors/codex/app-server-ui.js +328 -0
  20. package/dist/actors/codex/app-server.d.ts +44 -0
  21. package/dist/actors/codex/app-server.js +268 -0
  22. package/dist/actors/codex/qualified-versions.d.ts +45 -0
  23. package/dist/actors/codex/qualified-versions.js +87 -0
  24. package/dist/actors/codex/restricted-executable.d.ts +17 -0
  25. package/dist/actors/codex/restricted-executable.js +147 -0
  26. package/dist/actors/codex/restricted-launch.d.ts +116 -0
  27. package/dist/actors/codex/restricted-launch.js +222 -0
  28. package/dist/actors/codex/restricted-participant-policy.d.ts +46 -0
  29. package/dist/actors/codex/restricted-participant-policy.js +82 -0
  30. package/dist/actors/codex/restricted-participant.d.ts +20 -0
  31. package/dist/actors/codex/restricted-participant.js +432 -0
  32. package/dist/actors/codex/restricted-policy.d.ts +70 -0
  33. package/dist/actors/codex/restricted-policy.js +326 -0
  34. package/dist/actors/codex/restricted-session.d.ts +65 -0
  35. package/dist/actors/codex/restricted-session.js +432 -0
  36. package/dist/actors/codex/restricted-transport.d.ts +73 -0
  37. package/dist/actors/codex/restricted-transport.js +335 -0
  38. package/dist/actors/codex/restricted-turn.d.ts +62 -0
  39. package/dist/actors/codex/restricted-turn.js +284 -0
  40. package/dist/actors/computer-use/actor.d.ts +77 -0
  41. package/dist/actors/computer-use/actor.js +78 -0
  42. package/dist/actors/computer-use/admission-limit.d.ts +12 -0
  43. package/dist/actors/computer-use/admission-limit.js +19 -0
  44. package/dist/actors/computer-use/executor-error.js +48 -0
  45. package/dist/actors/computer-use/loop/actions.d.ts +26 -0
  46. package/dist/actors/computer-use/loop/actions.js +245 -0
  47. package/dist/actors/computer-use/loop/backstop.d.ts +42 -0
  48. package/dist/actors/computer-use/loop/backstop.js +202 -0
  49. package/dist/actors/computer-use/loop/debrief.d.ts +19 -0
  50. package/dist/actors/computer-use/loop/debrief.js +164 -0
  51. package/dist/actors/computer-use/loop/ending.d.ts +86 -0
  52. package/dist/actors/computer-use/loop/ending.js +316 -0
  53. package/dist/actors/computer-use/loop/observation.d.ts +55 -0
  54. package/dist/actors/computer-use/loop/observation.js +246 -0
  55. package/dist/actors/computer-use/loop/output-limit.d.ts +11 -0
  56. package/dist/actors/computer-use/loop/output-limit.js +21 -0
  57. package/dist/actors/computer-use/loop/provider-call.d.ts +19 -0
  58. package/dist/actors/computer-use/loop/provider-call.js +208 -0
  59. package/dist/actors/computer-use/loop/race.d.ts +34 -0
  60. package/dist/actors/computer-use/loop/race.js +79 -0
  61. package/dist/actors/computer-use/loop/session.d.ts +77 -0
  62. package/dist/actors/computer-use/loop/session.js +135 -0
  63. package/dist/actors/computer-use/loop/spend.d.ts +30 -0
  64. package/dist/actors/computer-use/loop/spend.js +85 -0
  65. package/dist/actors/computer-use/loop/trace.d.ts +35 -0
  66. package/dist/actors/computer-use/loop/trace.js +140 -0
  67. package/dist/actors/computer-use/loop/types.d.ts +404 -0
  68. package/dist/actors/computer-use/loop/types.js +1 -0
  69. package/dist/actors/computer-use/loop/usage.d.ts +102 -0
  70. package/dist/actors/computer-use/loop/usage.js +355 -0
  71. package/dist/actors/computer-use/loop.d.ts +11 -0
  72. package/dist/actors/computer-use/loop.js +248 -0
  73. package/dist/actors/computer-use/openai-provider.d.ts +107 -0
  74. package/dist/actors/computer-use/openai-provider.js +472 -0
  75. package/dist/actors/computer-use/openai-requests.d.ts +35 -0
  76. package/dist/actors/computer-use/openai-requests.js +74 -0
  77. package/dist/actors/computer-use/openai-wire.d.ts +67 -0
  78. package/dist/actors/computer-use/openai-wire.js +365 -0
  79. package/dist/actors/computer-use/provider-error.d.ts +27 -0
  80. package/dist/actors/computer-use/provider-error.js +86 -0
  81. package/dist/actors/computer-use/speech.js +6 -0
  82. package/dist/actors/contract.d.ts +315 -0
  83. package/dist/actors/contract.js +201 -0
  84. package/dist/actors/goal-source.js +38 -0
  85. package/dist/actors/local-agent/claude-session.d.ts +43 -0
  86. package/dist/actors/local-agent/claude-session.js +370 -0
  87. package/dist/actors/local-agent/cli.d.ts +115 -0
  88. package/dist/actors/local-agent/cli.js +413 -0
  89. package/dist/actors/local-agent/readiness.d.ts +31 -0
  90. package/dist/actors/local-agent/readiness.js +45 -0
  91. package/dist/actors/openai-endpoint.d.ts +2 -0
  92. package/dist/actors/openai-endpoint.js +2 -0
  93. package/dist/actors/output-token-limit.js +4 -0
  94. package/dist/actors/reasoning-effort.js +33 -0
  95. package/dist/actors/registry.d.ts +59 -0
  96. package/dist/actors/registry.js +89 -0
  97. package/dist/actors/scripted-browser/actor.d.ts +49 -0
  98. package/dist/actors/scripted-browser/actor.js +428 -0
  99. package/dist/actors/scripted-browser/browser-command.d.ts +1 -0
  100. package/dist/actors/scripted-browser/browser-command.js +48 -0
  101. package/dist/actors/scripted-browser/journey.d.ts +9 -0
  102. package/dist/actors/scripted-browser/journey.js +171 -0
  103. package/dist/actors/scripted-browser/steps.d.ts +60 -0
  104. package/dist/actors/scripted-browser/steps.js +384 -0
  105. package/dist/actors/scripted-browser/types.d.ts +135 -0
  106. package/dist/actors/scripted-browser/types.js +26 -0
  107. package/dist/actors/stop-cause.d.ts +9 -0
  108. package/dist/actors/stop-cause.js +42 -0
  109. package/dist/actors/stop-conditions.d.ts +40 -0
  110. package/dist/actors/stop-conditions.js +65 -0
  111. package/dist/analysis/automatic-completion.d.ts +38 -0
  112. package/dist/analysis/automatic-completion.js +90 -0
  113. package/dist/analysis/automatic-config.d.ts +41 -0
  114. package/dist/analysis/automatic-config.js +135 -0
  115. package/dist/analysis/automatic.d.ts +17 -0
  116. package/dist/analysis/automatic.js +279 -0
  117. package/dist/analysis/codex-config.d.ts +8 -0
  118. package/dist/analysis/codex-config.js +69 -0
  119. package/dist/analysis/evidence-sources.d.ts +23 -0
  120. package/dist/analysis/evidence-sources.js +251 -0
  121. package/dist/analysis/evidence.d.ts +18 -0
  122. package/dist/analysis/evidence.js +482 -0
  123. package/dist/analysis/execute.d.ts +52 -0
  124. package/dist/analysis/execute.js +504 -0
  125. package/dist/analysis/image-tokens.d.ts +1 -0
  126. package/dist/analysis/image-tokens.js +41 -0
  127. package/dist/analysis/job.d.ts +97 -0
  128. package/dist/analysis/job.js +311 -0
  129. package/dist/analysis/load.d.ts +3 -0
  130. package/dist/analysis/load.js +11 -0
  131. package/dist/analysis/provider.d.ts +57 -0
  132. package/dist/analysis/provider.js +227 -0
  133. package/dist/analysis/restricted-codex.d.ts +19 -0
  134. package/dist/analysis/restricted-codex.js +23 -0
  135. package/dist/analysis/service.d.ts +72 -0
  136. package/dist/analysis/service.js +399 -0
  137. package/dist/analysis/sharing.d.ts +11 -0
  138. package/dist/analysis/sharing.js +44 -0
  139. package/dist/analysis/store-executions.d.ts +27 -0
  140. package/dist/analysis/store-executions.js +224 -0
  141. package/dist/analysis/store.d.ts +31 -0
  142. package/dist/analysis/store.js +315 -0
  143. package/dist/analysis/types.d.ts +217 -0
  144. package/dist/analysis/types.js +12 -0
  145. package/dist/analysis/validation.d.ts +225 -0
  146. package/dist/analysis/validation.js +562 -0
  147. package/dist/browser-control/client.d.ts +18 -0
  148. package/dist/browser-control/client.js +239 -0
  149. package/dist/browser-control/dispatcher.d.ts +19 -0
  150. package/dist/browser-control/dispatcher.js +221 -0
  151. package/dist/browser-control/protocol.d.ts +492 -0
  152. package/dist/browser-control/protocol.js +360 -0
  153. package/dist/browser-control/recording-transfer.d.ts +3 -0
  154. package/dist/browser-control/recording-transfer.js +48 -0
  155. package/dist/browser-control/transport.d.ts +27 -0
  156. package/dist/browser-control/transport.js +189 -0
  157. package/dist/cli/commands/analysis-signals.d.ts +7 -0
  158. package/dist/cli/commands/analysis-signals.js +25 -0
  159. package/dist/cli/commands/analyze.d.ts +3 -0
  160. package/dist/cli/commands/analyze.js +212 -0
  161. package/dist/cli/commands/codex.d.ts +3 -0
  162. package/dist/cli/commands/codex.js +145 -0
  163. package/dist/cli/commands/comms.d.ts +3 -0
  164. package/dist/cli/commands/comms.js +225 -0
  165. package/dist/cli/commands/feedback.d.ts +3 -0
  166. package/dist/cli/commands/feedback.js +138 -0
  167. package/dist/cli/commands/lab-format.d.ts +8 -0
  168. package/dist/cli/commands/lab-format.js +105 -0
  169. package/dist/cli/commands/lab-route-computer-use.d.ts +20 -0
  170. package/dist/cli/commands/lab-route-computer-use.js +251 -0
  171. package/dist/cli/commands/lab-route-open.d.ts +27 -0
  172. package/dist/cli/commands/lab-route-open.js +37 -0
  173. package/dist/cli/commands/lab-route-preview.d.ts +20 -0
  174. package/dist/cli/commands/lab-route-preview.js +74 -0
  175. package/dist/cli/commands/lab-route-run.d.ts +17 -0
  176. package/dist/cli/commands/lab-route-run.js +31 -0
  177. package/dist/cli/commands/lab-route-scripted.d.ts +19 -0
  178. package/dist/cli/commands/lab-route-scripted.js +50 -0
  179. package/dist/cli/commands/lab-route-shared-world.d.ts +20 -0
  180. package/dist/cli/commands/lab-route-shared-world.js +135 -0
  181. package/dist/cli/commands/lab-route-terminal.d.ts +19 -0
  182. package/dist/cli/commands/lab-route-terminal.js +49 -0
  183. package/dist/cli/commands/lab-run.d.ts +9 -0
  184. package/dist/cli/commands/lab-run.js +166 -0
  185. package/dist/cli/commands/lab-scorer.d.ts +30 -0
  186. package/dist/cli/commands/lab-scorer.js +17 -0
  187. package/dist/cli/commands/lab.d.ts +3 -0
  188. package/dist/cli/commands/lab.js +193 -0
  189. package/dist/cli/commands/observe.d.ts +4 -0
  190. package/dist/cli/commands/observe.js +345 -0
  191. package/dist/cli/commands/runs.d.ts +11 -0
  192. package/dist/cli/commands/runs.js +314 -0
  193. package/dist/cli/commands/setup.d.ts +11 -0
  194. package/dist/cli/commands/setup.js +325 -0
  195. package/dist/cli/commands/tui.d.ts +32 -0
  196. package/dist/cli/commands/tui.js +305 -0
  197. package/dist/cli/commands/watch.d.ts +3 -0
  198. package/dist/cli/commands/watch.js +245 -0
  199. package/dist/cli/doctor.d.ts +39 -0
  200. package/dist/cli/doctor.js +263 -0
  201. package/dist/cli/first-run-path.d.ts +67 -0
  202. package/dist/cli/first-run-path.js +150 -0
  203. package/dist/cli/io.d.ts +85 -0
  204. package/dist/cli/io.js +232 -0
  205. package/dist/cli/observer-follow.d.ts +56 -0
  206. package/dist/cli/observer-follow.js +160 -0
  207. package/dist/cli/orientation.d.ts +30 -0
  208. package/dist/cli/orientation.js +114 -0
  209. package/dist/cli/program.d.ts +15 -0
  210. package/dist/cli/program.js +338 -0
  211. package/dist/cli/secret-prompt.js +39 -0
  212. package/dist/cli/telemetry.js +351 -0
  213. package/dist/cli.js +2 -4
  214. package/dist/comms/agentmail.d.ts +29 -0
  215. package/dist/comms/agentmail.js +568 -0
  216. package/dist/comms/capture-surface.d.ts +54 -0
  217. package/dist/comms/capture-surface.js +468 -0
  218. package/dist/comms/catch-host.d.ts +52 -0
  219. package/dist/comms/catch-host.js +192 -0
  220. package/dist/comms/connections.d.ts +20 -0
  221. package/dist/comms/connections.js +116 -0
  222. package/dist/comms/email-catch.d.ts +49 -0
  223. package/dist/comms/email-catch.js +236 -0
  224. package/dist/comms/extract.d.ts +7 -0
  225. package/dist/comms/extract.js +65 -0
  226. package/dist/comms/fake-inbox.d.ts +39 -0
  227. package/dist/comms/fake-inbox.js +136 -0
  228. package/dist/comms/images.d.ts +7 -0
  229. package/dist/comms/images.js +53 -0
  230. package/dist/comms/lease-store.d.ts +91 -0
  231. package/dist/comms/lease-store.js +600 -0
  232. package/dist/comms/providers.d.ts +30 -0
  233. package/dist/comms/providers.js +24 -0
  234. package/dist/comms/receiving-common.d.ts +17 -0
  235. package/dist/comms/receiving-common.js +82 -0
  236. package/dist/comms/receiving-evidence.d.ts +4 -0
  237. package/dist/comms/receiving-evidence.js +59 -0
  238. package/dist/comms/receiving-recovery.d.ts +22 -0
  239. package/dist/comms/receiving-recovery.js +100 -0
  240. package/dist/comms/receiving-render.d.ts +16 -0
  241. package/dist/comms/receiving-render.js +409 -0
  242. package/dist/comms/receiving-runtime.d.ts +41 -0
  243. package/dist/comms/receiving-runtime.js +66 -0
  244. package/dist/comms/receiving-surface.d.ts +8 -0
  245. package/dist/comms/receiving-surface.js +156 -0
  246. package/dist/comms/receiving-types.d.ts +122 -0
  247. package/dist/comms/receiving-types.js +3 -0
  248. package/dist/comms/receiving.d.ts +33 -0
  249. package/dist/comms/receiving.js +508 -0
  250. package/dist/comms/sandbox-catch-script.d.ts +16 -0
  251. package/dist/comms/sandbox-catch-script.js +331 -0
  252. package/dist/comms/sandbox-catch.d.ts +214 -0
  253. package/dist/comms/sandbox-catch.js +451 -0
  254. package/dist/comms/setup.d.ts +65 -0
  255. package/dist/comms/setup.js +259 -0
  256. package/dist/comms/thread-evidence.d.ts +28 -0
  257. package/dist/comms/thread-evidence.js +30 -0
  258. package/dist/comms/types.d.ts +76 -0
  259. package/dist/comms/types.js +8 -0
  260. package/dist/deprecated.d.ts +90 -0
  261. package/dist/deprecated.js +106 -0
  262. package/dist/evidence/browser-hygiene.js +42 -0
  263. package/dist/evidence/desktop-recorder.d.ts +39 -0
  264. package/dist/evidence/desktop-recorder.js +340 -0
  265. package/dist/evidence/desktop-recording-artifact.d.ts +5 -0
  266. package/dist/evidence/desktop-recording-artifact.js +53 -0
  267. package/dist/evidence/desktop-recording-types.d.ts +27 -0
  268. package/dist/evidence/desktop-recording-types.js +16 -0
  269. package/dist/evidence/encoded-text.d.ts +27 -0
  270. package/dist/evidence/encoded-text.js +243 -0
  271. package/dist/evidence/frame-signature.d.ts +17 -0
  272. package/dist/evidence/frame-signature.js +111 -0
  273. package/dist/evidence/image.d.ts +24 -0
  274. package/dist/evidence/image.js +170 -0
  275. package/dist/evidence/plain-text.d.ts +8 -0
  276. package/dist/evidence/plain-text.js +16 -0
  277. package/dist/evidence/redaction.d.ts +110 -0
  278. package/dist/evidence/redaction.js +329 -0
  279. package/dist/feedback/draft.d.ts +58 -0
  280. package/dist/feedback/draft.js +309 -0
  281. package/dist/feedback/export-bundle.d.ts +6 -0
  282. package/dist/feedback/export-bundle.js +585 -0
  283. package/dist/feedback/export.d.ts +50 -0
  284. package/dist/feedback/export.js +392 -0
  285. package/dist/feedback/feedback.d.ts +37 -0
  286. package/dist/feedback/feedback.js +282 -0
  287. package/dist/feedback/proof.d.ts +8 -0
  288. package/dist/feedback/proof.js +56 -0
  289. package/dist/guest/bootstrap.d.ts +55 -0
  290. package/dist/guest/bootstrap.js +348 -0
  291. package/dist/guest/browser-tools.d.ts +8 -0
  292. package/dist/guest/browser-tools.js +72 -0
  293. package/dist/guest/chromium-text.d.ts +62 -0
  294. package/dist/guest/chromium-text.js +386 -0
  295. package/dist/guest/desktop-executor.d.ts +57 -0
  296. package/dist/guest/desktop-executor.js +256 -0
  297. package/dist/guest/desktop-keys.d.ts +8 -0
  298. package/dist/guest/desktop-keys.js +89 -0
  299. package/dist/guest/desktop-media.d.ts +70 -0
  300. package/dist/guest/desktop-media.js +385 -0
  301. package/dist/guest/desktop-native.d.ts +14 -0
  302. package/dist/guest/desktop-native.js +156 -0
  303. package/dist/guest/media-config.d.ts +15 -0
  304. package/dist/guest/media-config.js +16 -0
  305. package/dist/guest/runtime-desktop.d.ts +45 -0
  306. package/dist/guest/runtime-desktop.js +420 -0
  307. package/dist/guest/runtime-processes.d.ts +28 -0
  308. package/dist/guest/runtime-processes.js +94 -0
  309. package/dist/guest/runtime.d.ts +31 -0
  310. package/dist/guest/runtime.js +124 -0
  311. package/dist/guest-media-worker.js +184 -48
  312. package/dist/guest-runtime-main.js +23 -10
  313. package/dist/guest-runtime-revision.js +1 -1
  314. package/dist/index.d.ts +32 -83
  315. package/dist/index.js +21 -42
  316. package/dist/keys/env-file.d.ts +15 -0
  317. package/dist/keys/env-file.js +109 -0
  318. package/dist/keys/key-resolution.d.ts +58 -0
  319. package/dist/keys/key-resolution.js +389 -0
  320. package/dist/lab/adapter-extension.d.ts +75 -0
  321. package/dist/lab/adapter-extension.js +253 -0
  322. package/dist/lab/adapter-scorer-loader.d.ts +43 -0
  323. package/dist/lab/adapter-scorer-loader.js +130 -0
  324. package/dist/lab/bag-overrides.d.ts +18 -0
  325. package/dist/lab/bag-overrides.js +121 -0
  326. package/dist/lab/composition-rules.d.ts +5 -0
  327. package/dist/lab/composition-rules.js +283 -0
  328. package/dist/lab/config.d.ts +7 -0
  329. package/dist/lab/config.js +208 -0
  330. package/dist/lab/device-presets.d.ts +97 -0
  331. package/dist/lab/device-presets.js +87 -0
  332. package/dist/lab/discover.d.ts +65 -0
  333. package/dist/lab/discover.js +422 -0
  334. package/dist/lab/doctor.d.ts +39 -0
  335. package/dist/lab/doctor.js +233 -0
  336. package/dist/lab/init-paths.d.ts +5 -0
  337. package/dist/lab/init-paths.js +89 -0
  338. package/dist/lab/init-plan.d.ts +20 -0
  339. package/dist/lab/init-plan.js +163 -0
  340. package/dist/lab/init-templates.d.ts +28 -0
  341. package/dist/lab/init-templates.js +416 -0
  342. package/dist/lab/init.d.ts +39 -0
  343. package/dist/lab/init.js +287 -0
  344. package/dist/lab/keys.d.ts +2 -0
  345. package/dist/lab/keys.js +264 -0
  346. package/dist/lab/parse/actors.d.ts +14 -0
  347. package/dist/lab/parse/actors.js +453 -0
  348. package/dist/lab/parse/comms.d.ts +12 -0
  349. package/dist/lab/parse/comms.js +246 -0
  350. package/dist/lab/parse/execution.d.ts +19 -0
  351. package/dist/lab/parse/execution.js +356 -0
  352. package/dist/lab/parse/subject-state.d.ts +30 -0
  353. package/dist/lab/parse/subject-state.js +179 -0
  354. package/dist/lab/parse/subject.d.ts +9 -0
  355. package/dist/lab/parse/subject.js +420 -0
  356. package/dist/lab/parse/values.d.ts +17 -0
  357. package/dist/lab/parse/values.js +65 -0
  358. package/dist/lab/participant-assignment.d.ts +12 -0
  359. package/dist/lab/participant-assignment.js +19 -0
  360. package/dist/lab/persona-resolve.d.ts +39 -0
  361. package/dist/lab/persona-resolve.js +118 -0
  362. package/dist/lab/persona.d.ts +39 -0
  363. package/dist/lab/persona.js +192 -0
  364. package/dist/lab/plan-base.d.ts +24 -0
  365. package/dist/lab/plan-base.js +113 -0
  366. package/dist/lab/plan-participants.d.ts +81 -0
  367. package/dist/lab/plan-participants.js +92 -0
  368. package/dist/lab/plan-types.d.ts +269 -0
  369. package/dist/lab/plan-types.js +5 -0
  370. package/dist/lab/plan.d.ts +27 -0
  371. package/dist/lab/plan.js +156 -0
  372. package/dist/lab/preflight-probes.d.ts +3 -0
  373. package/dist/lab/preflight-probes.js +312 -0
  374. package/dist/lab/preflight-result.d.ts +7 -0
  375. package/dist/lab/preflight-result.js +43 -0
  376. package/dist/lab/preflight.d.ts +95 -0
  377. package/dist/lab/preflight.js +117 -0
  378. package/dist/lab/route-inputs.d.ts +17 -0
  379. package/dist/lab/route-inputs.js +95 -0
  380. package/dist/lab/routing.d.ts +81 -0
  381. package/dist/lab/routing.js +214 -0
  382. package/dist/lab/run-lab-events.d.ts +69 -0
  383. package/dist/lab/run-lab-events.js +95 -0
  384. package/dist/lab/run-lab-options.d.ts +82 -0
  385. package/dist/lab/run-lab-options.js +399 -0
  386. package/dist/lab/summary.d.ts +60 -0
  387. package/dist/lab/summary.js +168 -0
  388. package/dist/lab/tasks.d.ts +91 -0
  389. package/dist/lab/tasks.js +139 -0
  390. package/dist/lab/types.d.ts +813 -0
  391. package/dist/lab/types.js +6 -0
  392. package/dist/lab/validation.d.ts +67 -0
  393. package/dist/lab/validation.js +387 -0
  394. package/dist/lab/warnings.d.ts +2 -0
  395. package/dist/lab/warnings.js +338 -0
  396. package/dist/observer/artifact.d.ts +23 -0
  397. package/dist/observer/artifact.js +235 -0
  398. package/dist/observer/data.d.ts +115 -0
  399. package/dist/observer/data.js +258 -0
  400. package/dist/observer/exposure.d.ts +64 -0
  401. package/dist/observer/exposure.js +143 -0
  402. package/dist/observer/http.js +50 -0
  403. package/dist/observer/library.js +190 -0
  404. package/dist/observer/listen.js +85 -0
  405. package/dist/observer/live.d.ts +9 -0
  406. package/dist/observer/live.js +25 -0
  407. package/dist/observer/pinned-files.d.ts +30 -0
  408. package/dist/observer/pinned-files.js +160 -0
  409. package/dist/observer/render.d.ts +69 -0
  410. package/dist/observer/render.js +350 -0
  411. package/dist/observer/run-inventory.d.ts +35 -0
  412. package/dist/observer/run-inventory.js +95 -0
  413. package/dist/observer/run-routes.d.ts +37 -0
  414. package/dist/observer/run-routes.js +373 -0
  415. package/dist/observer/serve.d.ts +97 -0
  416. package/dist/observer/serve.js +305 -0
  417. package/dist/observer/static.js +369 -0
  418. package/dist/observer/tunnel.js +112 -0
  419. package/dist/observer-app.html +9 -20
  420. package/dist/routes/computer-use/bundle-parts.d.ts +50 -0
  421. package/dist/routes/computer-use/bundle-parts.js +177 -0
  422. package/dist/routes/computer-use/bundle.d.ts +56 -0
  423. package/dist/routes/computer-use/bundle.js +184 -0
  424. package/dist/routes/computer-use/diagnostics.d.ts +38 -0
  425. package/dist/routes/computer-use/diagnostics.js +94 -0
  426. package/dist/routes/computer-use/e2b-desktop/comms.d.ts +65 -0
  427. package/dist/routes/computer-use/e2b-desktop/comms.js +225 -0
  428. package/dist/routes/computer-use/e2b-desktop/desktop.d.ts +3 -0
  429. package/dist/routes/computer-use/e2b-desktop/desktop.js +91 -0
  430. package/dist/routes/computer-use/e2b-desktop/fidelity.d.ts +65 -0
  431. package/dist/routes/computer-use/e2b-desktop/fidelity.js +156 -0
  432. package/dist/routes/computer-use/e2b-desktop/prepare.d.ts +18 -0
  433. package/dist/routes/computer-use/e2b-desktop/prepare.js +198 -0
  434. package/dist/routes/computer-use/e2b-desktop/start.d.ts +14 -0
  435. package/dist/routes/computer-use/e2b-desktop/start.js +193 -0
  436. package/dist/routes/computer-use/e2b-desktop/state.d.ts +65 -0
  437. package/dist/routes/computer-use/e2b-desktop/state.js +69 -0
  438. package/dist/routes/computer-use/e2b-desktop/teardown.d.ts +6 -0
  439. package/dist/routes/computer-use/e2b-desktop/teardown.js +152 -0
  440. package/dist/routes/computer-use/external-comms.d.ts +20 -0
  441. package/dist/routes/computer-use/external-comms.js +55 -0
  442. package/dist/routes/computer-use/fanout-bundle.d.ts +11 -0
  443. package/dist/routes/computer-use/fanout-bundle.js +274 -0
  444. package/dist/routes/computer-use/fanout-lanes.d.ts +15 -0
  445. package/dist/routes/computer-use/fanout-lanes.js +302 -0
  446. package/dist/routes/computer-use/in-process-desktop.d.ts +17 -0
  447. package/dist/routes/computer-use/in-process-desktop.js +40 -0
  448. package/dist/routes/computer-use/lanes.d.ts +35 -0
  449. package/dist/routes/computer-use/lanes.js +321 -0
  450. package/dist/routes/computer-use/legacy-lane-spec.d.ts +62 -0
  451. package/dist/routes/computer-use/legacy-lane-spec.js +39 -0
  452. package/dist/routes/computer-use/live-flush.d.ts +42 -0
  453. package/dist/routes/computer-use/live-flush.js +154 -0
  454. package/dist/routes/computer-use/local-tree-pack.d.ts +27 -0
  455. package/dist/routes/computer-use/local-tree-pack.js +47 -0
  456. package/dist/routes/computer-use/local-vm.d.ts +20 -0
  457. package/dist/routes/computer-use/local-vm.js +218 -0
  458. package/dist/routes/computer-use/participant-desktop.d.ts +63 -0
  459. package/dist/routes/computer-use/participant-desktop.js +33 -0
  460. package/dist/routes/computer-use/participant-facts.d.ts +6 -0
  461. package/dist/routes/computer-use/participant-facts.js +22 -0
  462. package/dist/routes/computer-use/participant-model.d.ts +30 -0
  463. package/dist/routes/computer-use/participant-model.js +222 -0
  464. package/dist/routes/computer-use/participant-prompt.d.ts +53 -0
  465. package/dist/routes/computer-use/participant-prompt.js +87 -0
  466. package/dist/routes/computer-use/participant-runs.d.ts +67 -0
  467. package/dist/routes/computer-use/participant-runs.js +255 -0
  468. package/dist/routes/computer-use/plan.d.ts +56 -0
  469. package/dist/routes/computer-use/plan.js +308 -0
  470. package/dist/routes/computer-use/preflight.d.ts +26 -0
  471. package/dist/routes/computer-use/preflight.js +85 -0
  472. package/dist/routes/computer-use/rerun-selection.d.ts +19 -0
  473. package/dist/routes/computer-use/rerun-selection.js +121 -0
  474. package/dist/routes/computer-use/result.d.ts +7 -0
  475. package/dist/routes/computer-use/result.js +292 -0
  476. package/dist/routes/computer-use/route.d.ts +33 -0
  477. package/dist/routes/computer-use/route.js +145 -0
  478. package/dist/routes/computer-use/run-lanes.d.ts +59 -0
  479. package/dist/routes/computer-use/run-lanes.js +135 -0
  480. package/dist/routes/computer-use/self-report.d.ts +21 -0
  481. package/dist/routes/computer-use/self-report.js +231 -0
  482. package/dist/routes/computer-use/setup.d.ts +136 -0
  483. package/dist/routes/computer-use/setup.js +371 -0
  484. package/dist/routes/computer-use/single-bundle.d.ts +97 -0
  485. package/dist/routes/computer-use/single-bundle.js +393 -0
  486. package/dist/routes/computer-use/subject-projection.d.ts +51 -0
  487. package/dist/routes/computer-use/subject-projection.js +159 -0
  488. package/dist/routes/computer-use/types.d.ts +539 -0
  489. package/dist/routes/computer-use/types.js +37 -0
  490. package/dist/routes/preview.d.ts +13 -0
  491. package/dist/routes/preview.js +36 -0
  492. package/dist/routes/scripted/bundle.d.ts +47 -0
  493. package/dist/routes/scripted/bundle.js +195 -0
  494. package/dist/routes/scripted/plan.d.ts +30 -0
  495. package/dist/routes/scripted/plan.js +95 -0
  496. package/dist/routes/scripted/result.d.ts +48 -0
  497. package/dist/routes/scripted/result.js +176 -0
  498. package/dist/routes/scripted/route.d.ts +28 -0
  499. package/dist/routes/scripted/route.js +218 -0
  500. package/dist/routes/scripted/scenario.d.ts +21 -0
  501. package/dist/routes/scripted/scenario.js +102 -0
  502. package/dist/routes/scripted/session-result.d.ts +8 -0
  503. package/dist/routes/scripted/session-result.js +60 -0
  504. package/dist/routes/scripted/setup.d.ts +45 -0
  505. package/dist/routes/scripted/setup.js +126 -0
  506. package/dist/routes/scripted/subject.d.ts +49 -0
  507. package/dist/routes/scripted/subject.js +156 -0
  508. package/dist/routes/scripted/surface-records.d.ts +26 -0
  509. package/dist/routes/scripted/surface-records.js +88 -0
  510. package/dist/routes/scripted/surface-sessions.d.ts +13 -0
  511. package/dist/routes/scripted/surface-sessions.js +40 -0
  512. package/dist/routes/scripted/types.d.ts +88 -0
  513. package/dist/routes/scripted/types.js +3 -0
  514. package/dist/routes/shared-world/bundle.d.ts +20 -0
  515. package/dist/routes/shared-world/bundle.js +446 -0
  516. package/dist/routes/shared-world/checkpoints.d.ts +25 -0
  517. package/dist/routes/shared-world/checkpoints.js +46 -0
  518. package/dist/routes/shared-world/comms.d.ts +68 -0
  519. package/dist/routes/shared-world/comms.js +176 -0
  520. package/dist/routes/shared-world/external-public.d.ts +21 -0
  521. package/dist/routes/shared-world/external-public.js +130 -0
  522. package/dist/routes/shared-world/handoff.d.ts +62 -0
  523. package/dist/routes/shared-world/handoff.js +348 -0
  524. package/dist/routes/shared-world/lobby-code.d.ts +25 -0
  525. package/dist/routes/shared-world/lobby-code.js +152 -0
  526. package/dist/routes/shared-world/plan.d.ts +27 -0
  527. package/dist/routes/shared-world/plan.js +174 -0
  528. package/dist/routes/shared-world/provenance.d.ts +27 -0
  529. package/dist/routes/shared-world/provenance.js +65 -0
  530. package/dist/routes/shared-world/provisioned.d.ts +57 -0
  531. package/dist/routes/shared-world/provisioned.js +505 -0
  532. package/dist/routes/shared-world/result.d.ts +18 -0
  533. package/dist/routes/shared-world/result.js +327 -0
  534. package/dist/routes/shared-world/route.d.ts +21 -0
  535. package/dist/routes/shared-world/route.js +277 -0
  536. package/dist/routes/shared-world/seat-records.d.ts +20 -0
  537. package/dist/routes/shared-world/seat-records.js +213 -0
  538. package/dist/routes/shared-world/seats.d.ts +38 -0
  539. package/dist/routes/shared-world/seats.js +208 -0
  540. package/dist/routes/shared-world/setup.d.ts +48 -0
  541. package/dist/routes/shared-world/setup.js +200 -0
  542. package/dist/routes/shared-world/types.d.ts +354 -0
  543. package/dist/routes/shared-world/types.js +29 -0
  544. package/dist/routes/terminal/adapter.d.ts +37 -0
  545. package/dist/routes/terminal/adapter.js +201 -0
  546. package/dist/routes/terminal/bundle.d.ts +79 -0
  547. package/dist/routes/terminal/bundle.js +305 -0
  548. package/dist/routes/terminal/credentials.d.ts +34 -0
  549. package/dist/routes/terminal/credentials.js +102 -0
  550. package/dist/routes/terminal/dry-run.d.ts +24 -0
  551. package/dist/routes/terminal/dry-run.js +157 -0
  552. package/dist/routes/terminal/encoding.js +79 -0
  553. package/dist/routes/terminal/ledger.d.ts +49 -0
  554. package/dist/routes/terminal/ledger.js +170 -0
  555. package/dist/routes/terminal/lifetime.d.ts +10 -0
  556. package/dist/routes/terminal/lifetime.js +22 -0
  557. package/dist/routes/terminal/live-finish.d.ts +31 -0
  558. package/dist/routes/terminal/live-finish.js +215 -0
  559. package/dist/routes/terminal/live-sandbox.d.ts +69 -0
  560. package/dist/routes/terminal/live-sandbox.js +452 -0
  561. package/dist/routes/terminal/persona.d.ts +20 -0
  562. package/dist/routes/terminal/persona.js +34 -0
  563. package/dist/routes/terminal/plan.d.ts +21 -0
  564. package/dist/routes/terminal/plan.js +115 -0
  565. package/dist/routes/terminal/recorder.d.ts +23 -0
  566. package/dist/routes/terminal/recorder.js +77 -0
  567. package/dist/routes/terminal/result.d.ts +47 -0
  568. package/dist/routes/terminal/result.js +114 -0
  569. package/dist/routes/terminal/route.d.ts +23 -0
  570. package/dist/routes/terminal/route.js +170 -0
  571. package/dist/routes/terminal/runtime-auth.d.ts +12 -0
  572. package/dist/routes/terminal/runtime-auth.js +23 -0
  573. package/dist/routes/terminal/runtime.d.ts +14 -0
  574. package/dist/routes/terminal/runtime.js +47 -0
  575. package/dist/routes/terminal/sandbox.d.ts +35 -0
  576. package/dist/routes/terminal/sandbox.js +126 -0
  577. package/dist/routes/terminal/session.d.ts +25 -0
  578. package/dist/routes/terminal/session.js +199 -0
  579. package/dist/routes/terminal/token-usage.d.ts +15 -0
  580. package/dist/routes/terminal/token-usage.js +93 -0
  581. package/dist/routes/terminal/trace.d.ts +40 -0
  582. package/dist/routes/terminal/trace.js +172 -0
  583. package/dist/routes/terminal/types.d.ts +342 -0
  584. package/dist/routes/terminal/types.js +31 -0
  585. package/dist/run/bundle-shape.d.ts +7 -0
  586. package/dist/run/bundle-shape.js +369 -0
  587. package/dist/run/bundle.d.ts +547 -0
  588. package/dist/run/bundle.js +44 -0
  589. package/dist/run/concurrency.js +24 -0
  590. package/dist/run/contained-output.d.ts +50 -0
  591. package/dist/run/contained-output.js +351 -0
  592. package/dist/run/cost-summary.d.ts +28 -0
  593. package/dist/run/cost-summary.js +213 -0
  594. package/dist/run/costs.js +140 -0
  595. package/dist/run/detail.d.ts +62 -0
  596. package/dist/run/detail.js +139 -0
  597. package/dist/run/dry-run-inputs.d.ts +9 -0
  598. package/dist/run/dry-run-inputs.js +90 -0
  599. package/dist/run/dry-run.d.ts +6 -0
  600. package/dist/run/dry-run.js +362 -0
  601. package/dist/run/feedback-shape.d.ts +7 -0
  602. package/dist/run/feedback-shape.js +89 -0
  603. package/dist/run/git-state.d.ts +35 -0
  604. package/dist/run/git-state.js +316 -0
  605. package/dist/run/git-workspace.d.ts +32 -0
  606. package/dist/run/git-workspace.js +450 -0
  607. package/dist/run/judge.d.ts +210 -0
  608. package/dist/run/judge.js +240 -0
  609. package/dist/run/locate.d.ts +22 -0
  610. package/dist/run/locate.js +122 -0
  611. package/dist/run/outcomes.d.ts +31 -0
  612. package/dist/run/outcomes.js +210 -0
  613. package/dist/run/participant-records.d.ts +28 -0
  614. package/dist/run/participant-records.js +63 -0
  615. package/dist/run/participant.d.ts +29 -0
  616. package/dist/run/participant.js +12 -0
  617. package/dist/run/paths.d.ts +79 -0
  618. package/dist/run/paths.js +356 -0
  619. package/dist/run/preflight-receipts.d.ts +46 -0
  620. package/dist/run/preflight-receipts.js +173 -0
  621. package/dist/run/pricing.d.ts +124 -0
  622. package/dist/run/pricing.js +298 -0
  623. package/dist/run/project.d.ts +8 -0
  624. package/dist/run/project.js +103 -0
  625. package/dist/run/projection.d.ts +184 -0
  626. package/dist/run/projection.js +356 -0
  627. package/dist/run/reclaim.d.ts +41 -0
  628. package/dist/run/reclaim.js +183 -0
  629. package/dist/run/results.d.ts +88 -0
  630. package/dist/run/results.js +3 -0
  631. package/dist/run/run-index.d.ts +107 -0
  632. package/dist/run/run-index.js +243 -0
  633. package/dist/run/run.d.ts +103 -0
  634. package/dist/run/run.js +199 -0
  635. package/dist/run/sandbox-receipts.d.ts +31 -0
  636. package/dist/run/sandbox-receipts.js +49 -0
  637. package/dist/run/shared-world-evidence.d.ts +220 -0
  638. package/dist/run/shared-world-evidence.js +49 -0
  639. package/dist/run/shared-world-shape.d.ts +26 -0
  640. package/dist/run/shared-world-shape.js +181 -0
  641. package/dist/run/stats.d.ts +82 -0
  642. package/dist/run/stats.js +258 -0
  643. package/dist/run/status.d.ts +111 -0
  644. package/dist/run/status.js +176 -0
  645. package/dist/run/stored-runs.d.ts +38 -0
  646. package/dist/run/stored-runs.js +273 -0
  647. package/dist/run/stream-shape.d.ts +4 -0
  648. package/dist/run/stream-shape.js +160 -0
  649. package/dist/run/streams.d.ts +257 -0
  650. package/dist/run/streams.js +12 -0
  651. package/dist/run/study-files.d.ts +35 -0
  652. package/dist/run/study-files.js +164 -0
  653. package/dist/run/terminal-contract.d.ts +25 -0
  654. package/dist/run/terminal-contract.js +47 -0
  655. package/dist/run/text.d.ts +2 -0
  656. package/dist/run/text.js +4 -0
  657. package/dist/run/transient-comms-secrets.js +72 -0
  658. package/dist/run/type-guards.d.ts +6 -0
  659. package/dist/run/type-guards.js +18 -0
  660. package/dist/run-lab.d.ts +110 -0
  661. package/dist/run-lab.js +148 -0
  662. package/dist/subject/clone.d.ts +37 -0
  663. package/dist/subject/clone.js +62 -0
  664. package/dist/subject/desktop-cli.d.ts +17 -0
  665. package/dist/subject/desktop-cli.js +51 -0
  666. package/dist/subject/local-tree-archive.d.ts +82 -0
  667. package/dist/subject/local-tree-archive.js +431 -0
  668. package/dist/subject/local-tree.d.ts +28 -0
  669. package/dist/subject/local-tree.js +58 -0
  670. package/dist/subject/node-bootstrap.d.ts +12 -0
  671. package/dist/subject/node-bootstrap.js +66 -0
  672. package/dist/subject/runtime.d.ts +13 -0
  673. package/dist/subject/runtime.js +50 -0
  674. package/dist/subject/serve.d.ts +39 -0
  675. package/dist/subject/serve.js +249 -0
  676. package/dist/subject/state.d.ts +23 -0
  677. package/dist/subject/state.js +58 -0
  678. package/dist/subject/steps.d.ts +37 -0
  679. package/dist/subject/steps.js +39 -0
  680. package/dist/substrates/command-failure.d.ts +23 -0
  681. package/dist/substrates/command-failure.js +45 -0
  682. package/dist/substrates/desktop-session.d.ts +41 -0
  683. package/dist/substrates/desktop-session.js +59 -0
  684. package/dist/substrates/detached.d.ts +55 -0
  685. package/dist/substrates/detached.js +139 -0
  686. package/dist/substrates/e2b/cdp-probe.d.ts +99 -0
  687. package/dist/substrates/e2b/cdp-probe.js +556 -0
  688. package/dist/substrates/e2b/desktop-browser.d.ts +81 -0
  689. package/dist/substrates/e2b/desktop-browser.js +410 -0
  690. package/dist/substrates/e2b/desktop-cdp.d.ts +77 -0
  691. package/dist/substrates/e2b/desktop-cdp.js +211 -0
  692. package/dist/substrates/e2b/desktop-executor.d.ts +92 -0
  693. package/dist/substrates/e2b/desktop-executor.js +302 -0
  694. package/dist/substrates/e2b/desktop-geometry.d.ts +83 -0
  695. package/dist/substrates/e2b/desktop-geometry.js +331 -0
  696. package/dist/substrates/e2b/desktop-media.d.ts +29 -0
  697. package/dist/substrates/e2b/desktop-media.js +151 -0
  698. package/dist/substrates/e2b/desktop-recording.d.ts +18 -0
  699. package/dist/substrates/e2b/desktop-recording.js +238 -0
  700. package/dist/substrates/e2b/desktop-resources.d.ts +11 -0
  701. package/dist/substrates/e2b/desktop-resources.js +35 -0
  702. package/dist/substrates/e2b/desktop-screenshot-cleanup.d.ts +15 -0
  703. package/dist/substrates/e2b/desktop-screenshot-cleanup.js +66 -0
  704. package/dist/substrates/e2b/lifetime.d.ts +12 -0
  705. package/dist/substrates/e2b/lifetime.js +18 -0
  706. package/dist/substrates/e2b/sandbox.d.ts +102 -0
  707. package/dist/substrates/e2b/sandbox.js +202 -0
  708. package/dist/substrates/e2b/sdk.d.ts +194 -0
  709. package/dist/substrates/e2b/sdk.js +168 -0
  710. package/dist/substrates/e2b/shell.d.ts +10 -0
  711. package/dist/substrates/e2b/shell.js +61 -0
  712. package/dist/substrates/local/captured-inbox.d.ts +6 -0
  713. package/dist/substrates/local/captured-inbox.js +87 -0
  714. package/dist/substrates/local/firecracker-desktop.d.ts +25 -0
  715. package/dist/substrates/local/firecracker-desktop.js +315 -0
  716. package/dist/substrates/local/runtime-config.d.ts +6 -0
  717. package/dist/substrates/local/runtime-config.js +91 -0
  718. package/dist/substrates/local/runtime-host.d.ts +30 -0
  719. package/dist/substrates/local/runtime-host.js +143 -0
  720. package/dist/substrates/local/runtime-release.d.ts +5 -0
  721. package/dist/substrates/local/runtime-release.js +32 -0
  722. package/dist/substrates/local/runtime-ssh.d.ts +9 -0
  723. package/dist/substrates/local/runtime-ssh.js +129 -0
  724. package/dist/substrates/local/runtime.d.ts +25 -0
  725. package/dist/substrates/local/runtime.js +190 -0
  726. package/dist/substrates/shell.d.ts +44 -0
  727. package/dist/substrates/shell.js +22 -0
  728. package/dist/tui/actions.d.ts +39 -0
  729. package/dist/tui/actions.js +203 -0
  730. package/dist/tui/contract.d.ts +144 -0
  731. package/dist/tui/contract.js +52 -0
  732. package/dist/tui/launch.js +172 -0
  733. package/dist/tui/project.d.ts +10 -0
  734. package/dist/tui/project.js +17 -0
  735. package/dist/tui-app.js +125 -125
  736. package/dist/verify/actor.d.ts +27 -0
  737. package/dist/verify/actor.js +292 -0
  738. package/dist/verify/artifacts.d.ts +47 -0
  739. package/dist/verify/artifacts.js +373 -0
  740. package/dist/verify/costs.d.ts +30 -0
  741. package/dist/verify/costs.js +129 -0
  742. package/dist/verify/liveness.d.ts +7 -0
  743. package/dist/verify/liveness.js +72 -0
  744. package/dist/verify/rerun.d.ts +3 -0
  745. package/dist/verify/rerun.js +46 -0
  746. package/dist/verify/shared-world-concurrent.d.ts +11 -0
  747. package/dist/verify/shared-world-concurrent.js +346 -0
  748. package/dist/verify/shared-world.d.ts +8 -0
  749. package/dist/verify/shared-world.js +295 -0
  750. package/dist/verify/subject.d.ts +17 -0
  751. package/dist/verify/subject.js +123 -0
  752. package/dist/verify/verify.d.ts +34 -0
  753. package/dist/verify/verify.js +333 -0
  754. package/docs/README.md +67 -0
  755. package/docs/architecture/actor-contract.md +215 -174
  756. package/docs/architecture/browser-control.md +51 -37
  757. package/docs/architecture/comms-inbox.md +8 -8
  758. package/docs/architecture/desktop-recording.md +2 -2
  759. package/docs/architecture/desktop-sessions.md +33 -21
  760. package/docs/architecture/external-public-shared-world.md +80 -64
  761. package/docs/architecture/github-feedback-loop.md +40 -27
  762. package/docs/architecture/guest-desktop.md +22 -10
  763. package/docs/architecture/local-browser-runtime.md +30 -21
  764. package/docs/architecture/observer-review.md +9 -9
  765. package/docs/architecture/observer.md +53 -57
  766. package/docs/architecture/participant-media.md +3 -3
  767. package/docs/architecture/project-layout.md +33 -137
  768. package/docs/architecture/real-email-receiving.md +7 -6
  769. package/docs/architecture/restricted-codex-analysis.md +211 -25
  770. package/docs/architecture/serve.md +89 -87
  771. package/docs/architecture/state-driven-executor.md +79 -64
  772. package/docs/architecture/task-protocol-support.md +18 -18
  773. package/docs/architecture/terminal-product-lane.md +92 -86
  774. package/docs/contracts/adapter-admission.md +8 -4
  775. package/docs/contracts/adapter-fixtures.md +6 -6
  776. package/docs/contracts/core.md +22 -24
  777. package/docs/contracts/feedback.md +33 -23
  778. package/docs/contracts/policy.md +28 -31
  779. package/docs/contracts/run-bundle.md +168 -82
  780. package/docs/contracts/schemas.md +594 -379
  781. package/docs/contracts/study-analysis.md +2 -2
  782. package/docs/contracts/study-costs.md +12 -12
  783. package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +36 -0
  784. package/docs/decisions/0002-observer-is-one-self-contained-file.md +30 -0
  785. package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +35 -0
  786. package/docs/decisions/0004-participant-desktops-stay-in-the-route.md +31 -0
  787. package/docs/decisions/README.md +16 -0
  788. package/docs/goals/current.md +22 -46
  789. package/docs/principles/actor-fidelity.md +51 -63
  790. package/docs/principles/engineering.md +81 -0
  791. package/docs/principles/invariants-and-defaults.md +48 -45
  792. package/docs/principles/self-driving-harness.md +11 -10
  793. package/docs/principles/three-roles.md +12 -13
  794. package/docs/product/automatic-analysis.md +39 -9
  795. package/docs/product/lobby-trivia-3player-external-public.md +14 -13
  796. package/docs/product/open-source-install-experience.md +34 -38
  797. package/docs/ramp/README.md +91 -213
  798. package/docs/release/open-source-readiness.md +5 -2
  799. package/docs/release/public-readiness-standard.md +1 -1
  800. package/examples/README.md +11 -0
  801. package/examples/participant/README.md +80 -0
  802. package/examples/participant/app.mjs +55 -0
  803. package/examples/participant/run.mjs +116 -0
  804. package/examples/scorer/README.md +29 -0
  805. package/examples/scorer/run.mjs +63 -0
  806. package/examples/scorer/scorer.mjs +20 -0
  807. package/package.json +61 -57
  808. package/skills/humanish/SKILL.md +112 -83
  809. package/skills/humanish/agents/openai.yaml +2 -2
  810. package/dist/actor-contract.d.ts +0 -288
  811. package/dist/actor-contract.js +0 -340
  812. package/dist/actor-contract.js.map +0 -1
  813. package/dist/actor-goal-source.js +0 -32
  814. package/dist/actor-goal-source.js.map +0 -1
  815. package/dist/actor-registry.d.ts +0 -77
  816. package/dist/actor-registry.js +0 -106
  817. package/dist/actor-registry.js.map +0 -1
  818. package/dist/actor-stop-cause.d.ts +0 -9
  819. package/dist/actor-stop-cause.js +0 -42
  820. package/dist/actor-stop-cause.js.map +0 -1
  821. package/dist/adapter-extension.d.ts +0 -76
  822. package/dist/adapter-extension.js +0 -273
  823. package/dist/adapter-extension.js.map +0 -1
  824. package/dist/adapter-scorer-loader.d.ts +0 -42
  825. package/dist/adapter-scorer-loader.js +0 -129
  826. package/dist/adapter-scorer-loader.js.map +0 -1
  827. package/dist/affordance.d.ts +0 -53
  828. package/dist/affordance.js +0 -145
  829. package/dist/affordance.js.map +0 -1
  830. package/dist/agent-session.js +0 -43
  831. package/dist/agent-session.js.map +0 -1
  832. package/dist/argv.d.ts +0 -1
  833. package/dist/argv.js +0 -8
  834. package/dist/argv.js.map +0 -1
  835. package/dist/artifact-reference.d.ts +0 -22
  836. package/dist/artifact-reference.js +0 -47
  837. package/dist/artifact-reference.js.map +0 -1
  838. package/dist/automatic-analysis-completion.d.ts +0 -25
  839. package/dist/automatic-analysis-completion.js +0 -56
  840. package/dist/automatic-analysis-completion.js.map +0 -1
  841. package/dist/automatic-analysis-config.d.ts +0 -38
  842. package/dist/automatic-analysis-config.js +0 -68
  843. package/dist/automatic-analysis-config.js.map +0 -1
  844. package/dist/automatic-study-analysis.d.ts +0 -17
  845. package/dist/automatic-study-analysis.js +0 -195
  846. package/dist/automatic-study-analysis.js.map +0 -1
  847. package/dist/browser-control-client.d.ts +0 -18
  848. package/dist/browser-control-client.js +0 -176
  849. package/dist/browser-control-client.js.map +0 -1
  850. package/dist/browser-control-dispatcher.d.ts +0 -19
  851. package/dist/browser-control-dispatcher.js +0 -147
  852. package/dist/browser-control-dispatcher.js.map +0 -1
  853. package/dist/browser-control-protocol.d.ts +0 -472
  854. package/dist/browser-control-protocol.js +0 -185
  855. package/dist/browser-control-protocol.js.map +0 -1
  856. package/dist/browser-control-recording-transfer.d.ts +0 -4
  857. package/dist/browser-control-recording-transfer.js +0 -42
  858. package/dist/browser-control-recording-transfer.js.map +0 -1
  859. package/dist/browser-control-transport.d.ts +0 -27
  860. package/dist/browser-control-transport.js +0 -177
  861. package/dist/browser-control-transport.js.map +0 -1
  862. package/dist/browser-evidence-hygiene.js +0 -43
  863. package/dist/browser-evidence-hygiene.js.map +0 -1
  864. package/dist/chrome-cdp-probe.d.ts +0 -97
  865. package/dist/chrome-cdp-probe.js +0 -536
  866. package/dist/chrome-cdp-probe.js.map +0 -1
  867. package/dist/claude-agent-sdk.d.ts +0 -94
  868. package/dist/claude-agent-sdk.js +0 -367
  869. package/dist/claude-agent-sdk.js.map +0 -1
  870. package/dist/cli.js.map +0 -1
  871. package/dist/codex-app-server-ui.d.ts +0 -39
  872. package/dist/codex-app-server-ui.js +0 -357
  873. package/dist/codex-app-server-ui.js.map +0 -1
  874. package/dist/codex-app-server.d.ts +0 -144
  875. package/dist/codex-app-server.js +0 -754
  876. package/dist/codex-app-server.js.map +0 -1
  877. package/dist/command-failure.d.ts +0 -43
  878. package/dist/command-failure.js +0 -73
  879. package/dist/command-failure.js.map +0 -1
  880. package/dist/comms-agentmail.d.ts +0 -25
  881. package/dist/comms-agentmail.js +0 -418
  882. package/dist/comms-agentmail.js.map +0 -1
  883. package/dist/comms-catch-host.d.ts +0 -52
  884. package/dist/comms-catch-host.js +0 -178
  885. package/dist/comms-catch-host.js.map +0 -1
  886. package/dist/comms-connections.d.ts +0 -52
  887. package/dist/comms-connections.js +0 -123
  888. package/dist/comms-connections.js.map +0 -1
  889. package/dist/comms-email-catch.d.ts +0 -54
  890. package/dist/comms-email-catch.js +0 -212
  891. package/dist/comms-email-catch.js.map +0 -1
  892. package/dist/comms-evidence.d.ts +0 -27
  893. package/dist/comms-evidence.js +0 -31
  894. package/dist/comms-evidence.js.map +0 -1
  895. package/dist/comms-fake-inbox.d.ts +0 -45
  896. package/dist/comms-fake-inbox.js +0 -184
  897. package/dist/comms-fake-inbox.js.map +0 -1
  898. package/dist/comms-images.d.ts +0 -7
  899. package/dist/comms-images.js +0 -39
  900. package/dist/comms-images.js.map +0 -1
  901. package/dist/comms-inbox.d.ts +0 -66
  902. package/dist/comms-inbox.js +0 -389
  903. package/dist/comms-inbox.js.map +0 -1
  904. package/dist/comms-lease-store.d.ts +0 -91
  905. package/dist/comms-lease-store.js +0 -495
  906. package/dist/comms-lease-store.js.map +0 -1
  907. package/dist/comms-receiving-evidence.d.ts +0 -46
  908. package/dist/comms-receiving-evidence.js +0 -35
  909. package/dist/comms-receiving-evidence.js.map +0 -1
  910. package/dist/comms-receiving-inbox.d.ts +0 -16
  911. package/dist/comms-receiving-inbox.js +0 -410
  912. package/dist/comms-receiving-inbox.js.map +0 -1
  913. package/dist/comms-receiving-runtime.d.ts +0 -25
  914. package/dist/comms-receiving-runtime.js +0 -45
  915. package/dist/comms-receiving-runtime.js.map +0 -1
  916. package/dist/comms-receiving-types.d.ts +0 -98
  917. package/dist/comms-receiving-types.js +0 -2
  918. package/dist/comms-receiving-types.js.map +0 -1
  919. package/dist/comms-receiving.d.ts +0 -58
  920. package/dist/comms-receiving.js +0 -554
  921. package/dist/comms-receiving.js.map +0 -1
  922. package/dist/comms-sandbox-catch.d.ts +0 -229
  923. package/dist/comms-sandbox-catch.js +0 -737
  924. package/dist/comms-sandbox-catch.js.map +0 -1
  925. package/dist/comms-setup.d.ts +0 -40
  926. package/dist/comms-setup.js +0 -121
  927. package/dist/comms-setup.js.map +0 -1
  928. package/dist/comms-types.d.ts +0 -72
  929. package/dist/comms-types.js +0 -10
  930. package/dist/comms-types.js.map +0 -1
  931. package/dist/computer-use-actor.d.ts +0 -76
  932. package/dist/computer-use-actor.js +0 -68
  933. package/dist/computer-use-actor.js.map +0 -1
  934. package/dist/computer-use.d.ts +0 -429
  935. package/dist/computer-use.js +0 -1749
  936. package/dist/computer-use.js.map +0 -1
  937. package/dist/concurrency.js +0 -27
  938. package/dist/concurrency.js.map +0 -1
  939. package/dist/concurrent-shared-world-lab.d.ts +0 -186
  940. package/dist/concurrent-shared-world-lab.js +0 -2179
  941. package/dist/concurrent-shared-world-lab.js.map +0 -1
  942. package/dist/core/git-state.d.ts +0 -33
  943. package/dist/core/git-state.js +0 -277
  944. package/dist/core/git-state.js.map +0 -1
  945. package/dist/core/git-workspace.d.ts +0 -31
  946. package/dist/core/git-workspace.js +0 -447
  947. package/dist/core/git-workspace.js.map +0 -1
  948. package/dist/core/index.d.ts +0 -4
  949. package/dist/core/index.js +0 -3
  950. package/dist/core/index.js.map +0 -1
  951. package/dist/core/run-primitives.d.ts +0 -66
  952. package/dist/core/run-primitives.js +0 -120
  953. package/dist/core/run-primitives.js.map +0 -1
  954. package/dist/cua-actor-lab.d.ts +0 -855
  955. package/dist/cua-actor-lab.js +0 -3545
  956. package/dist/cua-actor-lab.js.map +0 -1
  957. package/dist/cua-admission-limit.d.ts +0 -12
  958. package/dist/cua-admission-limit.js +0 -20
  959. package/dist/cua-admission-limit.js.map +0 -1
  960. package/dist/cua-desktop-lane.d.ts +0 -35
  961. package/dist/cua-desktop-lane.js +0 -13
  962. package/dist/cua-desktop-lane.js.map +0 -1
  963. package/dist/cua-diagnostics.d.ts +0 -38
  964. package/dist/cua-diagnostics.js +0 -68
  965. package/dist/cua-diagnostics.js.map +0 -1
  966. package/dist/cua-executor-error.js +0 -48
  967. package/dist/cua-executor-error.js.map +0 -1
  968. package/dist/cua-provider-error.d.ts +0 -16
  969. package/dist/cua-provider-error.js +0 -35
  970. package/dist/cua-provider-error.js.map +0 -1
  971. package/dist/cua-speech.js +0 -2
  972. package/dist/cua-speech.js.map +0 -1
  973. package/dist/desktop-recorder.d.ts +0 -39
  974. package/dist/desktop-recorder.js +0 -226
  975. package/dist/desktop-recorder.js.map +0 -1
  976. package/dist/desktop-recording-artifact.d.ts +0 -5
  977. package/dist/desktop-recording-artifact.js +0 -54
  978. package/dist/desktop-recording-artifact.js.map +0 -1
  979. package/dist/desktop-recording-types.d.ts +0 -24
  980. package/dist/desktop-recording-types.js +0 -14
  981. package/dist/desktop-recording-types.js.map +0 -1
  982. package/dist/desktop-session.d.ts +0 -41
  983. package/dist/desktop-session.js +0 -47
  984. package/dist/desktop-session.js.map +0 -1
  985. package/dist/device-presets.d.ts +0 -67
  986. package/dist/device-presets.js +0 -55
  987. package/dist/device-presets.js.map +0 -1
  988. package/dist/doctor-lab.d.ts +0 -38
  989. package/dist/doctor-lab.js +0 -126
  990. package/dist/doctor-lab.js.map +0 -1
  991. package/dist/e2b-cua-desktop.d.ts +0 -3
  992. package/dist/e2b-cua-desktop.js +0 -708
  993. package/dist/e2b-cua-desktop.js.map +0 -1
  994. package/dist/e2b-cua-provisioning.d.ts +0 -314
  995. package/dist/e2b-cua-provisioning.js +0 -1219
  996. package/dist/e2b-cua-provisioning.js.map +0 -1
  997. package/dist/e2b-desktop-executor.d.ts +0 -101
  998. package/dist/e2b-desktop-executor.js +0 -297
  999. package/dist/e2b-desktop-executor.js.map +0 -1
  1000. package/dist/e2b-desktop-launch.d.ts +0 -243
  1001. package/dist/e2b-desktop-launch.js +0 -234
  1002. package/dist/e2b-desktop-launch.js.map +0 -1
  1003. package/dist/e2b-desktop-media.d.ts +0 -14
  1004. package/dist/e2b-desktop-media.js +0 -58
  1005. package/dist/e2b-desktop-media.js.map +0 -1
  1006. package/dist/e2b-desktop-recording.d.ts +0 -16
  1007. package/dist/e2b-desktop-recording.js +0 -162
  1008. package/dist/e2b-desktop-recording.js.map +0 -1
  1009. package/dist/e2b-desktop-resources.d.ts +0 -11
  1010. package/dist/e2b-desktop-resources.js +0 -33
  1011. package/dist/e2b-desktop-resources.js.map +0 -1
  1012. package/dist/e2b-desktop-screenshot-cleanup.d.ts +0 -15
  1013. package/dist/e2b-desktop-screenshot-cleanup.js +0 -67
  1014. package/dist/e2b-desktop-screenshot-cleanup.js.map +0 -1
  1015. package/dist/e2b-desktop-session.d.ts +0 -7
  1016. package/dist/e2b-desktop-session.js +0 -29
  1017. package/dist/e2b-desktop-session.js.map +0 -1
  1018. package/dist/e2b-detached.d.ts +0 -53
  1019. package/dist/e2b-detached.js +0 -130
  1020. package/dist/e2b-detached.js.map +0 -1
  1021. package/dist/e2b-terminal-lab.d.ts +0 -395
  1022. package/dist/e2b-terminal-lab.js +0 -2041
  1023. package/dist/e2b-terminal-lab.js.map +0 -1
  1024. package/dist/env-file.d.ts +0 -14
  1025. package/dist/env-file.js +0 -108
  1026. package/dist/env-file.js.map +0 -1
  1027. package/dist/export-bundle.d.ts +0 -7
  1028. package/dist/export-bundle.js +0 -441
  1029. package/dist/export-bundle.js.map +0 -1
  1030. package/dist/export.d.ts +0 -49
  1031. package/dist/export.js +0 -308
  1032. package/dist/export.js.map +0 -1
  1033. package/dist/feedback-proof.d.ts +0 -8
  1034. package/dist/feedback-proof.js +0 -44
  1035. package/dist/feedback-proof.js.map +0 -1
  1036. package/dist/feedback.d.ts +0 -76
  1037. package/dist/feedback.js +0 -496
  1038. package/dist/feedback.js.map +0 -1
  1039. package/dist/first-run-path.d.ts +0 -55
  1040. package/dist/first-run-path.js +0 -136
  1041. package/dist/first-run-path.js.map +0 -1
  1042. package/dist/frame-signature.d.ts +0 -24
  1043. package/dist/frame-signature.js +0 -128
  1044. package/dist/frame-signature.js.map +0 -1
  1045. package/dist/guest-bootstrap.d.ts +0 -55
  1046. package/dist/guest-bootstrap.js +0 -285
  1047. package/dist/guest-bootstrap.js.map +0 -1
  1048. package/dist/guest-browser-tools.d.ts +0 -8
  1049. package/dist/guest-browser-tools.js +0 -66
  1050. package/dist/guest-browser-tools.js.map +0 -1
  1051. package/dist/guest-chromium-text.d.ts +0 -27
  1052. package/dist/guest-chromium-text.js +0 -281
  1053. package/dist/guest-chromium-text.js.map +0 -1
  1054. package/dist/guest-desktop-executor.d.ts +0 -22
  1055. package/dist/guest-desktop-executor.js +0 -178
  1056. package/dist/guest-desktop-executor.js.map +0 -1
  1057. package/dist/guest-desktop-media.d.ts +0 -33
  1058. package/dist/guest-desktop-media.js +0 -281
  1059. package/dist/guest-desktop-media.js.map +0 -1
  1060. package/dist/guest-desktop-native.d.ts +0 -14
  1061. package/dist/guest-desktop-native.js +0 -131
  1062. package/dist/guest-desktop-native.js.map +0 -1
  1063. package/dist/guest-media-config.d.ts +0 -15
  1064. package/dist/guest-media-config.js +0 -8
  1065. package/dist/guest-media-config.js.map +0 -1
  1066. package/dist/guest-media-worker.js.map +0 -1
  1067. package/dist/guest-runtime-desktop.d.ts +0 -42
  1068. package/dist/guest-runtime-desktop.js +0 -314
  1069. package/dist/guest-runtime-desktop.js.map +0 -1
  1070. package/dist/guest-runtime-main.js.map +0 -1
  1071. package/dist/guest-runtime-revision.js.map +0 -1
  1072. package/dist/guest-runtime.d.ts +0 -31
  1073. package/dist/guest-runtime.js +0 -99
  1074. package/dist/guest-runtime.js.map +0 -1
  1075. package/dist/image-evidence.d.ts +0 -2
  1076. package/dist/image-evidence.js +0 -47
  1077. package/dist/image-evidence.js.map +0 -1
  1078. package/dist/index.js.map +0 -1
  1079. package/dist/init-templates.d.ts +0 -25
  1080. package/dist/init-templates.js +0 -542
  1081. package/dist/init-templates.js.map +0 -1
  1082. package/dist/init.d.ts +0 -38
  1083. package/dist/init.js +0 -532
  1084. package/dist/init.js.map +0 -1
  1085. package/dist/key-resolution.d.ts +0 -67
  1086. package/dist/key-resolution.js +0 -375
  1087. package/dist/key-resolution.js.map +0 -1
  1088. package/dist/lab-app-runner.d.ts +0 -78
  1089. package/dist/lab-app-runner.js +0 -403
  1090. package/dist/lab-app-runner.js.map +0 -1
  1091. package/dist/lab-config.d.ts +0 -968
  1092. package/dist/lab-config.js +0 -2714
  1093. package/dist/lab-config.js.map +0 -1
  1094. package/dist/lab-engine.d.ts +0 -97
  1095. package/dist/lab-engine.js +0 -285
  1096. package/dist/lab-engine.js.map +0 -1
  1097. package/dist/lab-preflight.d.ts +0 -70
  1098. package/dist/lab-preflight.js +0 -388
  1099. package/dist/lab-preflight.js.map +0 -1
  1100. package/dist/lab-summary.d.ts +0 -57
  1101. package/dist/lab-summary.js +0 -126
  1102. package/dist/lab-summary.js.map +0 -1
  1103. package/dist/labs.d.ts +0 -59
  1104. package/dist/labs.js +0 -385
  1105. package/dist/labs.js.map +0 -1
  1106. package/dist/listen.js +0 -84
  1107. package/dist/listen.js.map +0 -1
  1108. package/dist/local-agent-claude-session.d.ts +0 -45
  1109. package/dist/local-agent-claude-session.js +0 -203
  1110. package/dist/local-agent-claude-session.js.map +0 -1
  1111. package/dist/local-agent-cli.d.ts +0 -121
  1112. package/dist/local-agent-cli.js +0 -427
  1113. package/dist/local-agent-cli.js.map +0 -1
  1114. package/dist/local-captured-inbox.d.ts +0 -6
  1115. package/dist/local-captured-inbox.js +0 -65
  1116. package/dist/local-captured-inbox.js.map +0 -1
  1117. package/dist/local-firecracker-desktop.d.ts +0 -23
  1118. package/dist/local-firecracker-desktop.js +0 -191
  1119. package/dist/local-firecracker-desktop.js.map +0 -1
  1120. package/dist/local-firecracker-study.d.ts +0 -9
  1121. package/dist/local-firecracker-study.js +0 -135
  1122. package/dist/local-firecracker-study.js.map +0 -1
  1123. package/dist/local-runtime-config.d.ts +0 -6
  1124. package/dist/local-runtime-config.js +0 -60
  1125. package/dist/local-runtime-config.js.map +0 -1
  1126. package/dist/local-runtime-host.d.ts +0 -31
  1127. package/dist/local-runtime-host.js +0 -128
  1128. package/dist/local-runtime-host.js.map +0 -1
  1129. package/dist/local-runtime-release.d.ts +0 -7
  1130. package/dist/local-runtime-release.js +0 -33
  1131. package/dist/local-runtime-release.js.map +0 -1
  1132. package/dist/local-runtime-ssh.d.ts +0 -27
  1133. package/dist/local-runtime-ssh.js +0 -91
  1134. package/dist/local-runtime-ssh.js.map +0 -1
  1135. package/dist/local-runtime.d.ts +0 -25
  1136. package/dist/local-runtime.js +0 -133
  1137. package/dist/local-runtime.js.map +0 -1
  1138. package/dist/observer-data.d.ts +0 -114
  1139. package/dist/observer-data.js +0 -224
  1140. package/dist/observer-data.js.map +0 -1
  1141. package/dist/observer-library.js +0 -191
  1142. package/dist/observer-library.js.map +0 -1
  1143. package/dist/observer-serve.d.ts +0 -99
  1144. package/dist/observer-serve.js +0 -284
  1145. package/dist/observer-serve.js.map +0 -1
  1146. package/dist/observer-static.js +0 -357
  1147. package/dist/observer-static.js.map +0 -1
  1148. package/dist/observer.d.ts +0 -118
  1149. package/dist/observer.js +0 -947
  1150. package/dist/observer.js.map +0 -1
  1151. package/dist/openai-responses-cu.d.ts +0 -158
  1152. package/dist/openai-responses-cu.js +0 -714
  1153. package/dist/openai-responses-cu.js.map +0 -1
  1154. package/dist/orientation.d.ts +0 -29
  1155. package/dist/orientation.js +0 -95
  1156. package/dist/orientation.js.map +0 -1
  1157. package/dist/oss-lab.d.ts +0 -55
  1158. package/dist/oss-lab.js +0 -344
  1159. package/dist/oss-lab.js.map +0 -1
  1160. package/dist/oss-meta-lab-scoring.d.ts +0 -15
  1161. package/dist/oss-meta-lab-scoring.js +0 -145
  1162. package/dist/oss-meta-lab-scoring.js.map +0 -1
  1163. package/dist/oss-meta-lab.d.ts +0 -287
  1164. package/dist/oss-meta-lab.js +0 -4551
  1165. package/dist/oss-meta-lab.js.map +0 -1
  1166. package/dist/oss-remote-telemetry.d.ts +0 -77
  1167. package/dist/oss-remote-telemetry.js +0 -403
  1168. package/dist/oss-remote-telemetry.js.map +0 -1
  1169. package/dist/output-token-limit.js +0 -5
  1170. package/dist/output-token-limit.js.map +0 -1
  1171. package/dist/participant-assignment.d.ts +0 -12
  1172. package/dist/participant-assignment.js +0 -15
  1173. package/dist/participant-assignment.js.map +0 -1
  1174. package/dist/persona-resolve.d.ts +0 -44
  1175. package/dist/persona-resolve.js +0 -109
  1176. package/dist/persona-resolve.js.map +0 -1
  1177. package/dist/persona.d.ts +0 -35
  1178. package/dist/persona.js +0 -141
  1179. package/dist/persona.js.map +0 -1
  1180. package/dist/pi-agent-core.d.ts +0 -77
  1181. package/dist/pi-agent-core.js +0 -207
  1182. package/dist/pi-agent-core.js.map +0 -1
  1183. package/dist/pricing.d.ts +0 -145
  1184. package/dist/pricing.js +0 -277
  1185. package/dist/pricing.js.map +0 -1
  1186. package/dist/program.d.ts +0 -96
  1187. package/dist/program.js +0 -4029
  1188. package/dist/program.js.map +0 -1
  1189. package/dist/reasoning-effort.js +0 -26
  1190. package/dist/reasoning-effort.js.map +0 -1
  1191. package/dist/reclaim.d.ts +0 -31
  1192. package/dist/reclaim.js +0 -90
  1193. package/dist/reclaim.js.map +0 -1
  1194. package/dist/redaction.d.ts +0 -90
  1195. package/dist/redaction.js +0 -294
  1196. package/dist/redaction.js.map +0 -1
  1197. package/dist/restricted-codex-analysis.d.ts +0 -15
  1198. package/dist/restricted-codex-analysis.js +0 -13
  1199. package/dist/restricted-codex-analysis.js.map +0 -1
  1200. package/dist/restricted-codex-participant-policy.d.ts +0 -44
  1201. package/dist/restricted-codex-participant-policy.js +0 -43
  1202. package/dist/restricted-codex-participant-policy.js.map +0 -1
  1203. package/dist/restricted-codex-participant-run.d.ts +0 -20
  1204. package/dist/restricted-codex-participant-run.js +0 -79
  1205. package/dist/restricted-codex-participant-run.js.map +0 -1
  1206. package/dist/restricted-codex-participant.d.ts +0 -20
  1207. package/dist/restricted-codex-participant.js +0 -250
  1208. package/dist/restricted-codex-participant.js.map +0 -1
  1209. package/dist/restricted-codex-policy.d.ts +0 -64
  1210. package/dist/restricted-codex-policy.js +0 -176
  1211. package/dist/restricted-codex-policy.js.map +0 -1
  1212. package/dist/restricted-codex-session.d.ts +0 -44
  1213. package/dist/restricted-codex-session.js +0 -657
  1214. package/dist/restricted-codex-session.js.map +0 -1
  1215. package/dist/restricted-codex-transport.d.ts +0 -69
  1216. package/dist/restricted-codex-transport.js +0 -296
  1217. package/dist/restricted-codex-transport.js.map +0 -1
  1218. package/dist/run-detail.d.ts +0 -61
  1219. package/dist/run-detail.js +0 -137
  1220. package/dist/run-detail.js.map +0 -1
  1221. package/dist/run-index.d.ts +0 -71
  1222. package/dist/run-index.js +0 -206
  1223. package/dist/run-index.js.map +0 -1
  1224. package/dist/run-narration-secrets.js +0 -68
  1225. package/dist/run-narration-secrets.js.map +0 -1
  1226. package/dist/run-paths.d.ts +0 -56
  1227. package/dist/run-paths.js +0 -355
  1228. package/dist/run-paths.js.map +0 -1
  1229. package/dist/run-projection.d.ts +0 -186
  1230. package/dist/run-projection.js +0 -350
  1231. package/dist/run-projection.js.map +0 -1
  1232. package/dist/run-status.d.ts +0 -139
  1233. package/dist/run-status.js +0 -218
  1234. package/dist/run-status.js.map +0 -1
  1235. package/dist/run.d.ts +0 -1255
  1236. package/dist/run.js +0 -5947
  1237. package/dist/run.js.map +0 -1
  1238. package/dist/sandbox-receipts.d.ts +0 -19
  1239. package/dist/sandbox-receipts.js +0 -51
  1240. package/dist/sandbox-receipts.js.map +0 -1
  1241. package/dist/screenshot-image.d.ts +0 -11
  1242. package/dist/screenshot-image.js +0 -26
  1243. package/dist/screenshot-image.js.map +0 -1
  1244. package/dist/scripted-browser-actor.d.ts +0 -297
  1245. package/dist/scripted-browser-actor.js +0 -1399
  1246. package/dist/scripted-browser-actor.js.map +0 -1
  1247. package/dist/scripted-browser-lab.d.ts +0 -121
  1248. package/dist/scripted-browser-lab.js +0 -852
  1249. package/dist/scripted-browser-lab.js.map +0 -1
  1250. package/dist/secret-prompt.js +0 -36
  1251. package/dist/secret-prompt.js.map +0 -1
  1252. package/dist/selected-output-paths.d.ts +0 -52
  1253. package/dist/selected-output-paths.js +0 -366
  1254. package/dist/selected-output-paths.js.map +0 -1
  1255. package/dist/serve-exposure.d.ts +0 -62
  1256. package/dist/serve-exposure.js +0 -129
  1257. package/dist/serve-exposure.js.map +0 -1
  1258. package/dist/serve-http.js +0 -45
  1259. package/dist/serve-http.js.map +0 -1
  1260. package/dist/serve-tunnel.js +0 -113
  1261. package/dist/serve-tunnel.js.map +0 -1
  1262. package/dist/shared-world-lab.d.ts +0 -269
  1263. package/dist/shared-world-lab.js +0 -1422
  1264. package/dist/shared-world-lab.js.map +0 -1
  1265. package/dist/source-archive.d.ts +0 -82
  1266. package/dist/source-archive.js +0 -408
  1267. package/dist/source-archive.js.map +0 -1
  1268. package/dist/stats.d.ts +0 -82
  1269. package/dist/stats.js +0 -219
  1270. package/dist/stats.js.map +0 -1
  1271. package/dist/stop-conditions.d.ts +0 -39
  1272. package/dist/stop-conditions.js +0 -66
  1273. package/dist/stop-conditions.js.map +0 -1
  1274. package/dist/study-analysis-codex-config.d.ts +0 -11
  1275. package/dist/study-analysis-codex-config.js +0 -34
  1276. package/dist/study-analysis-codex-config.js.map +0 -1
  1277. package/dist/study-analysis-engine.d.ts +0 -47
  1278. package/dist/study-analysis-engine.js +0 -327
  1279. package/dist/study-analysis-engine.js.map +0 -1
  1280. package/dist/study-analysis-evidence.d.ts +0 -27
  1281. package/dist/study-analysis-evidence.js +0 -660
  1282. package/dist/study-analysis-evidence.js.map +0 -1
  1283. package/dist/study-analysis-job.d.ts +0 -97
  1284. package/dist/study-analysis-job.js +0 -214
  1285. package/dist/study-analysis-job.js.map +0 -1
  1286. package/dist/study-analysis-provider.d.ts +0 -40
  1287. package/dist/study-analysis-provider.js +0 -170
  1288. package/dist/study-analysis-provider.js.map +0 -1
  1289. package/dist/study-analysis-service.d.ts +0 -59
  1290. package/dist/study-analysis-service.js +0 -257
  1291. package/dist/study-analysis-service.js.map +0 -1
  1292. package/dist/study-analysis-sharing.d.ts +0 -11
  1293. package/dist/study-analysis-sharing.js +0 -35
  1294. package/dist/study-analysis-sharing.js.map +0 -1
  1295. package/dist/study-analysis-store.d.ts +0 -47
  1296. package/dist/study-analysis-store.js +0 -493
  1297. package/dist/study-analysis-store.js.map +0 -1
  1298. package/dist/study-analysis-validation.d.ts +0 -618
  1299. package/dist/study-analysis-validation.js +0 -384
  1300. package/dist/study-analysis-validation.js.map +0 -1
  1301. package/dist/study-analysis.d.ts +0 -206
  1302. package/dist/study-analysis.js +0 -4
  1303. package/dist/study-analysis.js.map +0 -1
  1304. package/dist/study-costs.js +0 -114
  1305. package/dist/study-costs.js.map +0 -1
  1306. package/dist/subject-runtime.d.ts +0 -23
  1307. package/dist/subject-runtime.js +0 -70
  1308. package/dist/subject-runtime.js.map +0 -1
  1309. package/dist/tasks.d.ts +0 -90
  1310. package/dist/tasks.js +0 -144
  1311. package/dist/tasks.js.map +0 -1
  1312. package/dist/telemetry.js +0 -324
  1313. package/dist/telemetry.js.map +0 -1
  1314. package/dist/terminal-agent-actor.d.ts +0 -41
  1315. package/dist/terminal-agent-actor.js +0 -25
  1316. package/dist/terminal-agent-actor.js.map +0 -1
  1317. package/dist/terminal-encoding.js +0 -80
  1318. package/dist/terminal-encoding.js.map +0 -1
  1319. package/dist/terminal-node-bootstrap.d.ts +0 -4
  1320. package/dist/terminal-node-bootstrap.js +0 -58
  1321. package/dist/terminal-node-bootstrap.js.map +0 -1
  1322. package/dist/terminal-participant-activity.d.ts +0 -6
  1323. package/dist/terminal-participant-activity.js +0 -34
  1324. package/dist/terminal-participant-activity.js.map +0 -1
  1325. package/dist/terminal-runtime-auth.d.ts +0 -13
  1326. package/dist/terminal-runtime-auth.js +0 -24
  1327. package/dist/terminal-runtime-auth.js.map +0 -1
  1328. package/dist/terminal-runtime.d.ts +0 -16
  1329. package/dist/terminal-runtime.js +0 -42
  1330. package/dist/terminal-runtime.js.map +0 -1
  1331. package/dist/terminal-token-usage.d.ts +0 -15
  1332. package/dist/terminal-token-usage.js +0 -94
  1333. package/dist/terminal-token-usage.js.map +0 -1
  1334. package/dist/tui-actions.d.ts +0 -39
  1335. package/dist/tui-actions.js +0 -178
  1336. package/dist/tui-actions.js.map +0 -1
  1337. package/dist/tui-contract.d.ts +0 -141
  1338. package/dist/tui-contract.js +0 -54
  1339. package/dist/tui-contract.js.map +0 -1
  1340. package/dist/tui-launch.js +0 -167
  1341. package/dist/tui-launch.js.map +0 -1
  1342. package/dist/tui-project.d.ts +0 -9
  1343. package/dist/tui-project.js +0 -18
  1344. package/dist/tui-project.js.map +0 -1
  1345. package/docs/architecture/examples/state-driven-local-app/README.md +0 -80
  1346. package/docs/architecture/examples/state-driven-local-app/app.mjs +0 -48
  1347. package/docs/architecture/examples/state-driven-local-app/runner.mjs +0 -105
  1348. package/docs/architecture/local-codex-tui-actor.md +0 -213
  1349. package/docs/architecture/oss-lab-poc.md +0 -250
  1350. package/docs/architecture/runtime-broker-core.md +0 -30
  1351. package/docs/release/0.100.0-local-setup.md +0 -31
  1352. package/docs/release/0.100.1-participant-continuity.md +0 -20
  1353. package/docs/release/0.101.0-codex-ui-tools.md +0 -28
  1354. package/docs/release/0.102.0-participant-media.md +0 -43
  1355. package/docs/release/0.103.0-desktop-recording.md +0 -29
  1356. package/docs/release/0.103.1-analysis-capture-allocation.md +0 -18
  1357. package/docs/release/0.103.2-analysis-ending-context.md +0 -21
  1358. package/docs/release/0.104.0-local-captured-inboxes.md +0 -29
  1359. package/docs/release/0.104.1-recording-startup.md +0 -24
  1360. package/docs/release/0.104.2-recording-timestamps.md +0 -32
  1361. package/docs/release/0.104.3-local-smtp-catch.md +0 -15
  1362. package/docs/release/0.20.0-external-public-shared-world.md +0 -43
  1363. package/docs/release/0.84.0-redacted-evidence.md +0 -42
  1364. package/docs/release/0.84.1-portable-feedback.md +0 -20
  1365. package/docs/release/0.85.0-observer-review.md +0 -56
  1366. package/docs/release/0.85.1-observer-continuity.md +0 -20
  1367. package/docs/release/0.86.0-participant-evidence.md +0 -50
  1368. package/docs/release/0.86.1-task-preflight-saved-recordings.md +0 -40
  1369. package/docs/release/0.87.0-participant-endings-and-phone-review.md +0 -64
  1370. package/docs/release/0.88.0-study-diagnostics.md +0 -43
  1371. package/docs/release/0.88.1-completion-evidence-and-local-app.md +0 -79
  1372. package/docs/release/0.88.2-sequential-study-budgets.md +0 -57
  1373. package/docs/release/0.89.0-study-findings.md +0 -79
  1374. package/docs/release/0.89.1-analysis-finished-notice.md +0 -24
  1375. package/docs/release/0.90.0-automatic-analysis.md +0 -21
  1376. package/docs/release/0.91.0-analysis-quality-and-defaults.md +0 -33
  1377. package/docs/release/0.91.1-study-review-polish.md +0 -28
  1378. package/docs/release/0.92.0-grid-playback.md +0 -22
  1379. package/docs/release/0.93.0-global-playback.md +0 -26
  1380. package/docs/release/0.93.1-observer-review-controls.md +0 -24
  1381. package/docs/release/0.94.0-reliability.md +0 -48
  1382. package/docs/release/0.95.0-connections-setup.md +0 -38
  1383. package/docs/release/0.96.0-real-email-receiving.md +0 -47
  1384. package/docs/release/0.96.1-browser-navigation.md +0 -22
  1385. package/docs/release/0.97.0-codex-account-analysis.md +0 -45
  1386. package/docs/release/0.98.0-local-browser-studies.md +0 -31
  1387. package/docs/release/0.99.0-local-browser-mac.md +0 -34
  1388. package/docs/release/0.99.1-action-recovery.md +0 -19
  1389. /package/dist/{agent-session.d.ts → actors/agent-session.d.ts} +0 -0
  1390. /package/dist/{cua-executor-error.d.ts → actors/computer-use/executor-error.d.ts} +0 -0
  1391. /package/dist/{cua-speech.d.ts → actors/computer-use/speech.d.ts} +0 -0
  1392. /package/dist/{actor-goal-source.d.ts → actors/goal-source.d.ts} +0 -0
  1393. /package/dist/{output-token-limit.d.ts → actors/output-token-limit.d.ts} +0 -0
  1394. /package/dist/{reasoning-effort.d.ts → actors/reasoning-effort.d.ts} +0 -0
  1395. /package/dist/{secret-prompt.d.ts → cli/secret-prompt.d.ts} +0 -0
  1396. /package/dist/{telemetry.d.ts → cli/telemetry.d.ts} +0 -0
  1397. /package/dist/{browser-evidence-hygiene.d.ts → evidence/browser-hygiene.d.ts} +0 -0
  1398. /package/dist/{serve-http.d.ts → observer/http.d.ts} +0 -0
  1399. /package/dist/{observer-library.d.ts → observer/library.d.ts} +0 -0
  1400. /package/dist/{listen.d.ts → observer/listen.d.ts} +0 -0
  1401. /package/dist/{observer-static.d.ts → observer/static.d.ts} +0 -0
  1402. /package/dist/{serve-tunnel.d.ts → observer/tunnel.d.ts} +0 -0
  1403. /package/dist/{terminal-encoding.d.ts → routes/terminal/encoding.d.ts} +0 -0
  1404. /package/dist/{concurrency.d.ts → run/concurrency.d.ts} +0 -0
  1405. /package/dist/{study-costs.d.ts → run/costs.d.ts} +0 -0
  1406. /package/dist/{run-narration-secrets.d.ts → run/transient-comms-secrets.d.ts} +0 -0
  1407. /package/dist/{tui-launch.d.ts → tui/launch.d.ts} +0 -0
@@ -1,3545 +0,0 @@
1
- import { e2bDesktopTemplate } from "./e2b-desktop-media.js";
2
- export { inboxRecipientFor, laneHasInboxRecipient } from "./cua-desktop-lane.js";
3
- export { CUA_ACTOR_LAB_PROVIDER_METADATA, DEFAULT_MOBILE_USER_AGENT, SANDBOX_CAMERA_PATH, SANDBOX_MEDIA_DIR, SUBJECT_DIR, SYNTHETIC_CAMERA_COMMAND, applyMobileEmulation, buildFillDesktopWindowCommand, captureDesktopBrowserGeometry, commandDigestOf, declaredScreenForRender, desktopBrowserFamily, inspectDesktopScreenGeometry, makeChromeBrowserStateObserver, makeChromeDesktopGeometryObserver, parseXwininfoGeometry, prepareDesktopMedia, provisionCloneSubject, provisionLocalTreeSubject } from "./e2b-cua-provisioning.js";
4
- import { prepareReceivingRun, receivingPublication } from "./comms-receiving-runtime.js";
5
- import { laneHasInboxRecipient } from "./cua-desktop-lane.js";
6
- import { createE2BCuaDesktopLane } from "./e2b-cua-desktop.js";
7
- import { isLocalBrowserLab, LOCAL_BROWSER_LIFETIME_MS } from "./local-runtime-config.js";
8
- import { DEFAULT_STATE_STEP_TIMEOUT_MS, commandDigestOf, declaredScreenForRender } from "./e2b-cua-provisioning.js";
9
- import { receivingEmailValidationReason } from "./lab-config.js";
10
- import { withTransientCommsSecrets } from "./run-narration-secrets.js";
11
- // The computer-use lab backend: a subject (an app-url the caller provisioned, or a repo the
12
- // lab clones AND serves in-sandbox) driven by a REGISTRY-RESOLVED computer-use actor inside a
13
- // hosted E2B desktop. This is the path that makes `actors[].type` load-bearing — the
14
- // descriptor returned by the registry runs the session; the lab provisions the desktop and
15
- // subject, composes the prompt from config, persists the evidence bundle, and tears down.
16
- //
17
- // Substrate notes:
18
- // - The desktop is created via the shared loader in e2b-desktop-launch.ts with kill-on-timeout
19
- // lifecycle, so a dead host process can never orphan a sandbox past its server-side deadline.
20
- // - Env placement follows the doctrine (docs/principles/invariants-and-defaults.md): the
21
- // ACTOR's key never enters the sandbox (the model drives from outside via the provider API);
22
- // the SUBJECT's declared env NAMES are provisioned in on the clone route — values come from
23
- // the caller's environment and are never logged or persisted.
24
- // - The live stream URL is runtime-only (carries an auth key) and is never persisted into run
25
- // artifacts — only its presence is recorded, mirroring the meta lab's convention.
26
- // - Evidence redaction is mode-aware (docs/principles/invariants-and-defaults.md, the
27
- // capture-vs-publish rule): screenshots persist RAW (full fidelity) by default into gitignored
28
- // .humanish/; `policies.redactScreenshots: true` opts into blur-at-capture for a share-as-is
29
- // bundle. Length-only typed text and text redaction of reasoning/messages are UNCONDITIONAL;
30
- // harness errors are redacted at THIS boundary; the bundle's `stream.actor` carries the
31
- // conformant humanish.actor-trace.v1 projection, whose `redaction.screenshots` records the
32
- // run's actual mode ("raw" | "blurred" | "n/a") — every label downstream derives from it.
33
- import { randomBytes } from "node:crypto";
34
- import { readFile, realpath, rm } from "node:fs/promises";
35
- import path from "node:path";
36
- import { completeAutomaticAnalysis, markFinalizedStudyResult } from "./automatic-analysis-completion.js";
37
- import { resolveAutomaticAnalysis } from "./automatic-analysis-config.js";
38
- import { describeMissingKeys } from "./key-resolution.js";
39
- import { desktopMediaValidationReason, taskProtocolValidationReason } from "./lab-config.js";
40
- import { pathToFileURL } from "node:url";
41
- import { toErrorMessage } from "./command-failure.js";
42
- import { cuaLaneDiagnostics, summarizeCuaDiagnostics } from "./cua-diagnostics.js";
43
- import { feedbackProofCommands } from "./feedback-proof.js";
44
- import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
45
- import { actorEnding } from "./actor-stop-cause.js";
46
- import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-extension.js";
47
- import { FakeInbox } from "./comms-fake-inbox.js";
48
- import { recipientInboxUrl } from "./comms-inbox.js";
49
- import { collectExternalCommsThread, externalCatchHealthy, externalInboxUrl } from "./comms-sandbox-catch.js";
50
- import { mapWithConcurrency } from "./concurrency.js";
51
- import { DEFAULT_DEVICE_PRESET, isDevicePresetName, resolveDevicePreset } from "./device-presets.js";
52
- import {} from "./e2b-desktop-launch.js";
53
- import {} from "./e2b-desktop-resources.js";
54
- import {} from "./e2b-detached.js";
55
- import { assertScreenshotEvidence } from "./image-evidence.js";
56
- import { MAX_CUA_LANES, cuaLaneValidationReason, isHttpUrl, isLoopbackUrl, outputTokenLimitValidationReason, subjectStateInvalidReason } from "./lab-config.js";
57
- import { startClaudeSession } from "./local-agent-claude-session.js";
58
- import { checkHostedCodexCompatibility, createLocalAgentProvider, detectLocalAgents } from "./local-agent-cli.js";
59
- import { buildObserverData } from "./observer-data.js";
60
- import { attachObserverRuntimeStreamUrls, renderObserver } from "./observer.js";
61
- import { DEFAULT_OPENAI_CU_MODEL } from "./openai-responses-cu.js";
62
- import { participantAssignment } from "./participant-assignment.js";
63
- import { labPersonaIds, resolveCommittedPersonas } from "./persona-resolve.js";
64
- import { personaToDirectives, renderPersonaPromptSection } from "./persona.js";
65
- import { MODEL_RATES, estimateActorCostForExecution, estimateAllocatedDesktopCost, estimateDesktopCost, round6 } from "./pricing.js";
66
- import { containsSensitive, digestText, redactText } from "./redaction.js";
67
- import { createRestrictedCodexParticipant, } from "./restricted-codex-participant.js";
68
- import { prepareRunArtifactPaths, validatePreparedRunArtifactPaths } from "./run-paths.js";
69
- import { beginRunStatus, withRunStatusScope } from "./run-status.js";
70
- import { PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, aggregateTaskFunnels, buildRunSource, formatParticipantOutcomes, formatStudyTaskFunnel, loadRunBundle, tallyParticipantOutcomes, withCuaReviewProvenance } from "./run.js";
71
- import { assertPreparedSelectedOutputDirectory, assertSafeOutputPathSegment, prepareContainedOutputDirectory, prepareSelectedOutputDirectory, writeContainedOutputFile, writePreparedRunLatestPointer } from "./selected-output-paths.js";
72
- import { createLocalTreeArchive } from "./source-archive.js";
73
- import { renderTaskPrompt } from "./tasks.js";
74
- export const CUA_ACTOR_LAB_SCHEMA = "humanish.cua-lab-result.v2";
75
- // The only fan-out topology this slice ships: N lanes = N independent E2B desktop sandboxes,
76
- // each its own world (clone/serve + subject.state per lane). Shared-world is layer 7 (#164).
77
- export const CUA_FANOUT_STRATEGY = "per-lane-worlds";
78
- // Env override that may only LOWER the effective concurrency (never raise concurrent paid
79
- // desktops — invariant 3). Read names-only into a local; the value never persists.
80
- const CUA_MAX_CONCURRENCY_ENV = "HUMANISH_CUA_MAX_CONCURRENCY";
81
- // The DEFAULT session budget, sized so a study can FINISH (docs/principles/three-roles.md: a
82
- // session ends because the participant is done, not because a timer fired — the time-box is a
83
- // session-level cap a researcher sets generously; spend protection is the dollar caps' job).
84
- // The old 300s default ended real signup studies mid-flow: observed studies run 16-40 turns at
85
- // ~5-6s per turn BEFORE any email wait, so five minutes was the biggest single source of
86
- // budget_reached endings that read as participant failures.
87
- //
88
- // App-url and in-process routes default to 30 minutes. Provisioned routes (clone/local-tree)
89
- // default to whatever the 1-hour sandbox cap leaves after provisioning, declared state seeding,
90
- // and the teardown buffer — 20 minutes on a stateless clone — floored at the old five minutes so
91
- // a state-heavy lab still gets a session at all. An EXPLICIT execution.timeoutMs is never
92
- // adjusted: when it cannot be provisioned, the plan-time cap refusal shows the arithmetic.
93
- const DEFAULT_APP_URL_SESSION_TIMEOUT_MS = 30 * 60_000;
94
- const MIN_DERIVED_SESSION_TIMEOUT_MS = 5 * 60_000;
95
- function defaultSessionTimeoutMs(config) {
96
- const provisionedRoute = config.subject.source === "clone" || config.subject.source === "local-tree";
97
- if (!provisionedRoute)
98
- return DEFAULT_APP_URL_SESSION_TIMEOUT_MS;
99
- const stateBudgetMs = (config.subject.state?.seed ?? []).reduce((sum, step) => sum + (step.timeoutMs ?? DEFAULT_STATE_STEP_TIMEOUT_MS), 0);
100
- const room = MAX_SANDBOX_MS - SUBJECT_PROVISION_BUDGET_MS - stateBudgetMs - SANDBOX_TIMEOUT_BUFFER_MS;
101
- return Math.max(MIN_DERIVED_SESSION_TIMEOUT_MS, Math.min(DEFAULT_APP_URL_SESSION_TIMEOUT_MS, room));
102
- }
103
- // Device/screen size comes from the named-preset registry (device-presets.ts), selectable per run
104
- // via execution.desktop.device (default `desktop`=1440x950). NOTE: this is run-wide for now; a
105
- // per-PERSONA device dimension (N personas × devices, as the bespoke sims author) lands with
106
- // fan-out. On this E2B-desktop route only width/height physically render — isMobile/DSF are
107
- // honest metadata + a prompt signal, not rendered (device-presets.ts FIDELITY NOTE) — and the
108
- // rendered WIDTH is floored to MIN_DESKTOP_RENDER_WIDTH (Chrome's ~500px window minimum) so a mobile
109
- // screen the browser can't shrink to does not overflow + clip (see resolveLaneDevice / #221).
110
- // Server-side reclamation buffer past the loop's own wall-clock stop.
111
- const SANDBOX_TIMEOUT_BUFFER_MS = 10 * 60_000;
112
- // Room the clone route adds to the sandbox deadline for clone/install/build/start/probe.
113
- const SUBJECT_PROVISION_BUDGET_MS = 30 * 60_000;
114
- /** E2B refuses a sandbox lifetime over one hour ("400: Timeout cannot be greater than 1 hours").
115
- * The derived per-lane deadline has to stay under it, and saying so at plan time beats discovering
116
- * it from a raw provider 400 after a plan has already printed. */
117
- const MAX_SANDBOX_MS = 60 * 60_000;
118
- const DEFAULT_MISSION = "You are testing a web application. The browser is already open at the subject URL. Explore it, accomplish what the scenario asks, and stop when done.";
119
- /**
120
- * The participant's outcome as ONE fixed first line of its last message (#570, second half). The
121
- * free-text computer-use provider has no schema to fill; a fixed line is the next best thing, and
122
- * the loop reads it into the trace's declaredOutcome. Prompt-only control is weak in general, so
123
- * adherence is measured (declaredOutcome present or absent on the trace) and the regex over the
124
- * paragraph stays as the fallback when the line is missing. This is a report format, deliberately
125
- * not a behavioural instruction: it says how to label the ending, never how to act.
126
- */
127
- export const CLOSING_LINE_DIRECTIVE = "When you stop, make the FIRST line of your last message exactly one of these three, on its own line: "
128
- + "REACHED THE GOAL. / DID NOT REACH THE GOAL. / BLOCKED. "
129
- + "Then, from the next line, say what you did, what confused you, and where you hesitated.";
130
- /** Compose one lane's actor prompt: persona line + device line + mission + per-lane steer.
131
- * At N=1 (homogeneous, no roster) this reproduces the prior composeInstructions byte-for-byte. */
132
- export function composeLaneInstructions(args) {
133
- const { name, preset } = args.device;
134
- const deviceLine = preset.isMobile
135
- ? `You are a mobile user on a ${name} device (${preset.width}x${preset.height} @${preset.deviceScaleFactor}x). Expect a mobile/touch layout.`
136
- : `You are a desktop user (${name}, ${preset.width}x${preset.height}).`;
137
- // The protocol as the PARTICIPANT reads it: numbered goals, nothing else. The success criteria
138
- // are the researcher's instrument and must never reach this prompt — a persona told how it will
139
- // be measured optimizes for the measurement instead of using the product (src/tasks.ts).
140
- const taskLines = renderTaskPrompt(args.tasks ?? []);
141
- // A resolved persona contributes its compiled directives (friction tolerance, skill bias,
142
- // accessibility behavior, constraints) through the SAME persona.ts compiler the terminal lane
143
- // uses, so one persona file means one behavior across every route.
144
- const personaLine = args.resolvedPersona
145
- ? renderPersonaPromptSection(args.resolvedPersona)
146
- : args.persona ? `Persona: ${args.persona}.` : undefined;
147
- const traitsApplied = args.resolvedPersona ? personaToDirectives(args.resolvedPersona).traitsApplied : [];
148
- const surfaceLine = args.surface === "desktop-cli"
149
- ? "A terminal window is already open on this desktop, and there is a terminal in the dock at the bottom of the screen if you want another. Everything you need is on this machine; there is no browser task here."
150
- : undefined;
151
- const parts = [
152
- personaLine,
153
- deviceLine,
154
- surfaceLine,
155
- args.mission,
156
- taskLines,
157
- args.instruction ? `Lane focus: ${args.instruction}` : undefined,
158
- CLOSING_LINE_DIRECTIVE
159
- ].filter((part) => Boolean(part));
160
- const instructions = parts.join("\n\n");
161
- return {
162
- instructions,
163
- persona: {
164
- id: args.persona ?? "cua-operator",
165
- traitsApplied,
166
- promptDigest: digestText(instructions, 16)
167
- }
168
- };
169
- }
170
- /** Runtime-inject the persona inbox instruction into a lane's prompt (#297 slice B). The inbox URL is a
171
- * runtime loopback/getHost address (not secret), so — mirroring the lobby-code runtime injection — this
172
- * augments only the instructions the model receives; the authored prompt + its digest are unchanged.
173
- * Returns a new spec (never mutates). Shared by the CUA + concurrent shared-world routes. */
174
- export function withInboxMission(spec, inboxUrl, address, receiving = false) {
175
- // No assigned identity means no participant inbox; never fall back to the shared operator view.
176
- if (!address?.trim())
177
- return spec;
178
- // The address is half the handoff (#351): the drain matches captured mail against the DECLARED
179
- // address, so an actor that invents its own at signup gets an inbox that stays empty forever.
180
- // Telling it which address to use is what makes the funnel deterministic end to end. The
181
- // wait-steering sentence exists because a mid-flow model treats "we emailed you" as a blocker
182
- // and ends its session — the exact give-up class a live run documented — unless told the wait
183
- // is expected and the inbox is the next step.
184
- const identity = ` Your email address is ${address} — when the app asks for an email address, enter exactly that.`;
185
- if (receiving)
186
- return {
187
- ...spec,
188
- instructions: `${spec.instructions}\n\nEmail inbox:${identity} This is a fresh test identity; it does not replace an existing account's email address. When the app says it sent email, open ${inboxUrl} to check your inbox. Read the original email and use its verification link or code. Delivery may take a little time; refresh if needed. If mail remains missing or unavailable, report what you observed rather than assuming the app failed to send. The inbox may block remote images or undeclared destinations; those are harness limitations.`
189
- };
190
- return {
191
- ...spec,
192
- instructions: `${spec.instructions}\n\nEmail inbox:${identity} When the app tells you it has emailed you (a verification link, confirmation code, or magic link), open ${recipientInboxUrl(inboxUrl, address)} in the browser to read that email and follow its link or enter its code. All email the app sends you arrives there. Waiting for an email is normal, not a blocker — do not end your session while waiting; open the inbox and refresh it until the email appears.`
193
- };
194
- }
195
- /**
196
- * The narrowest browser WINDOW Chrome/Chromium will render on the E2B desktop. Chrome refuses to
197
- * make its window narrower than this (~500 CSS px observed: a 414-wide X screen produced a 500-wide
198
- * window that OVERFLOWED it, clipping the right edge of the page off-screen). So the physically
199
- * RENDERED screen width is floored here: a sub-500 mobile preset (mobile 414, small-mobile 360,
200
- * narrow-mobile 320) gets a 500-wide screen the window fits exactly — no clip. The device PRESET keeps
201
- * its true identity (isMobile, nominal width) for the persona prompt + metadata; only the rendered
202
- * screen is floored. True sub-500 CSS-viewport rendering (page laid out at 414 regardless of window
203
- * width, via CDP device-metric emulation) is the separate #221 upgrade.
204
- */
205
- export const MIN_DESKTOP_RENDER_WIDTH = 500;
206
- /** Floor a screen resolution's WIDTH to what Chrome can actually render (see MIN_DESKTOP_RENDER_WIDTH). */
207
- export function floorRenderResolution(resolution) {
208
- return [Math.max(resolution[0], MIN_DESKTOP_RENDER_WIDTH), resolution[1]];
209
- }
210
- /**
211
- * Resolve a lane's device + rendered resolution (most-specific wins, exactly as the single-lane
212
- * path always has): a raw execution.desktop.resolution escape hatch (only legal when no lane
213
- * sets a device — XOR enforced at parse) → the lane's named device → the run-wide
214
- * execution.desktop.device → the default preset. A raw resolution is an unnamed custom desktop
215
- * (non-mobile, DSF 1): we never claim a named preset's mobile/DPR for hand-set geometry. The rendered
216
- * `resolution` is floored to MIN_DESKTOP_RENDER_WIDTH so the browser window fits its X screen (no clip);
217
- * `preset` keeps the declared device identity (a mobile preset stays 414/isMobile for the prompt).
218
- */
219
- export function resolveLaneDevice(config, lane) {
220
- const rawResolution = config.execution?.desktop?.resolution;
221
- if (lane?.device === undefined && rawResolution) {
222
- const preset = { width: rawResolution[0], height: rawResolution[1], isMobile: false, deviceScaleFactor: 1 };
223
- return { name: "custom", preset, resolution: floorRenderResolution([rawResolution[0], rawResolution[1]]) };
224
- }
225
- const candidate = lane?.device ?? config.execution?.desktop?.device;
226
- const presetName = isDevicePresetName(candidate) ? candidate : DEFAULT_DEVICE_PRESET;
227
- const preset = resolveDevicePreset(presetName);
228
- return { name: presetName, preset, resolution: floorRenderResolution([preset.width, preset.height]) };
229
- }
230
- /** Per-lane sandbox deadline (each lane owns its own desktop). Mirrors the single-lane formula
231
- * verbatim so N=1 stays byte-stable: explicit sandboxTimeoutMs, else session budget + (clone
232
- * or local-tree: provision budget + Σ state-step budgets) + the server-side
233
- * reclamation buffer. Local-tree shares the clone route's provisioning budget: it swaps a
234
- * git clone for an upload+extract, but the shared install/build/state/start/probe pipeline
235
- * costs the same wall-clock room either way. */
236
- function resolvePerLaneSandboxMs(config) {
237
- if (isLocalBrowserLab(config))
238
- return LOCAL_BROWSER_LIFETIME_MS;
239
- const timeoutMs = config.execution?.timeoutMs ?? defaultSessionTimeoutMs(config);
240
- const provisionedRoute = config.subject.source === "clone" || config.subject.source === "local-tree";
241
- const stateBudgetMs = provisionedRoute
242
- ? (config.subject.state?.seed ?? []).reduce((sum, step) => sum + (step.timeoutMs ?? DEFAULT_STATE_STEP_TIMEOUT_MS), 0)
243
- : 0;
244
- return config.execution?.desktop?.sandboxTimeoutMs
245
- ?? timeoutMs + (provisionedRoute ? SUBJECT_PROVISION_BUDGET_MS + stateBudgetMs : 0) + SANDBOX_TIMEOUT_BUFFER_MS;
246
- }
247
- /**
248
- * Effective in-flight lane bound. Defaults to laneCount — every declared seat runs at once,
249
- * because a throttle nobody asked for silently turns "N actors live" into waves (#350); total
250
- * session count and spend are the same either way, only wall-clock and simultaneity differ. A
251
- * declared execution.concurrency is a CAP, clamped to [1, laneCount]; the env override may only
252
- * LOWER it (never raise concurrent paid desktops — invariant 3), and a lowering is reported via
253
- * envLoweredFrom so the plan never silently disagrees with the manifest. Pure given
254
- * (config, laneCount, env).
255
- */
256
- function resolveCuaConcurrency(config, laneCount, env) {
257
- const declared = config.execution?.concurrency;
258
- const base = Math.max(1, declared !== undefined ? Math.min(Math.max(1, declared), laneCount) : laneCount);
259
- const envLower = readPositiveInt(env[CUA_MAX_CONCURRENCY_ENV], 0);
260
- if (envLower > 0 && envLower < base) {
261
- return { bound: Math.max(1, Math.min(base, envLower, laneCount)), envLoweredFrom: base };
262
- }
263
- return { bound: base };
264
- }
265
- /** Build the lane specs AND the public plan from a config (pure). countOverride is the CLI
266
- * --count for homogeneous fan-out (ignored when a `lanes` roster is declared). */
267
- function laneSpecsAndPlan(config, opts = {}) {
268
- const env = opts.env ?? {};
269
- const actor = config.actors[0];
270
- const mission = actor?.mission ?? DEFAULT_MISSION;
271
- const tasks = actor?.tasks;
272
- const roster = actor?.lanes;
273
- const laneCount = roster ? roster.length : Math.max(1, opts.countOverride ?? actor?.count ?? 1);
274
- const lanes = [];
275
- for (let i = 0; i < laneCount; i += 1) {
276
- const lane = roster?.[i];
277
- const laneId = lane?.id ?? `lane-${String(i + 1).padStart(2, "0")}`;
278
- const simId = `sim-${String(i + 1).padStart(3, "0")}`;
279
- const streamId = `stream-${String(i + 1).padStart(3, "0")}`;
280
- const device = resolveLaneDevice(config, lane);
281
- // A lane's persona FALLS BACK to actors[0].persona, matching this field's own doc comment
282
- // in src/lab-config.ts and its sibling resolutions (stopWhen, reasoningEffort) two lines
283
- // below. Reading only lane.persona when a roster was present meant every fan-out lane of
284
- // every lab that declared actors[0].persona ran with no persona at all: no personaLine in
285
- // the prompt, traitsApplied [], and nothing warned (#512).
286
- const personaId = (lane?.persona ?? actor?.persona);
287
- const resolvedPersona = personaId === undefined ? undefined : opts.personas?.get(personaId);
288
- const composed = composeLaneInstructions({
289
- mission,
290
- ...(tasks === undefined ? {} : { tasks }),
291
- ...(personaId === undefined ? {} : { persona: personaId }),
292
- ...(resolvedPersona === undefined ? {} : { resolvedPersona }),
293
- ...(((roster ? lane?.instruction : actor?.laneFocus?.instruction)) === undefined ? {} : { instruction: (roster ? lane?.instruction : actor?.laneFocus?.instruction) }),
294
- device: { name: device.name, preset: device.preset },
295
- ...(config.subject.source === "desktop-cli" ? { surface: "desktop-cli" } : {})
296
- });
297
- lanes.push({
298
- laneId,
299
- ...(lane?.actorType === undefined ? {} : { actorType: lane.actorType }),
300
- ...(lane?.surface === undefined ? {} : { surface: lane.surface }),
301
- ...(lane?.caseGroup === undefined ? {} : { caseGroup: lane.caseGroup }),
302
- laneIndex: i,
303
- simId,
304
- streamId,
305
- persona: composed.persona,
306
- instructions: composed.instructions,
307
- assignment: {
308
- mission,
309
- ...((roster ? lane?.instruction : actor?.laneFocus?.instruction) === undefined
310
- ? {} : { focus: (roster ? lane?.instruction : actor?.laneFocus?.instruction) }),
311
- ...(tasks === undefined ? {} : { tasks: tasks.map(({ id, goal }) => ({ id, goal })) })
312
- },
313
- ...(lane?.target === undefined ? {} : { targetUrl: lane.target }),
314
- ...((lane?.stopWhen ?? actor?.stopWhen) === undefined ? {} : { stopWhen: (lane?.stopWhen ?? actor?.stopWhen) }),
315
- ...((lane?.dwell ?? actor?.dwell) === undefined ? {} : { dwell: (lane?.dwell ?? actor?.dwell) }),
316
- ...((lane?.reasoningEffort ?? actor?.reasoningEffort) === undefined
317
- ? {}
318
- : { reasoningEffort: (lane?.reasoningEffort ?? actor?.reasoningEffort) }),
319
- ...(actor?.maxOutputTokens === undefined ? {} : { maxOutputTokens: actor.maxOutputTokens }),
320
- ...(tasks === undefined ? {} : { tasks }),
321
- deviceName: device.name,
322
- devicePreset: device.preset,
323
- resolution: device.resolution,
324
- screenshotDir: laneCount === 1 ? "" : laneId,
325
- traceArtifactPath: laneCount === 1 ? "actor.json" : `actors/${streamId}.json`
326
- });
327
- }
328
- const resolved = resolveCuaConcurrency(config, laneCount, env);
329
- const concurrency = resolved.bound;
330
- const perLaneSessionBudgetMs = config.execution?.timeoutMs ?? defaultSessionTimeoutMs(config);
331
- const perLaneSandboxMs = resolvePerLaneSandboxMs(config);
332
- const plan = {
333
- strategy: CUA_FANOUT_STRATEGY,
334
- laneCount,
335
- concurrency,
336
- ...(resolved.envLoweredFrom === undefined ? {} : { envLoweredConcurrencyFrom: resolved.envLoweredFrom }),
337
- waves: Math.ceil(laneCount / concurrency),
338
- perLaneSessionBudgetMs,
339
- worstCaseSandboxMinutes: Math.round((laneCount * perLaneSandboxMs) / 60_000),
340
- dryRun: opts.dryRun === true,
341
- lanes: lanes.map((spec) => ({
342
- id: spec.laneId,
343
- ...(spec.actorType === undefined ? {} : { actorType: spec.actorType }),
344
- ...(spec.surface === undefined ? {} : { surface: spec.surface }),
345
- ...(spec.caseGroup === undefined ? {} : { caseGroup: spec.caseGroup }),
346
- index: spec.laneIndex + 1,
347
- persona: spec.persona.id,
348
- device: spec.deviceName,
349
- resolution: spec.resolution,
350
- instructionDigest: spec.persona.promptDigest,
351
- ...(spec.reasoningEffort === undefined ? {} : { reasoningEffort: spec.reasoningEffort }),
352
- ...(spec.maxOutputTokens === undefined ? {} : { maxOutputTokens: spec.maxOutputTokens }),
353
- ...(spec.targetUrl === undefined ? {} : { targetDigest: digestUrl(spec.targetUrl) })
354
- }))
355
- };
356
- return { lanes, plan };
357
- }
358
- async function resolveCuaRerunSelection(args) {
359
- const source = await loadRunBundle(args.cwd, args.sourceRunId);
360
- if (!source) {
361
- return { ok: false, message: `source run not found or invalid: ${args.sourceRunId}` };
362
- }
363
- const bundle = source.bundle;
364
- if (bundle.mode !== "live") {
365
- return { ok: false, message: `source run ${bundle.runId} is ${bundle.mode}; rerun selection only applies to live CUA fan-out evidence.` };
366
- }
367
- const fanoutEvent = bundle.events.some((event) => event.type === "cua-lab.fanout.plan");
368
- if (!fanoutEvent || bundle.streams.length < 2) {
369
- return { ok: false, message: `source run ${bundle.runId} is not a CUA fan-out run.` };
370
- }
371
- const prior = bundle.streams
372
- .map(snapshotPriorCuaLane)
373
- .filter((lane) => lane !== null);
374
- const priorById = new Map(prior.map((lane) => [lane.laneId, lane]));
375
- if (priorById.size < 2) {
376
- return { ok: false, message: `source run ${bundle.runId} does not expose multiple lane ids.` };
377
- }
378
- const explicitLaneIds = uniqueLaneIds(args.laneIds ?? []);
379
- const selectedLaneIds = explicitLaneIds.length > 0
380
- ? explicitLaneIds
381
- : prior.filter((lane) => lane.rerunnable).map((lane) => lane.laneId);
382
- if (selectedLaneIds.length === 0) {
383
- return { ok: false, message: `source run ${bundle.runId} has no failed, blocked, timed-out, or hollow lanes to rerun.` };
384
- }
385
- const missingPrior = selectedLaneIds.filter((laneId) => !priorById.has(laneId));
386
- if (missingPrior.length > 0) {
387
- return { ok: false, message: `selected lane id(s) were not present in source run ${bundle.runId}: ${missingPrior.join(", ")}` };
388
- }
389
- const specsById = new Map(args.laneSpecs.map((spec) => [spec.laneId, spec]));
390
- const missingCurrent = selectedLaneIds.filter((laneId) => !specsById.has(laneId));
391
- if (missingCurrent.length > 0) {
392
- return { ok: false, message: `selected lane id(s) are not present in the current lab config ${args.config.id}: ${missingCurrent.join(", ")}` };
393
- }
394
- const selectedSpecs = selectedLaneIds.map((laneId) => specsById.get(laneId));
395
- const selectedPlanLaneIds = new Set(selectedLaneIds);
396
- const selectedPlanEntries = args.plan.lanes.filter((lane) => selectedPlanLaneIds.has(lane.id));
397
- const concurrency = Math.max(1, Math.min(args.plan.concurrency, selectedSpecs.length));
398
- const plan = {
399
- ...args.plan,
400
- laneCount: selectedSpecs.length,
401
- concurrency,
402
- waves: Math.ceil(selectedSpecs.length / concurrency),
403
- worstCaseSandboxMinutes: Math.round((selectedSpecs.length * resolvePerLaneSandboxMs(args.config)) / 60_000),
404
- lanes: selectedPlanEntries
405
- };
406
- const previous = selectedLaneIds.map((laneId) => priorById.get(laneId).previous);
407
- return {
408
- ok: true,
409
- laneSpecs: selectedSpecs,
410
- plan,
411
- rerun: {
412
- sourceRunId: bundle.runId,
413
- selectedLaneIds,
414
- previous
415
- }
416
- };
417
- }
418
- function uniqueLaneIds(values) {
419
- const seen = new Set();
420
- const result = [];
421
- for (const value of values) {
422
- const laneId = value.trim();
423
- if (!laneId || seen.has(laneId))
424
- continue;
425
- seen.add(laneId);
426
- result.push(laneId);
427
- }
428
- return result;
429
- }
430
- function snapshotPriorCuaLane(stream) {
431
- if (stream.kind !== "browser" || typeof stream.laneId !== "string" || !stream.laneId.trim()) {
432
- return null;
433
- }
434
- const actorStatus = stream.actor?.status;
435
- const completionReason = stream.actor?.completionReason;
436
- const reason = stream.ui?.state ?? stream.actor?.reason;
437
- const actions = stream.actor?.counts.actions ?? 0;
438
- const messages = stream.actor?.counts.messages ?? 0;
439
- const hollow = completionReason === "goal_satisfied" && actions === 0 && messages === 0;
440
- const rerunnable = stream.status !== "passed"
441
- || actorStatus === "failed"
442
- || actorStatus === "blocked"
443
- || actorStatus === "timed_out"
444
- || completionReason === "harness_error"
445
- || hollow;
446
- return {
447
- laneId: stream.laneId,
448
- previous: {
449
- laneId: stream.laneId,
450
- streamId: stream.id,
451
- status: stream.status,
452
- ...(reason === undefined ? {} : { reason }),
453
- ...(actorStatus === undefined ? {} : { actorStatus }),
454
- ...(completionReason === undefined ? {} : { completionReason })
455
- },
456
- rerunnable
457
- };
458
- }
459
- /**
460
- * Pure pre-flight plan resolver (runs in dry-run AND live). Returns the lane table, the
461
- * effective concurrency, the wave count, the per-lane session budget, and the worst-case total
462
- * sandbox-minutes — BEFORE any sandbox or provider call. The same plan appears in dry-run,
463
- * marked $0 (dryRun: true).
464
- */
465
- export function resolveCuaLanePlan(config, opts = {}) {
466
- return laneSpecsAndPlan(config, opts).plan;
467
- }
468
- /** Print the lane plan to stderr BEFORE any sandbox/provider call (public-safe: ids, devices,
469
- * digests, and budgets only — no prompt text, no secrets). */
470
- function emitPreflightPlan(plan, labId) {
471
- const lines = [];
472
- lines.push(`humanish cua fan-out plan (${labId}): ${plan.laneCount} lane(s), strategy ${plan.strategy}, concurrency ${plan.concurrency}${plan.envLoweredConcurrencyFrom === undefined ? "" : ` (lowered from ${plan.envLoweredConcurrencyFrom} by ${CUA_MAX_CONCURRENCY_ENV})`}, ${plan.waves} wave(s).`);
473
- lines.push(` per-lane session budget ${Math.round(plan.perLaneSessionBudgetMs / 1000)}s; worst-case ~${plan.worstCaseSandboxMinutes} sandbox-minutes total${plan.dryRun ? " (dry-run: $0)" : ""}.`);
474
- for (const lane of plan.lanes) {
475
- lines.push(` - ${formatLanePlanEntry(lane)}`);
476
- }
477
- process.stderr.write(`${lines.join("\n")}\n`);
478
- }
479
- function formatLanePlanEntry(lane) {
480
- const taxonomy = [
481
- lane.actorType ? `type=${lane.actorType}` : undefined,
482
- lane.surface ? `surface=${lane.surface}` : undefined,
483
- lane.caseGroup ? `case=${lane.caseGroup}` : undefined,
484
- lane.reasoningEffort ? `effort=${lane.reasoningEffort}` : undefined
485
- ].filter((part) => part !== undefined);
486
- return `${lane.id}: persona=${lane.persona}${taxonomy.length > 0 ? ` ${taxonomy.join(" ")}` : ""} device=${lane.device} ${lane.resolution[0]}x${lane.resolution[1]} prompt#${lane.instructionDigest}${lane.targetDigest ? ` target#${lane.targetDigest}` : ""}`;
487
- }
488
- /** Short id-safe suffix for a subject-phase RunEvent: drops the shared prefix/suffix so each
489
- * phase gets a distinct bundle event id (e.g. "clone", "state-before-build"). */
490
- function phaseEventIdSuffix(type) {
491
- return type
492
- .replace(/^cua-lab\.subject\./, "")
493
- .replace(/\.(started|completed)$/, "")
494
- .replace(/\./g, "-");
495
- }
496
- export function makeCuaRunBudget(maxTotalUsd) {
497
- const laneEstimates = new Map();
498
- return {
499
- maxTotalUsd,
500
- note(laneId, estimateUsd) {
501
- if (estimateUsd !== null)
502
- laneEstimates.set(laneId, estimateUsd);
503
- let total = 0;
504
- for (const value of laneEstimates.values())
505
- total += value;
506
- return total;
507
- }
508
- };
509
- }
510
- /** Build a lane's writeScreenshot closure: writes under screenshots/<screenshotDir>/ and records
511
- * the relative path the trace references (screenshots/<name> at N=1; screenshots/<laneId>/<name>
512
- * at N>1). */
513
- export function makeLaneWriteScreenshot(artifactRoot, spec, screenshots) {
514
- if (spec.screenshotDir) {
515
- assertSafeOutputPathSegment(spec.screenshotDir, "Screenshot lane id");
516
- }
517
- const dirParts = spec.screenshotDir ? ["screenshots", spec.screenshotDir] : ["screenshots"];
518
- const relPrefix = spec.screenshotDir ? path.posix.join("screenshots", spec.screenshotDir) : "screenshots";
519
- return async (name, bytes) => {
520
- assertSafeOutputPathSegment(name, "Screenshot name");
521
- const rel = path.posix.join(relPrefix, name);
522
- assertScreenshotEvidence(rel, bytes);
523
- await writeContainedOutputFile(artifactRoot, path.join(...dirParts, name), bytes);
524
- screenshots.push(rel);
525
- return rel;
526
- };
527
- }
528
- /** A blocked lane outcome (pipeline gate / fail-fast skipped it before it ran). */
529
- function blockedLaneOutcome(spec, reason) {
530
- return {
531
- spec,
532
- killed: false,
533
- streamUrlPresent: false,
534
- screenshots: [],
535
- stateStepRecords: [],
536
- phaseRecords: [],
537
- warnings: [],
538
- skippedReason: reason,
539
- noEngagement: false,
540
- selfReportedBlocker: false,
541
- reportedFriction: false,
542
- harnessError: false
543
- };
544
- }
545
- // "can't" followed by a PERCEPTION verb describes what the screen showed, not an inability to
546
- // proceed: "the canvas truncates it so you can't even read the whole thing", "I can't tell from
547
- // the screen whether the rename is persisted", "so I could not read its full description". Five
548
- // of five completed live runs on 2026-09-01 (two on drawDB, three on the planted benchmark app)
549
- // were refused as "not a credible pass" on exactly these sentences, every one a defect report
550
- // written AFTER the participant reached the goal. The more precisely a participant describes a
551
- // display defect, the more likely the scan was to refuse the run — the incentive inversion #453
552
- // fixed for resolved arcs, back in a new shape. "could not complete", "could not connect",
553
- // "unable to get focus" still count: those name an inability to act.
554
- const PERCEPTION_AFTER_MODAL = /\b(can'?t|cannot|could ?not|couldn'?t|unable to|wasn'?t able to)\s+(even\s+|quite\s+|really\s+|fully\s+)?(read|see|tell|view|make out|verify|confirm|be sure|be certain|judge|know)\b/g;
555
- function hasBlockerLanguage(text) {
556
- return /\b(can'?t|cannot|could not|unable|blocked|blocker|failed|invalid|not set)\b/.test(text)
557
- || /\b(shows|showing|hit|encountered|returned|got)\b.{0,80}\berror\b/.test(text)
558
- || /\berror[:.]/.test(text)
559
- || /what would you like me to do|please tell me|need (the )?(task|credentials|instructions)/.test(text);
560
- }
561
- /** The friction scan (inclusive): does the narrative report ANY blocker-shaped language,
562
- * resolved or not? Feeds the participants `reportedFriction` tally and feedback candidates. */
563
- // Report-shaped language: what a participant writes when it finished AND has something to say.
564
- // Every one of the day's eleven drawDB reports (2026-09-01) opened a section "What confused me"
565
- // or "Accessibility defects:"; none contained a blocker word, so none became a feedback candidate
566
- // and a lane that had just replicated a keyboard-accessibility defect three times drafted "Live
567
- // study completed without a participant-reported finding". Friction is the INCLUSIVE scan; a
568
- // false positive here adds a candidate a person then reads, which is the cheap direction.
569
- const REPORTED_DEFECT_LANGUAGE = /\b(defects?|bugs?|accessibilit(y|ies)|inaccessible|not (keyboard|screen.?reader)[- ]?accessible|confus(ed|ing)|hesitat(ed|ion)|unexpected(ly)?|unclear|hard to (find|tell|see|read|reach)|no (visible )?focus|overlap(ped|ping|s)?|truncat(ed|es|ion)|cut off|did nothing|nothing happened|no effect)\b/;
570
- // The friction scan's own negations (#614). "Nothing was confusing", "no defects", "not unclear"
571
- // are what a participant writes when it has NOTHING to report, and until 2026-09-03 each of them
572
- // counted as reported friction and became a feedback candidate whose "actual" was a sentence
573
- // reporting no problem. Only the report-shaped adjectives are negatable here: "no visible focus",
574
- // "not keyboard-accessible" and "did nothing" are defects and stay.
575
- const NEGATED_REPORT_ITEM = String.raw `(?:confus(?:ed|ing|ion)|unclear(?:\s+error\s+output)?|unexpected(?:ly)?|hesitat(?:ed|ion|ions)|surpris(?:ed|ing|es)|defects?|bugs?|overlap(?:ped|ping|s)?|truncat(?:ed|es|ion)|hard to (?:find|tell|see|read|reach)|blockers?|blocking issues?|errors?(?:\s+output)?|failures?|problems?|issues?)`;
576
- const NEGATED_REPORT_QUALIFIERS = String.raw `(?:(?:really|particularly|especially|major|minor|real|actual|remaining|functional|obvious|noticeable|significant|any|a|an)\s+)*`;
577
- const NEGATED_REPORT_MODIFIERS = String.raw `(?:(?:was|were|felt|seemed|really|particularly|especially|major|minor|real|actual|remaining|functional|obvious|noticeable|significant|any|a|an|encounter(?:ed)?|experience(?:d)?|notice(?:d)?|observe(?:d)?|feel|find|found|have|had)\s+)*`;
578
- const NEGATED_REPORT_LANGUAGE = new RegExp(String.raw `\b(?:nothing|no|not|never|without|none|(?:did|do|does|was|were|has|have|had)n['’]t)\s+${NEGATED_REPORT_MODIFIERS}${NEGATED_REPORT_ITEM}\b`
579
- // Negation scopes over a coordinated report list, not the rest of the sentence. In
580
- // particular, leave "but the label was confusing" and "and Save did nothing" intact.
581
- + String.raw `(?:\s*,?\s+(?:or|nor|and)\s+${NEGATED_REPORT_QUALIFIERS}${NEGATED_REPORT_ITEM}\b)*`
582
- // Keep the predicate inside its negation: "No errors blocked me" reports no blocker.
583
- + String.raw `(?:\s+(?:blocked|stopped|prevented)\s+(?:me|us|it)\b)?`, "g");
584
- function stripNegatedReportLanguage(text) {
585
- // "Not without hesitation" reports hesitation; do not let the inner "without" erase it.
586
- return text.replace(/\b(?:not|never)\s+without\b/g, "with").replace(NEGATED_REPORT_LANGUAGE, " ");
587
- }
588
- function completionReasonContradictsGoal(reason) {
589
- // Preserve the full negated list before the older blocker-specific rules remove its first
590
- // noun ("no issues or hesitation"). Matching reports first also avoids their broad encounter
591
- // clause rule swallowing a genuine subsequent observation.
592
- const text = stripQuotedSpans(stripNegatedNonBlockerPhrases(stripNegatedReportLanguage(stripCodeExamples(reason).toLowerCase())));
593
- return hasBlockerLanguage(text) || REPORTED_DEFECT_LANGUAGE.test(text);
594
- }
595
- /** Code/documentation excerpts are quoted material, not participant observations. */
596
- function stripCodeExamples(text) {
597
- return text
598
- // Include an unterminated fence: copied text is not promoted just because its closing fence
599
- // was omitted. Match the same marker so backticks inside a tilde fence cannot end it early.
600
- .replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?(?:^[ \t]*\1[ \t]*$|(?![\s\S]))/gm, " ")
601
- .replace(/(`+)[^`\n]*\1/g, " ");
602
- }
603
- /** Interim messages also contain plans and hypotheses. Admit observed-report clauses only;
604
- * the established closing-report scan remains separate. This is a conservative text heuristic,
605
- * not an assertion that every mention of a defect is evidence that one happened. */
606
- function interimMessageReportsFriction(message) {
607
- const prose = stripQuotedSpans(stripCodeExamples(message).toLowerCase());
608
- const clauses = prose.split(/(?<=[.!?])\s+|[;\n]+|,\s*(?:but|so|however|yet)\s+|\s+so\s+(?=i\b|we\b)/);
609
- return clauses.some((clause) => {
610
- // A condition, question, intention, or conjecture does not assert an observed result.
611
- // Clause splitting above keeps "Save did nothing, so I will try Enter" observable.
612
- if (/\?|\b(?:if|unless|whether|maybe|perhaps|suppose|hypothetically|might|may|would|should)\b|\bcould\b(?!\s+not\b)/.test(clause)
613
- || /\b(?:i|we)(?:['’]ll|\s+(?:will|plan|intend|want|hope|suspect|wonder))\b|\bgoing to\b|\blet['’]s\b/.test(clause)
614
- || /\b(?:task|goal|mission|objective|plan)\s+(?:(?:is|was)\s+)?to\b|^\s*(?:check|test|look|checking|testing)\b/.test(clause))
615
- return false;
616
- // A topic is not a defect ("the accessibility guide is open", "shows error-handling docs").
617
- // Actual friction in those surfaces still qualifies: "the error guide was confusing".
618
- const observation = clause
619
- .replace(/\baccessibilit(?:y|ies)\b/g, " ")
620
- .replace(/\berror[- ]handling\b|\berror\s+(?:documentation|docs?|guides?|reference|examples?)\b/g, " ");
621
- const assertsObservation = /\b(?:is|are|was|were|has|had|did|does|shows?|showed|seems?|seemed|looks?|looked|found|noticed|saw|hit|encountered|felt|got|failed|returned|cannot|can['’]?t|unable|could not)\b|\b(?:overlap(?:ped|ping|s)?|truncat(?:ed|es)|cut off|nothing happened|no (?:visible )?focus)\b/.test(observation);
622
- return assertsObservation && completionReasonContradictsGoal(observation);
623
- });
624
- }
625
- /** The verdict scan (strict): like the friction scan, but resolved-arc segments are stripped
626
- * first — failure narration the participant itself reports as overcome is friction on the
627
- * road, not a blocker at the destination (#453). */
628
- function completionReasonBlocksVerdict(reason) {
629
- // Perception phrases are stripped for the VERDICT only: "I could not read the full description"
630
- // is friction worth a tally count and a feedback candidate (the friction scan above keeps it),
631
- // and it is not a reason to refuse the pass.
632
- return hasBlockerLanguage(stripResolvedArcSegments(stripQuotedSpans(stripNegatedNonBlockerPhrases(reason.toLowerCase())))
633
- .replace(PERCEPTION_AFTER_MODAL, ""));
634
- }
635
- // A failure segment counts as a resolved arc when the recovery is self-reported either in the
636
- // SAME segment ("the import failed but then went through") or — the common report shape — in the
637
- // immediately FOLLOWING segment as a retry/alternative that succeeded ("my first import failed
638
- // with a parser error. A simpler SQL import succeeded."). The lookahead demands the retry flavor
639
- // on purpose: unrelated praise ("Separately, the search box worked") must never launder an
640
- // unresolved failure. "Login failed so I gave up" has no recovery anywhere and stays a blocker.
641
- // (#453 — the run-1 false negative: a defect report after demonstrated success failed the lane,
642
- // an incentive inversion against exactly the participant behavior a study wants most.)
643
- const RESOLUTION_TERMS = /\b(succeed(?:ed|s)?|success(?:ful|fully)?|worked|works around|then worked|now works?|resolved|fixed|recovered|got it working|went through)\b/;
644
- const RETRY_RESOLUTION = /\b(simpler|simplified|retry(?:ing)?|retried|second (?:attempt|try)|another (?:attempt|try|approach)|different (?:approach|way|route)|instead|then|eventually|after that)\b[^.!?\n]{0,80}\b(succeed(?:ed|s)?|success(?:ful|fully)?|worked|went through|completed|passed)\b/;
645
- /** Drop sentence/bullet segments whose failure language is part of a self-reported RESOLVED arc. */
646
- function stripResolvedArcSegments(text) {
647
- const segments = text.split(/(?<=[.!?])\s+|\n+/);
648
- return segments
649
- .filter((segment, index) => {
650
- if (!hasBlockerLanguage(segment))
651
- return true;
652
- if (RESOLUTION_TERMS.test(segment))
653
- return false;
654
- const next = segments[index + 1];
655
- return !(next !== undefined && RETRY_RESOLUTION.test(next));
656
- })
657
- .join(" ");
658
- }
659
- // Negations that DESCRIBE a defect rather than deny one. Kept out of every clause drop below.
660
- const DEFECT_SHAPED_NEGATION = /\bno\s+(?:visible\s+)?focus\b|\bno\s+(?:keyboard|screen.?reader)[- ]?(?:access|path|route|way|alternative|equivalent)|\bnot\s+(?:keyboard|screen.?reader)[- ]?accessible\b|\bno\s+(?:effect|feedback|response)\b|\b(?:did|does)\s+nothing\b|\bnothing\s+happened\b/;
661
- function stripNegatedNonBlockerPhrases(text) {
662
- return text
663
- // FIRST, before the narrower rules eat the "no blockers" and leave "encountered ... error"
664
- // behind: "I encountered no blockers or unclear error output." refused a clean passing run on
665
- // 2026-09-01. A verb of encounter followed by "no" negates the whole clause, so drop the clause.
666
- // ... unless the clause names a DEFECT: "the delete control had no visible focus" is a
667
- // finding, and the verb it happens to use must not decide whether it counts (#622).
668
- .replace(/\b(?:encountered|hit|saw|found|met|had|got|ran into)\s+no\s+[^.!?\n]*/g, (clause) => DEFECT_SHAPED_NEGATION.test(clause) ? clause : " ")
669
- .replace(/\bno\s+(?:real\s+|remaining\s+|actual\s+)?(?:blocker|blockers|blocking issue|blocking issues|error|errors|failure|failures)\s+(?:was\s+|were\s+)?(?:encountered|observed|found|hit|seen|reported|detected)\b/g, "")
670
- .replace(/\bwithout\s+(?:a\s+|any\s+)?(?:real\s+|remaining\s+|actual\s+)?(?:blocker|blockers|blocking issue|blocking issues|error|errors|failure|failures)\b/g, "")
671
- .replace(/\bnot\s+(?:blocked|a blocker|an error|failed)\b/g, "")
672
- // "No functional failures blocked me" downgraded a clean passing run to a lab failure on
673
- // 2026-09-01. The adjective list above is closed (real|remaining|actual), so an ordinary
674
- // qualifier like "functional" slipped through and the trailing verb "blocked" tripped the
675
- // scan. Allow up to two intervening words, and cover the verb form directly.
676
- .replace(/\bno\s+(?:\w+\s+){0,2}(?:blocker|blockers|blocking issues?|errors?|failures?|problems?|issues?)\b(?:\s+(?:blocked|stopped|prevented)\s+(?:me|us|it))?/g, " ")
677
- .replace(/\bnothing\s+(?:\w+\s+){0,2}(?:blocked|stopped|prevented)\s+(?:me|us|it)\b/g, " ");
678
- }
679
- /**
680
- * Remove double-quoted spans and markdown blockquote lines before the blocker scan, so a persona
681
- * that faithfully QUOTES the subject app's own copy (e.g. a banner reading "cannot be undone") is
682
- * not misread as the actor reporting its OWN blocker. Only double quotes (straight and smart) and
683
- * `>` blockquotes are stripped — never single quotes, which would mangle contractions like `can't`.
684
- */
685
- function stripQuotedSpans(text) {
686
- return text
687
- .replace(/"[^"]*"/g, " ")
688
- .replace(/“[^”]*”/g, " ")
689
- .replace(/^\s*>.*$/gm, " ");
690
- }
691
- function traceHasStopWhenMatch(session) {
692
- return session.trace.items.some((item) => item.kind === "notice"
693
- && item.status === "matched"
694
- // A dwell window that ended the session (then: stop) is the same class of harness-owned,
695
- // structured completion as a matched stopWhen (#510).
696
- && (item.title.startsWith("stopWhen matched") || item.title === "dwell window complete"));
697
- }
698
- /**
699
- * A goal_satisfied lane counts as a self-reported blocker ONLY when its final narrative contradicts
700
- * the goal AND the run's own stop predicate did NOT fire. A matched stopWhen is independent,
701
- * structured completion evidence, so it overrides a text scan of the free-form narrative — which can
702
- * otherwise trip on the subject app's OWN quoted copy (e.g. a relayed "cannot be undone" banner).
703
- * Resolved-arc segments never block the verdict (#453). Returns the offending reason, or undefined
704
- * when the lane is a clean pass. Exported for testing.
705
- */
706
- export function resolveSelfReportedBlocker(session) {
707
- // The participant's own word wins when it gave one (#570): a declared "reached" is not re-read
708
- // for blocker-shaped phrases, and a declared "blocked" is a blocker whatever the paragraph says.
709
- const declared = session?.trace.declaredOutcome;
710
- if (session !== undefined && declared !== undefined) {
711
- return declared === "blocked" && session.completionReason === "goal_satisfied" ? session.reason : undefined;
712
- }
713
- return session?.completionReason === "goal_satisfied"
714
- && completionReasonBlocksVerdict(session.reason)
715
- && !traceHasStopWhenMatch(session)
716
- ? session.reason
717
- : undefined;
718
- }
719
- /**
720
- * Friction is independent of how a completed session ended (#657). Read the participant's
721
- * redacted messages, including earlier reports, rather than the harness-owned reason that
722
- * stopWhen/dwell writes. Reasoning, observations, and notices are not participant reports.
723
- * Resolved arcs still count (#453); quoted copy and negated reports still do not. This read
724
- * never changes the verdict. Exported for testing.
725
- */
726
- export function resolveSelfReportedFriction(session) {
727
- if (session?.completionReason !== "goal_satisfied")
728
- return undefined;
729
- // Friction stays a read of the narrative even when the outcome was declared: a participant who
730
- // reached the goal and described what was hard on the way has reported friction.
731
- if (session.trace.declaredOutcome === "blocked")
732
- return session.reason;
733
- const messages = session.trace.items
734
- .filter((item) => item.kind === "message" && item.id !== session.trace.debrief?.messageId)
735
- .map((item) => item.text?.trim() ?? "")
736
- .filter((text) => text.length > 0);
737
- // A custom session may keep its closing report only in reason even when earlier messages exist.
738
- // Structured stop/dwell reasons are controller text and never enter this closing-report path.
739
- const closingReport = traceHasStopWhenMatch(session) ? undefined : session.reason.trim();
740
- // One candidate per participant, with exact repeats removed (the closing report often repeats
741
- // a prior turn). Earlier turns require observed-report clauses, not arbitrary defect mentions.
742
- const typedReports = session.trace.debrief?.status === "completed"
743
- ? session.trace.debrief.report?.frictionReports ?? [] : [];
744
- const reports = [...new Set([...typedReports, ...messages.filter((message) => message === closingReport
745
- ? completionReasonContradictsGoal(message)
746
- : interimMessageReportsFriction(message))])];
747
- if (closingReport && completionReasonContradictsGoal(closingReport) && !reports.includes(closingReport)) {
748
- reports.push(closingReport);
749
- }
750
- if (reports.length > 0)
751
- return reports.join("\n\n");
752
- return undefined;
753
- }
754
- /** Run one participant against a prepared desktop. The adapter owns provisioning, final
755
- * evidence and cleanup; this runner owns the model, trace and participant outcome. */
756
- export async function runCuaLane(spec, deps) {
757
- const { config, env } = deps;
758
- let codexParticipant;
759
- let claudeSession;
760
- let localAgentProvider;
761
- const warnings = [];
762
- const screenshots = [];
763
- const writeScreenshot = makeLaneWriteScreenshot(deps.artifactRoot, spec, screenshots);
764
- let session;
765
- let sessionError;
766
- let provisioned = false;
767
- let signaled = false;
768
- const signal = (ok) => {
769
- if (!signaled && deps.signalProvisioned) {
770
- signaled = true;
771
- deps.signalProvisioned(ok);
772
- }
773
- };
774
- const desktopLane = deps.createDesktopLane?.(spec, warnings, deps.artifactRoot) ?? createE2BCuaDesktopLane(spec, deps, warnings);
775
- try {
776
- await desktopLane.prepare();
777
- const ready = await desktopLane.openSession();
778
- if (deps.hooks.buildProvider) {
779
- localAgentProvider = await deps.hooks.buildProvider({ config, actor: deps.descriptor, lane: spec, executor: ready.executor });
780
- }
781
- else if (deps.localAgent === "codex") {
782
- // Hosted local-agent studies use the same native participant engine as local desktops.
783
- // Operator auth deliberately retains the operator's Codex home, config and supported auth
784
- // stores instead of applying the isolated restricted-account profile used by local studies.
785
- codexParticipant = createRestrictedCodexParticipant({
786
- authMode: "operator",
787
- ...(spec.reasoningEffort === undefined ? {} : { reasoningEffort: spec.reasoningEffort }),
788
- ...(config.actors[0]?.model === undefined ? {} : { model: config.actors[0].model }),
789
- ...(ready.executor.speechEnabled === true ? { speechEnabled: true } : {}),
790
- session: { env }
791
- });
792
- localAgentProvider = codexParticipant.provider;
793
- }
794
- else if (deps.localAgent === "claude") {
795
- // One session for the whole run, like the codex thread above (#520). The one-shot
796
- // provider (createLocalAgentProvider) spawned `claude -p` per turn, and every turn
797
- // started with no memory of the last. HUMANISH_LOCAL_AGENT_ONE_SHOT=1 keeps that path
798
- // reachable as a MEASUREMENT switch: MemTrapBench (2026-08) reports memory frameworks
799
- // degrading agent performance by 10-40% on some tasks, so "remembers" has to be measured
800
- // against "does not" on the same lab, not assumed. The trace records which one ran.
801
- const oneShot = env.HUMANISH_LOCAL_AGENT_ONE_SHOT !== undefined
802
- && env.HUMANISH_LOCAL_AGENT_ONE_SHOT !== ""
803
- && env.HUMANISH_LOCAL_AGENT_ONE_SHOT !== "0";
804
- if (oneShot) {
805
- localAgentProvider = createLocalAgentProvider({
806
- agent: "claude",
807
- ...(spec.reasoningEffort === undefined ? {} : { reasoningEffort: spec.reasoningEffort }),
808
- ...(config.actors[0]?.model === undefined ? {} : { model: config.actors[0].model })
809
- });
810
- }
811
- else {
812
- claudeSession = await startClaudeSession({
813
- ...(spec.reasoningEffort === undefined ? {} : { reasoningEffort: spec.reasoningEffort }),
814
- ...(config.actors[0]?.model === undefined ? {} : { model: config.actors[0].model })
815
- });
816
- localAgentProvider = claudeSession.provider;
817
- }
818
- }
819
- // World is ready: release the pipeline gate so the remaining lanes may start.
820
- provisioned = true;
821
- signal(true);
822
- // The FAIL-CLOSED spend cap (execution.caps.maxUsd) is wired into the loop as maxUsd + an
823
- // injected pure per-turn estimator keyed on the resolved model. Preflight already refused a
824
- // cap on an unpriced model, so the estimate is measurable whenever a cap is in force. The
825
- // model id here matches provider.version (openai-responses-cu resolves the default when unset).
826
- const capModelId = config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL;
827
- const maxUsd = config.execution?.caps?.maxUsd;
828
- const sessionOptions = {
829
- instructions: ready.inbox
830
- ? withInboxMission(spec, ready.inbox.url, ready.inbox.address, ready.inbox.receiving).instructions
831
- : spec.instructions,
832
- persona: spec.persona,
833
- timeoutMs: deps.timeoutMs,
834
- // The brain is either a keyed API client or a CLI the operator is already signed in to.
835
- // Everything below this line — loop, executor, trace, affordances — is identical either
836
- // way, which is what makes a local-agent run comparable to an API one.
837
- ...(localAgentProvider === undefined ? {} : { provider: localAgentProvider }),
838
- openai: {
839
- apiKey: deps.openaiApiKey,
840
- ...(config.actors[0]?.model ? { model: config.actors[0].model } : {}),
841
- // Per-LANE, not per-actor: two lanes at different efforts is the control this exists for.
842
- ...(spec.reasoningEffort === undefined ? {} : { reasoningEffort: spec.reasoningEffort }),
843
- ...(spec.maxOutputTokens === undefined ? {} : { maxOutputTokens: spec.maxOutputTokens })
844
- },
845
- ...(maxUsd === undefined
846
- ? {}
847
- : {
848
- maxUsd,
849
- estimateTurnCostUsd: (usage) => estimateActorCostForExecution(usage, localAgentProvider?.version ?? capModelId, localAgentProvider?.executionProfile).estimatedCostUsd
850
- }),
851
- executor: ready.executor,
852
- redactScreenshots: deps.redactScreenshots,
853
- scrubText: deps.scrubKnownValues,
854
- writeScreenshot,
855
- ...(spec.idleSteps === undefined ? {} : { idleSteps: spec.idleSteps }),
856
- ...(spec.noProgressSteps === undefined ? {} : { noProgressSteps: spec.noProgressSteps }),
857
- ...(spec.stopWhen === undefined ? {} : { stopWhen: spec.stopWhen }),
858
- ...(spec.dwell === undefined ? {} : { dwell: spec.dwell }),
859
- ...(spec.tasks === undefined ? {} : { tasks: spec.tasks }),
860
- // The STUDY budget (#299): this lane notes its own running estimate on the shared ledger
861
- // and stops when the RUN total crosses the cap — independent of the per-lane maxUsd above.
862
- ...(deps.runBudget === undefined
863
- ? {}
864
- : {
865
- overRunBudget: (usage) => {
866
- const estimate = estimateActorCostForExecution(usage, localAgentProvider?.version ?? capModelId, localAgentProvider?.executionProfile).estimatedCostUsd;
867
- const totalUsd = deps.runBudget.note(spec.laneId, estimate);
868
- return totalUsd > deps.runBudget.maxTotalUsd
869
- ? `study budget reached: the run's estimated model spend $${round6(totalUsd)} crossed execution.caps.maxTotalUsd=$${deps.runBudget.maxTotalUsd}; this lane stops here and sibling lanes stop at their next turn`
870
- : null;
871
- }
872
- }),
873
- ...(deps.onObservedUrl === undefined ? {} : { onObservedUrl: deps.onObservedUrl }),
874
- ...(deps.onMessage === undefined ? {} : { onMessage: deps.onMessage }),
875
- ...(deps.onScreenshot === undefined ? {} : { onScreenshot: deps.onScreenshot }),
876
- ...(deps.onTrace === undefined
877
- ? {}
878
- : {
879
- // Forwards the RUNNING usage as well: the lane is where both are known, and usage
880
- // without it never reaches the flush — which is how the live cost stayed unknown.
881
- onTrace: (items, usage, metadata) => deps.onTrace?.(spec.laneId, items, usage, metadata)
882
- })
883
- };
884
- session = await deps.runSession(sessionOptions);
885
- }
886
- catch (error) {
887
- sessionError = redactText(deps.scrubKnownValues(toErrorMessage(error)));
888
- }
889
- finally {
890
- try {
891
- if (codexParticipant === undefined)
892
- await localAgentProvider?.close?.();
893
- }
894
- catch {
895
- warnings.push("Model provider cleanup is unconfirmed.");
896
- sessionError ??= "Model provider cleanup is unconfirmed.";
897
- }
898
- try {
899
- const cleanup = await codexParticipant?.close();
900
- if (cleanup?.status === "unconfirmed") {
901
- warnings.push("Model provider cleanup is unconfirmed.");
902
- sessionError ??= "Model provider cleanup is unconfirmed.";
903
- }
904
- }
905
- catch {
906
- warnings.push("Model provider cleanup is unconfirmed.");
907
- sessionError ??= "Model provider cleanup is unconfirmed.";
908
- }
909
- try {
910
- await claudeSession?.close();
911
- }
912
- catch {
913
- warnings.push('Claude session cleanup failed; desktop cleanup will still run.');
914
- }
915
- try {
916
- if (!provisioned)
917
- signal(false);
918
- }
919
- finally {
920
- await desktopLane.finalize({ failed: sessionError !== undefined || session === undefined });
921
- }
922
- }
923
- if (session) {
924
- // Per-lane model-token cost ESTIMATE, attached to the trace before it is persisted (the model
925
- // id is authoritative here — provider.version). Kept at the lab boundary so the pure loop
926
- // never depends on the operator rate table. estimateActorCost declares absent (null) for an
927
- // unknown rate / missing usage rather than guessing.
928
- session.trace.estimatedCost = estimateActorCostForExecution(session.trace.tokenUsage, session.trace.ids.model, session.trace.executionProfile);
929
- await writeContainedOutputFile(deps.artifactRoot, spec.traceArtifactPath, `${JSON.stringify(session.trace, null, 2)}\n`, "utf8");
930
- if (session.trace.redaction.screenshots === "raw") {
931
- warnings.push("Screenshots are full-fidelity (raw) for local use — the bundle stays in gitignored .humanish and nothing scans these pixels; review them before sharing anywhere. Set policies.redactScreenshots: true to blur a share-as-is bundle.");
932
- }
933
- }
934
- const noEngagement = session !== undefined
935
- && session.completionReason === "goal_satisfied"
936
- && (session.trace.counts.actions ?? 0) === 0
937
- && (session.trace.counts.messages ?? 0) === 0
938
- && !traceHasStopWhenMatch(session);
939
- if (noEngagement) {
940
- warnings.push("Actor returned goal_satisfied with ZERO actions and ZERO messages — it likely saw a blank or still-loading screen and stopped without engaging. NOT counted as a pass. Check the screenshot; raise execution.timeoutMs or confirm the subject painted before the first turn.");
941
- }
942
- const blockerReason = resolveSelfReportedBlocker(session);
943
- const selfReportedBlocker = blockerReason !== undefined;
944
- const reportedFriction = resolveSelfReportedFriction(session) !== undefined;
945
- if (selfReportedBlocker) {
946
- warnings.push(`Actor returned goal_satisfied while its final message describes a blocker or asks for missing instructions — NOT counted as a pass: ${redactText(deps.scrubKnownValues(blockerReason))}`);
947
- }
948
- const harnessError = sessionError !== undefined || session?.completionReason === "harness_error";
949
- return {
950
- spec,
951
- ...(session ? { session } : {}),
952
- ...(sessionError === undefined ? {} : { sessionError }),
953
- ...desktopLane.snapshot(),
954
- screenshots,
955
- warnings,
956
- noEngagement,
957
- selfReportedBlocker,
958
- reportedFriction,
959
- harnessError,
960
- };
961
- }
962
- /** Run the single IN-PROCESS lane (a custom executor + provider; NO E2B). Always one lane. */
963
- async function runInProcessLane(spec, deps) {
964
- const warnings = [];
965
- const screenshots = [];
966
- const writeScreenshot = makeLaneWriteScreenshot(deps.artifactRoot, spec, screenshots);
967
- let session;
968
- let sessionError;
969
- let provider;
970
- try {
971
- const executor = await deps.hooks.buildExecutor({ config: deps.config, actor: deps.descriptor, appUrl: deps.appUrl });
972
- provider = await deps.hooks.buildProvider({ config: deps.config, actor: deps.descriptor, lane: spec, executor });
973
- const sessionOptions = {
974
- instructions: spec.instructions,
975
- persona: spec.persona,
976
- timeoutMs: deps.timeoutMs,
977
- provider,
978
- executor,
979
- redactScreenshots: deps.redactScreenshots,
980
- scrubText: deps.scrubKnownValues,
981
- writeScreenshot,
982
- ...(deps.onTrace === undefined ? {} : { onTrace: (items, usage, metadata) => deps.onTrace?.(spec.laneId, items, usage, metadata) }),
983
- ...(spec.stopWhen === undefined ? {} : { stopWhen: spec.stopWhen }),
984
- ...(spec.dwell === undefined ? {} : { dwell: spec.dwell }),
985
- ...(spec.tasks === undefined ? {} : { tasks: spec.tasks })
986
- };
987
- session = await deps.runSession(sessionOptions);
988
- }
989
- catch (error) {
990
- sessionError = redactText(deps.scrubKnownValues(toErrorMessage(error)));
991
- }
992
- finally {
993
- try {
994
- await provider?.close?.();
995
- }
996
- catch {
997
- sessionError ??= "Model provider cleanup is unconfirmed.";
998
- }
999
- }
1000
- if (session) {
1001
- await writeContainedOutputFile(deps.artifactRoot, spec.traceArtifactPath, `${JSON.stringify(session.trace, null, 2)}\n`, "utf8");
1002
- if (session.trace.redaction.screenshots === "raw") {
1003
- warnings.push("Screenshots are full-fidelity (raw) for local use — the bundle stays in gitignored .humanish and nothing scans these pixels; review them before sharing anywhere. Set policies.redactScreenshots: true to blur a share-as-is bundle.");
1004
- }
1005
- }
1006
- const noEngagement = session !== undefined
1007
- && session.completionReason === "goal_satisfied"
1008
- && (session.trace.counts.actions ?? 0) === 0
1009
- && (session.trace.counts.messages ?? 0) === 0
1010
- && !traceHasStopWhenMatch(session);
1011
- if (noEngagement) {
1012
- warnings.push("Actor returned goal_satisfied with ZERO actions and ZERO messages — it likely saw a blank or still-loading screen and stopped without engaging. NOT counted as a pass. Check the screenshot; raise execution.timeoutMs or confirm the subject painted before the first turn.");
1013
- }
1014
- const blockerReason = resolveSelfReportedBlocker(session);
1015
- const selfReportedBlocker = blockerReason !== undefined;
1016
- const reportedFriction = resolveSelfReportedFriction(session) !== undefined;
1017
- if (selfReportedBlocker) {
1018
- warnings.push(`Actor returned goal_satisfied while its final message describes a blocker or asks for missing instructions — NOT counted as a pass: ${blockerReason}`);
1019
- }
1020
- return {
1021
- spec,
1022
- ...(session ? { session } : {}),
1023
- ...(sessionError === undefined ? {} : { sessionError }),
1024
- killed: false,
1025
- streamUrlPresent: false,
1026
- screenshots,
1027
- stateStepRecords: [],
1028
- phaseRecords: [],
1029
- warnings,
1030
- noEngagement,
1031
- selfReportedBlocker,
1032
- reportedFriction,
1033
- harnessError: sessionError !== undefined || session?.completionReason === "harness_error",
1034
- entryKind: "local-app"
1035
- };
1036
- }
1037
- /**
1038
- * Run N>1 E2B lanes with bounded concurrency, a pipeline gate (lane 1 provisions before the rest
1039
- * start), and session fail-fast on HARNESS errors only (queued lanes become `blocked` with a
1040
- * pinned reason + a fail-fast event; mission verdicts never trip it). Each lane tears down ITS
1041
- * OWN sandbox by id; nothing here ever enumerates.
1042
- */
1043
- /** Exported for the #342 total-runner tests: the injectable runner lets a test make one lane
1044
- * THROW (the exact class the guard exists for) without a live sandbox. Production always uses
1045
- * the default. */
1046
- export async function runCuaLanes(laneSpecs, deps, concurrency, runLane = runCuaLane) {
1047
- const failFast = { tripped: false, reason: "" };
1048
- let resolveGate;
1049
- let rejectGate;
1050
- const gate = new Promise((resolve, reject) => {
1051
- resolveGate = resolve;
1052
- rejectGate = () => reject(new Error("gate"));
1053
- });
1054
- // The gate is rejected on lane-0 provisioning failure; swallow the unhandled rejection if no
1055
- // later lane ever awaits it (concurrency could let lane 0 finish alone).
1056
- gate.catch(() => undefined);
1057
- const outcomes = await mapWithConcurrency(laneSpecs, concurrency, async (spec, index) => {
1058
- if (index > 0) {
1059
- try {
1060
- await gate;
1061
- }
1062
- catch {
1063
- return blockedLaneOutcome(spec, `skipped: lane ${laneSpecs[0]?.laneId ?? "lane-01"} failed to provision its world (pipeline gate)`);
1064
- }
1065
- }
1066
- if (failFast.tripped) {
1067
- return blockedLaneOutcome(spec, `skipped: ${failFast.reason}`);
1068
- }
1069
- // The lane runner is TOTAL (#342): every exit path returns a recorded outcome. Without this
1070
- // guard, one lane's late throw (e.g. its trace write hitting ENOSPC after its own sandbox was
1071
- // already torn down) rejected the whole map while sibling workers kept launching sandboxes
1072
- // nobody would ever record — the run spent money and then reported nothing.
1073
- let outcome;
1074
- try {
1075
- outcome = await runLane(spec, {
1076
- ...deps,
1077
- ...(index === 0
1078
- ? {
1079
- signalProvisioned: (ok) => {
1080
- if (ok) {
1081
- resolveGate?.();
1082
- }
1083
- else {
1084
- rejectGate?.();
1085
- }
1086
- }
1087
- }
1088
- : {})
1089
- });
1090
- }
1091
- catch (error) {
1092
- // Lane 0 may have thrown before signaling the provisioning gate — release the followers as
1093
- // blocked rather than leaving them awaiting a gate that will never settle.
1094
- if (index === 0)
1095
- rejectGate?.();
1096
- const detail = redactText(toErrorMessage(error));
1097
- outcome = {
1098
- spec,
1099
- killed: false,
1100
- streamUrlPresent: false,
1101
- screenshots: [],
1102
- stateStepRecords: [],
1103
- phaseRecords: [],
1104
- warnings: [],
1105
- noEngagement: false,
1106
- selfReportedBlocker: false,
1107
- reportedFriction: false,
1108
- harnessError: true,
1109
- sessionError: `lane runner threw outside the session guard: ${detail}`
1110
- };
1111
- }
1112
- if (outcome.harnessError && !failFast.tripped) {
1113
- failFast.tripped = true;
1114
- failFast.reason = `a prior lane (${outcome.spec.laneId}) ended in a harness error (fail-fast)`;
1115
- }
1116
- return outcome;
1117
- });
1118
- return { outcomes, ...(failFast.tripped ? { failFastReason: failFast.reason } : {}) };
1119
- }
1120
- /** Project one lane outcome (or a dry-run contract spec) into the public CuaLaneResult. */
1121
- function toLaneResult(spec, outcome, subject, dryRun) {
1122
- const base = {
1123
- id: spec.laneId,
1124
- ...(spec.actorType === undefined ? {} : { actorType: spec.actorType }),
1125
- ...(spec.surface === undefined ? {} : { surface: spec.surface }),
1126
- ...(spec.caseGroup === undefined ? {} : { caseGroup: spec.caseGroup }),
1127
- index: spec.laneIndex + 1,
1128
- persona: spec.persona.id,
1129
- device: spec.deviceName,
1130
- resolution: spec.resolution,
1131
- subject
1132
- };
1133
- if (!outcome || dryRun) {
1134
- return { ...base, status: "contract_proof_only", ok: dryRun, diagnostics: cuaLaneDiagnostics({ dryRun }) };
1135
- }
1136
- if (outcome.skippedReason !== undefined) {
1137
- return {
1138
- ...base,
1139
- status: "blocked",
1140
- ok: false,
1141
- diagnostics: cuaLaneDiagnostics({ dryRun, skipped: true }),
1142
- skippedReason: outcome.skippedReason,
1143
- error: { code: "HUMANISH_CUA_LAB_FAILED", message: outcome.skippedReason }
1144
- };
1145
- }
1146
- const session = outcome.session;
1147
- const laneOk = laneOutcomeOk(outcome, dryRun);
1148
- const status = session ? session.status : "failed";
1149
- return {
1150
- ...base,
1151
- status,
1152
- ok: laneOk,
1153
- diagnostics: cuaLaneDiagnostics({
1154
- dryRun, executionError: outcome.sessionError !== undefined, noEngagement: outcome.noEngagement,
1155
- ...(session ? { session: { status: session.status, completionReason: session.completionReason, ...(session.trace.stopCause === undefined ? {} : { stopCause: session.trace.stopCause }) } } : {})
1156
- }),
1157
- ...(session
1158
- ? {
1159
- session: {
1160
- status: session.status,
1161
- completionReason: session.completionReason,
1162
- ...(session.trace.stopCause === undefined ? {} : { stopCause: session.trace.stopCause }),
1163
- reason: session.reason,
1164
- screenshots: outcome.screenshots.length
1165
- }
1166
- }
1167
- : {}),
1168
- ...(outcome.sandboxId === undefined
1169
- ? {}
1170
- : { sandbox: { sandboxId: outcome.sandboxId, killed: outcome.killed, streamUrlPresent: outcome.streamUrlPresent } }),
1171
- ...(laneOk
1172
- ? {}
1173
- : {
1174
- error: {
1175
- code: outcome.failureCode ?? "HUMANISH_CUA_LAB_FAILED",
1176
- message: outcome.sessionError
1177
- ?? (outcome.noEngagement
1178
- ? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
1179
- : outcome.selfReportedBlocker
1180
- ? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
1181
- : session?.completionReason === "harness_error"
1182
- ? `Computer-use session ended with a harness error: ${session.reason}`
1183
- : session?.status !== "passed"
1184
- ? `Computer-use session ended with ${session?.status ?? "unknown"}: ${session?.reason ?? "no terminal reason"}`
1185
- : "Computer-use lab did not produce a terminal session.")
1186
- }
1187
- })
1188
- };
1189
- }
1190
- function laneOutcomeOk(outcome, dryRun) {
1191
- if (dryRun)
1192
- return true;
1193
- if (!outcome || outcome.skippedReason !== undefined)
1194
- return false;
1195
- return outcome.session !== undefined
1196
- && outcome.session.status === "passed"
1197
- && outcome.session.completionReason !== "harness_error"
1198
- && outcome.sessionError === undefined
1199
- && !outcome.noEngagement
1200
- && !outcome.selfReportedBlocker;
1201
- }
1202
- function fanoutReviewVerdict(args) {
1203
- if (args.dryRun)
1204
- return "contract_proof_only";
1205
- const outcomes = args.outcomes ?? [];
1206
- if (outcomes.length !== args.expectedLaneCount)
1207
- return "fail";
1208
- if (outcomes.some((outcome) => !laneOutcomeOk(outcome, false))) {
1209
- return outcomes.some((outcome) => outcome.session?.status === "timed_out")
1210
- ? "timed_out"
1211
- : "fail";
1212
- }
1213
- return "pass";
1214
- }
1215
- /** Build the per-lane subject projection (invariant 5). Local-tree lanes all share ONE
1216
- * host-packed archive, so every lane's projection carries the identical archiveSha256/
1217
- * commit/dirty (no per-lane divergence is possible, unlike the clone route's per-lane
1218
- * in-sandbox commit). */
1219
- function laneSubjectProjection(args) {
1220
- if (args.cloneRoute && args.publicRepo) {
1221
- return {
1222
- source: "clone",
1223
- repo: args.publicRepo,
1224
- ...(args.subjectCommit === undefined ? {} : { commit: args.subjectCommit }),
1225
- envNames: args.subjectEnvNames,
1226
- state: args.subjectState
1227
- };
1228
- }
1229
- if (args.localTreeRoute) {
1230
- const archive = args.localTreeArchive;
1231
- return {
1232
- source: "local-tree",
1233
- ...(archive === undefined ? {} : { archiveSha256: archive.archiveSha256 }),
1234
- ...(archive?.git === undefined ? {} : { commit: archive.git.commit, dirty: archive.git.dirty }),
1235
- envNames: args.subjectEnvNames,
1236
- state: args.subjectState
1237
- };
1238
- }
1239
- return { source: "app-url", state: args.subjectState };
1240
- }
1241
- /** Narrow a resolved CuaSubjectProjection into the shape buildCuaBundle/buildSingleLaneBundle's
1242
- * subjectProvenance param wants (provisioned-route sources only; app-url stays undeclared, the
1243
- * default branch buildCuaBundle already handles). */
1244
- function subjectProvenanceArg(subject, publicRepo, subjectEnvNames) {
1245
- if (subject.source === "clone" && publicRepo) {
1246
- return {
1247
- source: "clone",
1248
- repo: publicRepo,
1249
- ...(subject.commit === undefined ? {} : { commit: subject.commit }),
1250
- envNames: subjectEnvNames,
1251
- state: subject.state
1252
- };
1253
- }
1254
- if (subject.source === "local-tree") {
1255
- return {
1256
- source: "local-tree",
1257
- ...(subject.archiveSha256 === undefined ? {} : { archiveSha256: subject.archiveSha256 }),
1258
- ...(subject.commit === undefined ? {} : { commit: subject.commit }),
1259
- ...(subject.dirty === undefined ? {} : { dirty: subject.dirty }),
1260
- envNames: subjectEnvNames,
1261
- state: subject.state
1262
- };
1263
- }
1264
- return undefined;
1265
- }
1266
- /**
1267
- * Wrapped so a DIRECT library caller gets the same status-record lifetime the CLI does: returning
1268
- * from this function finalizes any record the run opened, whichever of its fail-closed exits it
1269
- * took. `runLab` establishes a scope too and nesting is harmless — the inner scope owns what it
1270
- * opened. Without this a test or an adopter calling the backend directly leaves the 5s cadence
1271
- * ticking into a directory something else is deleting, which surfaces as an unrelated ENOTEMPTY.
1272
- */
1273
- export async function runCuaActorLab(options) {
1274
- return withTransientCommsSecrets(() => runCuaActorLabWithSecrets(options));
1275
- }
1276
- async function runCuaActorLabWithSecrets(options) {
1277
- const analysisReason = resolveAutomaticAnalysis(options.config.review?.analysis);
1278
- const tasksReason = analysisReason.ok ? taskProtocolValidationReason(options.config, true) : analysisReason.message;
1279
- if (tasksReason)
1280
- return {
1281
- schema: CUA_ACTOR_LAB_SCHEMA, ok: false, cwd: path.resolve(options.cwd), labId: options.config.id,
1282
- actor: options.config.actors[0]?.type ?? "", dryRun: options.dryRun,
1283
- runId: options.runId ?? "not-created", appUrl: options.config.subject.appUrl ?? options.config.subject.serve?.url ?? "", lanes: [], warnings: [],
1284
- error: { code: analysisReason.ok ? "HUMANISH_LAB_TASKS_UNSUPPORTED" : "HUMANISH_LAB_ANALYSIS_INVALID", message: tasksReason }
1285
- };
1286
- const analysis = resolveAutomaticAnalysis(options.config.review?.analysis);
1287
- const result = await withRunStatusScope(() => runCuaActorLabInScope(options));
1288
- return completeAutomaticAnalysis(result, analysis.ok ? analysis.config : undefined, options.automaticAnalysis, options.config.review?.analysis === undefined ? "default" : "explicit", analysis.ok && analysis.preferLargerOutput === true);
1289
- }
1290
- async function runCuaActorLabInScope(options) {
1291
- const { config, dryRun } = options;
1292
- // Capture the physical project before reading or invoking any caller hook. A supported
1293
- // symlink cwd remains valid, but retargeting that alias from a hook cannot redirect source
1294
- // reads, local-tree packing, managed run storage, or Observer output into another project.
1295
- const physicalCwd = await realpath(path.resolve(options.cwd));
1296
- const projectRoot = await prepareSelectedOutputDirectory(path.dirname(physicalCwd), physicalCwd);
1297
- const cwd = projectRoot.physicalPath;
1298
- const hooks = options.hooks ?? {};
1299
- let liveObserver;
1300
- const runtimeStreamUrls = [];
1301
- const liveHooks = {
1302
- ...hooks,
1303
- onRuntimeStreamReady: async (stream) => {
1304
- await hooks.onRuntimeStreamReady?.(stream);
1305
- runtimeStreamUrls.push({ streamId: stream.streamId, url: stream.url });
1306
- if (liveObserver) {
1307
- attachObserverRuntimeStreamUrls(liveObserver, runtimeStreamUrls);
1308
- }
1309
- },
1310
- onRuntimeStreamEnded: async (stream) => {
1311
- await hooks.onRuntimeStreamEnded?.(stream);
1312
- // Mark, never remove: the tile needs to KNOW the live view ended (and say so) rather than
1313
- // have the stream silently vanish from the overlay (#357).
1314
- for (const entry of runtimeStreamUrls) {
1315
- if (entry.streamId === stream.streamId)
1316
- entry.ended = true;
1317
- }
1318
- if (liveObserver) {
1319
- attachObserverRuntimeStreamUrls(liveObserver, runtimeStreamUrls);
1320
- }
1321
- }
1322
- };
1323
- const env = hooks.env ?? process.env;
1324
- const render = hooks.renderObserverFn ?? renderObserver;
1325
- const cloneRoute = config.subject.source === "clone";
1326
- // A CLI studied at a desktop (#495): nothing cloned, no browser, a terminal instead.
1327
- const desktopCliRoute = config.subject.source === "desktop-cli";
1328
- const localTreeRoute = config.subject.source === "local-tree";
1329
- // Both routes provision the subject in-sandbox (clone via git, local-tree via pack+upload)
1330
- // and then share the identical install/build/state/start/probe pipeline, so every seam that
1331
- // gates on "does this route provision a subject" is keyed on this union, not on cloneRoute
1332
- // alone.
1333
- const provisionedRoute = cloneRoute || localTreeRoute;
1334
- const serve = config.subject.serve;
1335
- const appUrl = (provisionedRoute ? serve?.url : config.subject.appUrl) ?? "";
1336
- const subjectRepo = cloneRoute ? config.subject.repos?.[0] ?? "" : undefined;
1337
- const subjectEnvNames = provisionedRoute ? config.subject.env ?? [] : [];
1338
- const actor = config.actors[0];
1339
- const actorType = actor?.type ?? "";
1340
- const fail = (code, message, actorLabel) => ({
1341
- schema: CUA_ACTOR_LAB_SCHEMA,
1342
- ok: false,
1343
- cwd,
1344
- labId: config.id,
1345
- actor: actorLabel ?? actorType,
1346
- appUrl,
1347
- dryRun,
1348
- runId: options.runId ?? "not-created",
1349
- lanes: [],
1350
- warnings: [],
1351
- error: { code, message }
1352
- });
1353
- // Resolve the actor through the registry — the parse layer validated this, but the engine fails
1354
- // closed rather than trusting a config that arrived through another door.
1355
- const descriptor = actorRegistry[actorType];
1356
- if (!descriptor || !isCuaActorDescriptor(descriptor)) {
1357
- return fail("HUMANISH_CUA_LAB_ACTOR_UNSUPPORTED", `actors[0].type "${actorType}" is not a registered computer-use actor.`);
1358
- }
1359
- const runSession = hooks.runSession ?? descriptor.runSession;
1360
- const mediaReason = desktopMediaValidationReason(config);
1361
- if (mediaReason)
1362
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", mediaReason, descriptor.id);
1363
- const outputLimitReason = outputTokenLimitValidationReason(config);
1364
- if (outputLimitReason)
1365
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", outputLimitReason, descriptor.id);
1366
- if (actor?.maxOutputTokens !== undefined && (hooks.runSession || hooks.buildProvider || hooks.buildExecutor)) {
1367
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", "maxOutputTokens cannot be enforced by a custom runSession/provider/executor route.", descriptor.id);
1368
- }
1369
- const receivingReason = receivingEmailValidationReason(config);
1370
- if (receivingReason)
1371
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", receivingReason, descriptor.id);
1372
- const inProcessRoute = hooks.buildExecutor !== undefined;
1373
- if (inProcessRoute && config.comms?.email?.kind === "real")
1374
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", "Real email receiving requires hosted participant desktops.", descriptor.id);
1375
- if (inProcessRoute && config.execution?.desktop?.media !== undefined) {
1376
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", "execution.desktop.media is not provisioned by a caller-supplied executor. Remove the declaration or use a hosted computer-use browser lane.", descriptor.id);
1377
- }
1378
- if (inProcessRoute && config.execution?.desktop?.recording !== undefined) {
1379
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", "execution.desktop.recording is not provisioned by a caller-supplied executor.", descriptor.id);
1380
- }
1381
- const localAppSubject = config.subject.source === "local-app";
1382
- // Adopter-hosted comms plane on the app-url route (#380): humanish provisions no subject here,
1383
- // so it cannot host a catch — the OPERATOR runs one, and humanish still does every other part
1384
- // of the funnel: tells each persona its address and inbox URL, drains the catch over HTTP after
1385
- // the lanes, and writes the same digest-only evidence. Declaring `external` previously did
1386
- // nothing on this route (and, per #387, on every other) while its docs said otherwise.
1387
- const externalCommsConfig = !cloneRoute && !localTreeRoute && !inProcessRoute
1388
- ? config.comms?.email?.external
1389
- : undefined;
1390
- const externalCommsEmail = externalCommsConfig ? config.comms?.email : undefined;
1391
- // Engine re-enforcement of the clone-route structure (library API surface).
1392
- if (cloneRoute && (!serve || !subjectRepo || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(subjectRepo))) {
1393
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", !serve
1394
- ? "clone subjects on the computer-use route require `subject.serve` (start + url) — the lab serves the app in-sandbox."
1395
- : `subject.repos[0] must be an owner/repo slug (got "${subjectRepo ?? ""}").`, descriptor.id);
1396
- }
1397
- // Engine re-enforcement of the local-tree-route structure (library API surface): a caller
1398
- // driving this function directly (bypassing parseLabConfig) still gets the same fail-closed
1399
- // shape the parser enforces, naming which requirement is missing.
1400
- if (localTreeRoute && (!serve || config.execution?.target !== "e2b-desktop")) {
1401
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", !serve
1402
- ? "local-tree subjects on the computer-use route require `subject.serve` (start + url): the lab packs and serves the working tree in-sandbox."
1403
- : "local-tree subjects require `execution.target: e2b-desktop`: the packed working tree is provisioned and served inside a hosted desktop sandbox.", descriptor.id);
1404
- }
1405
- // Engine re-enforcement of the state declaration (library API surface).
1406
- if (config.subject.state) {
1407
- const stateReason = !provisionedRoute
1408
- ? "`subject.state` applies only to clone subjects or local-tree subjects (the lab seeds the state it serves)."
1409
- : subjectStateInvalidReason(config.subject.state, config.subject.env);
1410
- if (stateReason) {
1411
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", stateReason, descriptor.id);
1412
- }
1413
- }
1414
- // Re-enforce the entry-target boundary (library API surface). A desktop-cli study has no entry
1415
- // target at all — the subject is a program on the machine, not an address — so the boundary is
1416
- // vacuous there rather than violated by an empty string.
1417
- const allowPublicTargets = config.policies?.allowPublicTargets === true;
1418
- const declaredTargets = [appUrl, ...(actor?.lanes ?? []).map((lane) => lane.target).filter((target) => target !== undefined)];
1419
- const entryTargetSafe = desktopCliRoute || declaredTargets.every((target) => provisionedRoute || localAppSubject
1420
- ? isLoopbackUrl(target)
1421
- : allowPublicTargets
1422
- ? isHttpUrl(target)
1423
- : isLoopbackUrl(target));
1424
- if (!entryTargetSafe) {
1425
- return fail("HUMANISH_CUA_LAB_SUBJECT_UNSAFE", provisionedRoute || localAppSubject || !allowPublicTargets
1426
- ? "subject.appUrl and any actors[0].lanes[].target entries must be loopback (127.0.0.1 or localhost) unless policies.allowPublicTargets is set for an app-url subject."
1427
- : "subject.appUrl and actors[0].lanes[].target entries must be valid http(s) URLs.", descriptor.id);
1428
- }
1429
- // In-process route pairing guard (boot-time, BEFORE key-gating): a custom executor needs a
1430
- // custom provider too (the default OpenAI provider is vision-based and would fail closed).
1431
- if (hooks.buildExecutor !== undefined && hooks.buildProvider === undefined) {
1432
- return fail("HUMANISH_CUA_LAB_EXECUTOR_NO_PROVIDER", "cuaHooks.buildExecutor requires cuaHooks.buildProvider — a state-driven executor returns no screenshot, so it must be paired with a NON-vision provider (the default OpenAI computer-use provider is vision-based and would fail closed).", descriptor.id);
1433
- }
1434
- // local-app fail-closed (BEFORE key-gating): there is no built-in in-process driver.
1435
- if (localAppSubject && !inProcessRoute) {
1436
- return fail("HUMANISH_CUA_LAB_LOCAL_APP_NO_EXECUTOR", "subject.source: local-app requires a library caller to supply cuaHooks.buildExecutor + buildProvider; there is no built-in driver for an in-process JS contract. (Drive the app via runLab(..., { cuaHooks: { buildExecutor, buildProvider } }).)", descriptor.id);
1437
- }
1438
- if (config.subject.source === "app-url" && config.execution?.target === "local" && !hooks.createDesktopLane) {
1439
- return fail("HUMANISH_CUA_LAB_LOCAL_APP_NO_EXECUTOR", "Local browser studies require a configured local desktop runtime.", descriptor.id);
1440
- }
1441
- // Re-enforce the fan-out cross-validation (library API surface): lanes XOR count/laneFocus,
1442
- // device XOR raw resolution, cap, unique ids, allowPublicTargets+N>1, clone.fanout.
1443
- const fanoutReason = cuaLaneValidationReason(config);
1444
- if (fanoutReason) {
1445
- return fail("HUMANISH_CUA_LAB_FANOUT_INVALID", fanoutReason, descriptor.id);
1446
- }
1447
- // The sandbox deadline is DERIVED from the session budget, so a lab can ask for a session that
1448
- // cannot legally be provisioned. Catch it here, before anything is created, and show the
1449
- // arithmetic — the provider's own error names a limit but not which knob produced it.
1450
- const derivedSandboxMs = resolvePerLaneSandboxMs(config);
1451
- if (derivedSandboxMs > MAX_SANDBOX_MS) {
1452
- const provisionedRoute = config.subject.source === "clone" || config.subject.source === "local-tree";
1453
- const sessionMs = config.execution?.timeoutMs ?? defaultSessionTimeoutMs(config);
1454
- const headroomMs = derivedSandboxMs - sessionMs;
1455
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", `execution.timeoutMs ${Math.round(sessionMs / 60_000)}m derives a ${Math.round(derivedSandboxMs / 60_000)}m sandbox deadline, and a sandbox may not live longer than ${MAX_SANDBOX_MS / 60_000}m. The deadline is the session budget plus ${Math.round(headroomMs / 60_000)}m of provisioning and teardown headroom${provisionedRoute ? " (this route clones, installs, builds and serves the subject before the actor starts)" : ""}. Lower execution.timeoutMs to at most ${Math.round((MAX_SANDBOX_MS - headroomMs) / 60_000)}m, or set execution.desktop.sandboxTimeoutMs explicitly.`, descriptor.id);
1456
- }
1457
- // Compile any committed personas BEFORE planning, so the plan builder stays pure and each lane's
1458
- // prompt carries real behavioral directives rather than a bare `Persona: <id>.` label (#381).
1459
- const personaResolution = await resolveCommittedPersonas(projectRoot, labPersonaIds(config));
1460
- for (const warning of personaResolution.warnings) {
1461
- process.stderr.write(`humanish: ${warning}\n`);
1462
- }
1463
- // Resolve the lane plan (pure) — the SAME table for dry-run and live.
1464
- let { lanes: laneSpecs, plan } = laneSpecsAndPlan(config, {
1465
- ...(options.countOverride === undefined ? {} : { countOverride: options.countOverride }),
1466
- env,
1467
- dryRun,
1468
- personas: personaResolution.personas
1469
- });
1470
- let laneCount = laneSpecs.length;
1471
- if (laneCount > MAX_CUA_LANES) {
1472
- return fail("HUMANISH_CUA_LAB_FANOUT_INVALID", `Computer-use fan-out is capped at ${MAX_CUA_LANES} lanes (resolved ${laneCount}); N concurrent paid desktops is real spend.`, descriptor.id);
1473
- }
1474
- if (inProcessRoute && laneCount > 1) {
1475
- return fail("HUMANISH_CUA_LAB_FANOUT_INVALID", "Multi-lane fan-out is not supported on the in-process route (cuaHooks.buildExecutor) — fan-out provisions one independent E2B desktop per lane, which the in-process route deliberately skips. Run a single in-process lane, or fan out on the E2B route.", descriptor.id);
1476
- }
1477
- let rerunLineage;
1478
- if (options.rerun) {
1479
- const selected = await resolveCuaRerunSelection({
1480
- cwd,
1481
- config,
1482
- sourceRunId: options.rerun.sourceRunId,
1483
- ...(options.rerun.laneIds === undefined ? {} : { laneIds: options.rerun.laneIds }),
1484
- laneSpecs,
1485
- plan
1486
- });
1487
- if (!selected.ok) {
1488
- return fail("HUMANISH_CUA_LAB_RERUN_INVALID", selected.message, descriptor.id);
1489
- }
1490
- laneSpecs = selected.laneSpecs;
1491
- plan = selected.plan;
1492
- laneCount = laneSpecs.length;
1493
- rerunLineage = selected.rerun;
1494
- }
1495
- // Pre-flight plan: BEFORE any sandbox or provider call (dry-run AND live). The hook fires for
1496
- // every N (observable + testable); the stderr table prints for fan-out (N>1) so single-lane
1497
- // runs stay as quiet as they always were.
1498
- if (laneCount > 1) {
1499
- emitPreflightPlan(plan, config.id);
1500
- }
1501
- hooks.onPreflight?.(plan);
1502
- await assertPreparedSelectedOutputDirectory(projectRoot);
1503
- // Read keys once into locals (names only; values never logged or persisted).
1504
- const openaiApiKey = env.OPENAI_API_KEY?.trim() ?? "";
1505
- const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
1506
- // Literal scrubber for every known provisioned value (no secret "shape" to pattern-match).
1507
- const knownSecretValues = [
1508
- openaiApiKey,
1509
- e2bApiKey,
1510
- ...subjectEnvNames.map((name) => env[name] ?? "")
1511
- ].filter((value) => value.length >= 4);
1512
- const scrubKnownValues = (text) => knownSecretValues.reduce((current, value) => current.split(value).join("[REDACTED_SECRET]"), text);
1513
- // Sanitize the declarative snapshot before initial, partial, or final bundle construction.
1514
- for (const spec of laneSpecs) {
1515
- if (spec.assignment)
1516
- spec.assignment = participantAssignment(spec.assignment, scrubKnownValues);
1517
- spec.evidenceInstructions = redactText(scrubKnownValues(spec.instructions));
1518
- }
1519
- const redactRepoLabel = config.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
1520
- const publicRepo = cloneRoute && subjectRepo ? (redactRepoLabel ? "repo-01" : subjectRepo) : undefined;
1521
- const hasGithubToken = subjectEnvNames.includes("GITHUB_TOKEN");
1522
- // The operator's own signed-in coding agent is the brain, so there is no provider key to ask
1523
- // for — the entire point of the actor. E2B is still required: the persona needs a machine.
1524
- const localAgentRoute = actorType === "local-agent";
1525
- // Which local CLI, from its OWN field: `model` means the model, so that "Claude Code running
1526
- // Opus" is sayable. Preflight below refuses when the chosen one is missing or signed out — that
1527
- // news is worthless after a sandbox is paid for.
1528
- const preferredLocalAgent = config.actors[0]?.localAgent ?? "codex";
1529
- // Key-gating is route-aware: the in-process route uses the caller's OWN model + executor, and
1530
- // the local-agent route uses a CLI the operator has already signed in to.
1531
- if (!dryRun && !inProcessRoute) {
1532
- const missingKeys = [
1533
- ...(openaiApiKey || localAgentRoute || hooks.buildProvider ? [] : ["OPENAI_API_KEY"]),
1534
- ...(e2bApiKey || hooks.createDesktopLane ? [] : ["E2B_API_KEY"])
1535
- ];
1536
- if (missingKeys.length > 0) {
1537
- // The moment someone new actually hits the wall. If a signed-in coding agent is sitting
1538
- // right there, say so HERE rather than making them go and find an API key — that detour is
1539
- // where most people trying humanish stop.
1540
- const suggestion = missingKeys.includes("OPENAI_API_KEY")
1541
- ? await (async () => {
1542
- const ready = (await detectLocalAgents({ env })).filter((agent) => agent.authStatus === "authenticated");
1543
- return ready.length === 0
1544
- ? ""
1545
- : ` ${ready.map((agent) => agent.label).join(" and ")} reports authenticated on this machine`
1546
- + ` — set actors[0].type: local-agent to use ${ready.length === 1 ? "it" : "one"} instead of a key.`;
1547
- })()
1548
- : "";
1549
- return fail("HUMANISH_CUA_LAB_KEYS_MISSING", `Live computer-use labs need ${missingKeys.join(" and ")} in the environment (values are never persisted). ${describeMissingKeys(missingKeys, env)}${suggestion}`, descriptor.id);
1550
- }
1551
- if (localAgentRoute && !hooks.buildProvider) {
1552
- // Refuse HERE, before a sandbox exists. "codex is not installed" discovered after the
1553
- // machine is paid for is the same information delivered at the worst possible moment.
1554
- const available = await detectLocalAgents({ env });
1555
- const chosen = available.find((agent) => agent.id === preferredLocalAgent);
1556
- if (chosen === undefined) {
1557
- return fail("HUMANISH_CUA_LAB_KEYS_MISSING", `actors[0].type: local-agent needs the ${preferredLocalAgent} CLI on PATH and signed in. `
1558
- + `Install it, or set OPENAI_API_KEY and use actors[0].type: openai-computer-use instead.`, descriptor.id);
1559
- }
1560
- if (chosen.authStatus !== "authenticated") {
1561
- return fail("HUMANISH_CUA_LAB_KEYS_MISSING", chosen.authStatus === "unauthenticated"
1562
- ? `${chosen.label} reports not signed in — run \`${chosen.id === "codex" ? "codex login" : "claude auth login"}\`, then retry.`
1563
- : `${chosen.label} authentication status could not be checked. Run \`${chosen.id === "codex" ? "codex login status" : "claude auth status"}\` and update the CLI if needed. No desktop was launched.`, descriptor.id);
1564
- }
1565
- if (chosen.id === "codex") {
1566
- const compatibility = await checkHostedCodexCompatibility(chosen.binPath, { env });
1567
- if (compatibility !== "supported") {
1568
- return fail("HUMANISH_CUA_LAB_ACTOR_UNSUPPORTED", compatibility === "unsupported_platform"
1569
- ? `Hosted Codex participants require Linux or macOS on x64 or arm64. This host is ${process.platform}/${process.arch}; no desktop was launched.`
1570
- : `Hosted Codex participants require Codex CLI 0.154.0. Run \`codex --version\` and install the supported version before retrying; no desktop was launched.`, descriptor.id);
1571
- }
1572
- if (chosen.billing === "account-unknown" &&
1573
- (config.execution?.caps?.maxUsd !== undefined || config.execution?.caps?.maxTotalUsd !== undefined)) {
1574
- return fail("HUMANISH_CUA_LAB_UNPRICED_CAP", "A ChatGPT-account Codex participant has no API-dollar price, so execution.caps.maxUsd/maxTotalUsd cannot be enforced. Remove the dollar cap and use finite execution timeout/step limits, or use an API-backed participant; no desktop was launched.", descriptor.id);
1575
- }
1576
- }
1577
- }
1578
- const missingSubjectEnv = subjectEnvNames.filter((name) => !env[name]?.trim());
1579
- if (missingSubjectEnv.length > 0) {
1580
- return fail("HUMANISH_CUA_LAB_SUBJECT_ENV_MISSING", `subject.env declares ${missingSubjectEnv.join(", ")} but the environment does not provide ${missingSubjectEnv.length === 1 ? "it" : "them"} (pass via --env-file; values are never persisted).`, descriptor.id);
1581
- }
1582
- // FAIL-CLOSED CAP TENSION (discipline #3): a maxUsd cap needs a MEASURABLE per-turn estimate.
1583
- // If the operator set execution.caps.maxUsd but src/pricing.ts has no rate for the resolved
1584
- // model, the loop could not enforce the cap — and silently running uncapped would break the
1585
- // runaway-retry protection. Refuse at PREFLIGHT (before any sandbox/spend) rather than run
1586
- // uncapped: an unenforceable cap is more dangerous than none. The operator adds a rate to
1587
- // src/pricing.ts (the honest place) or removes the cap.
1588
- if (config.execution?.caps?.maxUsd !== undefined || config.execution?.caps?.maxTotalUsd !== undefined) {
1589
- const capModelId = (config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL).trim().toLowerCase();
1590
- if (!MODEL_RATES[capModelId]) {
1591
- return fail("HUMANISH_CUA_LAB_UNPRICED_CAP", `execution.caps declares a spend cap (maxUsd/maxTotalUsd) but src/pricing.ts has no rate for model "${config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL}"; add a rate or remove the cap — an unenforceable cap is refused rather than run uncapped.`, descriptor.id);
1592
- }
1593
- }
1594
- // Adopter-hosted comms catch (#380): fail closed BEFORE any sandbox is created — a comms lab
1595
- // whose catch is unreachable collects nothing while every lane still spends. The probe asserts
1596
- // OUR service marker in /health, so an adopter's proxy answering 200 for everything cannot
1597
- // pass for a catch.
1598
- if (externalCommsConfig && !(await externalCatchHealthy(externalCommsConfig))) {
1599
- return fail("HUMANISH_CUA_LAB_COMMS_CATCH_UNREACHABLE", "The external comms catch or inbox is unreachable or incompatible (GET /health must identify humanish-comms-catch and advertise recipient-inbox-v1). Update Humanish on the catch host and restart it with `humanish comms catch` on that host, or drop comms.email to run without the inbox funnel.", descriptor.id);
1600
- }
1601
- }
1602
- const runId = options.runId ?? makeCuaRunId();
1603
- const runPaths = await prepareRunArtifactPaths(cwd, runId);
1604
- // Identity + liveness on disk from the first moment (#455): anything watching the runs
1605
- // directory — the TUI, another terminal, an agent — can now tell which lab this is and that
1606
- // it is alive, without waiting for the interactive observer flush that used to be the only
1607
- // mid-run write. The success path finalizes it with the real outcome; the fail-closed returns
1608
- // below do not, so `runLab`'s status scope finalizes those with no outcome. A crash reaches
1609
- // neither and leaves the record stale, which reads as interrupted rather than as a lie.
1610
- const runStatus = beginRunStatus(runPaths, {
1611
- runId,
1612
- mode: dryRun ? "dry-run" : "live",
1613
- ...(options.lab === undefined ? {} : { lab: options.lab })
1614
- });
1615
- const artifactRoot = runPaths.absoluteRunRoot;
1616
- const physicalArtifactRoot = runPaths.physicalRunRoot;
1617
- const createdAt = new Date().toISOString();
1618
- const timeoutMs = config.execution?.timeoutMs ?? defaultSessionTimeoutMs(config);
1619
- const requestTimeoutMs = readPositiveInt(env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
1620
- const redactScreenshots = config.policies?.redactScreenshots === true;
1621
- await prepareContainedOutputDirectory(runPaths, "screenshots");
1622
- const source = await buildRunSource({
1623
- capturedAt: createdAt,
1624
- cwd,
1625
- humanishSource: "present",
1626
- packageName: "humanish"
1627
- });
1628
- // Pack the working tree ONCE per run, on the host, BEFORE any sandbox or provider call: every
1629
- // fan-out lane below uploads this SAME archive, so one archiveSha256 describes every lane's
1630
- // digest. Dry-run packs nothing (no fs side effects; the contract bundle carries no
1631
- // archiveSha256). A packing failure fails the run closed here, before createDesktopSandbox is
1632
- // ever reached.
1633
- let localTreeArchive;
1634
- let localTreeArchiveBuffer;
1635
- if (localTreeRoute && !dryRun) {
1636
- const packLocalTree = hooks.packLocalTree ?? defaultPackLocalTree;
1637
- try {
1638
- const packed = await packLocalTree({
1639
- root: cwd,
1640
- ...(config.subject.localTree?.exclude === undefined ? {} : { extraExclude: config.subject.localTree.exclude }),
1641
- ...(config.subject.localTree?.maxArchiveBytes === undefined ? {} : { maxArchiveBytes: config.subject.localTree.maxArchiveBytes })
1642
- });
1643
- localTreeArchive = packed.archive;
1644
- localTreeArchiveBuffer = packed.buffer;
1645
- // One operator-facing line (stderr, same channel as emitPreflightPlan): what left the
1646
- // host, by counts and digest only, never paths or file names.
1647
- process.stderr.write(`humanish local-tree: packed ${packed.archive.fileCount} entries, ${packed.archive.totalBytes} bytes, archiveSha256 ${packed.archive.archiveSha256}`
1648
- + `${packed.archive.git ? ` (commit ${packed.archive.git.commit.slice(0, 12)}, ${packed.archive.git.dirty ? "dirty" : "clean"} working tree)` : " (not a git work tree)"}\n`);
1649
- }
1650
- catch (error) {
1651
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", `local-tree packing failed: ${redactText(scrubKnownValues(toErrorMessage(error)))}`, descriptor.id);
1652
- }
1653
- }
1654
- // Live-trace flush seam (#441): assigned by the attached-Observer block below when a live
1655
- // run has an in-progress bundle to grow; lanes call it through deps.onTrace. Declared here
1656
- // (before deps) so deps can reference it as a stable indirection.
1657
- let flushLiveTrace;
1658
- let stopLiveFlush;
1659
- const deps = {
1660
- ...(hooks.createDesktopLane ? { createDesktopLane: hooks.createDesktopLane } : {}),
1661
- onTrace: (laneId, items, usage, metadata) => flushLiveTrace?.(laneId, items, usage, metadata),
1662
- config,
1663
- descriptor,
1664
- appUrl,
1665
- ...(localAgentRoute ? { localAgent: preferredLocalAgent } : {}),
1666
- cloneRoute,
1667
- desktopCliRoute,
1668
- localTreeRoute,
1669
- ...(serve === undefined ? {} : { serve }),
1670
- ...(subjectRepo === undefined ? {} : { subjectRepo }),
1671
- subjectEnvNames,
1672
- hasGithubToken,
1673
- ...(localTreeArchiveBuffer === undefined ? {} : { localTreeArchiveBuffer }),
1674
- env,
1675
- openaiApiKey,
1676
- e2bApiKey,
1677
- requestTimeoutMs,
1678
- perLaneSandboxMs: resolvePerLaneSandboxMs(config),
1679
- timeoutMs,
1680
- laneCount,
1681
- artifactRoot: runPaths,
1682
- labCwd: options.cwd,
1683
- redactScreenshots,
1684
- scrubKnownValues,
1685
- runSession,
1686
- // The study-level ledger exists once per RUN, shared by every lane (#299). Dry runs never
1687
- // spend, so they carry none.
1688
- ...(dryRun || config.execution?.caps?.maxTotalUsd === undefined
1689
- ? {}
1690
- : { runBudget: makeCuaRunBudget(config.execution.caps.maxTotalUsd) }),
1691
- ...(externalCommsConfig === undefined || externalCommsEmail === undefined
1692
- ? {}
1693
- : { externalComms: { email: externalCommsEmail, inboxUrl: externalInboxUrl(externalCommsConfig) } }),
1694
- now: hooks.now ?? Date.now,
1695
- hooks: liveHooks
1696
- };
1697
- const inProgressLaneSubjects = laneSpecs.map(() => laneSubjectProjection({
1698
- cloneRoute,
1699
- localTreeRoute,
1700
- ...(publicRepo === undefined ? {} : { publicRepo }),
1701
- subjectEnvNames,
1702
- ...(localTreeArchive === undefined ? {} : { localTreeArchive }),
1703
- subjectState: resolveSubjectState({
1704
- declared: provisionedRoute ? config.subject.state : undefined,
1705
- dryRun: false,
1706
- executed: []
1707
- })
1708
- }));
1709
- const inProgressAggregateSubject = inProgressLaneSubjects[0];
1710
- const inProgressProvenance = subjectProvenanceArg(inProgressAggregateSubject, publicRepo, subjectEnvNames);
1711
- // A live run writes what it is doing AS IT DOES IT, whether or not anyone is currently watching.
1712
- // This used to be gated on `options.onObserverReady` — the interactive Observer callback — so a
1713
- // run launched by an agent (`lab run --json`), detached, or from the terminal surface recorded
1714
- // nothing at all until it completed, and anything asking "what is this participant doing right
1715
- // now" got silence for the whole run. Who reads the evidence is not the run's business; the
1716
- // callback below stays conditional, the writing does not.
1717
- if (!dryRun) {
1718
- const inProgressBundle = laneCount === 1 && rerunLineage === undefined
1719
- ? buildSingleLaneBundle({
1720
- ...(options.lab === undefined ? {} : { lab: options.lab }),
1721
- spec: laneSpecs[0],
1722
- outcome: undefined,
1723
- descriptor,
1724
- appUrl: laneSpecs[0].targetUrl ?? appUrl,
1725
- createdAt,
1726
- dryRun: false,
1727
- config,
1728
- runId,
1729
- source,
1730
- redactScreenshots,
1731
- inProgress: true,
1732
- ...(inProgressProvenance === undefined ? {} : { subjectProvenance: inProgressProvenance }),
1733
- inProcessRoute,
1734
- localAppSubject
1735
- })
1736
- : buildCuaFanoutBundle({
1737
- ...(options.lab === undefined ? {} : { lab: options.lab }),
1738
- specs: laneSpecs,
1739
- laneSubjects: inProgressLaneSubjects,
1740
- aggregateSubject: inProgressAggregateSubject,
1741
- descriptor,
1742
- appUrl,
1743
- createdAt,
1744
- dryRun: false,
1745
- config,
1746
- runId,
1747
- source,
1748
- plan,
1749
- ...(rerunLineage === undefined ? {} : { rerun: rerunLineage }),
1750
- cloneRoute,
1751
- localTreeRoute,
1752
- ...(publicRepo === undefined ? {} : { publicRepo }),
1753
- subjectEnvNames,
1754
- inProgress: true
1755
- });
1756
- await writeCuaRunArtifacts(inProgressBundle, createdAt, runPaths);
1757
- liveObserver = observerResultForCuaArtifacts(cwd, runId, artifactRoot, [
1758
- "Live CUA Observer is attached before final verification; stream auth URLs are runtime-only and are not persisted."
1759
- ]);
1760
- if (options.onObserverReady)
1761
- await options.onObserverReady(liveObserver);
1762
- // Incremental live flush (#441): as each lane's loop reports its recorded-so-far items,
1763
- // rewrite the in-progress bundle with per-stream `liveActor` partials so the attached
1764
- // Observer's 5s poll sees the timeline grow. Throttled (one write per interval, trailing
1765
- // write guaranteed), serialized (never two writers), and CLOSED before the final artifact
1766
- // write so a stale flush can never resurrect the in-progress bundle. A flush failure is
1767
- // swallowed: mid-run observability must never break the run itself.
1768
- const streamIdByLane = new Map(laneSpecs.map((spec) => [spec.laneId, spec.streamId]));
1769
- // The persona each lane is running, so the live flush can say who is in it.
1770
- const personaByStream = new Map(laneSpecs
1771
- .map((spec) => [spec.streamId, spec.persona?.id])
1772
- .filter((entry) => typeof entry[1] === "string"));
1773
- const liveItemsByStream = new Map();
1774
- // Running token usage per lane, so a run in flight can price itself instead of reporting the
1775
- // cost as unknown until the moment it ends.
1776
- const liveUsageByStream = new Map();
1777
- const liveMetadataByStream = new Map();
1778
- // The rate the running usage prices at. Usage without its model is not a cost, so both travel
1779
- // together or neither does.
1780
- const modelForLiveCost = config.actors[0]?.model ?? DEFAULT_OPENAI_CU_MODEL;
1781
- let flushWriting;
1782
- let flushDirty = false;
1783
- let flushClosed = false;
1784
- let flushTimer;
1785
- let lastFlushAtMs = 0;
1786
- const FLUSH_MIN_INTERVAL_MS = 2_000;
1787
- const flushNow = async () => {
1788
- while (flushDirty && !flushClosed) {
1789
- flushDirty = false;
1790
- lastFlushAtMs = Date.now();
1791
- const updatedAt = new Date(lastFlushAtMs).toISOString();
1792
- const patched = {
1793
- ...inProgressBundle,
1794
- streams: inProgressBundle.streams.map((stream) => {
1795
- const liveItems = liveItemsByStream.get(stream.id);
1796
- return liveItems === undefined
1797
- ? stream
1798
- : {
1799
- ...stream,
1800
- liveActor: {
1801
- schema: "humanish.live-actor.v1",
1802
- updatedAt,
1803
- // WHO is in this lane, carried while the run is live. Without it a surface
1804
- // watching a live run can only name the lane, and "CUA browser — observer-live-
1805
- // check" is the harness talking about itself where the participant should be.
1806
- ...(personaByStream.get(stream.id) === undefined
1807
- ? {}
1808
- : { persona: { id: personaByStream.get(stream.id) } }),
1809
- ...(liveUsageByStream.get(stream.id) === undefined
1810
- ? {}
1811
- : {
1812
- tokenUsage: liveUsageByStream.get(stream.id),
1813
- // The model too: usage without the rate it prices at is not a cost.
1814
- ids: { model: modelForLiveCost }
1815
- }),
1816
- ...liveMetadataByStream.get(stream.id),
1817
- items: [...liveItems]
1818
- }
1819
- };
1820
- })
1821
- };
1822
- try {
1823
- await writeCuaRunArtifacts(patched, createdAt, runPaths);
1824
- }
1825
- catch {
1826
- // Swallowed by design; the final write is the evidence of record.
1827
- }
1828
- }
1829
- flushWriting = undefined;
1830
- };
1831
- const scheduleFlush = () => {
1832
- if (flushClosed || flushWriting !== undefined)
1833
- return;
1834
- const sinceMs = Date.now() - lastFlushAtMs;
1835
- if (sinceMs >= FLUSH_MIN_INTERVAL_MS) {
1836
- flushWriting = flushNow();
1837
- return;
1838
- }
1839
- if (flushTimer === undefined) {
1840
- flushTimer = setTimeout(() => {
1841
- flushTimer = undefined;
1842
- scheduleFlush();
1843
- }, FLUSH_MIN_INTERVAL_MS - sinceMs);
1844
- flushTimer.unref?.();
1845
- }
1846
- };
1847
- flushLiveTrace = (laneId, items, usage, metadata) => {
1848
- // An empty snapshot (the initial observation on a frameless route) carries no
1849
- // evidence worth a disk write; the first real item triggers the first flush.
1850
- if (items.length === 0)
1851
- return;
1852
- const streamId = streamIdByLane.get(laneId);
1853
- if (streamId === undefined)
1854
- return;
1855
- liveItemsByStream.set(streamId, items.slice());
1856
- if (metadata !== undefined)
1857
- liveMetadataByStream.set(streamId, metadata);
1858
- if (usage !== undefined)
1859
- liveUsageByStream.set(streamId, usage);
1860
- flushDirty = true;
1861
- scheduleFlush();
1862
- };
1863
- stopLiveFlush = async () => {
1864
- flushClosed = true;
1865
- if (flushTimer !== undefined) {
1866
- clearTimeout(flushTimer);
1867
- flushTimer = undefined;
1868
- }
1869
- await flushWriting;
1870
- };
1871
- }
1872
- const receivingWarnings = [];
1873
- let receiving;
1874
- if (!dryRun && config.comms?.email?.kind === "real") {
1875
- try {
1876
- receiving = await prepareReceivingRun({ cwd, runId, config, env, participants: laneSpecs.map(spec => spec.laneId), runPaths,
1877
- registerSecrets: values => { for (const value of values)
1878
- if (value.length >= 4 && !knownSecretValues.includes(value))
1879
- knownSecretValues.push(value); }
1880
- });
1881
- if (receiving)
1882
- deps.receiving = receiving;
1883
- }
1884
- catch {
1885
- await stopLiveFlush?.();
1886
- return fail("HUMANISH_CUA_LAB_SUBJECT_INVALID", "Real email setup failed before desktop allocation. Run humanish comms check --online and humanish comms recover to inspect authentication and pending cleanup.", descriptor.id);
1887
- }
1888
- }
1889
- // Run lanes (dry-run runs none). In-process is always one lane.
1890
- let outcomes;
1891
- let failFastReason;
1892
- try {
1893
- if (!dryRun) {
1894
- if (inProcessRoute) {
1895
- outcomes = [await runInProcessLane(laneSpecs[0], deps)];
1896
- }
1897
- else if (laneCount === 1) {
1898
- outcomes = [await runCuaLane(laneSpecs[0], deps)];
1899
- }
1900
- else {
1901
- const ran = await runCuaLanes(laneSpecs, deps, plan.concurrency);
1902
- outcomes = ran.outcomes;
1903
- failFastReason = ran.failFastReason;
1904
- }
1905
- }
1906
- }
1907
- finally {
1908
- try {
1909
- await receiving?.finish();
1910
- }
1911
- catch {
1912
- receivingWarnings.push("Email finalization could not complete. Inspect humanish comms recover; provider cleanup remains unresolved.");
1913
- }
1914
- }
1915
- // Close the live flush BEFORE any final artifact work: no new flush may start, and an
1916
- // in-flight one is awaited, so the final bundle write can never race a stale in-progress
1917
- // rewrite (which would resurrect `liveActor` after completion).
1918
- await stopLiveFlush?.();
1919
- const externalCommsWarnings = [];
1920
- // Adopter-hosted drain (#380): once per RUN, after every lane finished — the catch is one
1921
- // shared external endpoint, not a per-sandbox file. Same routing and digest-only artifact as
1922
- // the in-sandbox drain; the artifact is registered on every lane that declared a recipient
1923
- // address, since the thread carries each inbox's mail. A drain failure never fails the run.
1924
- if (!dryRun && externalCommsConfig && externalCommsEmail && outcomes !== undefined) {
1925
- try {
1926
- const commsChannel = new FakeInbox();
1927
- const commsInboxes = [];
1928
- for (const recipient of externalCommsEmail.recipients ?? []) {
1929
- if (recipient.address !== undefined) {
1930
- commsInboxes.push(await commsChannel.provisionAddress(recipient.lane, recipient.address));
1931
- }
1932
- }
1933
- const authToken = externalCommsConfig.authTokenEnv === undefined ? undefined : env[externalCommsConfig.authTokenEnv];
1934
- const collected = await collectExternalCommsThread({
1935
- external: { ...externalCommsConfig, ...(authToken === undefined ? {} : { authToken }) },
1936
- channel: commsChannel,
1937
- inboxes: commsInboxes
1938
- });
1939
- if (collected.artifact) {
1940
- const commsPath = "comms/thread.json";
1941
- await writeContainedOutputFile(runPaths, commsPath, `${JSON.stringify(collected.artifact, null, 2)}\n`, "utf8");
1942
- for (const [index, outcome] of outcomes.entries()) {
1943
- const laneId = laneSpecs[index]?.laneId;
1944
- if (laneId !== undefined && outcome.commsArtifactPath === undefined && laneHasInboxRecipient(externalCommsEmail, laneId)) {
1945
- outcome.commsArtifactPath = commsPath;
1946
- }
1947
- }
1948
- }
1949
- else if (collected.captured > 0) {
1950
- externalCommsWarnings.push(`Comms catch captured ${collected.captured} email send(s) but none matched a declared recipient inbox — no comms evidence written. Declare comms.email.recipients[].address to match the address the app sends to.`);
1951
- }
1952
- else {
1953
- externalCommsWarnings.push(`Comms catch captured ZERO email sends — your app never delivered mail through the catch at ${externalCommsConfig.catchBaseUrl}. Verify the app's email-API base URL points at it and that the flow reached an email step.`);
1954
- }
1955
- }
1956
- catch (error) {
1957
- externalCommsWarnings.push(`Comms evidence collection failed against the adopter-hosted catch (run continues): ${redactText(scrubKnownValues(toErrorMessage(error)))}`);
1958
- }
1959
- }
1960
- // Per-lane subject projections (invariant 5).
1961
- const laneSubjects = laneSpecs.map((_spec, index) => {
1962
- const outcome = outcomes?.[index];
1963
- const subjectState = resolveSubjectState({
1964
- declared: provisionedRoute ? config.subject.state : undefined,
1965
- dryRun,
1966
- executed: outcome?.stateStepRecords ?? []
1967
- });
1968
- return laneSubjectProjection({
1969
- cloneRoute,
1970
- localTreeRoute,
1971
- ...(publicRepo === undefined ? {} : { publicRepo }),
1972
- subjectEnvNames,
1973
- ...(outcome?.subjectCommit === undefined ? {} : { subjectCommit: outcome.subjectCommit }),
1974
- ...(localTreeArchive === undefined ? {} : { localTreeArchive }),
1975
- subjectState
1976
- });
1977
- });
1978
- // Aggregate subject (top-level + bundle): unanimity-gated commit (+ divergence warning) on
1979
- // the clone route. Local-tree lanes all pack from the SAME once-per-run archive, so every
1980
- // lane's projection already carries the identical archiveSha256/commit/dirty: the
1981
- // `first.source !== "clone"` branch below returns it directly, with no unanimity math needed
1982
- // (there is nothing that could diverge).
1983
- const aggregateWarnings = [...externalCommsWarnings];
1984
- // execution.caps.maxUsd is a PER-LANE cap: it is enforced INSIDE each lane's loop independently,
1985
- // so an N-lane fan-out can spend up to N × maxUsd before any lane aborts, while the run cost
1986
- // summary reports the (larger) aggregate. Warn at run level so the operator sees the true
1987
- // ceiling — unless the study declared the shared budget (#299), which caps the run as a whole.
1988
- const perLaneCapUsd = config.execution?.caps?.maxUsd;
1989
- if (perLaneCapUsd !== undefined && laneCount > 1 && config.execution?.caps?.maxTotalUsd === undefined) {
1990
- aggregateWarnings.push(`execution.caps.maxUsd ($${perLaneCapUsd}) is a PER-LANE cap; ${laneCount} lanes may spend up to ${laneCount} × $${perLaneCapUsd} (~$${round6(perLaneCapUsd * laneCount)} total) before any lane aborts. Set execution.caps.maxTotalUsd for a shared study budget.`);
1991
- }
1992
- const aggregateSubject = (() => {
1993
- const first = laneSubjects[0];
1994
- if (first.source !== "clone") {
1995
- return first;
1996
- }
1997
- const commits = (outcomes ?? []).map((outcome) => outcome.subjectCommit).filter((commit) => commit !== undefined);
1998
- const unanimous = !dryRun && commits.length === laneCount && new Set(commits).size === 1;
1999
- if (!dryRun && laneCount > 1 && new Set(commits).size > 1) {
2000
- aggregateWarnings.push("Fan-out lanes resolved DIVERGENT subject commits — the top-level subject.commit is omitted; see per-lane provenance in result.lanes for each lane's pinned commit.");
2001
- }
2002
- // Build without commit, then add it only when unanimous (avoids an explicit commit:undefined
2003
- // under exactOptionalPropertyTypes).
2004
- return {
2005
- source: "clone",
2006
- ...(first.repo === undefined ? {} : { repo: first.repo }),
2007
- ...(first.envNames === undefined ? {} : { envNames: first.envNames }),
2008
- state: first.state,
2009
- ...(unanimous && commits[0] !== undefined ? { commit: commits[0] } : {})
2010
- };
2011
- })();
2012
- const finalProvenance = subjectProvenanceArg(aggregateSubject, publicRepo, subjectEnvNames);
2013
- const bundle = laneCount === 1 && rerunLineage === undefined
2014
- ? buildSingleLaneBundle({
2015
- ...(options.lab === undefined ? {} : { lab: options.lab }),
2016
- spec: laneSpecs[0],
2017
- outcome: outcomes?.[0],
2018
- descriptor,
2019
- appUrl: laneSpecs[0].targetUrl ?? appUrl,
2020
- createdAt,
2021
- dryRun,
2022
- config,
2023
- runId,
2024
- source,
2025
- redactScreenshots,
2026
- ...(finalProvenance === undefined ? {} : { subjectProvenance: finalProvenance }),
2027
- inProcessRoute,
2028
- localAppSubject
2029
- })
2030
- : buildCuaFanoutBundle({
2031
- ...(options.lab === undefined ? {} : { lab: options.lab }),
2032
- specs: laneSpecs,
2033
- ...(outcomes === undefined ? {} : { outcomes }),
2034
- laneSubjects,
2035
- aggregateSubject,
2036
- descriptor,
2037
- appUrl,
2038
- createdAt,
2039
- dryRun,
2040
- config,
2041
- runId,
2042
- source,
2043
- plan,
2044
- ...(rerunLineage === undefined ? {} : { rerun: rerunLineage }),
2045
- ...(failFastReason === undefined ? {} : { failFastReason }),
2046
- cloneRoute,
2047
- localTreeRoute,
2048
- ...(publicRepo === undefined ? {} : { publicRepo }),
2049
- subjectEnvNames
2050
- });
2051
- const adapterWarnings = [];
2052
- const scorerResult = await applyBrowserAdapterHooks({
2053
- hooks,
2054
- bundle,
2055
- context: {
2056
- bundle,
2057
- runDir: physicalArtifactRoot,
2058
- labId: config.id,
2059
- runId,
2060
- actor: descriptor.id,
2061
- backend: "cua",
2062
- dryRun,
2063
- laneCount
2064
- },
2065
- sanitize: (text) => redactText(scrubKnownValues(text)),
2066
- warnings: adapterWarnings,
2067
- hookLabel: "cuaHooks",
2068
- ...(options.scorerProvenance === undefined ? {} : { scorerProvenance: options.scorerProvenance })
2069
- });
2070
- if (receiving)
2071
- bundle.commsReceiving = receiving.snapshot();
2072
- await writeCuaRunArtifacts(bundle, createdAt, runPaths);
2073
- // Finalize the status record from the bundle that was just written, so the index can never
2074
- // claim an outcome the evidence does not carry. A run that throws before reaching here leaves
2075
- // its record `running` and goes stale — read as interrupted, which is the truth.
2076
- await runStatus.finish({
2077
- ...(bundle.review?.verdict === undefined ? {} : { verdict: bundle.review.verdict }),
2078
- ...(bundle.review?.participants === undefined
2079
- ? {}
2080
- : {
2081
- participants: {
2082
- total: bundle.review.participants.total,
2083
- reachedGoal: bundle.review.participants.reachedGoal,
2084
- ...(bundle.review.participants.reportedFriction === undefined
2085
- ? {}
2086
- : { reportedFriction: bundle.review.participants.reportedFriction })
2087
- }
2088
- }),
2089
- ...(bundle.cost?.estimatedTotalUsd === undefined ? {} : { estimatedCostUsd: bundle.cost.estimatedTotalUsd })
2090
- });
2091
- const observer = await render(cwd, runId, { open: options.open === true });
2092
- if (observer.ok && runtimeStreamUrls.length > 0) {
2093
- attachObserverRuntimeStreamUrls(observer, runtimeStreamUrls);
2094
- }
2095
- // Lane-level pass: dry-run lanes are contract-ok; live lanes need a passed, engaged session.
2096
- const laneOk = (outcome) => laneOutcomeOk(outcome, dryRun);
2097
- const allLanesOk = laneSpecs.every((_, index) => laneOk(outcomes?.[index]));
2098
- const adapterFailure = adapterScoreFailureMessage(bundle);
2099
- const ok = observer.ok && allLanesOk && adapterFailure === undefined && scorerResult.declaredVerdictFailure === undefined;
2100
- const laneWarnings = (outcomes ?? []).flatMap((outcome) => outcome.warnings);
2101
- const warnings = [...receivingWarnings, ...laneWarnings, ...aggregateWarnings, ...adapterWarnings, ...observer.warnings];
2102
- const laneResults = laneSpecs.map((spec, index) => toLaneResult(spec, outcomes?.[index], laneSubjects[index], dryRun));
2103
- const laneSummary = buildLaneSummary(outcomes, laneCount, plan, dryRun);
2104
- const firstOutcome = outcomes?.[0];
2105
- const errorResult = (() => {
2106
- if (ok)
2107
- return undefined;
2108
- if (adapterFailure !== undefined) {
2109
- return {
2110
- code: "HUMANISH_CUA_LAB_FAILED",
2111
- message: adapterFailure
2112
- };
2113
- }
2114
- if (laneCount === 1) {
2115
- const outcome = firstOutcome;
2116
- return {
2117
- code: outcome?.failureCode ?? "HUMANISH_CUA_LAB_FAILED",
2118
- message: outcome?.sessionError
2119
- ?? (outcome?.noEngagement
2120
- ? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
2121
- // The lane result (toLaneResult) named this refusal; the N=1 envelope fell through to
2122
- // "did not produce a terminal session", which is false — it produced one and refused it.
2123
- : outcome?.selfReportedBlocker
2124
- ? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
2125
- : observer.ok
2126
- ? outcome?.session?.completionReason === "harness_error"
2127
- ? `Computer-use session ended with a harness error: ${outcome.session.reason}`
2128
- : outcome?.session?.status !== "passed"
2129
- ? `Computer-use session ended with ${outcome?.session?.status ?? "unknown"}: ${outcome?.session?.reason ?? "no terminal reason"}`
2130
- : "Computer-use lab did not produce a terminal session."
2131
- : observer.error?.message ?? "Observer failed for the computer-use lab run.")
2132
- };
2133
- }
2134
- const failingLane = (outcomes ?? []).find((outcome) => !laneOk(outcome));
2135
- const geometryLane = (outcomes ?? []).find((outcome) => outcome.failureCode === "HUMANISH_CUA_LAB_DEVICE_GEOMETRY");
2136
- const code = geometryLane?.failureCode ?? "HUMANISH_CUA_LAB_FAILED";
2137
- return {
2138
- code,
2139
- message: observer.ok
2140
- ? `Fan-out run failed: ${laneSummary.passed}/${laneCount} lane(s) passed (${laneSummary.skipped} skipped, ${laneSummary.harnessErrors} harness error(s), ${laneSummary.hollow} hollow)${failingLane?.sessionError ? `; first failure: ${failingLane.sessionError}` : ""}.`
2141
- : observer.error?.message ?? "Observer failed for the computer-use fan-out run."
2142
- };
2143
- })();
2144
- return markFinalizedStudyResult({
2145
- schema: CUA_ACTOR_LAB_SCHEMA,
2146
- ok,
2147
- cwd,
2148
- labId: config.id,
2149
- actor: descriptor.id,
2150
- appUrl,
2151
- dryRun,
2152
- runId,
2153
- ...(firstOutcome?.session
2154
- ? {
2155
- session: {
2156
- status: firstOutcome.session.status,
2157
- completionReason: firstOutcome.session.completionReason,
2158
- ...(firstOutcome.session.trace.stopCause === undefined ? {} : { stopCause: firstOutcome.session.trace.stopCause }),
2159
- reason: firstOutcome.session.reason,
2160
- screenshots: firstOutcome.screenshots.length
2161
- }
2162
- }
2163
- : {}),
2164
- ...(firstOutcome?.sandboxId
2165
- ? { sandbox: { sandboxId: firstOutcome.sandboxId, killed: firstOutcome.killed, streamUrlPresent: firstOutcome.streamUrlPresent } }
2166
- : {}),
2167
- subject: aggregateSubject,
2168
- plan,
2169
- lanes: laneResults,
2170
- diagnostics: summarizeCuaDiagnostics({ dryRun, evidenceInvalid: !observer.ok, lanes: laneResults }),
2171
- laneSummary,
2172
- ...(rerunLineage === undefined ? {} : { rerun: rerunLineage }),
2173
- observer,
2174
- warnings,
2175
- ...(errorResult === undefined ? {} : { error: errorResult })
2176
- }, runPaths);
2177
- }
2178
- /** Aggregate lane counts for the result projection. */
2179
- function buildLaneSummary(outcomes, laneCount, plan, dryRun) {
2180
- if (dryRun || !outcomes) {
2181
- return {
2182
- strategy: CUA_FANOUT_STRATEGY,
2183
- total: laneCount,
2184
- passed: 0,
2185
- skipped: 0,
2186
- harnessErrors: 0,
2187
- hollow: 0,
2188
- concurrency: plan.concurrency,
2189
- waves: plan.waves
2190
- };
2191
- }
2192
- let passed = 0;
2193
- let skipped = 0;
2194
- let harnessErrors = 0;
2195
- let hollow = 0;
2196
- for (const outcome of outcomes) {
2197
- if (outcome.skippedReason !== undefined) {
2198
- skipped += 1;
2199
- continue;
2200
- }
2201
- if (outcome.harnessError)
2202
- harnessErrors += 1;
2203
- if (outcome.noEngagement)
2204
- hollow += 1;
2205
- if (laneOutcomeOk(outcome, dryRun))
2206
- passed += 1;
2207
- }
2208
- return {
2209
- strategy: CUA_FANOUT_STRATEGY,
2210
- total: laneCount,
2211
- passed,
2212
- skipped,
2213
- harnessErrors,
2214
- hollow,
2215
- concurrency: plan.concurrency,
2216
- waves: plan.waves
2217
- };
2218
- }
2219
- async function writeCuaRunArtifacts(bundle, updatedAt, preparedRunPaths) {
2220
- const runPaths = await validatePreparedRunArtifactPaths(preparedRunPaths);
2221
- const publicBundle = {
2222
- ...bundle,
2223
- cwd: PUBLIC_TARGET_CWD
2224
- };
2225
- await writeContainedOutputFile(runPaths, "run.json", `${JSON.stringify(publicBundle, null, 2)}\n`, "utf8");
2226
- await writeContainedOutputFile(runPaths, "review.json", `${JSON.stringify(publicBundle.review, null, 2)}\n`, "utf8");
2227
- await writeContainedOutputFile(runPaths, "review.md", renderCuaReviewMarkdown(publicBundle), "utf8");
2228
- await writeContainedOutputFile(runPaths, "events.ndjson", `${publicBundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
2229
- await writeContainedOutputFile(runPaths, "observer/observer-data.json", `${JSON.stringify(buildObserverData(publicBundle), null, 2)}\n`, "utf8");
2230
- await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
2231
- schema: "humanish.latest-run.v1",
2232
- runId: publicBundle.runId,
2233
- path: runPaths.relativeRunRoot,
2234
- updatedAt
2235
- }, null, 2)}\n`, "utf8");
2236
- }
2237
- function observerResultForCuaArtifacts(cwd, runId, artifactRoot, warnings = []) {
2238
- const observerPath = path.join(artifactRoot, "observer", "index.html");
2239
- const observerDataPath = path.join(artifactRoot, "observer", "observer-data.json");
2240
- const eventsPath = path.join(artifactRoot, "events.ndjson");
2241
- return {
2242
- schema: "humanish.observer-result.v1",
2243
- ok: true,
2244
- cwd,
2245
- run: runId,
2246
- observerPath: path.relative(cwd, observerPath),
2247
- observerDataPath: path.relative(cwd, observerDataPath),
2248
- eventsPath: path.relative(cwd, eventsPath),
2249
- observerUrl: pathToFileURL(observerPath).href,
2250
- bundlePath: path.join(artifactRoot, "run.json"),
2251
- opened: false,
2252
- warnings
2253
- };
2254
- }
2255
- /** Build the N=1 bundle via the unchanged buildCuaBundle (byte-stable). */
2256
- function buildSingleLaneBundle(args) {
2257
- const { spec, outcome, config } = args;
2258
- const desktopTemplate = e2bDesktopTemplate(config);
2259
- return buildCuaBundle({
2260
- realEmail: config.comms?.email?.kind === "real",
2261
- ...(args.lab === undefined ? {} : { lab: args.lab }),
2262
- actorId: args.descriptor.id,
2263
- appUrl: args.appUrl,
2264
- laneId: spec.laneId,
2265
- ...(spec.actorType === undefined ? {} : { actorType: spec.actorType }),
2266
- ...(spec.surface === undefined ? {} : { surface: spec.surface }),
2267
- ...(spec.caseGroup === undefined ? {} : { caseGroup: spec.caseGroup }),
2268
- createdAt: args.createdAt,
2269
- dryRun: args.dryRun,
2270
- labId: config.id,
2271
- ...(config.title ? { labTitle: config.title } : {}),
2272
- mission: spec.evidenceInstructions ?? spec.instructions,
2273
- ...(spec.assignment === undefined ? {} : { assignment: spec.assignment }),
2274
- persona: spec.persona,
2275
- resolution: spec.resolution,
2276
- desktopRoute: !args.inProcessRoute,
2277
- feedbackSubstrate: args.inProcessRoute ? "local-filesystem" : args.config.execution?.target === "local" ? "local-desktop" : "e2b-desktop",
2278
- ...(outcome?.desktopGeometry === undefined ? {} : { desktopGeometry: outcome.desktopGeometry }),
2279
- ...(outcome?.recording === undefined ? {} : { recording: outcome.recording }),
2280
- isMobile: spec.devicePreset.isMobile,
2281
- runId: args.runId,
2282
- screenshots: outcome?.screenshots ?? [],
2283
- captureRedaction: args.redactScreenshots ? "blurred" : "raw",
2284
- ...(outcome?.session ? { session: outcome.session } : {}),
2285
- ...(outcome?.sessionError ? { sessionError: outcome.sessionError } : {}),
2286
- ...(outcome === undefined
2287
- ? {}
2288
- : {
2289
- credibility: {
2290
- noEngagement: outcome.noEngagement === true,
2291
- selfReportedBlocker: outcome.selfReportedBlocker === true,
2292
- reportedFriction: outcome.reportedFriction === true
2293
- }
2294
- }),
2295
- source: args.source,
2296
- ...(args.inProgress === undefined ? {} : { inProgress: args.inProgress }),
2297
- ...(args.subjectProvenance === undefined ? {} : { subjectProvenance: args.subjectProvenance }),
2298
- ...(desktopTemplate === undefined ? {} : { desktopTemplate }),
2299
- ...(outcome?.desktopBrowser === undefined ? {} : { desktopBrowser: outcome.desktopBrowser }),
2300
- providerResources: providerResourcesForOutcome({
2301
- outcome,
2302
- createdAt: args.createdAt,
2303
- simId: spec.simId,
2304
- streamId: spec.streamId,
2305
- laneId: spec.laneId
2306
- }),
2307
- ...(args.localAppSubject || args.inProcessRoute ? { entryKind: "local-app" } : {}),
2308
- ...(outcome?.session ? { traceArtifactPath: spec.traceArtifactPath } : {}),
2309
- ...(outcome?.commsArtifactPath === undefined ? {} : { commsArtifactPath: outcome.commsArtifactPath }),
2310
- ...(desktopSpanToMinutes(outcome?.desktopDurationMs) === undefined
2311
- ? {}
2312
- : { desktopMinutes: desktopSpanToMinutes(outcome?.desktopDurationMs) }),
2313
- ...(outcome?.sandboxId === undefined ? {} : { desktopUsage: {
2314
- laneId: spec.laneId,
2315
- minutes: desktopSpanToMinutes(outcome.desktopDurationMs),
2316
- observation: outcome.desktopResources,
2317
- lifetimeComplete: outcome.killed
2318
- } }),
2319
- phaseEvents: outcome?.phaseRecords ?? []
2320
- });
2321
- }
2322
- /**
2323
- * Default local-tree packing implementation: createLocalTreeArchive(root, opts) on the host,
2324
- * then a single read of the produced archive file into an ArrayBuffer for upload. The DI seam
2325
- * (CuaActorLabHooks.packLocalTree) overrides this in deterministic tests so they never require
2326
- * tar/git.
2327
- */
2328
- export async function defaultPackLocalTree(args) {
2329
- const archive = createLocalTreeArchive(args.root, {
2330
- ...(args.extraExclude === undefined ? {} : { extraExclude: args.extraExclude }),
2331
- ...(args.maxArchiveBytes === undefined ? {} : { maxArchiveBytes: args.maxArchiveBytes })
2332
- });
2333
- const bytes = await readFile(archive.archivePath);
2334
- const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2335
- // The archive was written to a fresh mkdtemp dir (no outputPath passed above); once the
2336
- // bytes are buffered the on-disk copy is pure residue, and a packed working tree left in
2337
- // the host tmpdir is itself a small leak surface. Best-effort removal.
2338
- await rm(path.dirname(archive.archivePath), { recursive: true, force: true }).catch(() => undefined);
2339
- return { archive, buffer };
2340
- }
2341
- /**
2342
- * Resolve the bundle's state marker from the declaration and what actually ran.
2343
- * Precedence: external declared → "unpinned" (seed records, if any, stay attached — a
2344
- * migrated external DB is still unpinned overall); else seed declared → "seeded" only when
2345
- * every declared step executed ok on a live run, otherwise "declared-not-run" (dry-run
2346
- * contract bundles and failed live provisioning); no declaration → "undeclared".
2347
- */
2348
- export function resolveSubjectState(args) {
2349
- const declared = args.declared;
2350
- if (!declared) {
2351
- return { provenance: "undeclared" };
2352
- }
2353
- const declaredSeed = declared.seed ?? [];
2354
- const external = declared.external ?? [];
2355
- // Dry-run: nothing executes (no sandbox) — record the DECLARED recipe: name, phase, and
2356
- // command digest only, with NO execution fields.
2357
- const seed = args.dryRun
2358
- ? declaredSeed.map((step) => ({
2359
- name: step.name,
2360
- when: step.when ?? "before-start",
2361
- commandDigest: commandDigestOf(step.command)
2362
- }))
2363
- : args.executed;
2364
- const allRanOk = !args.dryRun
2365
- && declaredSeed.length > 0
2366
- && seed.length === declaredSeed.length
2367
- && seed.every((record) => record.ok === true);
2368
- const provenance = external.length > 0
2369
- ? "unpinned"
2370
- : declaredSeed.length === 0
2371
- ? "undeclared"
2372
- : allRanOk
2373
- ? "seeded"
2374
- : "declared-not-run";
2375
- return {
2376
- provenance,
2377
- ...(seed.length > 0 ? { seed } : {}),
2378
- ...(external.length > 0 ? { externalEnvNames: external } : {})
2379
- };
2380
- }
2381
- /** The human-readable state story appended to the provenance event (and review.md via it). */
2382
- function describeSubjectState(state, dryRun) {
2383
- switch (state.provenance) {
2384
- case "seeded":
2385
- return `seeded (${state.seed?.length ?? 0} step(s): ${(state.seed ?? []).map((record) => record.name).join(", ")})`;
2386
- case "unpinned":
2387
- return `UNPINNED (external: ${(state.externalEnvNames ?? []).join(", ")})`;
2388
- case "declared-not-run":
2389
- return `declared, not run (${dryRun ? "dry-run contract" : "provisioning did not complete"})`;
2390
- case "undeclared":
2391
- return "undeclared";
2392
- case "external-public":
2393
- return "external-public (operator-declared, operator-owned public deployment; neither provisioned nor seeded)";
2394
- }
2395
- }
2396
- export function buildCuaCostSummary(args) {
2397
- const breakdown = [];
2398
- let sumInput = 0;
2399
- let sumOutput = 0;
2400
- for (const lane of args.lanes) {
2401
- const usage = lane.trace.tokenUsage;
2402
- if (usage) {
2403
- sumInput += usage.input ?? 0;
2404
- sumOutput += usage.output ?? 0;
2405
- }
2406
- // A stalled/ambiguous interaction can remain unreported after a later successful retry.
2407
- // Keep known token estimates and make the additional unknown explicit.
2408
- if (lane.trace.interactionUsageIncomplete === true) {
2409
- breakdown.push({
2410
- kind: "model-tokens",
2411
- ...(lane.laneId === undefined ? {} : { laneId: lane.laneId }),
2412
- ...(lane.trace.providerVersion === undefined ? {} : { modelId: lane.trace.providerVersion }),
2413
- estimatedCostUsd: null,
2414
- reason: "interaction_usage_unreported",
2415
- ratesAsOf: null
2416
- });
2417
- }
2418
- // An attempted closing request has its own accounting boundary.
2419
- if (lane.trace.debrief?.usageReported === false) {
2420
- breakdown.push({
2421
- kind: "model-tokens",
2422
- ...(lane.laneId === undefined ? {} : { laneId: lane.laneId }),
2423
- ...(lane.trace.providerVersion === undefined ? {} : { modelId: lane.trace.providerVersion }),
2424
- estimatedCostUsd: null,
2425
- reason: "closing_usage_unreported",
2426
- ratesAsOf: null
2427
- });
2428
- }
2429
- const est = lane.trace.executionProfile?.billing === "account-unknown"
2430
- ? estimateActorCostForExecution(usage, lane.trace.ids.model, lane.trace.executionProfile) : lane.trace.estimatedCost;
2431
- if (!est) {
2432
- continue;
2433
- }
2434
- breakdown.push({
2435
- kind: "model-tokens",
2436
- ...(lane.laneId === undefined ? {} : { laneId: lane.laneId }),
2437
- ...(est.modelId === undefined ? {} : { modelId: est.modelId }),
2438
- estimatedCostUsd: est.estimatedCostUsd,
2439
- ...(est.reason === undefined ? {} : { reason: est.reason }),
2440
- ratesAsOf: est.ratesAsOf,
2441
- ...(est.source === undefined ? {} : { source: est.source }),
2442
- ...(est.placeholder ? { placeholder: true } : {})
2443
- });
2444
- }
2445
- for (const usage of args.desktops ?? []) {
2446
- const observation = usage.observation;
2447
- const resources = observation && "resources" in observation ? observation.resources : undefined;
2448
- const estimate = estimateAllocatedDesktopCost(usage.minutes, resources);
2449
- breakdown.push({
2450
- kind: "desktop-minutes",
2451
- ...(usage.laneId === undefined ? {} : { laneId: usage.laneId }),
2452
- estimatedCostUsd: estimate.estimatedCostUsd,
2453
- ...(estimate.reason === undefined ? {} : { reason: estimate.reason }),
2454
- ratesAsOf: estimate.ratesAsOf,
2455
- ...(estimate.source === undefined ? {} : { source: estimate.source }),
2456
- desktop: {
2457
- minutes: estimate.minutes,
2458
- durationBasis: "host-acquired-to-cleanup",
2459
- ...(resources === undefined ? {} : { resources, resourceSource: "e2b.getInfo" }),
2460
- ...(observation && "reason" in observation ? { resourceUnavailableReason: observation.reason } : {}),
2461
- ...(estimate.usdPerSecond === undefined ? {} : { usdPerSecond: estimate.usdPerSecond })
2462
- }
2463
- });
2464
- if (!usage.lifetimeComplete) {
2465
- breakdown.push({ kind: "desktop-minutes", ...(usage.laneId === undefined ? {} : { laneId: usage.laneId }),
2466
- estimatedCostUsd: null, reason: "desktop_lifetime_incomplete", ratesAsOf: null });
2467
- }
2468
- }
2469
- if (args.desktops === undefined && args.desktopMinutes !== undefined) {
2470
- const desktop = estimateDesktopCost(args.desktopMinutes);
2471
- breakdown.push({
2472
- kind: "desktop-minutes",
2473
- estimatedCostUsd: desktop.estimatedCostUsd,
2474
- ...(desktop.reason === undefined ? {} : { reason: desktop.reason }),
2475
- ratesAsOf: desktop.ratesAsOf,
2476
- ...(desktop.source === undefined ? {} : { source: desktop.source }),
2477
- ...(desktop.placeholder ? { placeholder: true } : {})
2478
- });
2479
- }
2480
- if (breakdown.length === 0) {
2481
- return undefined;
2482
- }
2483
- let knownSum = 0;
2484
- let anyKnown = false;
2485
- let anyNull = false;
2486
- let placeholder = false;
2487
- // Aggregate freshness is CONSERVATIVE: an aggregate estimate is only as current as its OLDEST
2488
- // contributing rate, so ratesAsOf takes the MIN (oldest) asOf — MAX would overclaim freshness the
2489
- // moment operator-edited rates in src/pricing.ts diverge. Each breakdown line keeps its own true asOf.
2490
- let minRatesAsOf = null;
2491
- for (const line of breakdown) {
2492
- if (line.estimatedCostUsd === null) {
2493
- anyNull = true;
2494
- continue;
2495
- }
2496
- anyKnown = true;
2497
- knownSum += line.estimatedCostUsd;
2498
- if (line.placeholder)
2499
- placeholder = true;
2500
- if (line.ratesAsOf !== null && (minRatesAsOf === null || line.ratesAsOf < minRatesAsOf)) {
2501
- minRatesAsOf = line.ratesAsOf;
2502
- }
2503
- }
2504
- const estimatedTotalUsd = anyKnown ? round6(knownSum) : null;
2505
- const estimateNote = estimatedTotalUsd === null
2506
- ? `No priced spend lines this run — every cost line is DECLARED ABSENT (unknown rate / no usage / no duration); nothing is guessed. ${args.lanes.some(lane => lane.trace.executionProfile?.billing === "account-unknown") ? "Account billing remains unknown; API prices do not measure account spend." : "Add a rate to src/pricing.ts to estimate this model."}`
2507
- : `Estimated ${estimatedTotalUsd} USD total${anyNull ? " (LOWER BOUND — some lines unmeasured/unpriced)" : ""}${placeholder ? "; includes PLACEHOLDER rate(s) — confirm before trusting the magnitude" : ""}. Every figure is an ESTIMATE (rates as of ${minRatesAsOf} — the OLDEST contributing rate, since an aggregate is only as fresh as its stalest input), a rate-table multiply, NOT an authoritative provider charge.`;
2508
- const note = estimateNote + ((args.desktops?.length ?? 0) > 0
2509
- ? args.desktops.some(usage => usage.observation !== undefined && "resources" in usage.observation)
2510
- ? " Desktop compute uses observed CPU/RAM and a host-acquired-to-cleanup span where available; pre-handle startup, plan fees, credits, and negotiated pricing are excluded."
2511
- : " Desktop compute is unmeasured: no allocation CPU/RAM observation is available."
2512
- : "");
2513
- return {
2514
- schema: "humanish.run-cost-summary.v1",
2515
- currency: "usd",
2516
- estimatedTotalUsd,
2517
- ratesAsOf: minRatesAsOf,
2518
- fullyEstimated: !anyNull,
2519
- placeholder,
2520
- breakdown,
2521
- tokenUsage: args.lanes.some(lane => lane.trace.executionProfile?.billing === "account-unknown") ? {
2522
- ...(args.lanes.some(lane => lane.trace.tokenUsage?.input !== undefined) ? { input: sumInput } : {}),
2523
- ...(args.lanes.some(lane => lane.trace.tokenUsage?.output !== undefined) ? { output: sumOutput } : {}),
2524
- ...(args.lanes.every(lane => lane.trace.tokenUsage?.input !== undefined && lane.trace.tokenUsage?.output !== undefined &&
2525
- lane.trace.interactionUsageIncomplete !== true && lane.trace.debrief?.usageReported !== false &&
2526
- (lane.trace.executionProfile === undefined || lane.trace.providerRequests?.every(r => r.usageComplete) === true))
2527
- ? { total: sumInput + sumOutput } : {})
2528
- } : { input: sumInput, output: sumOutput, total: sumInput + sumOutput },
2529
- desktopMinutes: args.desktops === undefined ? args.desktopMinutes ?? null
2530
- : args.desktops.some(usage => usage.minutes !== undefined)
2531
- ? round6(args.desktops.reduce((sum, usage) => sum + (usage.minutes ?? 0), 0)) : null,
2532
- note
2533
- };
2534
- }
2535
- // Convert a host-side desktop span (ms) into billed minutes, or undefined when no sandbox ran.
2536
- function desktopSpanToMinutes(desktopDurationMs) {
2537
- return desktopDurationMs === undefined ? undefined : desktopDurationMs / 60_000;
2538
- }
2539
- /**
2540
- * Feedback candidates derived from what LIVE participants actually reported (#392).
2541
- *
2542
- * A live run's feedback draft used to fall through to a dry-run template, because no browser route
2543
- * ever built a candidate. The candidate worth filing is the one the study produced: a participant
2544
- * who reported friction on the way (the most valuable thing a run captures), or one who stopped
2545
- * trying. A clean pass files nothing here — feedback exists to carry findings, and a run without
2546
- * any falls back to an honest live summary in the draft layer instead of a template.
2547
- *
2548
- * Everything quoted is already scrub+redacted — participant messages and `session.reason` pass
2549
- * through redactNarration in the loop — and passes redactText again here as defense-in-depth.
2550
- */
2551
- export function participantFeedbackCandidates(args) {
2552
- const candidates = [];
2553
- for (const lane of args.lanes) {
2554
- const session = lane.session;
2555
- if (session === undefined)
2556
- continue;
2557
- const friction = resolveSelfReportedFriction(session);
2558
- const abandoned = session.status === "abandoned";
2559
- if (friction === undefined && !abandoned)
2560
- continue;
2561
- const summary = friction !== undefined
2562
- ? `Participant ${lane.personaId} (${lane.laneId}) reported friction on the way through the study goal`
2563
- : `Participant ${lane.personaId} (${lane.laneId}) stopped before completing the study goal`;
2564
- const lastScreenshot = lane.screenshots[lane.screenshots.length - 1];
2565
- candidates.push({
2566
- schema: "humanish.feedback-candidate.v1",
2567
- id: `participant-report-${lane.laneId}`,
2568
- run_id: args.runId,
2569
- stream_id: lane.streamId,
2570
- adapter_id: args.adapterId,
2571
- scenario_id: args.scenarioId,
2572
- persona_id: lane.personaId,
2573
- actor: "computer-use",
2574
- substrate: args.substrate,
2575
- // The participant is reporting on the PRODUCT: friction and abandonment are target-app
2576
- // findings by the three-roles rule. A harness failure never reaches this builder — it is
2577
- // not a participant report.
2578
- failure_owner: "target-app",
2579
- summary,
2580
- expected: args.goal,
2581
- actual: redactText(friction ?? session.reason),
2582
- evidence: [
2583
- ...(lane.traceArtifactPath === undefined ? [] : [{
2584
- path: lane.traceArtifactPath,
2585
- kind: "trace",
2586
- note: "Full actor trace: turns, actions, and the participant's own report."
2587
- }]),
2588
- ...(lastScreenshot === undefined ? [] : [{
2589
- path: lastScreenshot,
2590
- kind: "screenshot",
2591
- note: "Final screenshot at the moment the session ended."
2592
- }]),
2593
- ...(lane.commsArtifactPath === undefined ? [] : [{
2594
- path: lane.commsArtifactPath,
2595
- kind: "log",
2596
- note: "Digest-only comms thread captured in-sandbox."
2597
- }])
2598
- ],
2599
- redaction: {
2600
- status: "passed",
2601
- notes: "Quoted participant text passed the loop's known-value scrub and pattern redaction before persisting, and redactText again here."
2602
- },
2603
- idempotency_key: `humanish:${args.runId}:${lane.laneId}:participant-report`,
2604
- proposed_next_state: "study-quality-review",
2605
- acceptance_proof: [
2606
- feedbackProofCommands(args.runId).verify,
2607
- feedbackProofCommands(args.runId).watch
2608
- ]
2609
- });
2610
- }
2611
- return candidates;
2612
- }
2613
- export function buildCuaBundle(args) {
2614
- const publicAppUrl = publicSafeAppUrlLabel(args.appUrl);
2615
- // Run-level cost ESTIMATE (advisory; omitted when nothing was priced and no sandbox ran).
2616
- const cost = buildCuaCostSummary({
2617
- lanes: args.session ? [{ ...(args.laneId === undefined ? {} : { laneId: args.laneId }), trace: args.session.trace }] : [],
2618
- desktopMinutes: args.desktopMinutes,
2619
- ...(args.desktopUsage === undefined ? {} : { desktops: [args.desktopUsage] })
2620
- });
2621
- const status = args.inProgress === true
2622
- ? "running"
2623
- : args.session
2624
- ? args.session.status
2625
- : args.sessionError
2626
- ? "failed"
2627
- : "contract_proof_only";
2628
- const reason = args.inProgress === true
2629
- ? "Live computer-use session is running; stream auth URL is available only through the attached Observer server."
2630
- : args.session?.reason
2631
- ?? args.sessionError
2632
- ?? "Contract bundle only: dry-run produced the evidence shape without launching a desktop or spending provider tokens.";
2633
- const lastScreenshot = args.screenshots[args.screenshots.length - 1];
2634
- const desktopGeometry = args.desktopRoute === false
2635
- ? undefined
2636
- : args.desktopGeometry ?? {
2637
- screen: { requested: { width: args.resolution[0], height: args.resolution[1] } }
2638
- };
2639
- // Honest labels (invariant 6: claims match mechanism): every screenshot label names the
2640
- // run's ACTUAL mode. The session trace is the evidence-of-record; the capture policy covers
2641
- // frames written before a mid-session failure produced a trace.
2642
- const traceScreenshotMode = args.session?.trace.redaction.screenshots;
2643
- const screenshotMode = traceScreenshotMode === "raw" || traceScreenshotMode === "blurred"
2644
- ? traceScreenshotMode
2645
- : args.captureRedaction ?? "raw";
2646
- const simulation = {
2647
- id: "sim-001",
2648
- index: 1,
2649
- personaId: args.persona.id,
2650
- scenarioId: `cua-${args.labId}`,
2651
- status,
2652
- streamKind: "browser",
2653
- mode: "browser-sim",
2654
- progress: args.inProgress === true ? 20 : 100,
2655
- currentStep: reason,
2656
- summary: args.session
2657
- ? `Computer-use actor (${args.actorId}) drove the subject app in a hosted desktop browser; ${args.session.completionReason}.`
2658
- : args.inProgress === true
2659
- ? `Computer-use actor (${args.actorId}) is driving the subject app in a hosted desktop browser.`
2660
- : args.sessionError
2661
- ? `Computer-use lab failed before a terminal session verdict: ${args.sessionError}`
2662
- : `Contract lane for the computer-use actor (${args.actorId}) against ${publicAppUrl}.`,
2663
- streamIds: ["stream-001"],
2664
- startedAt: args.createdAt,
2665
- updatedAt: args.createdAt
2666
- };
2667
- const stream = {
2668
- id: "stream-001",
2669
- simId: "sim-001",
2670
- laneId: args.laneId ?? "lane-01",
2671
- ...(args.assignment === undefined ? {} : { assignment: participantAssignment(args.assignment) }),
2672
- ...(args.actorType === undefined ? {} : { actorType: args.actorType }),
2673
- ...(args.surface === undefined ? {} : { surface: args.surface }),
2674
- ...(args.caseGroup === undefined ? {} : { caseGroup: args.caseGroup }),
2675
- kind: "browser",
2676
- label: `CUA browser — ${args.labId}`,
2677
- status,
2678
- transport: "snapshot",
2679
- updatedAt: args.createdAt,
2680
- embed: lastScreenshot
2681
- ? { kind: "screenshot", url: lastScreenshot, title: `CUA desktop (${screenshotMode})` }
2682
- : { kind: "placeholder", title: "CUA desktop" },
2683
- ...(desktopGeometry?.viewport === undefined
2684
- ? {}
2685
- : {
2686
- viewport: {
2687
- width: desktopGeometry.viewport.width,
2688
- height: desktopGeometry.viewport.height,
2689
- deviceScaleFactor: desktopGeometry.viewport.deviceScaleFactor,
2690
- ...(args.isMobile === undefined ? {} : { isMobile: args.isMobile })
2691
- }
2692
- }),
2693
- ...(desktopGeometry === undefined ? {} : { desktopGeometry }),
2694
- ...(args.recording === undefined ? {} : { recording: args.recording }),
2695
- ui: {
2696
- route: publicAppUrl,
2697
- intent: "Watch the computer-use actor drive the subject app in a hosted desktop browser.",
2698
- state: reason,
2699
- ...(args.session ? { actorStatus: args.session.status } : {}),
2700
- ...(lastScreenshot ? { screenshotUrl: lastScreenshot } : {})
2701
- },
2702
- // The seam this lab exists to fill: the provider-neutral actor evidence projection.
2703
- ...(args.session ? { actor: args.session.trace } : {}),
2704
- artifacts: [
2705
- { label: "run bundle", path: "run.json", kind: "bundle" },
2706
- { label: "review", path: "review.md", kind: "review" },
2707
- { label: "events", path: "events.ndjson", kind: "events" },
2708
- ...(args.traceArtifactPath
2709
- ? [{ label: "actor trace", path: args.traceArtifactPath, kind: "trace" }]
2710
- : []),
2711
- ...(args.commsArtifactPath
2712
- ? [{ label: "comms thread", path: args.commsArtifactPath, kind: "log" }]
2713
- : []),
2714
- ...(args.recording ? [{ label: "desktop recording", path: args.recording.path, kind: "recording" }] : []),
2715
- ...args.screenshots.map((screenshot, index) => ({
2716
- label: `screenshot ${String(index + 1).padStart(2, "0")} (${screenshotMode})`,
2717
- path: screenshot,
2718
- kind: "screenshot"
2719
- }))
2720
- ]
2721
- };
2722
- const events = [
2723
- {
2724
- id: "event-000-created",
2725
- at: args.createdAt,
2726
- level: "info",
2727
- type: "cua-lab.run.created",
2728
- message: `Created computer-use lab run for ${args.labId} (actor ${args.actorId}).`
2729
- },
2730
- args.subjectProvenance
2731
- ? {
2732
- id: "event-001-subject",
2733
- at: args.createdAt,
2734
- level: "info",
2735
- type: "cua-lab.subject.provenance",
2736
- // HONEST WORDING: claim "cloned/packed and served" only when it actually happened.
2737
- message: `${subjectProvenanceMessage(args.subjectProvenance, publicAppUrl, args.dryRun, args.session !== undefined)} (subject env names: ${args.subjectProvenance.envNames.length > 0 ? args.subjectProvenance.envNames.join(", ") : "none"}; values never persisted); state: ${describeSubjectState(args.subjectProvenance.state, args.dryRun)}.`,
2738
- simId: "sim-001",
2739
- streamId: "stream-001"
2740
- }
2741
- : {
2742
- id: "event-001-subject",
2743
- at: args.createdAt,
2744
- level: "info",
2745
- type: "cua-lab.subject.declared",
2746
- // Invariant 5: declare what the subject WAS, including the ABSENCE of a pin. A
2747
- // local-app / in-process subject is an already-running LOCAL dev server the caller
2748
- // provisioned; it cannot be commit-pinned, so its provenance is honestly UNPINNED and
2749
- // no E2B desktop was created. A plain app-url entry runs inside the desktop sandbox.
2750
- message: args.entryKind === "local-app"
2751
- ? `Subject app declared at ${publicAppUrl} (already-running LOCAL dev server driven in-process; NO clone, NO E2B desktop). Provenance: caller-provisioned and UNPINNED — a running dev server cannot be commit-pinned.`
2752
- : `Subject app declared at ${publicAppUrl} (loopback inside the desktop sandbox).`,
2753
- simId: "sim-001",
2754
- streamId: "stream-001"
2755
- },
2756
- args.session
2757
- ? {
2758
- id: "event-002-session",
2759
- at: args.createdAt,
2760
- level: args.session.status === "passed" ? "info" : "warn",
2761
- type: `cua-lab.session.${args.session.completionReason}`,
2762
- message: `${args.session.status}: ${args.session.reason}`,
2763
- simId: "sim-001",
2764
- streamId: "stream-001"
2765
- }
2766
- : args.inProgress === true
2767
- ? {
2768
- id: "event-002-running",
2769
- at: args.createdAt,
2770
- level: "info",
2771
- type: "cua-lab.session.running",
2772
- message: "Live computer-use session is running; terminal evidence has not been written yet.",
2773
- simId: "sim-001",
2774
- streamId: "stream-001"
2775
- }
2776
- : args.sessionError
2777
- ? {
2778
- id: "event-002-session",
2779
- at: args.createdAt,
2780
- level: "error",
2781
- type: "cua-lab.session.error",
2782
- message: args.sessionError,
2783
- simId: "sim-001",
2784
- streamId: "stream-001"
2785
- }
2786
- : {
2787
- id: "event-002-contract",
2788
- at: args.createdAt,
2789
- level: "info",
2790
- type: "cua-lab.contract.ready",
2791
- message: "Dry-run contract bundle ready; switch scenario.mode to live for a real desktop session.",
2792
- simId: "sim-001",
2793
- streamId: "stream-001"
2794
- }
2795
- ];
2796
- // Persisted phase trail (real boot timing, not just a coarse provenance sentence): one
2797
- // RunEvent per COMPLETED phase boundary (started events never persist here; they carry no
2798
- // durationMs). ok:false phases warn rather than error, since the failing phase's own thrown
2799
- // error already becomes the terminal cua-lab.session.error event above.
2800
- let phaseEventSeq = 3;
2801
- for (const phase of args.phaseEvents ?? []) {
2802
- events.push({
2803
- id: `event-${String(phaseEventSeq++).padStart(3, "0")}-phase-${phaseEventIdSuffix(phase.type)}`,
2804
- at: phase.at,
2805
- level: phase.ok === false ? "warn" : "info",
2806
- type: phase.type,
2807
- message: phase.durationMs === undefined ? phase.message : `${phase.message} (${phase.durationMs}ms)`,
2808
- simId: "sim-001",
2809
- streamId: "stream-001"
2810
- });
2811
- }
2812
- for (const warning of desktopGeometry?.warnings ?? []) {
2813
- events.push({
2814
- id: `event-${String(phaseEventSeq++).padStart(3, "0")}-geometry-warning`,
2815
- at: args.createdAt,
2816
- level: "warn",
2817
- type: "cua-lab.geometry.warning",
2818
- message: warning,
2819
- simId: "sim-001",
2820
- streamId: "stream-001"
2821
- });
2822
- }
2823
- // A funnel with a denominator of one is still the funnel — and its absence stays honest: no
2824
- // declared protocol (or a dry run) means no `tasks` field, never an empty one.
2825
- const singleStudyTasks = args.inProgress !== true && args.session?.trace.taskFunnel !== undefined
2826
- ? aggregateTaskFunnels([args.session.trace.taskFunnel])
2827
- : undefined;
2828
- // What happened to the participant, as the LANE judged it — the same rule the fan-out roll-up
2829
- // applies (participantStatusForOutcome). Before #476 this read the actor's own status, so a
2830
- // run the lane refused as "not a credible pass" was written up as verdict pass, 1/1 reached
2831
- // the goal, and every projection of the bundle (Observer tally, `runs`, the status index)
2832
- // repeated it. Found on a real drawDB run whose participant wrote "Blocked after partial
2833
- // completion".
2834
- const participantStatus = args.session === undefined
2835
- ? undefined
2836
- : participantStatusForCredibility(args.session.status, args.credibility);
2837
- const credibilityNote = args.session === undefined || participantStatus === args.session.status
2838
- ? undefined
2839
- : args.credibility?.noEngagement === true
2840
- ? "Not counted as a pass: the participant took no actions and said nothing."
2841
- : "Not counted as a pass: the participant's final message described a blocker.";
2842
- const review = withCuaReviewProvenance({
2843
- schema: REVIEW_SCHEMA,
2844
- verdict: args.inProgress === true
2845
- ? "contract_proof_only"
2846
- : participantStatus !== undefined
2847
- ? verdictForStatus(participantStatus)
2848
- : args.sessionError
2849
- ? "fail"
2850
- : "contract_proof_only",
2851
- // One lane is still a study with a denominator of one, and saying so keeps a single-lane
2852
- // result from being read as though it generalized.
2853
- ...(participantStatus !== undefined && args.inProgress !== true
2854
- ? { participants: tallyParticipantOutcomes([participantStatus], [args.credibility?.reportedFriction === true]) }
2855
- : {}),
2856
- ...(singleStudyTasks === undefined ? {} : { tasks: singleStudyTasks }),
2857
- summary: credibilityNote === undefined ? reason : `${credibilityNote} ${reason}`,
2858
- gaps: args.session || args.sessionError
2859
- ? []
2860
- : args.inProgress === true
2861
- ? ["Live desktop session is still running."]
2862
- : ["Live desktop session not yet run (dry-run contract only)."]
2863
- }, [stream]);
2864
- return {
2865
- schema: RUN_BUNDLE_SCHEMA,
2866
- ...(args.realEmail && !args.dryRun ? { publication: { restrictions: ["real-communications"] } } : {}),
2867
- runId: args.runId,
2868
- mode: args.dryRun ? "dry-run" : "live",
2869
- simCount: 1,
2870
- createdAt: args.createdAt,
2871
- cwd: PUBLIC_TARGET_CWD,
2872
- artifactRoot: path.join(".humanish", "runs", args.runId),
2873
- ...(args.lab === undefined ? {} : { lab: args.lab }),
2874
- source: args.source,
2875
- persona: {
2876
- id: args.persona.id,
2877
- name: `Computer-use operator (${args.persona.id})`,
2878
- source: `lab:${args.labId}`,
2879
- sourceDigest: args.persona.promptDigest
2880
- },
2881
- scenario: {
2882
- id: `cua-${args.labId}`,
2883
- title: args.labTitle ?? `Computer-use lab: ${args.labId}`,
2884
- goal: redactText(args.mission),
2885
- source: `lab:${args.labId}`,
2886
- sourceDigest: args.persona.promptDigest
2887
- },
2888
- lifecycle: [
2889
- {
2890
- at: args.createdAt,
2891
- event: "cua-lab.run.created",
2892
- message: `Created computer-use lab run with one desktop browser lane (actor ${args.actorId}).`
2893
- }
2894
- ],
2895
- simulations: [simulation],
2896
- streams: [stream],
2897
- events,
2898
- redaction: {
2899
- status: "passed",
2900
- notes: traceScreenshotMode === "raw"
2901
- ? "Typed text recorded as length only and reasoning/messages pass through text redaction. Screenshots are FULL-FIDELITY (raw), retained for local use — NOT redacted for publishing; set policies.redactScreenshots: true to blur a share-as-is bundle."
2902
- : traceScreenshotMode === "blurred"
2903
- ? "Typed text recorded as length only and reasoning/messages pass through text redaction. Screenshots are blurred at capture (policies.redactScreenshots: true) for a share-as-is bundle."
2904
- : args.screenshots.length > 0
2905
- ? `Session ended before a trace was recorded; ${args.screenshots.length} already-written frame(s) follow the capture policy (${screenshotMode}). Typed text is recorded as length only and reasoning/messages pass through text redaction.`
2906
- : "No screenshots captured. Typed text is recorded as length only and reasoning/messages pass through text redaction whenever a session runs."
2907
- },
2908
- artifacts: {
2909
- run: "run.json",
2910
- reviewJson: "review.json",
2911
- reviewMarkdown: "review.md",
2912
- observerData: "observer/observer-data.json",
2913
- events: "events.ndjson"
2914
- },
2915
- review,
2916
- // What the participant reported, when it reported anything (#392). Dry-run and in-progress
2917
- // bundles carry none — there is no participant yet to quote.
2918
- feedbackCandidates: args.dryRun || args.inProgress === true
2919
- ? []
2920
- : participantFeedbackCandidates({
2921
- runId: args.runId,
2922
- scenarioId: `cua-${args.labId}`,
2923
- adapterId: args.labId,
2924
- goal: redactText(args.mission),
2925
- substrate: args.feedbackSubstrate ?? (args.desktopRoute === false ? "local-filesystem" : "e2b-desktop"),
2926
- lanes: [{
2927
- laneId: args.laneId ?? "lane-01",
2928
- streamId: "stream-001",
2929
- personaId: args.persona.id,
2930
- ...(args.session === undefined ? {} : { session: args.session }),
2931
- ...(args.traceArtifactPath === undefined ? {} : { traceArtifactPath: args.traceArtifactPath }),
2932
- screenshots: args.screenshots,
2933
- ...(args.commsArtifactPath === undefined ? {} : { commsArtifactPath: args.commsArtifactPath })
2934
- }]
2935
- }),
2936
- // Custom desktop image provenance (omitted on the stock-template default → byte-stable).
2937
- ...(args.desktopTemplate === undefined ? {} : { desktopTemplate: args.desktopTemplate }),
2938
- ...(args.desktopBrowser === undefined ? {} : { desktopBrowser: args.desktopBrowser }),
2939
- ...(args.providerResources === undefined || args.providerResources.length === 0 ? {} : { providerResources: args.providerResources }),
2940
- // Structured subject provenance (invariant 5): code pin + state story. Uniform and
2941
- // honest on app-url bundles too — the caller minted the URL, its state is the caller's.
2942
- // CuaSubjectProvenanceArg's two variants (clone, local-tree) are already RunSubjectProvenance-
2943
- // shaped, so no reconstruction is needed beyond the app-url fallback.
2944
- subject: args.subjectProvenance ?? { source: "app-url", state: { provenance: "undeclared" } },
2945
- ...(cost === undefined ? {} : { cost })
2946
- };
2947
- }
2948
- /** Human-readable provenance line for the single-lane subject.provenance event (invariant 5):
2949
- * claims "cloned/packed and served" ONLY when it actually happened. */
2950
- function subjectProvenanceMessage(provenance, publicAppUrl, dryRun, hasSession) {
2951
- if (provenance.source === "clone") {
2952
- if (dryRun) {
2953
- return `Subject declared: clone of ${provenance.repo}, to be served at ${publicAppUrl} in-sandbox (dry-run contract; nothing cloned)`;
2954
- }
2955
- if (provenance.commit) {
2956
- return hasSession
2957
- ? `Subject cloned from ${provenance.repo}@${provenance.commit} and served at ${publicAppUrl} in-sandbox`
2958
- : `Subject cloned from ${provenance.repo}@${provenance.commit}; serving at ${publicAppUrl} did not complete (see session error)`;
2959
- }
2960
- return `Subject clone attempted from ${provenance.repo}; commit unresolved (provisioning failed before resolution)`;
2961
- }
2962
- if (dryRun) {
2963
- return `Subject declared: local working tree, to be packed and served at ${publicAppUrl} in-sandbox (dry-run contract; nothing packed)`;
2964
- }
2965
- if (provenance.archiveSha256) {
2966
- const dirtyLabel = provenance.dirty === true ? ", dirty working tree" : provenance.dirty === false ? ", clean working tree" : "";
2967
- return hasSession
2968
- ? `Subject packed (archiveSha256 ${provenance.archiveSha256}${dirtyLabel}) and served at ${publicAppUrl} in-sandbox`
2969
- : `Subject packed (archiveSha256 ${provenance.archiveSha256}${dirtyLabel}); serving at ${publicAppUrl} did not complete (see session error)`;
2970
- }
2971
- return "Subject local-tree packing attempted; archive digest unresolved (provisioning failed before resolution)";
2972
- }
2973
- /**
2974
- * Project N>1 fan-out lanes into a humanish.run-bundle.v1 (the evidence schema is unchanged; this
2975
- * is a new producer for the multi-stream shape). One sim + one stream per lane; per-lane
2976
- * provenance/session events; a recorded `cua-lab.fanout.plan` event (and a `cua-lab.fanout.fail-fast`
2977
- * event when a harness error skipped queued lanes). N-ary verify/Observer already handle multiple
2978
- * streams. The N=1 path NEVER reaches here (buildCuaBundle owns it, byte-stable).
2979
- */
2980
- export function buildCuaFanoutBundle(args) {
2981
- const { specs, outcomes, config } = args;
2982
- const simulations = [];
2983
- const streams = [];
2984
- const events = [];
2985
- events.push({
2986
- id: "event-000-created",
2987
- at: args.createdAt,
2988
- level: "info",
2989
- type: "cua-lab.run.created",
2990
- message: `Created computer-use fan-out run for ${config.id} (actor ${args.descriptor.id}, ${specs.length} lanes, per-lane worlds).`
2991
- });
2992
- events.push({
2993
- id: "event-001-fanout-plan",
2994
- at: args.createdAt,
2995
- level: "info",
2996
- type: "cua-lab.fanout.plan",
2997
- message: `Fan-out plan: ${args.plan.laneCount} lane(s) (${args.plan.strategy}), concurrency ${args.plan.concurrency}, ${args.plan.waves} wave(s); per-lane session budget ${Math.round(args.plan.perLaneSessionBudgetMs / 1000)}s; worst-case ~${args.plan.worstCaseSandboxMinutes} sandbox-minutes${args.dryRun ? " (dry-run: $0)" : ""}. Lanes: ${args.plan.lanes.map(formatLanePlanEntry).join(", ")}.`
2998
- });
2999
- let eventSeq = 2;
3000
- const nextEventId = (suffix) => `event-${String(eventSeq++).padStart(3, "0")}-${suffix}`;
3001
- if (args.rerun) {
3002
- events.push({
3003
- id: nextEventId("fanout-rerun"),
3004
- at: args.createdAt,
3005
- level: "info",
3006
- type: "cua-lab.fanout.rerun",
3007
- message: `Rerun selected ${args.rerun.selectedLaneIds.length} lane(s) from ${args.rerun.sourceRunId}: ${args.rerun.previous.map((lane) => `${lane.laneId} was ${lane.status}${lane.completionReason ? `/${lane.completionReason}` : ""}`).join(", ")}. This is a new linked run; the source run verdict is unchanged.`
3008
- });
3009
- }
3010
- specs.forEach((spec, index) => {
3011
- const outcome = outcomes?.[index];
3012
- const laneAppUrl = spec.targetUrl ?? args.appUrl;
3013
- const publicLaneAppUrl = publicSafeAppUrlLabel(laneAppUrl);
3014
- const subject = args.laneSubjects[index];
3015
- const session = outcome?.session;
3016
- const fallbackDeclared = declaredScreenForRender(spec.devicePreset, spec.deviceName, spec.resolution);
3017
- const desktopGeometry = outcome?.desktopGeometry ?? {
3018
- screen: {
3019
- requested: { width: spec.resolution[0], height: spec.resolution[1] },
3020
- ...(fallbackDeclared ? { declared: fallbackDeclared } : {})
3021
- }
3022
- };
3023
- const screenshots = outcome?.screenshots ?? [];
3024
- const lastScreenshot = screenshots[screenshots.length - 1];
3025
- const status = args.inProgress === true && outcome === undefined
3026
- ? "running"
3027
- : outcome?.skippedReason !== undefined
3028
- ? "blocked"
3029
- : session
3030
- ? session.status
3031
- : outcome?.sessionError
3032
- ? "failed"
3033
- : "contract_proof_only";
3034
- const reason = args.inProgress === true && outcome === undefined
3035
- ? "Live computer-use lane is running; stream auth URL is available only through the attached Observer server."
3036
- : outcome?.skippedReason
3037
- ?? session?.reason
3038
- ?? outcome?.sessionError
3039
- ?? "Contract bundle only: dry-run produced the evidence shape without launching a desktop or spending provider tokens.";
3040
- const traceScreenshotMode = session?.trace.redaction.screenshots;
3041
- const screenshotMode = traceScreenshotMode === "raw" || traceScreenshotMode === "blurred"
3042
- ? traceScreenshotMode
3043
- : config.policies?.redactScreenshots === true ? "blurred" : "raw";
3044
- simulations.push({
3045
- id: spec.simId,
3046
- index: index + 1,
3047
- personaId: spec.persona.id,
3048
- scenarioId: `cua-${config.id}`,
3049
- status,
3050
- streamKind: "browser",
3051
- mode: "browser-sim",
3052
- progress: args.inProgress === true && outcome === undefined ? 20 : 100,
3053
- currentStep: reason,
3054
- summary: session
3055
- ? `Lane ${spec.laneId} (${spec.persona.id}/${spec.deviceName}): computer-use actor (${args.descriptor.id}) drove the subject app; ${session.completionReason}.`
3056
- : args.inProgress === true && outcome === undefined
3057
- ? `Lane ${spec.laneId} (${spec.persona.id}/${spec.deviceName}): computer-use actor (${args.descriptor.id}) is driving the subject app.`
3058
- : outcome?.skippedReason !== undefined
3059
- ? `Lane ${spec.laneId} ${outcome.skippedReason}.`
3060
- : outcome?.sessionError
3061
- ? `Lane ${spec.laneId} failed before a terminal session verdict: ${outcome.sessionError}`
3062
- : `Contract lane ${spec.laneId} (${spec.persona.id}/${spec.deviceName}) for ${args.descriptor.id} against ${publicLaneAppUrl}.`,
3063
- streamIds: [spec.streamId],
3064
- startedAt: args.createdAt,
3065
- updatedAt: args.createdAt
3066
- });
3067
- streams.push({
3068
- id: spec.streamId,
3069
- simId: spec.simId,
3070
- laneId: spec.laneId,
3071
- ...(spec.assignment === undefined ? {} : { assignment: participantAssignment(spec.assignment) }),
3072
- ...(spec.actorType === undefined ? {} : { actorType: spec.actorType }),
3073
- ...(spec.surface === undefined ? {} : { surface: spec.surface }),
3074
- ...(spec.caseGroup === undefined ? {} : { caseGroup: spec.caseGroup }),
3075
- kind: "browser",
3076
- label: `CUA lane ${spec.laneId} — ${config.id}`,
3077
- status,
3078
- transport: "snapshot",
3079
- updatedAt: args.createdAt,
3080
- embed: lastScreenshot
3081
- ? { kind: "screenshot", url: lastScreenshot, title: `CUA desktop ${spec.laneId} (${screenshotMode})` }
3082
- : { kind: "placeholder", title: `CUA desktop ${spec.laneId}` },
3083
- ...(desktopGeometry.viewport === undefined
3084
- ? {}
3085
- : {
3086
- viewport: {
3087
- width: desktopGeometry.viewport.width,
3088
- height: desktopGeometry.viewport.height,
3089
- deviceScaleFactor: desktopGeometry.viewport.deviceScaleFactor,
3090
- isMobile: spec.devicePreset.isMobile
3091
- }
3092
- }),
3093
- desktopGeometry,
3094
- ...(outcome?.recording === undefined ? {} : { recording: outcome.recording }),
3095
- ui: {
3096
- route: publicLaneAppUrl,
3097
- intent: `Watch lane ${spec.laneId} (${spec.persona.id}/${spec.deviceName}) drive the subject app in its own hosted desktop.`,
3098
- state: reason,
3099
- ...(session ? { actorStatus: session.status } : {}),
3100
- ...(lastScreenshot ? { screenshotUrl: lastScreenshot } : {})
3101
- },
3102
- ...(session ? { actor: session.trace } : {}),
3103
- artifacts: [
3104
- { label: "run bundle", path: "run.json", kind: "bundle" },
3105
- { label: "review", path: "review.md", kind: "review" },
3106
- { label: "events", path: "events.ndjson", kind: "events" },
3107
- ...(session
3108
- ? [{ label: `lane ${spec.laneId} actor trace`, path: spec.traceArtifactPath, kind: "trace" }]
3109
- : []),
3110
- ...(outcome?.commsArtifactPath
3111
- ? [{ label: `lane ${spec.laneId} comms thread`, path: outcome.commsArtifactPath, kind: "log" }]
3112
- : []),
3113
- ...(outcome?.recording ? [{ label: "desktop recording", path: outcome.recording.path, kind: "recording" }] : []),
3114
- ...screenshots.map((screenshot, screenshotIndex) => ({
3115
- label: `lane ${spec.laneId} screenshot ${String(screenshotIndex + 1).padStart(2, "0")} (${screenshotMode})`,
3116
- path: screenshot,
3117
- kind: "screenshot"
3118
- }))
3119
- ]
3120
- });
3121
- // Per-lane subject provenance (invariant 5).
3122
- if (args.cloneRoute && args.publicRepo) {
3123
- events.push({
3124
- id: nextEventId(`subject-${spec.laneId}`),
3125
- at: args.createdAt,
3126
- level: "info",
3127
- type: "cua-lab.subject.provenance",
3128
- message: `Lane ${spec.laneId}: ${args.dryRun
3129
- ? `subject declared — clone of ${args.publicRepo}, served at ${publicLaneAppUrl} in-sandbox (dry-run contract; nothing cloned)`
3130
- : subject.commit
3131
- ? session
3132
- ? `subject cloned from ${args.publicRepo}@${subject.commit} and served at ${publicLaneAppUrl} in-sandbox`
3133
- : `subject cloned from ${args.publicRepo}@${subject.commit}; serving did not complete (see session error)`
3134
- : `subject clone attempted from ${args.publicRepo}; commit unresolved`} (subject env names: ${args.subjectEnvNames.length > 0 ? args.subjectEnvNames.join(", ") : "none"}; values never persisted); state: ${describeSubjectState(subject.state, args.dryRun)}.`,
3135
- simId: spec.simId,
3136
- streamId: spec.streamId
3137
- });
3138
- }
3139
- else if (subject.source === "local-tree") {
3140
- events.push({
3141
- id: nextEventId(`subject-${spec.laneId}`),
3142
- at: args.createdAt,
3143
- level: "info",
3144
- type: "cua-lab.subject.provenance",
3145
- message: `Lane ${spec.laneId}: ${args.dryRun
3146
- ? `subject declared: local working tree, to be packed and served at ${publicLaneAppUrl} in-sandbox (dry-run contract; nothing packed)`
3147
- : subject.archiveSha256
3148
- ? session
3149
- ? `subject packed (archiveSha256 ${subject.archiveSha256}${subject.dirty === true ? ", dirty working tree" : subject.dirty === false ? ", clean working tree" : ""}) and served at ${publicLaneAppUrl} in-sandbox`
3150
- : `subject packed (archiveSha256 ${subject.archiveSha256}); serving did not complete (see session error)`
3151
- : "subject local-tree packing attempted; archive digest unresolved"} (subject env names: ${args.subjectEnvNames.length > 0 ? args.subjectEnvNames.join(", ") : "none"}; values never persisted); state: ${describeSubjectState(subject.state, args.dryRun)}.`,
3152
- simId: spec.simId,
3153
- streamId: spec.streamId
3154
- });
3155
- }
3156
- else {
3157
- events.push({
3158
- id: nextEventId(`subject-${spec.laneId}`),
3159
- at: args.createdAt,
3160
- level: "info",
3161
- type: "cua-lab.subject.declared",
3162
- message: `Lane ${spec.laneId}: subject app declared at ${publicLaneAppUrl} (loopback inside the lane's own desktop sandbox).`,
3163
- simId: spec.simId,
3164
- streamId: spec.streamId
3165
- });
3166
- }
3167
- // Per-lane session event.
3168
- if (session) {
3169
- events.push({
3170
- id: nextEventId(`session-${spec.laneId}`),
3171
- at: args.createdAt,
3172
- level: session.status === "passed" ? "info" : "warn",
3173
- type: `cua-lab.session.${session.completionReason}`,
3174
- message: `Lane ${spec.laneId}: ${session.status} — ${session.reason}`,
3175
- simId: spec.simId,
3176
- streamId: spec.streamId
3177
- });
3178
- }
3179
- else if (args.inProgress === true && outcome === undefined) {
3180
- events.push({
3181
- id: nextEventId(`running-${spec.laneId}`),
3182
- at: args.createdAt,
3183
- level: "info",
3184
- type: "cua-lab.session.running",
3185
- message: `Lane ${spec.laneId}: live computer-use session is running; terminal evidence has not been written yet.`,
3186
- simId: spec.simId,
3187
- streamId: spec.streamId
3188
- });
3189
- }
3190
- else if (outcome?.skippedReason !== undefined) {
3191
- events.push({
3192
- id: nextEventId(`blocked-${spec.laneId}`),
3193
- at: args.createdAt,
3194
- level: "warn",
3195
- type: "cua-lab.session.blocked",
3196
- message: `Lane ${spec.laneId} ${outcome.skippedReason}.`,
3197
- simId: spec.simId,
3198
- streamId: spec.streamId
3199
- });
3200
- }
3201
- else if (outcome?.sessionError) {
3202
- events.push({
3203
- id: nextEventId(`session-error-${spec.laneId}`),
3204
- at: args.createdAt,
3205
- level: "error",
3206
- type: "cua-lab.session.error",
3207
- message: `Lane ${spec.laneId}: ${outcome.sessionError}`,
3208
- simId: spec.simId,
3209
- streamId: spec.streamId
3210
- });
3211
- }
3212
- else {
3213
- events.push({
3214
- id: nextEventId(`contract-${spec.laneId}`),
3215
- at: args.createdAt,
3216
- level: "info",
3217
- type: "cua-lab.contract.ready",
3218
- message: `Lane ${spec.laneId}: dry-run contract lane ready; switch scenario.mode to live for a real desktop session.`,
3219
- simId: spec.simId,
3220
- streamId: spec.streamId
3221
- });
3222
- }
3223
- for (const warning of desktopGeometry.warnings ?? []) {
3224
- events.push({
3225
- id: nextEventId(`geometry-warning-${spec.laneId}`),
3226
- at: args.createdAt,
3227
- level: "warn",
3228
- type: "cua-lab.geometry.warning",
3229
- message: warning,
3230
- simId: spec.simId,
3231
- streamId: spec.streamId
3232
- });
3233
- }
3234
- // Persisted per-lane phase trail (real boot timing): one RunEvent per COMPLETED phase
3235
- // boundary this lane recorded (started events never persist here; they carry no durationMs).
3236
- for (const phase of outcome?.phaseRecords ?? []) {
3237
- events.push({
3238
- id: nextEventId(`phase-${spec.laneId}-${phaseEventIdSuffix(phase.type)}`),
3239
- at: phase.at,
3240
- level: phase.ok === false ? "warn" : "info",
3241
- type: phase.type,
3242
- message: phase.durationMs === undefined
3243
- ? `Lane ${spec.laneId}: ${phase.message}`
3244
- : `Lane ${spec.laneId}: ${phase.message} (${phase.durationMs}ms)`,
3245
- simId: spec.simId,
3246
- streamId: spec.streamId
3247
- });
3248
- }
3249
- });
3250
- if (args.failFastReason) {
3251
- events.push({
3252
- id: nextEventId("fanout-fail-fast"),
3253
- at: args.createdAt,
3254
- level: "warn",
3255
- type: "cua-lab.fanout.fail-fast",
3256
- message: `Fan-out fail-fast: ${args.failFastReason}. In-flight lanes finished; queued lanes were skipped (blocked) — completed evidence is retained.`
3257
- });
3258
- }
3259
- // Worst-of review verdict across lanes; live fan-out must prove every lane.
3260
- const verdict = args.inProgress === true
3261
- ? "contract_proof_only"
3262
- : fanoutReviewVerdict({
3263
- dryRun: args.dryRun,
3264
- expectedLaneCount: specs.length,
3265
- outcomes
3266
- });
3267
- const passedLanes = (outcomes ?? []).filter((outcome) => outcome.skippedReason === undefined
3268
- && outcome.session !== undefined
3269
- && outcome.session.status === "passed"
3270
- && outcome.session.completionReason !== "harness_error"
3271
- && outcome.sessionError === undefined
3272
- && !outcome.noEngagement
3273
- && !outcome.selfReportedBlocker).length;
3274
- // What happened to the PARTICIPANTS, with the denominator attached. The verdict above has to
3275
- // collapse the run to one word; this does not (docs/principles/three-roles.md).
3276
- const terminalOutcomes = (outcomes ?? []).filter((outcome) => outcome?.session?.status !== undefined);
3277
- const participants = terminalOutcomes.length > 0
3278
- ? tallyParticipantOutcomes(
3279
- // A NO-ENGAGEMENT lane is not a participant who reached the goal. It said "done" having
3280
- // taken zero actions and said nothing, and `passedLanes` below already refuses to count
3281
- // it — but `reachedGoal` was reading the trace status directly, so one run could be both
3282
- // "not a passed lane" AND "1/1 reached the goal". The headline number a researcher reads
3283
- // first was the dishonest one. Found by a provider bug that ended a study on turn one.
3284
- terminalOutcomes.map((outcome) => participantStatusForCredibility(outcome.session.status, {
3285
- noEngagement: outcome.noEngagement === true,
3286
- selfReportedBlocker: outcome.selfReportedBlocker === true
3287
- })),
3288
- // A participant who reached the goal AND told you the road there was broken is the most
3289
- // useful result a study produces; reporting only the outcome would bury it.
3290
- terminalOutcomes.map((outcome) => outcome.reportedFriction === true))
3291
- : undefined;
3292
- // The study funnel: per-task completion rates across every session that measured one. This is
3293
- // "where did people get stuck" as data, next to WHO got stuck (participants) above.
3294
- const participantFunnels = (outcomes ?? [])
3295
- .map((outcome) => outcome?.session?.trace.taskFunnel)
3296
- .filter((funnel) => funnel !== undefined);
3297
- const studyTasks = args.inProgress === true ? undefined : aggregateTaskFunnels(participantFunnels);
3298
- const participantEndings = terminalOutcomes.map((outcome) => {
3299
- const ending = actorEnding(outcome.session.trace);
3300
- return { status: outcome.session.status, ...(ending === undefined ? {} : { label: ending.label }) };
3301
- });
3302
- const review = withCuaReviewProvenance({
3303
- schema: REVIEW_SCHEMA,
3304
- verdict,
3305
- ...(participants === undefined ? {} : { participants }),
3306
- ...(studyTasks === undefined ? {} : { tasks: studyTasks }),
3307
- summary: args.inProgress === true
3308
- ? `Live computer-use fan-out is running (${specs.length} per-lane worlds); terminal lane evidence has not been written yet.`
3309
- : args.dryRun
3310
- ? `${args.rerun ? `Rerun contract from ${args.rerun.sourceRunId}: ` : ""}Dry-run fan-out contract: ${specs.length} per-lane-world lanes composed for ${args.descriptor.id} against ${args.appUrl}; no desktops launched, $0 spend.`
3311
- : `${args.rerun ? `Rerun from ${args.rerun.sourceRunId}: ` : ""}Computer-use fan-out (${specs.length} per-lane worlds): ${passedLanes}/${specs.length} lane(s) reached a terminal, engaged verdict${participants ? ` — ${formatParticipantOutcomes(participants, participantEndings)}` : ""}${studyTasks ? `; tasks: ${formatStudyTaskFunnel(studyTasks)}` : ""}.`,
3312
- gaps: args.inProgress === true
3313
- ? ["Live fan-out session is still running."]
3314
- : args.dryRun
3315
- ? ["Live fan-out session not yet run (dry-run contract only)."]
3316
- : specs
3317
- .map((spec, index) => ({ spec, outcome: outcomes?.[index] }))
3318
- .filter(({ outcome }) => outcome === undefined
3319
- || outcome.skippedReason !== undefined
3320
- || outcome.sessionError !== undefined
3321
- || outcome.noEngagement
3322
- || outcome.selfReportedBlocker
3323
- || outcome.session === undefined
3324
- || outcome.session.status !== "passed")
3325
- .map(({ spec, outcome }) => `${spec.laneId}: ${outcome?.skippedReason ?? outcome?.sessionError ?? outcome?.session?.reason ?? "did not pass"}`)
3326
- }, streams);
3327
- const anyRaw = (outcomes ?? []).some((outcome) => outcome.session?.trace.redaction.screenshots === "raw");
3328
- const ranLive = (outcomes ?? []).some((outcome) => outcome.session !== undefined || outcome.sessionError !== undefined);
3329
- const configuredBrowser = config.execution?.desktop?.browser;
3330
- const desktopTemplate = e2bDesktopTemplate(config);
3331
- const resolvedBrowsers = (outcomes ?? [])
3332
- .map((outcome) => outcome.desktopBrowser?.resolved)
3333
- .filter((value) => value !== undefined);
3334
- const unanimousResolvedBrowser = resolvedBrowsers.length > 0 && new Set(resolvedBrowsers).size === 1
3335
- ? resolvedBrowsers[0]
3336
- : undefined;
3337
- const providerResources = (outcomes ?? []).flatMap((outcome) => providerResourcesForOutcome({
3338
- outcome,
3339
- createdAt: args.createdAt,
3340
- simId: outcome.spec.simId,
3341
- streamId: outcome.spec.streamId,
3342
- laneId: outcome.spec.laneId
3343
- }));
3344
- // Run-level cost ESTIMATE: one model-token line per lane that ran a session (from its persisted
3345
- // trace.estimatedCost) + a desktop line per owned allocation, priced at its observed resources.
3346
- // Per-lane worlds have no shared provisioning to double-count. Omitted on a pure dry-run.
3347
- const costLanes = specs
3348
- .map((spec, index) => ({ laneId: spec.laneId, outcome: outcomes?.[index] }))
3349
- .filter((entry) => entry.outcome?.session !== undefined)
3350
- .map((entry) => ({ laneId: entry.laneId, trace: entry.outcome.session.trace }));
3351
- const desktops = (outcomes ?? []).filter(outcome => outcome.sandboxId !== undefined).map(outcome => ({
3352
- laneId: outcome.spec.laneId, minutes: desktopSpanToMinutes(outcome.desktopDurationMs),
3353
- observation: outcome.desktopResources, lifetimeComplete: outcome.killed
3354
- }));
3355
- const cost = buildCuaCostSummary({ lanes: costLanes, desktops });
3356
- return {
3357
- schema: RUN_BUNDLE_SCHEMA,
3358
- ...receivingPublication(args.config, args.dryRun),
3359
- runId: args.runId,
3360
- mode: args.dryRun ? "dry-run" : "live",
3361
- simCount: specs.length,
3362
- createdAt: args.createdAt,
3363
- cwd: PUBLIC_TARGET_CWD,
3364
- artifactRoot: path.join(".humanish", "runs", args.runId),
3365
- ...(args.lab === undefined ? {} : { lab: args.lab }),
3366
- source: args.source,
3367
- persona: {
3368
- id: specs[0].persona.id,
3369
- name: `Computer-use fan-out (${specs.length} lanes)`,
3370
- source: `lab:${config.id}`,
3371
- sourceDigest: specs[0].persona.promptDigest
3372
- },
3373
- scenario: {
3374
- id: `cua-${config.id}`,
3375
- title: config.title ?? `Computer-use fan-out: ${config.id}`,
3376
- // Redacted at WRITE time, like every other raw-text surface in the bundle. Lane records are
3377
- // digest-only by design, but scenario.goal keeps one lane's composed instructions verbatim —
3378
- // and an adopter whose authored lane text must name a runtime world URL (an inbox on a route
3379
- // where the harness does not inject one) put an *.e2b.app address in it. That landed raw here
3380
- // and in observer-data.json, the sensitive-text scanner matched it, and verify failed a bundle
3381
- // this writer produced. The only adopter-side workaround was scanner evasion (#412).
3382
- //
3383
- // The instructions the model actually receives are untouched; only the persisted copy changes.
3384
- goal: redactText(specs[0].evidenceInstructions ?? specs[0].instructions),
3385
- source: `lab:${config.id}`,
3386
- sourceDigest: specs[0].persona.promptDigest
3387
- },
3388
- lifecycle: [
3389
- {
3390
- at: args.createdAt,
3391
- event: "cua-lab.run.created",
3392
- message: `Created computer-use fan-out run with ${specs.length} per-lane desktop browser lanes (actor ${args.descriptor.id}).`
3393
- }
3394
- ],
3395
- simulations,
3396
- streams,
3397
- events,
3398
- ...(args.rerun === undefined ? {} : { rerun: args.rerun }),
3399
- redaction: {
3400
- status: "passed",
3401
- notes: ranLive
3402
- ? anyRaw
3403
- ? "Typed text recorded as length only and reasoning/messages pass through text redaction. Some lanes captured FULL-FIDELITY (raw) screenshots, retained for local use — NOT redacted for publishing; set policies.redactScreenshots: true to blur a share-as-is bundle."
3404
- : "Typed text recorded as length only and reasoning/messages pass through text redaction. Screenshots are blurred at capture (policies.redactScreenshots: true) for a share-as-is bundle."
3405
- : "Dry-run fan-out contract bundle: no desktops launched and no screenshots captured. Typed text is recorded as length only and reasoning/messages pass through text redaction whenever a session runs."
3406
- },
3407
- artifacts: {
3408
- run: "run.json",
3409
- reviewJson: "review.json",
3410
- reviewMarkdown: "review.md",
3411
- observerData: "observer/observer-data.json",
3412
- events: "events.ndjson"
3413
- },
3414
- review,
3415
- // What the participants reported, when any reported anything (#392). Dry-run and in-progress
3416
- // bundles carry none — there is no participant yet to quote.
3417
- feedbackCandidates: args.dryRun || args.inProgress === true
3418
- ? []
3419
- : participantFeedbackCandidates({
3420
- runId: args.runId,
3421
- scenarioId: `cua-${config.id}`,
3422
- adapterId: config.id,
3423
- goal: redactText(specs[0].evidenceInstructions ?? specs[0].instructions),
3424
- substrate: config.execution?.target === "local" ? "local-desktop" : "e2b-desktop",
3425
- lanes: specs.map((spec, index) => {
3426
- const outcome = outcomes?.[index];
3427
- return {
3428
- laneId: spec.laneId,
3429
- streamId: spec.streamId,
3430
- personaId: spec.persona.id,
3431
- ...(outcome?.session === undefined ? {} : { session: outcome.session }),
3432
- ...(outcome?.session === undefined ? {} : { traceArtifactPath: spec.traceArtifactPath }),
3433
- screenshots: outcome?.screenshots ?? [],
3434
- ...(outcome?.commsArtifactPath === undefined ? {} : { commsArtifactPath: outcome.commsArtifactPath })
3435
- };
3436
- })
3437
- }),
3438
- // Selected hosted image, including the optional speech default; omitted on the stock desktop.
3439
- ...(desktopTemplate === undefined ? {} : { desktopTemplate }),
3440
- ...(configuredBrowser === undefined
3441
- ? {}
3442
- : { desktopBrowser: { requested: configuredBrowser, ...(unanimousResolvedBrowser === undefined ? {} : { resolved: unanimousResolvedBrowser }) } }),
3443
- ...(providerResources.length === 0 ? {} : { providerResources }),
3444
- subject: args.aggregateSubject,
3445
- ...(cost === undefined ? {} : { cost })
3446
- };
3447
- }
3448
- function providerResourcesForOutcome(args) {
3449
- if (args.outcome?.sandboxId === undefined) {
3450
- return [];
3451
- }
3452
- return [{
3453
- schema: "humanish.provider-resource.v1",
3454
- provider: "e2b-desktop",
3455
- kind: "sandbox",
3456
- id: args.outcome.sandboxId,
3457
- owner: "humanish",
3458
- status: args.outcome.killed ? "killed" : "running",
3459
- simId: args.simId,
3460
- streamId: args.streamId,
3461
- laneId: args.laneId,
3462
- createdAt: args.createdAt,
3463
- cleanup: {
3464
- killed: args.outcome.killed,
3465
- reason: args.outcome.killed
3466
- ? "killed during normal lane teardown"
3467
- : "not killed during normal lane teardown; cleanup may reclaim by exact recorded id"
3468
- }
3469
- }];
3470
- }
3471
- /**
3472
- * The status a participant is TALLIED under, given what the lane made of the session. A
3473
- * goal_satisfied claim with zero engagement is a session that ran out before anything happened;
3474
- * one whose final message describes a blocker is a participant who could not proceed and said so.
3475
- * Both keep their trace status (the claim is evidence); neither is a participant who reached the
3476
- * goal. One rule for the single lane and the fan-out roll-up (#476).
3477
- */
3478
- export function participantStatusForCredibility(status, credibility) {
3479
- if (status !== "passed" || credibility === undefined)
3480
- return status;
3481
- if (credibility.noEngagement)
3482
- return "incomplete";
3483
- if (credibility.selfReportedBlocker)
3484
- return "blocked";
3485
- return status;
3486
- }
3487
- function verdictForStatus(status) {
3488
- switch (status) {
3489
- case "passed":
3490
- return "pass";
3491
- case "failed":
3492
- return "fail";
3493
- case "blocked":
3494
- return "blocked";
3495
- case "timed_out":
3496
- return "timed_out";
3497
- // A participant who abandoned, or a session that ran out before the goal, did not pass — but the
3498
- // harness did not fail either. The run reports what happened rather than a verdict on the tool.
3499
- case "abandoned":
3500
- case "incomplete":
3501
- return "fail";
3502
- }
3503
- }
3504
- function renderCuaReviewMarkdown(bundle) {
3505
- const trace = bundle.streams[0]?.actor;
3506
- const provenance = bundle.events.find((event) => event.type === "cua-lab.subject.provenance");
3507
- return [
3508
- `# ${bundle.scenario.title}`,
3509
- "",
3510
- `- run: ${bundle.runId}`,
3511
- `- mode: ${bundle.mode}`,
3512
- `- run gate: ${bundle.review.verdict}`,
3513
- `- summary: ${bundle.review.summary}`,
3514
- ...(provenance ? [`- subject: ${provenance.message}`] : []),
3515
- ...(trace
3516
- ? [
3517
- `- actor: ${trace.provider} (${trace.lane}/${trace.protocol})`,
3518
- // Honest count: name the trace's actual screenshot mode ("raw" | "blurred"); say
3519
- // nothing when no frames exist ("n/a") rather than claim a redaction that never ran.
3520
- `- evidence: ${trace.items.length} trace item(s), ${trace.counts.screenshots ?? 0} ${trace.redaction.screenshots === "raw" || trace.redaction.screenshots === "blurred"
3521
- ? `${trace.redaction.screenshots} screenshot(s)`
3522
- : "screenshot(s)"}`
3523
- ]
3524
- : []),
3525
- ...(bundle.review.gaps.length > 0 ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)] : []),
3526
- ""
3527
- ].join("\n");
3528
- }
3529
- function makeCuaRunId() {
3530
- const stamp = new Date().toISOString().replace(/[:.]/g, "-");
3531
- return `cua-${stamp}-${randomBytes(4).toString("hex")}`;
3532
- }
3533
- function publicSafeAppUrlLabel(url) {
3534
- return containsSensitive(url) ? `[target-url:${digestUrl(url)}]` : url;
3535
- }
3536
- function digestUrl(url) {
3537
- return digestText(url, 16);
3538
- }
3539
- function readPositiveInt(value, fallback) {
3540
- if (value === undefined)
3541
- return fallback;
3542
- const parsed = Number.parseInt(value, 10);
3543
- return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
3544
- }
3545
- //# sourceMappingURL=cua-actor-lab.js.map