devharness 0.8.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 (627) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/build/atomic-write.d.ts +28 -0
  4. package/build/atomic-write.d.ts.map +1 -0
  5. package/build/atomic-write.js +126 -0
  6. package/build/atomic-write.js.map +1 -0
  7. package/build/cdp-manager.d.ts +524 -0
  8. package/build/cdp-manager.d.ts.map +1 -0
  9. package/build/cdp-manager.js +1828 -0
  10. package/build/cdp-manager.js.map +1 -0
  11. package/build/cdp-manager.test.d.ts +6 -0
  12. package/build/cdp-manager.test.d.ts.map +1 -0
  13. package/build/cdp-manager.test.js +546 -0
  14. package/build/cdp-manager.test.js.map +1 -0
  15. package/build/chrome-launcher.d.ts +400 -0
  16. package/build/chrome-launcher.d.ts.map +1 -0
  17. package/build/chrome-launcher.js +1071 -0
  18. package/build/chrome-launcher.js.map +1 -0
  19. package/build/chrome-launcher.test.d.ts +11 -0
  20. package/build/chrome-launcher.test.d.ts.map +1 -0
  21. package/build/chrome-launcher.test.js +204 -0
  22. package/build/chrome-launcher.test.js.map +1 -0
  23. package/build/clickable-cache.d.ts +73 -0
  24. package/build/clickable-cache.d.ts.map +1 -0
  25. package/build/clickable-cache.js +90 -0
  26. package/build/clickable-cache.js.map +1 -0
  27. package/build/command-recorder.d.ts +401 -0
  28. package/build/command-recorder.d.ts.map +1 -0
  29. package/build/command-recorder.js +701 -0
  30. package/build/command-recorder.js.map +1 -0
  31. package/build/config.d.ts +390 -0
  32. package/build/config.d.ts.map +1 -0
  33. package/build/config.js +826 -0
  34. package/build/config.js.map +1 -0
  35. package/build/config.test.d.ts +2 -0
  36. package/build/config.test.d.ts.map +1 -0
  37. package/build/config.test.js +112 -0
  38. package/build/config.test.js.map +1 -0
  39. package/build/connection-manager.d.ts +159 -0
  40. package/build/connection-manager.d.ts.map +1 -0
  41. package/build/connection-manager.js +432 -0
  42. package/build/connection-manager.js.map +1 -0
  43. package/build/connection-manager.test.d.ts +11 -0
  44. package/build/connection-manager.test.d.ts.map +1 -0
  45. package/build/connection-manager.test.js +114 -0
  46. package/build/connection-manager.test.js.map +1 -0
  47. package/build/console-monitor.d.ts +150 -0
  48. package/build/console-monitor.d.ts.map +1 -0
  49. package/build/console-monitor.js +454 -0
  50. package/build/console-monitor.js.map +1 -0
  51. package/build/dashboard/bundle.js +1 -0
  52. package/build/dashboard/dashboard-client.d.ts +42 -0
  53. package/build/dashboard/dashboard-client.d.ts.map +1 -0
  54. package/build/dashboard/dashboard-client.js +184 -0
  55. package/build/dashboard/dashboard-client.js.map +1 -0
  56. package/build/dashboard/dashboard-hub.d.ts +50 -0
  57. package/build/dashboard/dashboard-hub.d.ts.map +1 -0
  58. package/build/dashboard/dashboard-hub.js +654 -0
  59. package/build/dashboard/dashboard-hub.js.map +1 -0
  60. package/build/dashboard/hub-lock.d.ts +16 -0
  61. package/build/dashboard/hub-lock.d.ts.map +1 -0
  62. package/build/dashboard/hub-lock.js +77 -0
  63. package/build/dashboard/hub-lock.js.map +1 -0
  64. package/build/dashboard/index.d.ts +25 -0
  65. package/build/dashboard/index.d.ts.map +1 -0
  66. package/build/dashboard/index.js +77 -0
  67. package/build/dashboard/index.js.map +1 -0
  68. package/build/dashboard/types.d.ts +83 -0
  69. package/build/dashboard/types.d.ts.map +1 -0
  70. package/build/dashboard/types.js +6 -0
  71. package/build/dashboard/types.js.map +1 -0
  72. package/build/debug-logger.d.ts +85 -0
  73. package/build/debug-logger.d.ts.map +1 -0
  74. package/build/debug-logger.js +195 -0
  75. package/build/debug-logger.js.map +1 -0
  76. package/build/debugger-aware-wrapper.d.ts +37 -0
  77. package/build/debugger-aware-wrapper.d.ts.map +1 -0
  78. package/build/debugger-aware-wrapper.js +128 -0
  79. package/build/debugger-aware-wrapper.js.map +1 -0
  80. package/build/dom-change-monitor.d.ts +92 -0
  81. package/build/dom-change-monitor.d.ts.map +1 -0
  82. package/build/dom-change-monitor.js +437 -0
  83. package/build/dom-change-monitor.js.map +1 -0
  84. package/build/element-collector.d.ts +16 -0
  85. package/build/element-collector.d.ts.map +1 -0
  86. package/build/element-collector.js +207 -0
  87. package/build/element-collector.js.map +1 -0
  88. package/build/error-helpers.d.ts +43 -0
  89. package/build/error-helpers.d.ts.map +1 -0
  90. package/build/error-helpers.js +143 -0
  91. package/build/error-helpers.js.map +1 -0
  92. package/build/formatters/console-formatter.d.ts +160 -0
  93. package/build/formatters/console-formatter.d.ts.map +1 -0
  94. package/build/formatters/console-formatter.js +338 -0
  95. package/build/formatters/console-formatter.js.map +1 -0
  96. package/build/helpers/parser-plugins.d.ts +44 -0
  97. package/build/helpers/parser-plugins.d.ts.map +1 -0
  98. package/build/helpers/parser-plugins.js +102 -0
  99. package/build/helpers/parser-plugins.js.map +1 -0
  100. package/build/helpers/paths.d.ts +79 -0
  101. package/build/helpers/paths.d.ts.map +1 -0
  102. package/build/helpers/paths.js +168 -0
  103. package/build/helpers/paths.js.map +1 -0
  104. package/build/helpers/process-liveness.d.ts +24 -0
  105. package/build/helpers/process-liveness.d.ts.map +1 -0
  106. package/build/helpers/process-liveness.js +36 -0
  107. package/build/helpers/process-liveness.js.map +1 -0
  108. package/build/helpers/process-liveness.test.d.ts +2 -0
  109. package/build/helpers/process-liveness.test.d.ts.map +1 -0
  110. package/build/helpers/process-liveness.test.js +40 -0
  111. package/build/helpers/process-liveness.test.js.map +1 -0
  112. package/build/helpers/sequence-tree.d.ts +27 -0
  113. package/build/helpers/sequence-tree.d.ts.map +1 -0
  114. package/build/helpers/sequence-tree.js +80 -0
  115. package/build/helpers/sequence-tree.js.map +1 -0
  116. package/build/helpers/sequence-tree.test.d.ts +2 -0
  117. package/build/helpers/sequence-tree.test.d.ts.map +1 -0
  118. package/build/helpers/sequence-tree.test.js +122 -0
  119. package/build/helpers/sequence-tree.test.js.map +1 -0
  120. package/build/index.d.ts +3 -0
  121. package/build/index.d.ts.map +1 -0
  122. package/build/index.js +2007 -0
  123. package/build/index.js.map +1 -0
  124. package/build/interaction-recorder.d.ts +214 -0
  125. package/build/interaction-recorder.d.ts.map +1 -0
  126. package/build/interaction-recorder.js +2007 -0
  127. package/build/interaction-recorder.js.map +1 -0
  128. package/build/issue-tracker.d.ts +107 -0
  129. package/build/issue-tracker.d.ts.map +1 -0
  130. package/build/issue-tracker.js +582 -0
  131. package/build/issue-tracker.js.map +1 -0
  132. package/build/issue-tracker.test.d.ts +2 -0
  133. package/build/issue-tracker.test.d.ts.map +1 -0
  134. package/build/issue-tracker.test.js +88 -0
  135. package/build/issue-tracker.test.js.map +1 -0
  136. package/build/launch-chrome-port.test.d.ts +14 -0
  137. package/build/launch-chrome-port.test.d.ts.map +1 -0
  138. package/build/launch-chrome-port.test.js +97 -0
  139. package/build/launch-chrome-port.test.js.map +1 -0
  140. package/build/log-processor/orchestrator.d.ts +32 -0
  141. package/build/log-processor/orchestrator.d.ts.map +1 -0
  142. package/build/log-processor/orchestrator.js +27 -0
  143. package/build/log-processor/orchestrator.js.map +1 -0
  144. package/build/logpoint-execution-tracker.d.ts +57 -0
  145. package/build/logpoint-execution-tracker.d.ts.map +1 -0
  146. package/build/logpoint-execution-tracker.js +112 -0
  147. package/build/logpoint-execution-tracker.js.map +1 -0
  148. package/build/mcp-supervisor.d.ts +3 -0
  149. package/build/mcp-supervisor.d.ts.map +1 -0
  150. package/build/mcp-supervisor.js +211 -0
  151. package/build/mcp-supervisor.js.map +1 -0
  152. package/build/messages.d.ts +165 -0
  153. package/build/messages.d.ts.map +1 -0
  154. package/build/messages.js +574 -0
  155. package/build/messages.js.map +1 -0
  156. package/build/network-monitor.d.ts +172 -0
  157. package/build/network-monitor.d.ts.map +1 -0
  158. package/build/network-monitor.js +350 -0
  159. package/build/network-monitor.js.map +1 -0
  160. package/build/overlays.d.ts +49 -0
  161. package/build/overlays.d.ts.map +1 -0
  162. package/build/overlays.js +409 -0
  163. package/build/overlays.js.map +1 -0
  164. package/build/persistent-profiles.test.d.ts +15 -0
  165. package/build/persistent-profiles.test.d.ts.map +1 -0
  166. package/build/persistent-profiles.test.js +470 -0
  167. package/build/persistent-profiles.test.js.map +1 -0
  168. package/build/port-reserver.d.ts +32 -0
  169. package/build/port-reserver.d.ts.map +1 -0
  170. package/build/port-reserver.js +118 -0
  171. package/build/port-reserver.js.map +1 -0
  172. package/build/puppeteer-manager.d.ts +47 -0
  173. package/build/puppeteer-manager.d.ts.map +1 -0
  174. package/build/puppeteer-manager.js +116 -0
  175. package/build/puppeteer-manager.js.map +1 -0
  176. package/build/recording-coverage.test.d.ts +12 -0
  177. package/build/recording-coverage.test.d.ts.map +1 -0
  178. package/build/recording-coverage.test.js +72 -0
  179. package/build/recording-coverage.test.js.map +1 -0
  180. package/build/reference-validator.d.ts +47 -0
  181. package/build/reference-validator.d.ts.map +1 -0
  182. package/build/reference-validator.js +86 -0
  183. package/build/reference-validator.js.map +1 -0
  184. package/build/reference-validator.test.d.ts +5 -0
  185. package/build/reference-validator.test.d.ts.map +1 -0
  186. package/build/reference-validator.test.js +243 -0
  187. package/build/reference-validator.test.js.map +1 -0
  188. package/build/replay-cursor.d.ts +27 -0
  189. package/build/replay-cursor.d.ts.map +1 -0
  190. package/build/replay-cursor.js +240 -0
  191. package/build/replay-cursor.js.map +1 -0
  192. package/build/runners/docker-compose-runner.d.ts +69 -0
  193. package/build/runners/docker-compose-runner.d.ts.map +1 -0
  194. package/build/runners/docker-compose-runner.js +438 -0
  195. package/build/runners/docker-compose-runner.js.map +1 -0
  196. package/build/runners/docker-runner.d.ts +65 -0
  197. package/build/runners/docker-runner.d.ts.map +1 -0
  198. package/build/runners/docker-runner.js +414 -0
  199. package/build/runners/docker-runner.js.map +1 -0
  200. package/build/runners/index.d.ts +19 -0
  201. package/build/runners/index.d.ts.map +1 -0
  202. package/build/runners/index.js +35 -0
  203. package/build/runners/index.js.map +1 -0
  204. package/build/runners/native-runner.d.ts +95 -0
  205. package/build/runners/native-runner.d.ts.map +1 -0
  206. package/build/runners/native-runner.js +370 -0
  207. package/build/runners/native-runner.js.map +1 -0
  208. package/build/runners/types.d.ts +147 -0
  209. package/build/runners/types.d.ts.map +1 -0
  210. package/build/runners/types.js +25 -0
  211. package/build/runners/types.js.map +1 -0
  212. package/build/self-restart.d.ts +12 -0
  213. package/build/self-restart.d.ts.map +1 -0
  214. package/build/self-restart.js +41 -0
  215. package/build/self-restart.js.map +1 -0
  216. package/build/self-restart.test.d.ts +2 -0
  217. package/build/self-restart.test.d.ts.map +1 -0
  218. package/build/self-restart.test.js +34 -0
  219. package/build/self-restart.test.js.map +1 -0
  220. package/build/server-claims.d.ts +161 -0
  221. package/build/server-claims.d.ts.map +1 -0
  222. package/build/server-claims.js +382 -0
  223. package/build/server-claims.js.map +1 -0
  224. package/build/server-claims.test.d.ts +2 -0
  225. package/build/server-claims.test.d.ts.map +1 -0
  226. package/build/server-claims.test.js +197 -0
  227. package/build/server-claims.test.js.map +1 -0
  228. package/build/server-manager.d.ts +438 -0
  229. package/build/server-manager.d.ts.map +1 -0
  230. package/build/server-manager.js +1674 -0
  231. package/build/server-manager.js.map +1 -0
  232. package/build/server-manager.test.d.ts +17 -0
  233. package/build/server-manager.test.d.ts.map +1 -0
  234. package/build/server-manager.test.js +122 -0
  235. package/build/server-manager.test.js.map +1 -0
  236. package/build/server-ownership.test.d.ts +2 -0
  237. package/build/server-ownership.test.d.ts.map +1 -0
  238. package/build/server-ownership.test.js +238 -0
  239. package/build/server-ownership.test.js.map +1 -0
  240. package/build/server-watcher.d.ts +21 -0
  241. package/build/server-watcher.d.ts.map +1 -0
  242. package/build/server-watcher.js +105 -0
  243. package/build/server-watcher.js.map +1 -0
  244. package/build/session-detector.d.ts +47 -0
  245. package/build/session-detector.d.ts.map +1 -0
  246. package/build/session-detector.js +229 -0
  247. package/build/session-detector.js.map +1 -0
  248. package/build/session-detector.test.d.ts +9 -0
  249. package/build/session-detector.test.d.ts.map +1 -0
  250. package/build/session-detector.test.js +45 -0
  251. package/build/session-detector.test.js.map +1 -0
  252. package/build/sourcemap-handler.d.ts +114 -0
  253. package/build/sourcemap-handler.d.ts.map +1 -0
  254. package/build/sourcemap-handler.js +479 -0
  255. package/build/sourcemap-handler.js.map +1 -0
  256. package/build/supervisor/child-manager.d.ts +43 -0
  257. package/build/supervisor/child-manager.d.ts.map +1 -0
  258. package/build/supervisor/child-manager.js +148 -0
  259. package/build/supervisor/child-manager.js.map +1 -0
  260. package/build/supervisor/child-manager.test.d.ts +2 -0
  261. package/build/supervisor/child-manager.test.d.ts.map +1 -0
  262. package/build/supervisor/child-manager.test.js +107 -0
  263. package/build/supervisor/child-manager.test.js.map +1 -0
  264. package/build/supervisor/client-watcher.d.ts +48 -0
  265. package/build/supervisor/client-watcher.d.ts.map +1 -0
  266. package/build/supervisor/client-watcher.js +186 -0
  267. package/build/supervisor/client-watcher.js.map +1 -0
  268. package/build/supervisor/client-watcher.test.d.ts +2 -0
  269. package/build/supervisor/client-watcher.test.d.ts.map +1 -0
  270. package/build/supervisor/client-watcher.test.js +121 -0
  271. package/build/supervisor/client-watcher.test.js.map +1 -0
  272. package/build/supervisor/idle-config.d.ts +30 -0
  273. package/build/supervisor/idle-config.d.ts.map +1 -0
  274. package/build/supervisor/idle-config.js +76 -0
  275. package/build/supervisor/idle-config.js.map +1 -0
  276. package/build/supervisor/idle-config.test.d.ts +2 -0
  277. package/build/supervisor/idle-config.test.d.ts.map +1 -0
  278. package/build/supervisor/idle-config.test.js +119 -0
  279. package/build/supervisor/idle-config.test.js.map +1 -0
  280. package/build/supervisor/ndjson-reader.d.ts +47 -0
  281. package/build/supervisor/ndjson-reader.d.ts.map +1 -0
  282. package/build/supervisor/ndjson-reader.js +75 -0
  283. package/build/supervisor/ndjson-reader.js.map +1 -0
  284. package/build/supervisor/ndjson-reader.test.d.ts +2 -0
  285. package/build/supervisor/ndjson-reader.test.d.ts.map +1 -0
  286. package/build/supervisor/ndjson-reader.test.js +92 -0
  287. package/build/supervisor/ndjson-reader.test.js.map +1 -0
  288. package/build/supervisor/pidfile.d.ts +11 -0
  289. package/build/supervisor/pidfile.d.ts.map +1 -0
  290. package/build/supervisor/pidfile.js +29 -0
  291. package/build/supervisor/pidfile.js.map +1 -0
  292. package/build/supervisor/pidfile.test.d.ts +8 -0
  293. package/build/supervisor/pidfile.test.d.ts.map +1 -0
  294. package/build/supervisor/pidfile.test.js +46 -0
  295. package/build/supervisor/pidfile.test.js.map +1 -0
  296. package/build/supervisor/restart-coordinator.d.ts +73 -0
  297. package/build/supervisor/restart-coordinator.d.ts.map +1 -0
  298. package/build/supervisor/restart-coordinator.js +300 -0
  299. package/build/supervisor/restart-coordinator.js.map +1 -0
  300. package/build/supervisor/restart-coordinator.test.d.ts +2 -0
  301. package/build/supervisor/restart-coordinator.test.d.ts.map +1 -0
  302. package/build/supervisor/restart-coordinator.test.js +434 -0
  303. package/build/supervisor/restart-coordinator.test.js.map +1 -0
  304. package/build/test-support/fake-execute-tool-call.d.ts +11 -0
  305. package/build/test-support/fake-execute-tool-call.d.ts.map +1 -0
  306. package/build/test-support/fake-execute-tool-call.js +19 -0
  307. package/build/test-support/fake-execute-tool-call.js.map +1 -0
  308. package/build/tool-error.d.ts +14 -0
  309. package/build/tool-error.d.ts.map +1 -0
  310. package/build/tool-error.js +18 -0
  311. package/build/tool-error.js.map +1 -0
  312. package/build/tool-response.d.ts +293 -0
  313. package/build/tool-response.d.ts.map +1 -0
  314. package/build/tool-response.js +365 -0
  315. package/build/tool-response.js.map +1 -0
  316. package/build/tools/assert-tools.d.ts +62 -0
  317. package/build/tools/assert-tools.d.ts.map +1 -0
  318. package/build/tools/assert-tools.js +330 -0
  319. package/build/tools/assert-tools.js.map +1 -0
  320. package/build/tools/breakpoint-await-abort.test.d.ts +2 -0
  321. package/build/tools/breakpoint-await-abort.test.d.ts.map +1 -0
  322. package/build/tools/breakpoint-await-abort.test.js +124 -0
  323. package/build/tools/breakpoint-await-abort.test.js.map +1 -0
  324. package/build/tools/breakpoint-tools.d.ts +99 -0
  325. package/build/tools/breakpoint-tools.d.ts.map +1 -0
  326. package/build/tools/breakpoint-tools.js +1197 -0
  327. package/build/tools/breakpoint-tools.js.map +1 -0
  328. package/build/tools/config-tools.d.ts +58 -0
  329. package/build/tools/config-tools.d.ts.map +1 -0
  330. package/build/tools/config-tools.js +180 -0
  331. package/build/tools/config-tools.js.map +1 -0
  332. package/build/tools/connection-routing.test.d.ts +12 -0
  333. package/build/tools/connection-routing.test.d.ts.map +1 -0
  334. package/build/tools/connection-routing.test.js +206 -0
  335. package/build/tools/connection-routing.test.js.map +1 -0
  336. package/build/tools/console-tools.d.ts +84 -0
  337. package/build/tools/console-tools.d.ts.map +1 -0
  338. package/build/tools/console-tools.js +241 -0
  339. package/build/tools/console-tools.js.map +1 -0
  340. package/build/tools/content-tools.d.ts +74 -0
  341. package/build/tools/content-tools.d.ts.map +1 -0
  342. package/build/tools/content-tools.js +582 -0
  343. package/build/tools/content-tools.js.map +1 -0
  344. package/build/tools/dashboard-tools.d.ts +38 -0
  345. package/build/tools/dashboard-tools.d.ts.map +1 -0
  346. package/build/tools/dashboard-tools.js +123 -0
  347. package/build/tools/dashboard-tools.js.map +1 -0
  348. package/build/tools/dom-tools.d.ts +41 -0
  349. package/build/tools/dom-tools.d.ts.map +1 -0
  350. package/build/tools/dom-tools.js +277 -0
  351. package/build/tools/dom-tools.js.map +1 -0
  352. package/build/tools/download-tools.d.ts +55 -0
  353. package/build/tools/download-tools.d.ts.map +1 -0
  354. package/build/tools/download-tools.js +280 -0
  355. package/build/tools/download-tools.js.map +1 -0
  356. package/build/tools/execution-tools.d.ts +40 -0
  357. package/build/tools/execution-tools.d.ts.map +1 -0
  358. package/build/tools/execution-tools.js +121 -0
  359. package/build/tools/execution-tools.js.map +1 -0
  360. package/build/tools/input-abort.test.d.ts +2 -0
  361. package/build/tools/input-abort.test.d.ts.map +1 -0
  362. package/build/tools/input-abort.test.js +227 -0
  363. package/build/tools/input-abort.test.js.map +1 -0
  364. package/build/tools/input-ambiguous-selector.test.d.ts +2 -0
  365. package/build/tools/input-ambiguous-selector.test.d.ts.map +1 -0
  366. package/build/tools/input-ambiguous-selector.test.js +34 -0
  367. package/build/tools/input-ambiguous-selector.test.js.map +1 -0
  368. package/build/tools/input-tools.d.ts +168 -0
  369. package/build/tools/input-tools.d.ts.map +1 -0
  370. package/build/tools/input-tools.js +1397 -0
  371. package/build/tools/input-tools.js.map +1 -0
  372. package/build/tools/inspection-tools.d.ts +112 -0
  373. package/build/tools/inspection-tools.d.ts.map +1 -0
  374. package/build/tools/inspection-tools.js +637 -0
  375. package/build/tools/inspection-tools.js.map +1 -0
  376. package/build/tools/interpolation.d.ts +30 -0
  377. package/build/tools/interpolation.d.ts.map +1 -0
  378. package/build/tools/interpolation.js +143 -0
  379. package/build/tools/interpolation.js.map +1 -0
  380. package/build/tools/issues-abort-hygiene.test.d.ts +2 -0
  381. package/build/tools/issues-abort-hygiene.test.d.ts.map +1 -0
  382. package/build/tools/issues-abort-hygiene.test.js +79 -0
  383. package/build/tools/issues-abort-hygiene.test.js.map +1 -0
  384. package/build/tools/issues-tools.d.ts +98 -0
  385. package/build/tools/issues-tools.d.ts.map +1 -0
  386. package/build/tools/issues-tools.js +821 -0
  387. package/build/tools/issues-tools.js.map +1 -0
  388. package/build/tools/issues-tools.test.d.ts +8 -0
  389. package/build/tools/issues-tools.test.d.ts.map +1 -0
  390. package/build/tools/issues-tools.test.js +184 -0
  391. package/build/tools/issues-tools.test.js.map +1 -0
  392. package/build/tools/modal-tools.d.ts +73 -0
  393. package/build/tools/modal-tools.d.ts.map +1 -0
  394. package/build/tools/modal-tools.js +161 -0
  395. package/build/tools/modal-tools.js.map +1 -0
  396. package/build/tools/network-tools.d.ts +72 -0
  397. package/build/tools/network-tools.d.ts.map +1 -0
  398. package/build/tools/network-tools.js +432 -0
  399. package/build/tools/network-tools.js.map +1 -0
  400. package/build/tools/page-tools.d.ts +78 -0
  401. package/build/tools/page-tools.d.ts.map +1 -0
  402. package/build/tools/page-tools.js +255 -0
  403. package/build/tools/page-tools.js.map +1 -0
  404. package/build/tools/plugin-tools.d.ts +16 -0
  405. package/build/tools/plugin-tools.d.ts.map +1 -0
  406. package/build/tools/plugin-tools.js +17 -0
  407. package/build/tools/plugin-tools.js.map +1 -0
  408. package/build/tools/replay-abort.test.d.ts +2 -0
  409. package/build/tools/replay-abort.test.d.ts.map +1 -0
  410. package/build/tools/replay-abort.test.js +259 -0
  411. package/build/tools/replay-abort.test.js.map +1 -0
  412. package/build/tools/replay-add-conditional.test.d.ts +2 -0
  413. package/build/tools/replay-add-conditional.test.d.ts.map +1 -0
  414. package/build/tools/replay-add-conditional.test.js +205 -0
  415. package/build/tools/replay-add-conditional.test.js.map +1 -0
  416. package/build/tools/replay-background-run.test.d.ts +2 -0
  417. package/build/tools/replay-background-run.test.d.ts.map +1 -0
  418. package/build/tools/replay-background-run.test.js +176 -0
  419. package/build/tools/replay-background-run.test.js.map +1 -0
  420. package/build/tools/replay-capture-variables.test.d.ts +2 -0
  421. package/build/tools/replay-capture-variables.test.d.ts.map +1 -0
  422. package/build/tools/replay-capture-variables.test.js +331 -0
  423. package/build/tools/replay-capture-variables.test.js.map +1 -0
  424. package/build/tools/replay-conditional-idb-and-launch.test.d.ts +11 -0
  425. package/build/tools/replay-conditional-idb-and-launch.test.d.ts.map +1 -0
  426. package/build/tools/replay-conditional-idb-and-launch.test.js +429 -0
  427. package/build/tools/replay-conditional-idb-and-launch.test.js.map +1 -0
  428. package/build/tools/replay-conditional-timeout.test.d.ts +14 -0
  429. package/build/tools/replay-conditional-timeout.test.d.ts.map +1 -0
  430. package/build/tools/replay-conditional-timeout.test.js +126 -0
  431. package/build/tools/replay-conditional-timeout.test.js.map +1 -0
  432. package/build/tools/replay-declare.test.d.ts +2 -0
  433. package/build/tools/replay-declare.test.d.ts.map +1 -0
  434. package/build/tools/replay-declare.test.js +106 -0
  435. package/build/tools/replay-declare.test.js.map +1 -0
  436. package/build/tools/replay-declared-browser-cleanup.test.d.ts +2 -0
  437. package/build/tools/replay-declared-browser-cleanup.test.d.ts.map +1 -0
  438. package/build/tools/replay-declared-browser-cleanup.test.js +103 -0
  439. package/build/tools/replay-declared-browser-cleanup.test.js.map +1 -0
  440. package/build/tools/replay-declared-profile.test.d.ts +2 -0
  441. package/build/tools/replay-declared-profile.test.d.ts.map +1 -0
  442. package/build/tools/replay-declared-profile.test.js +116 -0
  443. package/build/tools/replay-declared-profile.test.js.map +1 -0
  444. package/build/tools/replay-executor.d.ts +556 -0
  445. package/build/tools/replay-executor.d.ts.map +1 -0
  446. package/build/tools/replay-executor.js +2347 -0
  447. package/build/tools/replay-executor.js.map +1 -0
  448. package/build/tools/replay-folder-save.test.d.ts +2 -0
  449. package/build/tools/replay-folder-save.test.d.ts.map +1 -0
  450. package/build/tools/replay-folder-save.test.js +63 -0
  451. package/build/tools/replay-folder-save.test.js.map +1 -0
  452. package/build/tools/replay-formatters.d.ts +148 -0
  453. package/build/tools/replay-formatters.d.ts.map +1 -0
  454. package/build/tools/replay-formatters.js +856 -0
  455. package/build/tools/replay-formatters.js.map +1 -0
  456. package/build/tools/replay-kill-chrome-on-finish.test.d.ts +2 -0
  457. package/build/tools/replay-kill-chrome-on-finish.test.d.ts.map +1 -0
  458. package/build/tools/replay-kill-chrome-on-finish.test.js +175 -0
  459. package/build/tools/replay-kill-chrome-on-finish.test.js.map +1 -0
  460. package/build/tools/replay-multi-connection.test.d.ts +2 -0
  461. package/build/tools/replay-multi-connection.test.d.ts.map +1 -0
  462. package/build/tools/replay-multi-connection.test.js +547 -0
  463. package/build/tools/replay-multi-connection.test.js.map +1 -0
  464. package/build/tools/replay-output-and-record-options.test.d.ts +2 -0
  465. package/build/tools/replay-output-and-record-options.test.d.ts.map +1 -0
  466. package/build/tools/replay-output-and-record-options.test.js +240 -0
  467. package/build/tools/replay-output-and-record-options.test.js.map +1 -0
  468. package/build/tools/replay-production-error-shape.test.d.ts +7 -0
  469. package/build/tools/replay-production-error-shape.test.d.ts.map +1 -0
  470. package/build/tools/replay-production-error-shape.test.js +93 -0
  471. package/build/tools/replay-production-error-shape.test.js.map +1 -0
  472. package/build/tools/replay-rebase.test.d.ts +2 -0
  473. package/build/tools/replay-rebase.test.d.ts.map +1 -0
  474. package/build/tools/replay-rebase.test.js +58 -0
  475. package/build/tools/replay-rebase.test.js.map +1 -0
  476. package/build/tools/replay-retry-classification.test.d.ts +9 -0
  477. package/build/tools/replay-retry-classification.test.d.ts.map +1 -0
  478. package/build/tools/replay-retry-classification.test.js +57 -0
  479. package/build/tools/replay-retry-classification.test.js.map +1 -0
  480. package/build/tools/replay-run-registry.d.ts +55 -0
  481. package/build/tools/replay-run-registry.d.ts.map +1 -0
  482. package/build/tools/replay-run-registry.js +65 -0
  483. package/build/tools/replay-run-registry.js.map +1 -0
  484. package/build/tools/replay-sequence-reload.test.d.ts +2 -0
  485. package/build/tools/replay-sequence-reload.test.d.ts.map +1 -0
  486. package/build/tools/replay-sequence-reload.test.js +82 -0
  487. package/build/tools/replay-sequence-reload.test.js.map +1 -0
  488. package/build/tools/replay-socket-scope.test.d.ts +2 -0
  489. package/build/tools/replay-socket-scope.test.d.ts.map +1 -0
  490. package/build/tools/replay-socket-scope.test.js +75 -0
  491. package/build/tools/replay-socket-scope.test.js.map +1 -0
  492. package/build/tools/replay-step-connection.test.d.ts +2 -0
  493. package/build/tools/replay-step-connection.test.d.ts.map +1 -0
  494. package/build/tools/replay-step-connection.test.js +292 -0
  495. package/build/tools/replay-step-connection.test.js.map +1 -0
  496. package/build/tools/replay-step-timeout.test.d.ts +2 -0
  497. package/build/tools/replay-step-timeout.test.d.ts.map +1 -0
  498. package/build/tools/replay-step-timeout.test.js +204 -0
  499. package/build/tools/replay-step-timeout.test.js.map +1 -0
  500. package/build/tools/replay-tags.test.d.ts +2 -0
  501. package/build/tools/replay-tags.test.d.ts.map +1 -0
  502. package/build/tools/replay-tags.test.js +99 -0
  503. package/build/tools/replay-tags.test.js.map +1 -0
  504. package/build/tools/replay-teardown-and-foreach.test.d.ts +13 -0
  505. package/build/tools/replay-teardown-and-foreach.test.d.ts.map +1 -0
  506. package/build/tools/replay-teardown-and-foreach.test.js +318 -0
  507. package/build/tools/replay-teardown-and-foreach.test.js.map +1 -0
  508. package/build/tools/replay-tool-name-validation.test.d.ts +2 -0
  509. package/build/tools/replay-tool-name-validation.test.d.ts.map +1 -0
  510. package/build/tools/replay-tool-name-validation.test.js +280 -0
  511. package/build/tools/replay-tool-name-validation.test.js.map +1 -0
  512. package/build/tools/replay-tools.d.ts +288 -0
  513. package/build/tools/replay-tools.d.ts.map +1 -0
  514. package/build/tools/replay-tools.js +2994 -0
  515. package/build/tools/replay-tools.js.map +1 -0
  516. package/build/tools/request-abort.test.d.ts +2 -0
  517. package/build/tools/request-abort.test.d.ts.map +1 -0
  518. package/build/tools/request-abort.test.js +130 -0
  519. package/build/tools/request-abort.test.js.map +1 -0
  520. package/build/tools/request-tools.d.ts +56 -0
  521. package/build/tools/request-tools.d.ts.map +1 -0
  522. package/build/tools/request-tools.js +174 -0
  523. package/build/tools/request-tools.js.map +1 -0
  524. package/build/tools/screenshot-tools.d.ts +135 -0
  525. package/build/tools/screenshot-tools.d.ts.map +1 -0
  526. package/build/tools/screenshot-tools.js +636 -0
  527. package/build/tools/screenshot-tools.js.map +1 -0
  528. package/build/tools/server-tools.d.ts +88 -0
  529. package/build/tools/server-tools.d.ts.map +1 -0
  530. package/build/tools/server-tools.js +485 -0
  531. package/build/tools/server-tools.js.map +1 -0
  532. package/build/tools/source-tools.d.ts +46 -0
  533. package/build/tools/source-tools.d.ts.map +1 -0
  534. package/build/tools/source-tools.js +47 -0
  535. package/build/tools/source-tools.js.map +1 -0
  536. package/build/tools/storage-tools.d.ts +141 -0
  537. package/build/tools/storage-tools.d.ts.map +1 -0
  538. package/build/tools/storage-tools.js +894 -0
  539. package/build/tools/storage-tools.js.map +1 -0
  540. package/build/tools/storage-tools.test.d.ts +13 -0
  541. package/build/tools/storage-tools.test.d.ts.map +1 -0
  542. package/build/tools/storage-tools.test.js +707 -0
  543. package/build/tools/storage-tools.test.js.map +1 -0
  544. package/build/tools/tab-tools.d.ts +42 -0
  545. package/build/tools/tab-tools.d.ts.map +1 -0
  546. package/build/tools/tab-tools.js +296 -0
  547. package/build/tools/tab-tools.js.map +1 -0
  548. package/build/tools/wait-tools.d.ts +85 -0
  549. package/build/tools/wait-tools.d.ts.map +1 -0
  550. package/build/tools/wait-tools.js +236 -0
  551. package/build/tools/wait-tools.js.map +1 -0
  552. package/build/tools/wait-tools.test.d.ts +9 -0
  553. package/build/tools/wait-tools.test.d.ts.map +1 -0
  554. package/build/tools/wait-tools.test.js +193 -0
  555. package/build/tools/wait-tools.test.js.map +1 -0
  556. package/build/types.d.ts +133 -0
  557. package/build/types.d.ts.map +1 -0
  558. package/build/types.js +5 -0
  559. package/build/types.js.map +1 -0
  560. package/build/ui-verifier.d.ts +88 -0
  561. package/build/ui-verifier.d.ts.map +1 -0
  562. package/build/ui-verifier.js +540 -0
  563. package/build/ui-verifier.js.map +1 -0
  564. package/build/utils/abort.d.ts +68 -0
  565. package/build/utils/abort.d.ts.map +1 -0
  566. package/build/utils/abort.js +159 -0
  567. package/build/utils/abort.js.map +1 -0
  568. package/build/utils/abort.test.d.ts +2 -0
  569. package/build/utils/abort.test.d.ts.map +1 -0
  570. package/build/utils/abort.test.js +174 -0
  571. package/build/utils/abort.test.js.map +1 -0
  572. package/build/utils/modal-detection-core.d.ts +150 -0
  573. package/build/utils/modal-detection-core.d.ts.map +1 -0
  574. package/build/utils/modal-detection-core.js +332 -0
  575. package/build/utils/modal-detection-core.js.map +1 -0
  576. package/build/utils/modal-detection-core.test.d.ts +5 -0
  577. package/build/utils/modal-detection-core.test.d.ts.map +1 -0
  578. package/build/utils/modal-detection-core.test.js +492 -0
  579. package/build/utils/modal-detection-core.test.js.map +1 -0
  580. package/build/utils/modal-detector.d.ts +29 -0
  581. package/build/utils/modal-detector.d.ts.map +1 -0
  582. package/build/utils/modal-detector.js +281 -0
  583. package/build/utils/modal-detector.js.map +1 -0
  584. package/build/utils/modal-dismissal.d.ts +32 -0
  585. package/build/utils/modal-dismissal.d.ts.map +1 -0
  586. package/build/utils/modal-dismissal.js +190 -0
  587. package/build/utils/modal-dismissal.js.map +1 -0
  588. package/build/utils/port-check.d.ts +31 -0
  589. package/build/utils/port-check.d.ts.map +1 -0
  590. package/build/utils/port-check.js +87 -0
  591. package/build/utils/port-check.js.map +1 -0
  592. package/build/utils/port-check.test.d.ts +2 -0
  593. package/build/utils/port-check.test.d.ts.map +1 -0
  594. package/build/utils/port-check.test.js +66 -0
  595. package/build/utils/port-check.test.js.map +1 -0
  596. package/build/utils/selector-resolver.d.ts +66 -0
  597. package/build/utils/selector-resolver.d.ts.map +1 -0
  598. package/build/utils/selector-resolver.js +200 -0
  599. package/build/utils/selector-resolver.js.map +1 -0
  600. package/build/validation-helpers.d.ts +41 -0
  601. package/build/validation-helpers.d.ts.map +1 -0
  602. package/build/validation-helpers.js +236 -0
  603. package/build/validation-helpers.js.map +1 -0
  604. package/build/window-sizing.d.ts +26 -0
  605. package/build/window-sizing.d.ts.map +1 -0
  606. package/build/window-sizing.js +87 -0
  607. package/build/window-sizing.js.map +1 -0
  608. package/build/window-sizing.test.d.ts +8 -0
  609. package/build/window-sizing.test.d.ts.map +1 -0
  610. package/build/window-sizing.test.js +113 -0
  611. package/build/window-sizing.test.js.map +1 -0
  612. package/docs/README.md +74 -0
  613. package/docs/automation.md +562 -0
  614. package/docs/debugging.md +229 -0
  615. package/docs/installation.md +133 -0
  616. package/docs/instructions.md +227 -0
  617. package/docs/mcp-instructions.md +15 -0
  618. package/docs/message-system.md +327 -0
  619. package/docs/messages.md +3480 -0
  620. package/docs/parser-plugins.md +132 -0
  621. package/docs/replay.md +1411 -0
  622. package/docs/troubleshooting.md +272 -0
  623. package/docs/working_with_llm_notes.md +5 -0
  624. package/package.json +87 -0
  625. package/skills/devharness/SKILL.md +195 -0
  626. package/skills/devharness/references/sequences.md +613 -0
  627. package/skills/devharness/references/tool-categories.md +99 -0
package/docs/replay.md ADDED
@@ -0,0 +1,1411 @@
1
+ # Command Replay
2
+
3
+ Record and replay command sequences for testing, automation, and debugging workflows.
4
+
5
+ > **Tip:** Use the `replay-agent` (`.claude/agents/replay-agent.md`) to build sequences through investigation - it records your tool calls automatically.
6
+ >
7
+ > For a condensed, agent-facing version of this material see
8
+ > `skills/cdp-tools/references/sequences.md`. This document is the fuller
9
+ > reference with worked examples.
10
+
11
+ ## Actions
12
+
13
+ Every capability below is the one `replay` tool, dispatched on `action`:
14
+
15
+ | Group | Actions |
16
+ |---|---|
17
+ | History | `history`, `repeat`, `runFromLog` |
18
+ | Authoring | `create`, `recordInteraction`, `insert`, `addConditional`, `declare` |
19
+ | Managing | `list`, `get`, `delete`, `export`, `load`, `listSaved`, `deleteSaved` |
20
+ | Running | `run`, `runAll`, `step`, `finish`, `status`, `cancel` |
21
+
22
+ There is no `stopInteraction` and no `save` action - see
23
+ [Recording Interactions](#recording-interactions) and
24
+ [Saving and Loading](#saving-and-loading).
25
+
26
+ ## Recording Interactions
27
+
28
+ The easiest way to create a sequence is to record your interactions directly in
29
+ the browser.
30
+
31
+ ```javascript
32
+ // Launch Chrome with a meaningful name
33
+ launchChrome({ reference: "my-signup-test" })
34
+
35
+ // Start recording - THIS CALL BLOCKS until you finish in the browser
36
+ replay({ action: 'recordInteraction', connectionReason: 'my-signup-test' })
37
+ ```
38
+
39
+ **`recordInteraction` blocks until the person finishes in the browser overlay.**
40
+ There is no separate stop call. The tool call returns only once you click ✓
41
+ (complete) or ✕ (cancel) in the overlay, and its response *is* the created
42
+ sequence summary. Because it waits on a human, don't call it unattended.
43
+
44
+ If no connection exists for `connectionReason`, Chrome is auto-launched - but
45
+ only if you also pass `startUrl` (or an `issueId` whose issue carries one). If a
46
+ connection already exists and you pass `startUrl`, the page navigates there
47
+ first.
48
+
49
+ ### The Recording Overlay
50
+
51
+ A visual overlay appears showing:
52
+ - Recording status (`REC` / `PAUSED`)
53
+ - Event count and duration
54
+ - Coordinates and element info
55
+ - Buttons: 💬 Comment | ⏸ Pause | ↺ Reset | ✓ Complete | ✕ Cancel
56
+
57
+ Pass `showOverlay: false` to suppress it.
58
+
59
+ ### Add Comments
60
+
61
+ During recording, add comments to document expected behavior:
62
+ - Click the 💬 button in the overlay
63
+ - Or press **Ctrl/Cmd+Shift+C** (plain comment), **Ctrl/Cmd+Shift+B** (bug),
64
+ **Ctrl/Cmd+Shift+F** (feature)
65
+
66
+ Comments are attached to the previous action and appear in exported tests.
67
+
68
+ **Bug and feature comments create issues.** Each `bug`/`feature` comment becomes
69
+ an entry in the issue tracker with its own copy of the sequence saved as
70
+ `<type>-<id>-repro`. When a recording contains any such comment, no plain
71
+ in-memory sequence is created - the issue sequences are the output.
72
+
73
+ ### Naming
74
+
75
+ The sequence is named, in order of preference:
76
+ 1. the `name` you passed,
77
+ 2. `<issueType>-<issueId>-repro` when you passed `issueId`,
78
+ 3. the `connectionReason`.
79
+
80
+ If a sequence with that name already exists you get a conflict response. Re-run
81
+ with a different `name`, or with `overwrite: true`.
82
+
83
+ ### Recording Options
84
+
85
+ The conversion from raw input events to sequence commands is tunable. All four
86
+ flags default to today's behaviour, so omitting them changes nothing:
87
+
88
+ | Option | Default | Effect |
89
+ |---|---|---|
90
+ | `simplifyEvents` | `true` | Collapse noisy raw events (mousemove runs, key repeats) before conversion. `false` keeps them all. |
91
+ | `includeHovers` | `false` | Emit `input({ action: 'mousemove' })` steps for hovers. Needs `simplifyEvents: false` to keep more than the settled positions. |
92
+ | `preferCoordinates` | `false` | Emit `x,y` clicks even where a selector was captured. Use for canvas/3D/drag-heavy UIs. |
93
+ | `preferSelectors` | `false` | Emit selector clicks wherever a selector exists - *including* canvas elements, which otherwise fall back to coordinates. |
94
+
95
+ If both `preferCoordinates` and `preferSelectors` are `true`, **`preferSelectors`
96
+ wins** - the more portable of the two is chosen.
97
+
98
+ ```javascript
99
+ replay({
100
+ action: 'recordInteraction',
101
+ connectionReason: 'canvas-bug',
102
+ preferCoordinates: true, // a WebGL canvas has no useful selectors
103
+ includeHovers: true, // the bug is a hover artefact
104
+ simplifyEvents: false
105
+ })
106
+ ```
107
+
108
+ `recordInteraction` also accepts `outputFormat`, which appends a dump to the
109
+ usual recording summary:
110
+
111
+ - `events` - the raw captured input events as JSON (this is the only place they
112
+ are ever available; they are not stored with the sequence)
113
+ - `commands` - the converted command list as JSON
114
+ - `review` - a human-readable walkthrough of the captured events: one numbered
115
+ entry per interaction with its coordinates, the element and selector found for
116
+ it, plus navigations, pastes and the comments the person left while recording
117
+ - `playwright` / `puppeteer` - generated test code for the fresh recording
118
+
119
+ Use `outputFormat: 'events'` when a recording produced surprising commands and
120
+ you need to see what the recorder actually captured; `review` is the same
121
+ information in a form you can read, and is the better choice when you want to
122
+ decide whether a step should use a selector or coordinates.
123
+
124
+ ### Recording Against an Issue
125
+
126
+ ```javascript
127
+ replay({ action: 'recordInteraction', connectionReason: 'bug-7', issueId: 7 })
128
+ ```
129
+
130
+ The issue's `type`, `title` and `startUrl` are used automatically, a fullscreen
131
+ issue overlay is shown, and the finished sequence is saved into the issues
132
+ folder and linked to issue #7.
133
+
134
+ ## Exporting Tests
135
+
136
+ Export sequences as Playwright or Puppeteer tests:
137
+
138
+ ```javascript
139
+ // Export as Playwright test
140
+ replay({ action: 'export', name: 'my-signup-test', format: 'playwright' })
141
+ // Creates: tests/e2e/my-signup-test.spec.ts
142
+ // Also saves: .cdp-tools/sequences/my-signup-test.json
143
+
144
+ // Export as Puppeteer test
145
+ replay({ action: 'export', name: 'my-signup-test', format: 'puppeteer' })
146
+ // Creates: tests/puppeteer/my-signup-test.test.js
147
+
148
+ // Export sequence JSON only (default format)
149
+ replay({ action: 'export', name: 'my-signup-test', format: 'sequence' })
150
+ // Creates: .cdp-tools/sequences/my-signup-test.json
151
+ ```
152
+
153
+ `export` always writes the sequence JSON first, whatever the format. If a target
154
+ file already exists you get a conflict response; re-run with `overwrite: true`.
155
+
156
+ Only `navigate` and `input` steps are translated into Playwright/Puppeteer code
157
+ - debugging steps (`breakpoint`, `inspect`, `request`, ...) have no equivalent
158
+ and are dropped from the generated test.
159
+
160
+ ### Configure Export Paths
161
+
162
+ In `.cdp-tools/config.json`:
163
+
164
+ ```json
165
+ {
166
+ "replay": {
167
+ "playwrightExportPath": "./tests/e2e",
168
+ "puppeteerExportPath": "./tests/puppeteer"
169
+ }
170
+ }
171
+ ```
172
+
173
+ ### Preview Before Export
174
+
175
+ ```javascript
176
+ // Preview as Playwright code
177
+ replay({ action: 'get', name: 'my-signup-test', outputFormat: 'playwright' })
178
+
179
+ // Preview as Puppeteer code
180
+ replay({ action: 'get', name: 'my-signup-test', outputFormat: 'puppeteer' })
181
+
182
+ // The raw command list as JSON (what actually gets executed)
183
+ replay({ action: 'get', name: 'my-signup-test', outputFormat: 'commands' })
184
+ ```
185
+
186
+ `outputFormat` on `get` accepts `commands`, `playwright` and `puppeteer`.
187
+ `events` and `review` are not valid here and each returns an error explaining
188
+ why: a stored sequence holds converted *commands*, never the raw input events,
189
+ and both of those formats render events. The raw events exist only during a
190
+ recording - see [Recording Options](#recording-options) for
191
+ `outputFormat: 'events'` and `outputFormat: 'review'` on `recordInteraction`.
192
+
193
+ ## Visual Replay Cursor
194
+
195
+ When replaying sequences, a visual cursor shows where clicks happen:
196
+
197
+ - **Animated cursor** moves to the click position before clicking
198
+ - **Green ripple** on the click
199
+ - **Key press toast** shows keyboard input
200
+
201
+ The cursor is only driven for *coordinate* clicks (`input({ action: 'click', x,
202
+ y })`) and for `input({ action: 'press' })`. Selector-based clicks execute
203
+ without a cursor effect.
204
+
205
+ Configure in `.cdp-tools/config.json`:
206
+
207
+ ```json
208
+ {
209
+ "replay": {
210
+ "showCursor": true
211
+ }
212
+ }
213
+ ```
214
+
215
+ ## Creating Sequences from History
216
+
217
+ ### From Command History
218
+
219
+ ```javascript
220
+ // View command history
221
+ replay({ action: 'history', limit: 20 })
222
+
223
+ // Create sequence from history indices
224
+ replay({
225
+ action: 'create',
226
+ name: 'login-flow',
227
+ indices: [1, 2, 3, 4, 5]
228
+ })
229
+ ```
230
+
231
+ Every tool response footer shows its own history index, so you usually don't
232
+ need to call `history` first.
233
+
234
+ ### With Metadata
235
+
236
+ Add description, expected outcome and a start URL for better documentation:
237
+
238
+ ```javascript
239
+ replay({
240
+ action: 'create',
241
+ name: 'login-flow',
242
+ description: 'Logs into the application with test credentials',
243
+ expectedOutcome: 'User should be redirected to dashboard with welcome message',
244
+ startUrl: 'http://localhost:3000/login',
245
+ indices: [1, 2, 3, 4, 5]
246
+ })
247
+ ```
248
+
249
+ These fields are saved to disk and displayed when listing sequences.
250
+
251
+ ### Re-running History Directly
252
+
253
+ ```javascript
254
+ // Execute commands straight from history without making a sequence
255
+ replay({ action: 'repeat', indices: [12, 13] })
256
+
257
+ // Execute lines from .cdp-tools/history.log (1-indexed, line 1 = most recent)
258
+ replay({ action: 'runFromLog', lines: [3, 4, 5] })
259
+ ```
260
+
261
+ Both stop at the first failing command. Both infer `connectionReason` from a
262
+ `launchChrome`/`connectDebugger` command in the selection if you don't pass one,
263
+ and error out if the commands need a connection and none can be determined.
264
+
265
+ ## Tool-Name Validation
266
+
267
+ `create` and `load` reject a sequence whose steps name a tool that doesn't
268
+ exist, **before anything runs**:
269
+
270
+ ```
271
+ Error: Sequence "login-flow" references 1 unknown tool name
272
+ The "load" action was rejected before any step ran, so no browser state was changed.
273
+
274
+ - Step 4: `navigatee` is not a known tool - did you mean `navigate`?
275
+
276
+ **Fix:** correct the `tool` field on the listed step(s).
277
+ ```
278
+
279
+ Notes:
280
+ - Only the tool *name* is checked. Params are deliberately not validated against
281
+ the tools' schemas, because a param may legitimately hold a `{{var:...}}` or
282
+ `{{timestamp}}` token at rest that only resolves to its real type at run time.
283
+ - `conditional` is exempt - it is a virtual step tool the executor handles
284
+ itself and it is never a registered tool (see [Conditional Steps](#conditional-steps)).
285
+ - A rejected `create` does not clobber an existing same-named sequence; a
286
+ rejected `load` is dropped from memory so it can't be run by id.
287
+
288
+ ## Managing Sequences
289
+
290
+ ```javascript
291
+ // List all in-memory sequences
292
+ replay({ action: 'list' })
293
+
294
+ // View sequence details (by sequenceId or name)
295
+ replay({ action: 'get', sequenceId: 'seq-1234567890' })
296
+ replay({ action: 'get', name: 'login-flow' }) // memory first, then disk (fuzzy name match)
297
+
298
+ // Delete a sequence from memory
299
+ replay({ action: 'delete', sequenceId: 'seq-1234567890' })
300
+ ```
301
+
302
+ ## Saving and Loading
303
+
304
+ ```javascript
305
+ // Export sequence to disk (working directory)
306
+ replay({ action: 'export', sequenceId: 'seq-1234567890', format: 'sequence' })
307
+ // Saves to: .cdp-tools/sequences/<name>.json
308
+
309
+ // Export to global location (accessible from any directory)
310
+ replay({ action: 'export', sequenceId: 'seq-1234567890', format: 'sequence', global: true })
311
+ // Saves to: ~/.cdp-tools/sequences/<name>.json
312
+
313
+ // List saved sequences on disk (add showAll: true to include completed issues)
314
+ replay({ action: 'listSaved' })
315
+
316
+ // Load sequence from disk
317
+ replay({ action: 'load', filename: 'login-flow.json' })
318
+
319
+ // Load into history (for editing)
320
+ replay({ action: 'load', filename: 'login-flow.json', intoHistory: true })
321
+
322
+ // Delete saved file
323
+ replay({ action: 'deleteSaved', filename: 'login-flow.json' })
324
+ ```
325
+
326
+ `run` and `get` load by `name` from disk on their own, so `load` is only needed
327
+ when you want the sequence in memory (or in history) first.
328
+
329
+ ### Editing a sequence mid-session
330
+
331
+ Edit the file and run it - the edit is what runs. The sequences directories are
332
+ watched the way a managed dev server watches its own sources, so an edited file
333
+ is re-read into memory shortly after you save it, and a run re-checks the file's
334
+ timestamp on the way past rather than waiting for the watcher. Both are needed:
335
+ the watcher keeps memory honest while you work, and the check at run time closes
336
+ the gap between saving a file and immediately running it.
337
+
338
+ In-memory copies used to shadow disk for the rest of the session - you edited a
339
+ sequence, ran it by name, and silently got the previous version, while `runAll`
340
+ reloaded the tree first and ran the new one. The same sequence behaved
341
+ differently depending on how it was invoked.
342
+
343
+ A file that is missing or mid-write leaves the loaded copy in place: a watcher
344
+ fires as readily during a write as after one, and dropping a good sequence
345
+ because it was caught half-saved is worse than the staleness this replaces. A
346
+ sequence built from history and never saved is untouched by any of this - there
347
+ is no file to reload it from.
348
+
349
+ ## Running Sequences
350
+
351
+ ### Basic Run
352
+
353
+ ```javascript
354
+ replay({
355
+ action: 'run',
356
+ sequenceId: 'seq-1234567890',
357
+ connectionReason: 'test-session'
358
+ })
359
+ ```
360
+
361
+ Or by name, which also finds it on disk:
362
+
363
+ ```javascript
364
+ replay({ action: 'run', name: 'login-flow', connectionReason: 'test-session' })
365
+ ```
366
+
367
+ ### Background by Default (breaking change in 0.7)
368
+
369
+ **`run` no longer blocks.** It validates the request (sequence exists, tool
370
+ names known, `startFrom` in range, variables supplied), registers a run, and
371
+ returns immediately with a run id:
372
+
373
+ ```javascript
374
+ replay({ action: 'run', name: 'login-flow' })
375
+ // -> Run started in the background ... Run id: `run-3-mdkq1x2`
376
+
377
+ replay({ action: 'status', runId: 'run-3-mdkq1x2' }) // progress while running,
378
+ // the full result once settled
379
+ replay({ action: 'cancel', runId: 'run-3-mdkq1x2' }) // stop it
380
+ ```
381
+
382
+ While a run executes, `status` with its `runId` reports the current step and
383
+ tool; once the run settles it returns exactly what the old blocking `run`
384
+ would have returned (step results, debug state, `killChromeOnFinish` outcome).
385
+ Post-run cleanup - cursor/overlay removal, debug state, `killChromeOnFinish` -
386
+ happens in the background before the run's status turns terminal, exactly once.
387
+
388
+ Run states: `running` → (`cancelling` →) one of `completed`, `failed`,
389
+ `cancelled`, or `paused` (stepTo / breakpoint / click-validation - drive it
390
+ with `step`/`finish` as before).
391
+
392
+ Lifetime and limits:
393
+
394
+ - Several runs can execute concurrently, including two runs of the same
395
+ sequence - the run id is what tells them apart.
396
+ - Settled runs and their results are kept **in memory for 30 minutes** (at
397
+ most 50 records). After that, or after a server restart (including the
398
+ supervisor's hot-restart on rebuild, which also kills any in-flight run),
399
+ `status`/`cancel` with that id return `REPLAY_RUN_NOT_FOUND`.
400
+ - `cancel` with a `runId` aborts the run's controller. The run's signal is
401
+ forwarded to every step's tool handler; cancellation also reaches nested
402
+ sequences (`conditional` flows, nested `replay run` steps) - they share the
403
+ parent run's signal. Status shows `cancelling` until the run actually stops.
404
+ What a cancel does to the step that is currently in flight depends on the
405
+ tool, and the three levels are genuinely different (see the table below).
406
+
407
+ **Which tools honour cancellation** (canonical list - `#110`):
408
+
409
+ | Tool | On cancel | What that means |
410
+ | --- | --- | --- |
411
+ | `wait` (all forms: `selector`, `selectorGone`, `expression`, `ms`) | **cancelled** | Polling/sleep stops mid-step. Nothing was in flight to abandon. |
412
+ | `request` (`destination: 'node'`) | **cancelled** | The external signal is composed into the fetch's controller, so the **socket is closed** - the server sees the request aborted. Its own `timeoutMs` is still reported as a timeout, separately. |
413
+ | `navigate` (`goto`, `reload`, `back`, `forward`) | **stops waiting** | Deliberate owner decision: `Page.stopLoading` is *not* called, because a half-loaded page that later steps act on is worse than a loaded one. The page finishes loading in the background; the step stops waiting for it. |
414
+ | `inspect` (`evaluateExpression`) | **stops waiting** | CDP cannot recall a `Runtime.evaluate`; the expression keeps running in the target. Other `inspect` actions read captured state and only get the entry checkpoint. |
415
+ | `content` (`parse`) | **stops waiting** | The plugin's `waitFor` predicate (default up to 8s) is abandoned. Extraction itself is one `page.evaluate`. |
416
+ | `input` (every action) | **checkpoint only** | Input events cannot be recalled: once `Input.dispatchMouseEvent` is on the wire, Chrome **will** process it. Cancellation prevents events that had *not* gone out yet - checkpoints sit after connection/selector resolution, immediately before each dispatch, and between events in multi-dispatch paths (Tab loops, drag stepping, clear-and-retype, pinch). A cancelled drag still releases the mouse button. **Nothing already dispatched is undone.** |
417
+ | `breakpoint({ action: 'await' })` | **checkpoint + fails the step** | Cancelling the wait removes a breakpoint the step created and fails the step. (Before #110 it reported `success: true` - a cancelled step recorded as passed.) |
418
+ | `request` (`destination: 'browser'`) | **checkpoint only** | The `fetch` runs inside the page and is unreachable from the server; it runs to its own timeout. |
419
+ | `screenshot`, `content` (other actions), `inspect` (other actions) | **checkpoint only** | Enough not to *start* a capture/read after the cancel; the capture itself is a single call with nothing to cancel. |
420
+ | `dom`, `network`, everything else | **next step boundary** | No real wait or loop to interrupt (`network` reads an in-memory buffer). The in-flight call finishes and the run stops before the next step. |
421
+
422
+ Two caveats that apply to every row: work already dispatched to the browser
423
+ may still take effect, and a "stops waiting" step leaves work running in the
424
+ target that no one is watching any more.
425
+ - A nested run started by a sequence step (a `conditional` flow, or a
426
+ `replay run` step - which is forced to `wait: true`) is part of its parent
427
+ run, never a separate top-level run.
428
+
429
+ **Migration:** pass `wait: true` to keep the pre-0.7 blocking behaviour:
430
+
431
+ ```javascript
432
+ replay({ action: 'run', name: 'login-flow', wait: true }) // blocks, returns full result
433
+ ```
434
+
435
+ ### Auto-Launch Chrome
436
+
437
+ If the sequence starts with `launchChrome`, no `connectionReason` is needed -
438
+ the launch step's `reference` becomes the run's connection.
439
+
440
+ ```javascript
441
+ replay({ action: 'run', sequenceId: 'seq-my-flow' })
442
+ ```
443
+
444
+ Otherwise, if the sequence needs a browser and no connection is active, Chrome
445
+ is launched as a **fresh instance** using `connectionReason` (or a reference
446
+ derived from the sequence name when you didn't pass one).
447
+
448
+ ### Retargeting a Run
449
+
450
+ ```javascript
451
+ // Point a staging-recorded sequence at local
452
+ replay({ action: 'run', name: 'checkout', baseUrl: 'http://localhost:3000' })
453
+
454
+ // Enter through a freshly minted link, once
455
+ replay({ action: 'run', name: 'magic-link-login', startUrl: 'https://app.example.com/m/abc123' })
456
+ ```
457
+
458
+ - **`baseUrl`** rewrites the origin of *every absolute* `http(s)` URL in the
459
+ sequence - the stored `startUrl` and any string param in any step (a
460
+ `navigate goto` url, a `request` url, ...) - keeping path, query and hash.
461
+ Relative URLs are untouched. The stored sequence is never mutated.
462
+ - **`startUrl`** replaces the sequence's start URL wholesale for this run,
463
+ applied after any rebasing.
464
+ - Neither is preserved across a mid-run pause and `step`/`finish` resume, which
465
+ re-reads the stored sequence.
466
+
467
+ ### Timeout Configuration
468
+
469
+ ```javascript
470
+ replay({
471
+ action: 'run',
472
+ sequenceId: 'seq-slow-flow',
473
+ connectionReason: 'test-session',
474
+ stepTimeout: 60000, // per step (default: 30000)
475
+ totalTimeout: 600000 // whole run (default: 300000)
476
+ })
477
+ ```
478
+
479
+ Each step's tool call is bounded by `min(stepTimeout, remaining totalTimeout)`.
480
+ A step that exceeds its bound fails the run at that step, like any other step
481
+ failure - the error names the step, the tool, and the limit that fired. The
482
+ run stops immediately; the timed-out tool call is not interrupted and may
483
+ still complete in the background (its side effects can still land).
484
+
485
+ Exceptions:
486
+
487
+ - `wait` steps are exempt from `stepTimeout` - a wait carries its own
488
+ `timeoutMs` bound (default 15000) and fails itself on expiry. It is still
489
+ capped by the remaining `totalTimeout`.
490
+ - Breakpoint pauses are unaffected: a step that hits a breakpoint returns
491
+ immediately with pause info, so an intentional pause never trips the
492
+ step timeout.
493
+
494
+ ### Start From a Specific Step
495
+
496
+ ```javascript
497
+ replay({
498
+ action: 'run',
499
+ name: 'login-flow',
500
+ connectionReason: 'test-session',
501
+ startFrom: 5 // Skip steps 1-4, start at step 5 (1-indexed)
502
+ })
503
+ ```
504
+
505
+ `startFrom` beyond the sequence length is rejected.
506
+
507
+ ### Pause, Step, Finish
508
+
509
+ ```javascript
510
+ replay({ action: 'run', name: 'login-flow', stepTo: 3 }) // run steps 1-3, then pause
511
+ replay({ action: 'status' }) // where am I?
512
+ replay({ action: 'step', stepCount: 2 }) // run the next 2 steps
513
+ replay({ action: 'finish' }) // run the rest
514
+ replay({ action: 'cancel' }) // drop the paused session
515
+ ```
516
+
517
+ A bare `cancel` prefers the paused session; with background runs in flight,
518
+ address the one you mean with `runId` (a bare `cancel` also works when exactly
519
+ one run is executing and nothing is paused). `status` without `runId` shows
520
+ the paused session plus all recent runs.
521
+
522
+ While paused you can run tools by hand and then splice them into the sequence:
523
+
524
+ ```javascript
525
+ replay({ action: 'history' }) // required before inserting by index
526
+ replay({ action: 'insert' }) // show what's insertable
527
+ replay({ action: 'insert', insertIndices: [42, 43], insertAfterStep: 3 })
528
+ ```
529
+
530
+ By default `insert` creates a new sequence named `<name>-modified` (override
531
+ with `newName`); pass `overwrite: true` to edit the sequence in place.
532
+
533
+ ### Closing Chrome Afterwards
534
+
535
+ ```javascript
536
+ replay({ action: 'run', name: 'smoke-test', connectionReason: 'ci-run',
537
+ killChromeOnFinish: true })
538
+ ```
539
+
540
+ `killChromeOnFinish` kills the browsers the run **owns**: its own (run-level)
541
+ connection, plus every browser a `launchChrome` step actually created. It runs
542
+ only after the run finishes - it is skipped on pause, breakpoint,
543
+ click-validation failure or abort.
544
+
545
+ Ownership is read from the launch itself, not guessed from the sequence: a
546
+ `launchChrome` step against a reference that already exists hands back someone
547
+ else's browser (`CHROME_CONNECTION_REUSED`), and those are left running - that
548
+ is the long-lived instance you started by hand, and killing it would take state
549
+ you cannot get back.
550
+
551
+ For the same reason the kill is **skipped entirely when another live connection
552
+ shares the port** — a `launchChrome` step normally opens a tab in the existing
553
+ instance rather than a new process, so killing by port would take those
554
+ browsers down too. The run says so instead: *"Chrome left running (port 9224
555
+ also serves duo-member-two, killChromeOnFinish)"*. Disconnect or close the other
556
+ connections first if you want the instance gone.
557
+
558
+ ### Preview Sequence
559
+
560
+ ```javascript
561
+ replay({ action: 'get', name: 'login-flow' })
562
+ // Shows: commands, substitutable variables, metadata, and run instructions
563
+ ```
564
+
565
+ ## Variables: Two Unrelated Mechanisms
566
+
567
+ `replay` has two features that both get called "variables". They share nothing
568
+ but the word.
569
+
570
+ | | `variables` on `run` | `saveAs` + `{{var:...}}` |
571
+ |---|---|---|
572
+ | What it does | Replaces **recorded typed text** before the run | Captures a **value produced mid-run** for later steps |
573
+ | Set where | A parameter of `replay({ action: 'run' })` | A param on an individual sequence step |
574
+ | Read where | Only by `input({ action: 'type' })` steps | Any step param, via `{{var:name}}` |
575
+ | Naming | Auto-generated `var_<i>_<selector>` keys | Names you choose |
576
+
577
+ ### 1. `variables` - substituting recorded typed text
578
+
579
+ Every recorded `input({ action: 'type' })` step gets an auto-generated key of
580
+ the form `var_<0-based step index>_<selector, non-alphanumerics replaced by _>`
581
+ (or `var_<i>_text` when the step has no selector).
582
+
583
+ ```javascript
584
+ // Original recording had: input({ action: 'type', selector: '#email', text: 'original@email.com' })
585
+ replay({
586
+ action: 'run',
587
+ sequenceId: 'seq-login-flow',
588
+ connectionReason: 'test-session',
589
+ variables: {
590
+ 'var_2_#email': 'new@email.com',
591
+ 'var_3_#password': 'newpassword'
592
+ }
593
+ })
594
+ ```
595
+
596
+ **If the sequence contains any typed text and you omit `variables` entirely,
597
+ `run` does not execute** - it returns a prompt listing the substitutable keys
598
+ and their recorded values. Pass `variables: {}` to accept the recorded values
599
+ as-is, or supply the keys you want to change.
600
+
601
+ ### 2. `saveAs` and `{{var:name.path}}` - capturing values mid-run
602
+
603
+ A step can capture its own result into the run's variable store with `saveAs`.
604
+ Later steps read it back with `{{var:name}}` / `{{var:name.path}}` in any
605
+ param.
606
+
607
+ Supported on exactly two tools today:
608
+
609
+ | Step | What gets stored |
610
+ |---|---|
611
+ | `request` | the **whole response object** - address into it: `{{var:login.body.token}}` |
612
+ | `inspect({ action: 'evaluateExpression' })` | the **evaluated value itself** - use it directly: `{{var:pairingUrl}}` |
613
+
614
+ ```json
615
+ { "tool": "request", "params": {
616
+ "url": "https://api.example.com/login", "method": "POST", "saveAs": "login" } }
617
+
618
+ { "tool": "inspect", "params": {
619
+ "action": "evaluateExpression",
620
+ "expression": "document.querySelector('#pair').href",
621
+ "saveAs": "pairingUrl" } }
622
+
623
+ { "tool": "navigate", "params": { "action": "goto", "url": "{{var:pairingUrl}}" } }
624
+
625
+ { "tool": "assert", "params": {
626
+ "left": "{{var:login.body.token}}", "operator": "exists" } }
627
+ ```
628
+
629
+ Behaviour worth knowing:
630
+
631
+ - A `saveAs` that cannot be honoured (unsupported tool, or a call that produced
632
+ nothing capturable) **fails the step**. It is never a silent no-op, because the
633
+ failure would otherwise surface far away as a confusing "no variable named ..."
634
+ message.
635
+ - Async expressions work: a Promise returned by `evaluateExpression` is awaited
636
+ by default, so an async IIFE (IndexedDB read, `crypto.subtle`, `fetch`)
637
+ captures its **settled value**, not the Promise object. A rejection fails the
638
+ step with the expression's own error.
639
+ - JSON-serializable results are captured **by value** (exact - a string `"42"`
640
+ stays a string). Values that only render as a description (`[HTMLDivElement]`,
641
+ `Array(3)`) come back as strings - capture a specific field, not a whole DOM
642
+ object.
643
+ - The store is shared by reference across the whole run, including nested
644
+ `conditional` sequences and steps running on other connections, and it
645
+ survives a mid-run pause into `step`/`finish`.
646
+
647
+ ### Interpolation Tokens
648
+
649
+ `{{var:...}}` and `{{timestamp}}` are resolved in a step's params immediately
650
+ before it executes.
651
+
652
+ - `{{timestamp}}` - milliseconds, computed **once per run** and reused by every
653
+ step (including later `step`/`finish` calls), so all steps agree on it.
654
+ - `{{timestamp+3600000}}` / `{{timestamp-1000}}` - the same run timestamp with a
655
+ millisecond offset, for expiry-style fields.
656
+ - `{{var:a.b.c}}` - dot-separated path. For a key containing a literal dot, use
657
+ bracket notation for that segment: `{{var:a.b['exec.t1.s2'].c}}`.
658
+ - **Whole-string tokens keep their type.** `"right": "{{var:r.body.count}}"`
659
+ resolves to the number `3`; `"name": "kit{{timestamp}}"` resolves to the
660
+ string `"kit1699999999999"`.
661
+ - An unresolvable token fails the step with a message telling you which
662
+ `saveAs` to add.
663
+
664
+ ## Per-Step Connections (Multi-Device Sequences)
665
+
666
+ Any step may carry its own `connectionReason`. Steps that don't get the
667
+ run-level connection injected (for the tools that accept one).
668
+
669
+ ```json
670
+ { "tool": "input", "params": { "action": "click", "selector": "#pair",
671
+ "connectionReason": "device-a" } }
672
+ { "tool": "inspect", "params": { "action": "evaluateExpression",
673
+ "expression": "document.querySelector('#code').textContent",
674
+ "saveAs": "code", "connectionReason": "device-a" } }
675
+ { "tool": "navigate", "params": { "action": "goto", "url": "{{var:code}}",
676
+ "connectionReason": "device-b" } }
677
+ ```
678
+
679
+ A per-step connection is honoured for **everything wrapped around the step**,
680
+ not just for dispatching it: pre/post-click state capture, click validation,
681
+ navigation validation, typed-text validation, breakpoint/pause detection,
682
+ failure diagnostics, and the wait for the *next* step's element (which follows
683
+ the next step's connection). That is what makes "device A scans, device B
684
+ confirms" work in a single run.
685
+
686
+ Details:
687
+
688
+ - Connection injection applies to `navigate`, `content`, `input`, `console`,
689
+ `network`, `dom`, `screenshot`, `storage`, `inspect`, `execution`,
690
+ `breakpoint`, `getSourceCode`, `detectModals`, `dismissModal`.
691
+ - `request` is handled separately: it only receives a connection when the step
692
+ sets `destination: 'browser'`, so Node-targeted sequences never drag a Chrome
693
+ launch in.
694
+ - Only the browser-only tools (the first group above) make a sequence
695
+ "need a connection" and trigger Chrome auto-launch. A Node-only debugging
696
+ sequence won't spuriously launch Chrome.
697
+ ### Recording a multi-connection sequence
698
+
699
+ Recording **preserves** `connectionReason` (it used to be stripped, which meant
700
+ a recorded two-browser sequence silently replayed in one). Pass it explicitly on
701
+ **every** call while you drive the browsers, including the one that is already
702
+ active, then `create` decides what to do with it:
703
+
704
+ | Recording | `create` result |
705
+ |---|---|
706
+ | All steps on one connection | Hoisted off the steps, so the sequence stays portable and `run({ connectionReason })` still retargets it |
707
+ | Genuinely spans connections | Kept per step |
708
+ | **Mixed** — some steps named, others driven implicitly | Kept as-is, with a warning |
709
+
710
+ The mixed case can't be resolved automatically: nothing knows which browser the
711
+ bare steps belonged to, so hoisting could pin them to the wrong one. `create`
712
+ says so in its output; re-record naming every step rather than shipping it.
713
+
714
+ "Bare" means any step that would have the run-level connection injected — which
715
+ includes the tools whose `connectionReason` is *optional* (`inspect`,
716
+ `execution`, `storage`, `network`, `breakpoint`, `request`, `getSourceCode`),
717
+ not just the browser-only ones. Those are the ones people actually leave off.
718
+ `wait({ ms })` is a plain sleep and doesn't count; every other `wait` form does.
719
+
720
+ A sequence can be both multi-connection **and** mixed, and that combination is
721
+ the dangerous one: the bare steps land in a different browser depending on the
722
+ run-level `connectionReason`, and the run reports success either way. `create`
723
+ warns about both.
724
+
725
+ ### Inserting into an existing sequence
726
+
727
+ `insert` splices history commands (which now carry their connection) into a
728
+ sequence whose own steps had theirs hoisted off. To compare like with like it
729
+ first re-stamps the hoisted connection — recorded on the sequence as
730
+ `recordedConnection` — onto its bare steps, then re-normalizes the merged array:
731
+
732
+ | Inserted steps came from | Result |
733
+ |---|---|
734
+ | The same browser as the sequence | Hoisted again — still portable |
735
+ | A different browser | Every step made explicit, so the sequence is a real multi-connection one and the run-time existence check applies |
736
+
737
+ Without that re-stamp the merge always looked "mixed" (one named reference plus
738
+ the sequence's own bare steps), the hoist was skipped, and an ordinary
739
+ same-browser insert silently left the sequence half-pinned to this session — so
740
+ a later `run({ connectionReason })` split it across two browsers and passed.
741
+
742
+ ### Rebinding references at run time
743
+
744
+ Recorded references are per-session, so a sequence recorded elsewhere needs its
745
+ names mapped onto this session's:
746
+
747
+ ```javascript
748
+ replay({
749
+ action: 'run',
750
+ sequenceId: 'duo-stock-propagation',
751
+ connections: { 'duo-member-two': 'my-second-browser' },
752
+ })
753
+ ```
754
+
755
+ Recorded name on the left, a reference from this session on the right. Both
756
+ sides are sanitized, so spaced forms work. A key matching nothing in the
757
+ sequence is rejected before anything runs, listing the references the sequence
758
+ actually uses — a typo fails loudly instead of being ignored. "The sequence"
759
+ includes any sequence reached through a `conditional` step, since a setup
760
+ sequence normally lives behind one; when such a sub-sequence can't be resolved
761
+ in memory the key is accepted rather than guessed at.
762
+
763
+ A step naming a connection other than the run's is checked against the live
764
+ session before it runs, whether or not the sequence spans several connections,
765
+ so a missing browser fails as *"step 3 needs connection duo-member-two, which
766
+ does not exist in this session"* instead of a generic "not connected to browser"
767
+ from inside the tool.
768
+
769
+ Mapping also renames the `reference` on `launchChrome` / `connectDebugger`
770
+ steps; otherwise a mapped sequence would launch the recorded name and then drive
771
+ a different one. Where a mapping renames a launch, it wins over the run-level
772
+ `connectionReason`, which would otherwise rename it straight back.
773
+
774
+ **Two recorded references cannot be mapped onto one browser.** That would run
775
+ the whole multi-browser sequence in a single browser and report success — the
776
+ original bug, re-entered through the API that exists to prevent it — so it is
777
+ rejected before anything runs.
778
+
779
+ The run-level connection is mapped too when it was *derived* from the sequence
780
+ (e.g. from a `launchChrome` step) rather than passed explicitly; otherwise it
781
+ would point at a reference that doesn't exist here and the `startUrl` navigation
782
+ and cursor injection would silently no-op.
783
+
784
+ `issues({ action: 'workOn' | 'resolve' })` accepts `connections` as well, so a
785
+ multi-browser repro sequence attached to an issue can be replayed in a fresh
786
+ session.
787
+
788
+ ### Declaring the browsers a sequence needs
789
+
790
+ A multi-browser sequence can say which browsers it needs, instead of expecting
791
+ whoever runs it to have launched them first. Set it with `declare`:
792
+
793
+ ```javascript
794
+ replay({
795
+ action: 'declare',
796
+ name: 'duo-stock-propagation',
797
+ requiredConnections: [
798
+ { reference: 'duo-member-two', profile: 'device-a', role: 'the member who draws stock' }
799
+ ],
800
+ requiredSockets: ['/api/sync/socket'],
801
+ })
802
+ ```
803
+
804
+ Each list **replaces** its field and `[]` clears it — a declaration is a whole
805
+ statement about the run, and merging would make "drop the second browser"
806
+ unexpressible. Passing only one list leaves the other alone. The sequence is
807
+ written back to the file it came from; a memory-only sequence waits for
808
+ `export`. Declarations that cannot mean what they say (two references on one
809
+ profile, a reference declared twice, a profile name that is not a safe
810
+ directory segment, an empty socket pattern that would match every socket) are
811
+ refused here rather than on the next run.
812
+
813
+ It lands on the sequence next to `commands`:
814
+
815
+ ```json
816
+ {
817
+ "name": "duo-stock-propagation",
818
+ "startUrl": "http://localhost:5173/",
819
+ "requiredConnections": [
820
+ { "reference": "duo-member-two", "role": "the member who draws stock",
821
+ "url": "http://localhost:5173/login" }
822
+ ],
823
+ "commands": [ ... ]
824
+ }
825
+ ```
826
+
827
+ | Field | Meaning |
828
+ |---|---|
829
+ | `reference` | The reference the steps use |
830
+ | `profile` | Named persistent Chrome profile to come up on (see below) |
831
+ | `url` | Opened on launch (defaults to the sequence's `startUrl`) |
832
+ | `forceNewInstance` | A separate browser process rather than a tab, default **true** — two identities sharing one browser share its storage, which defeats the point. Defaults to **false** when `profile` is set |
833
+ | `role` | Why this browser exists, for the run summary |
834
+
835
+ The run brings each one up before the first step. A reference already bound to
836
+ a live browser is reused, so a browser you launched by hand is not duplicated,
837
+ and a `connections` mapping wins over the declaration — the declaration
838
+ supplies a default, it does not override where the caller points the steps. A
839
+ browser that cannot be launched fails the run before any step executes, naming
840
+ the reference and its role.
841
+
842
+ The run **closes what it launched**, on every terminal outcome: completed,
843
+ failed, and cancelled. A pause is the exception — those browsers are the state
844
+ you stopped to inspect — but whatever ends the pause (`cancel`, `finish`, or
845
+ stepping off the end) closes them then. Browsers that were already up are left
846
+ alone, and a browser sharing its port with another live connection is left
847
+ running too. The run says which it closed:
848
+
849
+ ```
850
+ **Browsers closed** (declared and launched): duo-member-two
851
+ ```
852
+
853
+ #### Declaring the device, not just the browser
854
+
855
+ `profile` names a persistent Chrome profile (the same ones
856
+ `launchChrome({ profile })` creates, under `~/.cdp-tools/profiles`):
857
+
858
+ ```json
859
+ "requiredConnections": [
860
+ { "reference": "device-a", "profile": "device-a", "role": "the enrolled device" }
861
+ ]
862
+ ```
863
+
864
+ The profile is the durable half. Its cookies, localStorage and IndexedDB —
865
+ including non-extractable CryptoKeys — survive between runs, so a device
866
+ enrolled once stays enrolled, while the reference is only a name for this
867
+ session. Declaring the pair is what lets a saved multi-device sequence be re-run
868
+ tomorrow without rewiring which reference means which device.
869
+
870
+ Steps still address browsers by `connectionReason`. There is no per-step
871
+ `profile`: a step names a browser, the declaration decides what that browser
872
+ is.
873
+
874
+ Two consequences worth knowing:
875
+
876
+ - **A profile implies reuse.** Only one live Chrome may hold a profile, so
877
+ `forceNewInstance` defaults to `false` here — a Chrome already running that
878
+ profile *is* the browser the declaration wants, whatever reference it is
879
+ bound to. Set `forceNewInstance: true` explicitly if you really want a spawn
880
+ attempt.
881
+ - **A profile-bearing reference cannot be rebound.** Elsewhere a `connections`
882
+ mapping wins over a declaration, because a declaration is only a default. A
883
+ profile is an identity claim: pointing `device-a` at another browser would run
884
+ device-a's steps somewhere that is not device-a and report success. The run is
885
+ refused instead. Two declarations naming the same profile are refused for the
886
+ same reason — they would be one browser wearing two names.
887
+
888
+ Teardown kills the browser, never the profile: the directory is persistent, so
889
+ the next run finds the device exactly as this one left it.
890
+
891
+ ### Saying what kind of sequence it is
892
+
893
+ `tags` are free-form labels the suite runner selects on:
894
+
895
+ ```javascript
896
+ replay({ action: 'declare', name: 'spine-09-retire-asset', tags: ['ui'] })
897
+ replay({ action: 'declare', name: 'story-b1-pool', tags: ['contract', 'slow'] })
898
+
899
+ replay({ action: 'runAll', tags: ['ui'] }) // only those
900
+ replay({ action: 'runAll', folder: 'spine', tags: ['ui'] }) // composes with folder
901
+ ```
902
+
903
+ Several tags mean **any of**. Tags are lowercased, trimmed and de-duplicated on
904
+ the way in — a tag is matched, not displayed, and `tags: ['UI']` skipping a
905
+ sequence tagged `ui` would quietly run less than you asked for. Spaces are
906
+ refused for the same reason: `slow ui` is ambiguous in a filter, `slow-ui`
907
+ isn't.
908
+
909
+ Every `runAll` reports the split, filtered or not:
910
+
911
+ ```
912
+ runAll folder "tagcheck": 3 passed, 0 failed (1 contract, 1 ui, 1 untagged)
913
+ ```
914
+
915
+ That line is the point of the feature. A suite of 43 sequences reporting "36
916
+ passed" reads as interface coverage, and 14 of those may never issue an `input`
917
+ step — `navigate` → `request` → `assert`, with the browser present only to hold
918
+ the auth cookie. Good contract tests, but no UI regression can fail one of
919
+ them. The split makes the balance visible on every run instead of on an audit.
920
+
921
+ Folders can't carry this: they're already spoken for by scenario shape
922
+ (`spine/`, `story/`, `_helpers/`), and a sequence has one folder but can
923
+ legitimately be both `contract` and `slow`.
924
+
925
+ ### Declaring the sockets a sequence depends on
926
+
927
+ `requiredSockets` is the same idea for transports: a list of URL substrings
928
+ naming the WebSockets the sequence's assertions ride on. Set by the same
929
+ action:
930
+
931
+ ```javascript
932
+ replay({ action: 'declare', name: 'duo-stock-propagation',
933
+ requiredSockets: ['/api/sync/socket'] })
934
+ ```
935
+
936
+ A sequence that declares them is checked whether or not the caller asks for it
937
+ (`requireSockets: true` is only needed for a sequence that declares none). For
938
+ each entry the run fails when a matching socket closed or hit frame errors
939
+ while it executed, or when none is open at the end — including one that never
940
+ opened at all, which no "is it up now" final assertion can catch. Closes the
941
+ run did not cause are not blamed on it: a socket torn down with its target by a
942
+ navigation, or hung up by the page itself, is normal.
943
+
944
+ Match on the app's own path rather than the origin, so the declaration survives
945
+ `baseUrl` retargeting. Dev-server sockets (Vite HMR and friends) simply go
946
+ undeclared and are ignored.
947
+
948
+ ### repeat / runFromLog
949
+
950
+ History retains the connection each command was recorded with, so both replay
951
+ each command against its own connection by default. An explicit
952
+ `connectionReason`:
953
+
954
+ - **retargets** a batch that used a single connection (what the parameter has
955
+ always meant), and
956
+ - is **refused** for a batch spanning several connections, since no single value
957
+ is honest there.
958
+
959
+ It is deliberately not silently ignored — for a while it was, which broke a
960
+ documented parameter with no signal at all.
961
+
962
+ ### Exported test code
963
+
964
+ `outputFormat: 'playwright' | 'puppeteer'` gives each recorded connection its own
965
+ page (`page`, `pageDuoMemberTwo`, …), with a header naming the browsers. A
966
+ single-connection sequence generates exactly what it always did. Emitting every
967
+ step against one `page` would relocate the same silent collapse into the
968
+ exported test.
969
+
970
+ The generators only know `navigate` and `input` steps. Anything else —
971
+ `conditional`, `launchChrome`, `inspect`, `storage`, `wait`, `breakpoint` —
972
+ becomes a `// [not generated]` comment naming the step, and a sequence where
973
+ *nothing* could be generated exports a test that **throws** rather than an empty
974
+ one that passes. A setup sequence made of a conditional and a launch has no
975
+ Playwright equivalent at all; run it with `replay({ action: 'run' })` instead of
976
+ exporting it.
977
+
978
+ ### Two deliberate non-behaviours
979
+
980
+ - **A run-level `connectionReason` does not override a step's own.** The step
981
+ wins.
982
+ - **A per-step reference that doesn't exist fails the step.** It never falls
983
+ back to the run-level connection.
984
+
985
+ The second is the whole point. Falling back is what let a two-browser sequence
986
+ replay in one browser and report success — the "member" steps ran in the owner's
987
+ browser, the owner saw their own optimistic update, and a cross-user propagation
988
+ assertion went green having never involved a second user.
989
+
990
+ ## Conditional Steps
991
+
992
+ `conditional` is a virtual step tool: it is handled inside the executor, never
993
+ appears in the tool list, and is exempt from tool-name validation.
994
+
995
+ Not being a real tool, it is never recorded, so `create` and `insert` — which
996
+ both build steps out of recorded history — cannot produce one. `addConditional`
997
+ is its authoring action:
998
+
999
+ ```javascript
1000
+ replay({ action: 'addConditional',
1001
+ name: 'checkout-flow', // or sequenceId
1002
+ condition: '{{selector:.cookie-banner}}',
1003
+ thenSequence: 'dismiss-cookie-banner',
1004
+ insertAfterStep: 2, // omit to append; 0 puts it first
1005
+ comment: 'EU builds only' }) // optional
1006
+ ```
1007
+
1008
+ which stores the step as:
1009
+
1010
+ ```json
1011
+ { "tool": "conditional", "params": {
1012
+ "if": "{{selector:.cookie-banner}}",
1013
+ "then": "dismiss-cookie-banner" } }
1014
+ ```
1015
+
1016
+ Rejected before the sequence is touched: a condition that doesn't parse, an
1017
+ unknown type, an uncompilable or over-long `url:matches` regex, a malformed
1018
+ `indexedDB` path, a `thenSequence` naming no known sequence or naming this one
1019
+ (which would recurse to `maxConditionalDepth`), an out-of-range
1020
+ `insertAfterStep`. Values holding a `{{var:...}}` token are skipped — they are
1021
+ substituted at run time.
1022
+
1023
+ A sequence already saved on disk is rewritten in place; otherwise it waits for
1024
+ `export`. The response says which.
1025
+
1026
+ `then` is the name of another sequence, loaded and run inline when the condition
1027
+ holds, and it shares the parent run's captured variables. A `launchChrome` step
1028
+ inside it is skipped when that reference is already connected, and run when it
1029
+ isn't - so a setup sequence spanning two browsers can create the second one.
1030
+
1031
+ Which browser the sub-sequence's *bare* steps run in follows from that:
1032
+
1033
+ | The nested `launchChrome` | Bare steps run in |
1034
+ |---|---|
1035
+ | **ran** (that browser didn't exist) | the browser it just launched |
1036
+ | **skipped** (already connected), or absent | the calling run's connection |
1037
+
1038
+ That split is what makes both shapes work. A setup sequence is a launch plus
1039
+ bare steps (`create` hoists the connection off them), so its steps have to
1040
+ follow the browser it created - otherwise the run opens a browser, does the work
1041
+ in the *caller's* browser, and still reports success. A nested login sequence
1042
+ whose browser already exists keeps running in whatever browser called it. Steps
1043
+ that name their own `connectionReason` are unaffected either way.
1044
+
1045
+ > **Two connections are not two devices.** A plain `launchChrome` reuses the
1046
+ > running instance and opens a *tab* in it, so both references share one profile
1047
+ > - one set of cookies, one localStorage, one IndexedDB. A duo test built that
1048
+ > way has a single device identity wearing two names, and a "does it propagate
1049
+ > to the other user" check passes without a second device ever existing. Give
1050
+ > the second browser its own `profile` (and `forceNewInstance: true`) when the
1051
+ > point of the test is that the two sides are genuinely separate:
1052
+ >
1053
+ > ```javascript
1054
+ > launchChrome({ reference: 'duo-member-two', profile: 'member', forceNewInstance: true })
1055
+ > ```
1056
+ >
1057
+ > `listConnections` shows the giveaway: same `port` means same instance and
1058
+ > therefore shared storage.
1059
+
1060
+ Supported conditions:
1061
+
1062
+ | Condition | True when |
1063
+ |---|---|
1064
+ | `{{selector:CSS}}` / `{{!selector:CSS}}` | element exists / doesn't |
1065
+ | `{{url:contains:STRING}}` | current URL contains the string |
1066
+ | `{{url:matches:REGEX}}` | current URL matches the regex |
1067
+ | `{{url:EXACT}}` | current URL equals the value |
1068
+ | `{{cookie:NAME}}` / `{{!cookie:NAME}}` | cookie exists / doesn't |
1069
+ | `{{localStorage:KEY}}` / `{{!localStorage:KEY}}` | key exists / doesn't |
1070
+ | `{{indexedDB:DB/STORE/KEY}}` / `{{!indexedDB:...}}` | that record exists / doesn't |
1071
+ | `{{indexedDB:DB/STORE}}` | the object store holds at least one record |
1072
+
1073
+ An element that isn't on the page counts as *absent*, not an error, so
1074
+ `{{!selector:...}}` skips correctly. A malformed selector or a disconnected
1075
+ browser still fails the run. The page is probed once with no retry (precede an
1076
+ async marker with a `wait` step), and a hidden element counts as present.
1077
+
1078
+ Every condition reads the tool's structured result, never its printed text, so
1079
+ stored *data* cannot answer a question about *structure*: a localStorage value
1080
+ of `"null"` (or one containing "not found") is present, an empty string is
1081
+ present, a cookie name matches exactly rather than as a suffix, and a URL
1082
+ containing a comma compares in full.
1083
+
1084
+ A database or store that doesn't exist yet counts as *absent*, not as an
1085
+ evaluation error - that is the state a wiped profile is in, and the state a
1086
+ healing setup sequence exists to fix. A value that cannot be represented in
1087
+ JSON (a non-extractable `CryptoKey`, a `Blob`) still counts as present.
1088
+ Presence comes from the storage tool's structured result, not its printed text,
1089
+ so a record whose *value* happens to read "No record found for this key." is
1090
+ still present. A condition is written as text, so an all-digits key is probed as
1091
+ a string and then, if that misses, as a number - IndexedDB keys `42` and `"42"`
1092
+ are different keys.
1093
+
1094
+ A condition is interpolated like any other step parameter, so a captured
1095
+ variable can drive it — `{{indexedDB:identity/keys/{{var:deviceId}}}}` after an
1096
+ earlier `inspect({ saveAs: 'deviceId' })`.
1097
+
1098
+ A condition that is legitimately *not met* skips the nested sequence and the
1099
+ step counts as a success. A condition that cannot be *evaluated* (bad format,
1100
+ unknown type, invalid or over-long regex, tool error) fails the run.
1101
+
1102
+ Nesting is capped by `replay.maxConditionalDepth` (default 10) and regexes by
1103
+ `replay.maxRegexLength` (default 500); both are `.cdp-tools/config.json`
1104
+ settings. Oscillating chains (A→B→A) are allowed up to the depth limit.
1105
+
1106
+ ## forEach Steps
1107
+
1108
+ `forEach` is the second virtual step tool: handled inside the executor, never a
1109
+ registered tool, exempt from tool-name validation via `VIRTUAL_STEP_TOOLS`.
1110
+
1111
+ ```json
1112
+ { "tool": "forEach", "params": {
1113
+ "in": "{{var:shares}}",
1114
+ "as": "share",
1115
+ "do": "revoke-one-share",
1116
+ "where": "item.name !== 'Employees'",
1117
+ "maxItems": 50 } }
1118
+ ```
1119
+
1120
+ It exists because conditions are single-subject. `{{selector:X}}` answers "does
1121
+ X exist"; there is no condition that answers "what is there", so a sequence could
1122
+ provision a missing fixture but never remove an unexpected one. That asymmetry is
1123
+ what made shared-fixture suites drift.
1124
+
1125
+ `in` resolves in one of two ways. `{{var:name}}` is a whole-string interpolation
1126
+ token, so **the run's normal param interpolation resolves it before the step is
1127
+ dispatched** and — because whole-string tokens preserve type — what the step
1128
+ receives is the captured array itself, not a string. `resolveForEachItems`
1129
+ therefore accepts an array directly; its string branches only matter for direct
1130
+ calls and for `{{selectorAll:CSS}}`, which is not a var token and arrives
1131
+ unresolved. `{{selectorAll:CSS}}` evaluates in the page and yields one plain
1132
+ descriptor per element (`index`, `text`, `id`, `className`, `href`, `value`) —
1133
+ DOM nodes cannot cross the CDP boundary.
1134
+
1135
+ `where` is evaluated as JavaScript in the page with `item` and `index` in scope,
1136
+ **not** in the `{{...}}` condition grammar. A filter reads fields off an
1137
+ arbitrary object, which that grammar cannot express, and adding a second
1138
+ mini-language beside it would leave two half-expressive syntaxes. A `where` that
1139
+ throws fails the run rather than excluding the item, matching how an
1140
+ unevaluatable condition behaves.
1141
+
1142
+ Per-iteration the item is written to the shared variable store under `as` (and
1143
+ its position under `<as>Index`). The store is shared by reference with nested
1144
+ runs, so bindings are replaced rather than scoped, and a body's own `saveAs`
1145
+ captures persist across iterations.
1146
+
1147
+ Budget is the parent's *remaining* total, decremented per iteration, so a loop
1148
+ cannot extend the run's total the way a fresh copy would. Depth shares
1149
+ `maxConditionalDepth`: a loop body that loops is the same runaway risk as a
1150
+ conditional chain. `maxItems` (default 100) is a backstop, and hitting it is
1151
+ logged rather than silently truncating.
1152
+
1153
+ An empty source is a success with `iterations: 0`, rendered as "N item(s) found,
1154
+ none ran" — a converge loop with nothing to do must not look like a broken
1155
+ selector.
1156
+
1157
+ ## Teardown Steps
1158
+
1159
+ `CommandSequence.teardown` is an optional second command array, run by
1160
+ `runTeardown` after the main loop in `executeSteps` reaches a terminal state.
1161
+
1162
+ Terminal means: all steps ran, a step failed, the run was aborted, or the total
1163
+ timeout expired. It explicitly does **not** include the pause paths — `stepTo`
1164
+ (detected as `targetEnd < commands.length` with nothing failed), an unexpected
1165
+ breakpoint, or a click-validation failure, which return early and never reach the
1166
+ teardown call. A paused run is not finished, and tearing down would destroy the
1167
+ state the user paused to inspect.
1168
+
1169
+ Three deliberate departures from how nested sequences are otherwise run:
1170
+
1171
+ | Property | Why |
1172
+ |---|---|
1173
+ | Own `teardownTimeout` (default 60s), not `totalTimeout` | The commonest reason a run needs cleanup is that it timed out. Drawing on the exhausted parent budget would skip teardown in exactly that case. |
1174
+ | The run's `AbortSignal` is not forwarded | `replay cancel` must stop the work, not the cleanup. A cancelled run is precisely one that has left something behind. |
1175
+ | Shares `ctx.variableStore` | Teardown revokes what setup minted, and the capturing step may have run long before the failure. |
1176
+
1177
+ The synthetic sequence passed to the nested `executeSteps` call sets
1178
+ `teardown: undefined`; without it the teardown run reaches the same code and runs
1179
+ the teardown again, unboundedly.
1180
+
1181
+ Results land on `ExecutionResult.teardownResults` / `teardownFailed` and are
1182
+ rendered in their own section — never merged into `results` or the
1183
+ successful/failed counts. A broken cleanup must not turn a passing run red, nor
1184
+ make a failing one look like it failed somewhere it did not.
1185
+
1186
+ Teardown is best-effort by construction: a killed cdp-tools process takes any
1187
+ pending teardown with it. It reduces accumulation; it cannot guarantee a clean
1188
+ world, so assertions that depend on absence remain order-dependent regardless.
1189
+
1190
+ ## Debug-Aware Replay
1191
+
1192
+ Replay handles debugging sequences specially.
1193
+
1194
+ ### Fresh callFrameId Replacement
1195
+
1196
+ When replaying `inspect({ action: 'getVariables' })` with a recorded
1197
+ `callFrameId`, replay fetches the current call stack on that step's connection
1198
+ and swaps in a fresh ID.
1199
+
1200
+ ### Auto-Resume
1201
+
1202
+ If the debugger is already paused when a run starts (or when checking an
1203
+ existing connection), replay resumes it first, so a leftover pause from a
1204
+ previous run can't stall the sequence.
1205
+
1206
+ ### Expected vs Unexpected Breakpoints
1207
+
1208
+ Replay tracks breakpoints that the sequence itself sets (`breakpoint({ action:
1209
+ 'set' })`), including ±1 line to absorb CDP's 0-based/1-based resolution. After
1210
+ each step it checks whether execution is paused:
1211
+
1212
+ - paused at a breakpoint this sequence set → **keep going**;
1213
+ - paused anywhere else → **stop and report the breakpoint hit**, with the pause
1214
+ location and the connection to inspect it on.
1215
+
1216
+ ### Debug State Output
1217
+
1218
+ After a run that completed with no failures, if breakpoints are active or
1219
+ execution is paused, replay appends the current debug state:
1220
+
1221
+ ```
1222
+ ## Debug State
1223
+
1224
+ ⏸️ **Execution paused** at http://localhost:3101/client.js:6
1225
+
1226
+ **Next steps:**
1227
+ - Inspect call stack: `inspect({ action: 'getCallStack', connectionReason: '...' })`
1228
+ - Get variables: `inspect({ action: 'getVariables', connectionReason: '...', callFrameId: '<from call stack>' })`
1229
+ - Resume execution: `execution({ action: 'resume', connectionReason: '...' })`
1230
+ - Step over: `execution({ action: 'stepOver', connectionReason: '...' })`
1231
+
1232
+ 🔴 **1 active breakpoint**
1233
+ - List breakpoints: `breakpoint({ action: 'list', connectionReason: '...' })`
1234
+ ```
1235
+
1236
+ ## Step Robustness
1237
+
1238
+ Beyond click validation, each step gets some automatic help:
1239
+
1240
+ - **Retries:** `input` `click` / `type` / `hover` retry up to 5 times, 500ms
1241
+ apart, when the failure looks like "element not found" - enough for a
1242
+ component that hasn't mounted yet.
1243
+ - **Element pre-wait:** after a `navigate` step or a click, if the next step is
1244
+ an `input` with a selector, replay waits for that selector (5 tries, 500ms
1245
+ apart) on the *next step's* connection.
1246
+ - **Navigation validation:** after every `navigate` step, replay checks the page
1247
+ didn't land on `about:blank`, a `chrome-error://` page, an `ERR_*`, or a
1248
+ "site can't be reached" title.
1249
+ - **Port check:** before navigating to a `localhost` URL (including the
1250
+ sequence's `startUrl`), the port is checked and the run fails fast with a
1251
+ clear message rather than loading an error page.
1252
+ - **Typed-text validation:** after `input({ action: 'type', selector })`, the
1253
+ field's value (or `innerText` for contenteditable) is compared against what
1254
+ was typed - exact match, or "ends with" when `append: true`.
1255
+ - **Recorded delays:** delays captured during interaction recording are replayed
1256
+ (capped by `replay.maxDelayMs`, default 1000ms).
1257
+
1258
+ These are best-effort niceties with short, fixed budgets. When a step
1259
+ genuinely depends on async work settling - a page load after `location.href`,
1260
+ a spinner clearing, an async probe writing a global - add an explicit `wait`
1261
+ step instead of relying on them.
1262
+
1263
+ ## Explicit Waits (`wait` steps)
1264
+
1265
+ `wait` is a first-class sequence step for "the previous step kicked off async
1266
+ work". Exactly one of four mutually exclusive forms:
1267
+
1268
+ ```javascript
1269
+ { tool: 'wait', params: { selector: 'button:has-text("Join")' } } // element appears
1270
+ { tool: 'wait', params: { selectorGone: '.spinner' } } // element disappears
1271
+ { tool: 'wait', params: { expression: 'window.__probe !== "PENDING"' } }
1272
+ { tool: 'wait', params: { ms: 500 } } // fixed sleep, last resort
1273
+ ```
1274
+
1275
+ - The condition forms are polled **from the MCP side** as a synchronous check
1276
+ (default `pollIntervalMs` 100, `timeoutMs` 15000). Because nothing waits
1277
+ inside the page, a wait survives a navigation that happens mid-wait (each
1278
+ poll simply runs in whatever document exists at that moment) and never
1279
+ depends on in-page timers or promises resolving.
1280
+ - `expression` must be synchronous - don't `await` in it. Kick async work off
1281
+ in a prior step, have it write a global, and wait on the global.
1282
+ - On timeout the step returns an error (`WAIT_TIMEOUT`, including the last
1283
+ evaluation error if the predicate was throwing), which stops the sequence
1284
+ like any other failed step. A `wait` never hangs a run.
1285
+ - The run-level `connectionReason` is injected like any other step, and a
1286
+ per-step `connectionReason` is honoured (multi-device sequences).
1287
+ - `wait({ ms })` needs no browser at all and never triggers a Chrome
1288
+ auto-launch; `wait({ expression })` also works against a Node.js target.
1289
+
1290
+ ## Click Validation
1291
+
1292
+ Click steps validate their effects. When validation fails, the sequence pauses
1293
+ for inspection rather than failing outright.
1294
+
1295
+ ### What Gets Validated
1296
+
1297
+ - **Console errors**: new errors after the click (default: enabled, fail mode `error`)
1298
+ - **Navigation**: if the click caused navigation, that it succeeded (default: enabled)
1299
+ - **DOM mutations**: whether the click changed anything (default: disabled)
1300
+ - **Network requests**: failed POST requests (default: disabled)
1301
+
1302
+ New console warnings and logs are recorded as information only.
1303
+
1304
+ ### Pause on Failure
1305
+
1306
+ When validation fails the sequence pauses **on the failed step** - the active
1307
+ sequence's cursor is rewound so the next `step` re-runs it. You can:
1308
+
1309
+ - inspect the error state with `console`, `network`, `dom`, `screenshot`, ...
1310
+ - retry the failed step: `replay({ action: 'step', stepCount: 1 })`
1311
+ - run the rest: `replay({ action: 'finish' })`
1312
+ - abandon it: `replay({ action: 'cancel' })`
1313
+
1314
+ There is no "skip this step" action - `step` always re-runs the step it is
1315
+ parked on.
1316
+
1317
+ ### Configuration
1318
+
1319
+ Configure in `.cdp-tools/config.json` (values shown are the defaults):
1320
+
1321
+ ```json
1322
+ {
1323
+ "clickValidation": {
1324
+ "enabled": true,
1325
+ "validateNavigation": true,
1326
+ "requireDomChanges": false,
1327
+ "domChangesFailMode": "warn",
1328
+ "failOnConsoleErrors": true,
1329
+ "consoleErrorsFailMode": "error",
1330
+ "validateNetworkPayload": false,
1331
+ "networkFailMode": "warn",
1332
+ "postClickDelayMs": 100
1333
+ }
1334
+ }
1335
+ ```
1336
+
1337
+ **Fail modes:**
1338
+ - `error`: pauses the sequence for inspection
1339
+ - `warn`: logs a warning and continues
1340
+
1341
+ ## Use Cases
1342
+
1343
+ ### Regression Testing
1344
+
1345
+ ```javascript
1346
+ // Record interactions directly - this call blocks until you click ✓ in the browser
1347
+ launchChrome({ reference: "checkout-test" })
1348
+ replay({ action: 'recordInteraction', connectionReason: 'checkout-test' })
1349
+
1350
+ // Export as Playwright test
1351
+ replay({ action: 'export', name: 'checkout-test', format: 'playwright' })
1352
+
1353
+ // Run anytime to verify
1354
+ replay({ action: 'run', name: 'checkout-test', connectionReason: 'test-run' })
1355
+ ```
1356
+
1357
+ ### Debugging Workflows
1358
+
1359
+ ```javascript
1360
+ // Create a debug sequence from what you just did
1361
+ replay({
1362
+ action: 'create',
1363
+ name: 'debug-auth-bug',
1364
+ description: 'Sets breakpoint on auth handler and triggers login',
1365
+ expectedOutcome: 'Debugger pauses at auth.js:42 showing user object',
1366
+ indices: [0, 1, 2, 3, 4, 5]
1367
+ })
1368
+
1369
+ // Run to debug - debug state is shown automatically after the run
1370
+ replay({ action: 'run', name: 'debug-auth-bug' })
1371
+ ```
1372
+
1373
+ ### Verifying the Same Flow on Two Deployments
1374
+
1375
+ ```javascript
1376
+ replay({ action: 'run', name: 'checkout', baseUrl: 'http://localhost:3000' })
1377
+ replay({ action: 'run', name: 'checkout', baseUrl: 'https://staging.example.com' })
1378
+ ```
1379
+
1380
+ ### Cross-Device Handoff
1381
+
1382
+ Hand-author steps with per-step `connectionReason` and capture the handoff value
1383
+ with `saveAs` - see [Per-Step Connections](#per-step-connections-multi-device-sequences).
1384
+
1385
+ ### Automation
1386
+
1387
+ ```javascript
1388
+ replay({
1389
+ action: 'create',
1390
+ name: 'daily-smoke-test',
1391
+ description: 'Navigates key pages and checks for console errors',
1392
+ expectedOutcome: 'All pages load without errors',
1393
+ indices: [0, 1, 2, 3, 4, 5, 6, 7]
1394
+ })
1395
+
1396
+ replay({ action: 'run', name: 'daily-smoke-test', killChromeOnFinish: true })
1397
+ ```
1398
+
1399
+ ## Notes
1400
+
1401
+ - **Recording:** only tool calls are recorded, not responses.
1402
+ - **Replay:** steps execute sequentially, and the run stops at the first failing
1403
+ step.
1404
+ - **Persistence:** in-memory sequences are cleared on restart; use
1405
+ `export`/`load` for disk persistence. `run` and `get` load from disk by name
1406
+ automatically.
1407
+ - **Connection stripping:** `connectionReason` is removed from commands as they
1408
+ are recorded into history, for portability.
1409
+ - **Validation timing:** tool *names* are validated at `create`/`load` time;
1410
+ `run` does not re-validate, so a sequence edited on disk by hand is best
1411
+ round-tripped through `load` before running.