swipium 1.4.0 → 2.0.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 (541) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/README.md +175 -219
  3. package/THREAT_MODEL.md +223 -47
  4. package/dist/appMap/automationLink.js +39 -26
  5. package/dist/appMap/automationLink.js.map +1 -1
  6. package/dist/appMap/build.js +68 -20
  7. package/dist/appMap/build.js.map +1 -1
  8. package/dist/appMap/codeIndex.js +31 -5
  9. package/dist/appMap/codeIndex.js.map +1 -1
  10. package/dist/appMap/featureIndex.js +23 -5
  11. package/dist/appMap/featureIndex.js.map +1 -1
  12. package/dist/appMap/featureModel.js +44 -8
  13. package/dist/appMap/featureModel.js.map +1 -1
  14. package/dist/appMap/firstRunApply.js +24 -6
  15. package/dist/appMap/firstRunApply.js.map +1 -1
  16. package/dist/appMap/fsWalk.js +19 -2
  17. package/dist/appMap/fsWalk.js.map +1 -1
  18. package/dist/appMap/issues.js +10 -30
  19. package/dist/appMap/issues.js.map +1 -1
  20. package/dist/appMap/migrations.js +6 -6
  21. package/dist/appMap/migrations.js.map +1 -1
  22. package/dist/appMap/prelaunch.js +2 -2
  23. package/dist/appMap/prelaunch.js.map +1 -1
  24. package/dist/appMap/projectRegistry.js +83 -11
  25. package/dist/appMap/projectRegistry.js.map +1 -1
  26. package/dist/appMap/provenance.js +2 -2
  27. package/dist/appMap/provenance.js.map +1 -1
  28. package/dist/appMap/query.js +21 -5
  29. package/dist/appMap/query.js.map +1 -1
  30. package/dist/appMap/runtimeMerge.js +12 -5
  31. package/dist/appMap/runtimeMerge.js.map +1 -1
  32. package/dist/appMap/schema.js +1 -1
  33. package/dist/appMap/schema.js.map +1 -1
  34. package/dist/appMap/screenMatch.js +6 -2
  35. package/dist/appMap/screenMatch.js.map +1 -1
  36. package/dist/appMap/staticScan.js +151 -39
  37. package/dist/appMap/staticScan.js.map +1 -1
  38. package/dist/appMap/store.js +58 -85
  39. package/dist/appMap/store.js.map +1 -1
  40. package/dist/appMap/tsAstScan.js +34 -12
  41. package/dist/appMap/tsAstScan.js.map +1 -1
  42. package/dist/artifacts/bundletool.js +49 -12
  43. package/dist/artifacts/bundletool.js.map +1 -1
  44. package/dist/artifacts/resolve.js +41 -14
  45. package/dist/artifacts/resolve.js.map +1 -1
  46. package/dist/automation/capabilities.js +112 -30
  47. package/dist/automation/capabilities.js.map +1 -1
  48. package/dist/automation/gestures.js +6 -2
  49. package/dist/automation/gestures.js.map +1 -1
  50. package/dist/automation/plan.js +71 -23
  51. package/dist/automation/plan.js.map +1 -1
  52. package/dist/automation/report.js +15 -5
  53. package/dist/automation/report.js.map +1 -1
  54. package/dist/automation/selectors.js +22 -28
  55. package/dist/automation/selectors.js.map +1 -1
  56. package/dist/automation/types.js +1 -1
  57. package/dist/automation/types.js.map +1 -1
  58. package/dist/automation/waits.js +14 -8
  59. package/dist/automation/waits.js.map +1 -1
  60. package/dist/automation/webview.js +9 -3
  61. package/dist/automation/webview.js.map +1 -1
  62. package/dist/automationGen/appiumModel.js +16 -8
  63. package/dist/automationGen/appiumModel.js.map +1 -1
  64. package/dist/automationGen/ciEmitter.js +6 -6
  65. package/dist/automationGen/ciEmitter.js.map +1 -1
  66. package/dist/automationGen/identifiers.js +224 -0
  67. package/dist/automationGen/identifiers.js.map +1 -0
  68. package/dist/automationGen/jsEmitter.js +288 -121
  69. package/dist/automationGen/jsEmitter.js.map +1 -1
  70. package/dist/automationGen/packagePatch.js +2 -2
  71. package/dist/automationGen/packagePatch.js.map +1 -1
  72. package/dist/automationGen/platformResolve.js +36 -0
  73. package/dist/automationGen/platformResolve.js.map +1 -0
  74. package/dist/automationGen/projectProfile.js +76 -27
  75. package/dist/automationGen/projectProfile.js.map +1 -1
  76. package/dist/automationGen/pythonEmitter.js +278 -107
  77. package/dist/automationGen/pythonEmitter.js.map +1 -1
  78. package/dist/automationGen/readmeEmitter.js +7 -6
  79. package/dist/automationGen/readmeEmitter.js.map +1 -1
  80. package/dist/automationGen/run.js +388 -0
  81. package/dist/automationGen/run.js.map +1 -0
  82. package/dist/automationGen/suitePlan.js +17 -10
  83. package/dist/automationGen/suitePlan.js.map +1 -1
  84. package/dist/automationGen/validation.js +52 -12
  85. package/dist/automationGen/validation.js.map +1 -1
  86. package/dist/build/parseBuildLog.js +235 -31
  87. package/dist/build/parseBuildLog.js.map +1 -1
  88. package/dist/build/plan.js +39 -18
  89. package/dist/build/plan.js.map +1 -1
  90. package/dist/ci/preflight.js +13 -4
  91. package/dist/ci/preflight.js.map +1 -1
  92. package/dist/cli/gc.js +74 -0
  93. package/dist/cli/gc.js.map +1 -0
  94. package/dist/cli/init.js +217 -26
  95. package/dist/cli/init.js.map +1 -1
  96. package/dist/cli/main.js +50 -0
  97. package/dist/cli/main.js.map +1 -0
  98. package/dist/cli/report.js +218 -0
  99. package/dist/cli/report.js.map +1 -0
  100. package/dist/cli/scan.js +36 -15
  101. package/dist/cli/scan.js.map +1 -1
  102. package/dist/cli/suite.js +7 -7
  103. package/dist/cli/suite.js.map +1 -1
  104. package/dist/cli/verify.js +7 -7
  105. package/dist/cli/verify.js.map +1 -1
  106. package/dist/consent/consent.js +162 -5
  107. package/dist/consent/consent.js.map +1 -1
  108. package/dist/context/detect.js +55 -18
  109. package/dist/context/detect.js.map +1 -1
  110. package/dist/context/findApps.js +22 -8
  111. package/dist/context/findApps.js.map +1 -1
  112. package/dist/context/projectRoot.js +174 -19
  113. package/dist/context/projectRoot.js.map +1 -1
  114. package/dist/context/scan.js +1 -1
  115. package/dist/context/scan.js.map +1 -1
  116. package/dist/core/capabilityGroups.js +90 -0
  117. package/dist/core/capabilityGroups.js.map +1 -0
  118. package/dist/core/target.js +10 -8
  119. package/dist/core/target.js.map +1 -1
  120. package/dist/core/targetPlan.js +160 -39
  121. package/dist/core/targetPlan.js.map +1 -1
  122. package/dist/drivers/DirectDriver.js +317 -43
  123. package/dist/drivers/DirectDriver.js.map +1 -1
  124. package/dist/drivers/SimctlDriver.js +124 -8
  125. package/dist/drivers/SimctlDriver.js.map +1 -1
  126. package/dist/drivers/WdaDriver.js +365 -55
  127. package/dist/drivers/WdaDriver.js.map +1 -1
  128. package/dist/explore/candidates.js +7 -3
  129. package/dist/explore/candidates.js.map +1 -1
  130. package/dist/explore/graph.js +15 -6
  131. package/dist/explore/graph.js.map +1 -1
  132. package/dist/explore/memory.js +4 -1
  133. package/dist/explore/memory.js.map +1 -1
  134. package/dist/explore/planner.js +54 -9
  135. package/dist/explore/planner.js.map +1 -1
  136. package/dist/explore/policy.js +11 -6
  137. package/dist/explore/policy.js.map +1 -1
  138. package/dist/explore/runner.js +363 -190
  139. package/dist/explore/runner.js.map +1 -1
  140. package/dist/explore/signatures.js +4 -2
  141. package/dist/explore/signatures.js.map +1 -1
  142. package/dist/explore/suite.js +3 -1
  143. package/dist/explore/suite.js.map +1 -1
  144. package/dist/featureTesting/executionBootstrap.js +137 -29
  145. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  146. package/dist/featureTesting/featureMap.js +6 -3
  147. package/dist/featureTesting/featureMap.js.map +1 -1
  148. package/dist/featureTesting/featureScope.js +101 -20
  149. package/dist/featureTesting/featureScope.js.map +1 -1
  150. package/dist/featureTesting/mapFeatureScope.js +44 -15
  151. package/dist/featureTesting/mapFeatureScope.js.map +1 -1
  152. package/dist/featureTesting/objectiveModel.js +15 -11
  153. package/dist/featureTesting/objectiveModel.js.map +1 -1
  154. package/dist/featureTesting/resultMerge.js +16 -8
  155. package/dist/featureTesting/resultMerge.js.map +1 -1
  156. package/dist/featureTesting/sources.js +8 -5
  157. package/dist/featureTesting/sources.js.map +1 -1
  158. package/dist/featureTesting/suiteBridge.js +8 -2
  159. package/dist/featureTesting/suiteBridge.js.map +1 -1
  160. package/dist/featureTesting/synonyms.js +32 -4
  161. package/dist/featureTesting/synonyms.js.map +1 -1
  162. package/dist/featureTesting/testCaseFactory.js +27 -15
  163. package/dist/featureTesting/testCaseFactory.js.map +1 -1
  164. package/dist/featureTesting/testPlan.js +34 -12
  165. package/dist/featureTesting/testPlan.js.map +1 -1
  166. package/dist/firstRun/authStateMachine.js +1 -1
  167. package/dist/firstRun/authStateMachine.js.map +1 -1
  168. package/dist/firstRun/classifyScreen.js +9 -6
  169. package/dist/firstRun/classifyScreen.js.map +1 -1
  170. package/dist/firstRun/firstRunPlanner.js +16 -13
  171. package/dist/firstRun/firstRunPlanner.js.map +1 -1
  172. package/dist/firstRun/firstRunRunner.js +92 -31
  173. package/dist/firstRun/firstRunRunner.js.map +1 -1
  174. package/dist/firstRun/generatedDataPolicy.js +25 -15
  175. package/dist/firstRun/generatedDataPolicy.js.map +1 -1
  176. package/dist/firstRun/inputPlanner.js +45 -11
  177. package/dist/firstRun/inputPlanner.js.map +1 -1
  178. package/dist/firstRun/onboardingStateMachine.js +1 -1
  179. package/dist/firstRun/onboardingStateMachine.js.map +1 -1
  180. package/dist/firstRun/paywallPolicy.js +3 -3
  181. package/dist/firstRun/paywallPolicy.js.map +1 -1
  182. package/dist/firstRun/types.js +1 -1
  183. package/dist/firstRun/types.js.map +1 -1
  184. package/dist/fixtures/catalog.js +72 -9
  185. package/dist/fixtures/catalog.js.map +1 -1
  186. package/dist/fixtures/load.js +50 -0
  187. package/dist/fixtures/load.js.map +1 -0
  188. package/dist/flows/discover.js +2 -2
  189. package/dist/flows/discover.js.map +1 -1
  190. package/dist/flows/generate.js +68 -17
  191. package/dist/flows/generate.js.map +1 -1
  192. package/dist/flows/lint.js +88 -14
  193. package/dist/flows/lint.js.map +1 -1
  194. package/dist/flows/pack.js +21 -5
  195. package/dist/flows/pack.js.map +1 -1
  196. package/dist/flows/paths.js +57 -0
  197. package/dist/flows/paths.js.map +1 -0
  198. package/dist/flows/repair.js +176 -37
  199. package/dist/flows/repair.js.map +1 -1
  200. package/dist/flows/run.js +308 -99
  201. package/dist/flows/run.js.map +1 -1
  202. package/dist/flows/schema.js +107 -25
  203. package/dist/flows/schema.js.map +1 -1
  204. package/dist/flows/seedExec.js +8 -4
  205. package/dist/flows/seedExec.js.map +1 -1
  206. package/dist/flows/templates.js +5 -5
  207. package/dist/index.js +50 -8
  208. package/dist/index.js.map +1 -1
  209. package/dist/ios/signing.js +32 -3
  210. package/dist/ios/signing.js.map +1 -1
  211. package/dist/issues/classify.js +28 -20
  212. package/dist/issues/classify.js.map +1 -1
  213. package/dist/issues/fingerprint.js +35 -9
  214. package/dist/issues/fingerprint.js.map +1 -1
  215. package/dist/issues/index.js +93 -46
  216. package/dist/issues/index.js.map +1 -1
  217. package/dist/issues/metrics.js +6 -5
  218. package/dist/issues/metrics.js.map +1 -1
  219. package/dist/issues/recurrence.js +35 -8
  220. package/dist/issues/recurrence.js.map +1 -1
  221. package/dist/issues/report.js +12 -8
  222. package/dist/issues/report.js.map +1 -1
  223. package/dist/issues/reportBridge.js +40 -9
  224. package/dist/issues/reportBridge.js.map +1 -1
  225. package/dist/issues/schema.js +2 -2
  226. package/dist/issues/schema.js.map +1 -1
  227. package/dist/issues/sourceRevision.js +2 -2
  228. package/dist/issues/sourceRevision.js.map +1 -1
  229. package/dist/issues/store.js +110 -32
  230. package/dist/issues/store.js.map +1 -1
  231. package/dist/lib/abortScope.js +48 -0
  232. package/dist/lib/abortScope.js.map +1 -0
  233. package/dist/lib/android.js +84 -8
  234. package/dist/lib/android.js.map +1 -1
  235. package/dist/lib/coordSpace.js +1 -1
  236. package/dist/lib/coordSpace.js.map +1 -1
  237. package/dist/lib/device.js +53 -12
  238. package/dist/lib/device.js.map +1 -1
  239. package/dist/lib/gestures.js +141 -0
  240. package/dist/lib/gestures.js.map +1 -0
  241. package/dist/lib/gitignore.js +4 -3
  242. package/dist/lib/gitignore.js.map +1 -1
  243. package/dist/lib/image.js +12 -3
  244. package/dist/lib/image.js.map +1 -1
  245. package/dist/lib/lockfile.js +399 -0
  246. package/dist/lib/lockfile.js.map +1 -0
  247. package/dist/lib/needsInput.js +9 -6
  248. package/dist/lib/needsInput.js.map +1 -1
  249. package/dist/lib/png.js +4 -3
  250. package/dist/lib/png.js.map +1 -1
  251. package/dist/lib/redact.js +170 -9
  252. package/dist/lib/redact.js.map +1 -1
  253. package/dist/lib/result.js +94 -10
  254. package/dist/lib/result.js.map +1 -1
  255. package/dist/lib/schemaHash.js +7 -5
  256. package/dist/lib/schemaHash.js.map +1 -1
  257. package/dist/lib/sensitive.js +6 -3
  258. package/dist/lib/sensitive.js.map +1 -1
  259. package/dist/lib/simctl.js +3 -3
  260. package/dist/lib/simctl.js.map +1 -1
  261. package/dist/lib/spawn.js +78 -17
  262. package/dist/lib/spawn.js.map +1 -1
  263. package/dist/lib/toolAnnotations.js +114 -0
  264. package/dist/lib/toolAnnotations.js.map +1 -0
  265. package/dist/lib/wda.js +321 -25
  266. package/dist/lib/wda.js.map +1 -1
  267. package/dist/lib/wdaConfig.js +12 -5
  268. package/dist/lib/wdaConfig.js.map +1 -1
  269. package/dist/lib/wdaTune.js +52 -23
  270. package/dist/lib/wdaTune.js.map +1 -1
  271. package/dist/mobileAudit/checks.js +150 -28
  272. package/dist/mobileAudit/checks.js.map +1 -1
  273. package/dist/mobileAudit/evidence.js +4 -4
  274. package/dist/mobileAudit/evidence.js.map +1 -1
  275. package/dist/mobileAudit/profiles.js +121 -20
  276. package/dist/mobileAudit/profiles.js.map +1 -1
  277. package/dist/mobileAudit/results.js +9 -4
  278. package/dist/mobileAudit/results.js.map +1 -1
  279. package/dist/mobileAudit/runner.js +214 -35
  280. package/dist/mobileAudit/runner.js.map +1 -1
  281. package/dist/oracle/auth.js +1 -1
  282. package/dist/oracle/auth.js.map +1 -1
  283. package/dist/oracle/failures.js +1085 -145
  284. package/dist/oracle/failures.js.map +1 -1
  285. package/dist/oracle/health.js +148 -26
  286. package/dist/oracle/health.js.map +1 -1
  287. package/dist/oracle/locator.js +148 -19
  288. package/dist/oracle/locator.js.map +1 -1
  289. package/dist/oracle/record.js +3 -3
  290. package/dist/oracle/record.js.map +1 -1
  291. package/dist/orchestration/envelope.js +2 -2
  292. package/dist/orchestration/envelope.js.map +1 -1
  293. package/dist/orchestration/goal.js +7 -13
  294. package/dist/orchestration/goal.js.map +1 -1
  295. package/dist/orchestration/testThis/execute.js +167 -0
  296. package/dist/orchestration/testThis/execute.js.map +1 -0
  297. package/dist/orchestration/testThis/pipeline.js +413 -0
  298. package/dist/orchestration/testThis/pipeline.js.map +1 -0
  299. package/dist/orchestration/testThis/plan.js +519 -0
  300. package/dist/orchestration/testThis/plan.js.map +1 -0
  301. package/dist/orchestration/testThis/sessionIntent.js +72 -0
  302. package/dist/orchestration/testThis/sessionIntent.js.map +1 -0
  303. package/dist/orchestration/testThis/terminal.js +136 -0
  304. package/dist/orchestration/testThis/terminal.js.map +1 -0
  305. package/dist/orchestration/testThis/types.js +12 -0
  306. package/dist/orchestration/testThis/types.js.map +1 -0
  307. package/dist/plan/plan.js +9 -9
  308. package/dist/plan/plan.js.map +1 -1
  309. package/dist/prompts/index.js +24 -24
  310. package/dist/prompts/index.js.map +1 -1
  311. package/dist/report/coverage.js +0 -1
  312. package/dist/report/coverage.js.map +1 -1
  313. package/dist/report/evidence.js.map +1 -1
  314. package/dist/report/export.js +449 -72
  315. package/dist/report/export.js.map +1 -1
  316. package/dist/report/findingsDedupe.js +41 -0
  317. package/dist/report/findingsDedupe.js.map +1 -0
  318. package/dist/report/flake.js +13 -14
  319. package/dist/report/flake.js.map +1 -1
  320. package/dist/report/history.js +72 -38
  321. package/dist/report/history.js.map +1 -1
  322. package/dist/report/policy.js +18 -7
  323. package/dist/report/policy.js.map +1 -1
  324. package/dist/report/qaLevel.js +10 -16
  325. package/dist/report/qaLevel.js.map +1 -1
  326. package/dist/report/readiness.js +42 -12
  327. package/dist/report/readiness.js.map +1 -1
  328. package/dist/report/sarifSources.js +160 -0
  329. package/dist/report/sarifSources.js.map +1 -0
  330. package/dist/report/summary.js +2 -2
  331. package/dist/report/summary.js.map +1 -1
  332. package/dist/report/testCatalog.js +4 -4
  333. package/dist/report/testCatalog.js.map +1 -1
  334. package/dist/report/timing.js +23 -23
  335. package/dist/report/timing.js.map +1 -1
  336. package/dist/report/toolHealth.js +124 -0
  337. package/dist/report/toolHealth.js.map +1 -0
  338. package/dist/server.js +501 -69
  339. package/dist/server.js.map +1 -1
  340. package/dist/services/automationGenerate.js +8 -2
  341. package/dist/services/automationGenerate.js.map +1 -1
  342. package/dist/services/build.js +10 -4
  343. package/dist/services/build.js.map +1 -1
  344. package/dist/services/flowGenerate.js +75 -0
  345. package/dist/services/flowGenerate.js.map +1 -0
  346. package/dist/services/preflight.js +40 -8
  347. package/dist/services/preflight.js.map +1 -1
  348. package/dist/services/prepareAndroid.js +92 -18
  349. package/dist/services/prepareAndroid.js.map +1 -1
  350. package/dist/services/prepareIos.js +94 -17
  351. package/dist/services/prepareIos.js.map +1 -1
  352. package/dist/services/report.js +271 -80
  353. package/dist/services/report.js.map +1 -1
  354. package/dist/services/smoke.js +68 -22
  355. package/dist/services/smoke.js.map +1 -1
  356. package/dist/services/suiteGenerate.js +43 -6
  357. package/dist/services/suiteGenerate.js.map +1 -1
  358. package/dist/services/testSuiteKnowledge.js +78 -33
  359. package/dist/services/testSuiteKnowledge.js.map +1 -1
  360. package/dist/session/attach.js +284 -4
  361. package/dist/session/attach.js.map +1 -1
  362. package/dist/session/processRegistry.js +433 -0
  363. package/dist/session/processRegistry.js.map +1 -0
  364. package/dist/session/progress.js +1 -1
  365. package/dist/session/progress.js.map +1 -1
  366. package/dist/session/retention.js +201 -0
  367. package/dist/session/retention.js.map +1 -0
  368. package/dist/session/store.js +459 -59
  369. package/dist/session/store.js.map +1 -1
  370. package/dist/snapshot/overlays.js +60 -10
  371. package/dist/snapshot/overlays.js.map +1 -1
  372. package/dist/snapshot/parse.js +30 -12
  373. package/dist/snapshot/parse.js.map +1 -1
  374. package/dist/snapshot/present.js +36 -6
  375. package/dist/snapshot/present.js.map +1 -1
  376. package/dist/snapshot/settle.js +39 -9
  377. package/dist/snapshot/settle.js.map +1 -1
  378. package/dist/state/consent.js +99 -0
  379. package/dist/state/consent.js.map +1 -0
  380. package/dist/state/profile.js +82 -11
  381. package/dist/state/profile.js.map +1 -1
  382. package/dist/suite/compile.js +15 -6
  383. package/dist/suite/compile.js.map +1 -1
  384. package/dist/suite/lint.js +22 -4
  385. package/dist/suite/lint.js.map +1 -1
  386. package/dist/suite/pom.js +75 -44
  387. package/dist/suite/pom.js.map +1 -1
  388. package/dist/suite/secretGuard.js +266 -0
  389. package/dist/suite/secretGuard.js.map +1 -0
  390. package/dist/suite/testcase.js +15 -11
  391. package/dist/suite/testcase.js.map +1 -1
  392. package/dist/testSuite/exporter.js +7 -3
  393. package/dist/testSuite/exporter.js.map +1 -1
  394. package/dist/testSuite/generator.js +43 -14
  395. package/dist/testSuite/generator.js.map +1 -1
  396. package/dist/testSuite/history.js +1 -1
  397. package/dist/testSuite/history.js.map +1 -1
  398. package/dist/testSuite/issueLinks.js +3 -3
  399. package/dist/testSuite/issueLinks.js.map +1 -1
  400. package/dist/testSuite/lint.js +4 -4
  401. package/dist/testSuite/lint.js.map +1 -1
  402. package/dist/testSuite/merge.js +13 -7
  403. package/dist/testSuite/merge.js.map +1 -1
  404. package/dist/testSuite/schema.js +21 -16
  405. package/dist/testSuite/schema.js.map +1 -1
  406. package/dist/testSuite/store.js +12 -4
  407. package/dist/testSuite/store.js.map +1 -1
  408. package/dist/testSuite/traceability.js +2 -2
  409. package/dist/testSuite/traceability.js.map +1 -1
  410. package/dist/tools/act.js +868 -174
  411. package/dist/tools/act.js.map +1 -1
  412. package/dist/tools/agent.js +521 -112
  413. package/dist/tools/agent.js.map +1 -1
  414. package/dist/tools/appControl.js +82 -19
  415. package/dist/tools/appControl.js.map +1 -1
  416. package/dist/tools/appMap.js +393 -189
  417. package/dist/tools/appMap.js.map +1 -1
  418. package/dist/tools/build.js +92 -51
  419. package/dist/tools/build.js.map +1 -1
  420. package/dist/tools/bundletool.js +141 -40
  421. package/dist/tools/bundletool.js.map +1 -1
  422. package/dist/tools/clearOverlay.js +98 -18
  423. package/dist/tools/clearOverlay.js.map +1 -1
  424. package/dist/tools/device.js +108 -19
  425. package/dist/tools/device.js.map +1 -1
  426. package/dist/tools/doctor.js +98 -35
  427. package/dist/tools/doctor.js.map +1 -1
  428. package/dist/tools/explore.js +110 -42
  429. package/dist/tools/explore.js.map +1 -1
  430. package/dist/tools/featureTesting.js +152 -130
  431. package/dist/tools/featureTesting.js.map +1 -1
  432. package/dist/tools/firstRun.js +94 -66
  433. package/dist/tools/firstRun.js.map +1 -1
  434. package/dist/tools/flow.js +347 -132
  435. package/dist/tools/flow.js.map +1 -1
  436. package/dist/tools/flowRepair.js +42 -10
  437. package/dist/tools/flowRepair.js.map +1 -1
  438. package/dist/tools/generate.js +199 -0
  439. package/dist/tools/generate.js.map +1 -0
  440. package/dist/tools/getArtifact.js +37 -8
  441. package/dist/tools/getArtifact.js.map +1 -1
  442. package/dist/tools/health.js +17 -12
  443. package/dist/tools/health.js.map +1 -1
  444. package/dist/tools/ios.js +148 -194
  445. package/dist/tools/ios.js.map +1 -1
  446. package/dist/tools/issues.js +256 -171
  447. package/dist/tools/issues.js.map +1 -1
  448. package/dist/tools/jobs.js +55 -11
  449. package/dist/tools/jobs.js.map +1 -1
  450. package/dist/tools/metro.js +84 -38
  451. package/dist/tools/metro.js.map +1 -1
  452. package/dist/tools/mobileAudit.js +83 -26
  453. package/dist/tools/mobileAudit.js.map +1 -1
  454. package/dist/tools/network.js +36 -14
  455. package/dist/tools/network.js.map +1 -1
  456. package/dist/tools/note.js +31 -17
  457. package/dist/tools/note.js.map +1 -1
  458. package/dist/tools/prepareIosTarget.js +31 -14
  459. package/dist/tools/prepareIosTarget.js.map +1 -1
  460. package/dist/tools/prepareTarget.js +193 -47
  461. package/dist/tools/prepareTarget.js.map +1 -1
  462. package/dist/tools/report.js +16 -8
  463. package/dist/tools/report.js.map +1 -1
  464. package/dist/tools/resolveArtifact.js +12 -12
  465. package/dist/tools/resolveArtifact.js.map +1 -1
  466. package/dist/tools/resolveTarget.js +81 -18
  467. package/dist/tools/resolveTarget.js.map +1 -1
  468. package/dist/tools/screenRecord.js +85 -20
  469. package/dist/tools/screenRecord.js.map +1 -1
  470. package/dist/tools/screenshot.js +42 -18
  471. package/dist/tools/screenshot.js.map +1 -1
  472. package/dist/tools/smoke.js +29 -12
  473. package/dist/tools/smoke.js.map +1 -1
  474. package/dist/tools/snapshot.js +157 -107
  475. package/dist/tools/snapshot.js.map +1 -1
  476. package/dist/tools/startSession.js +117 -84
  477. package/dist/tools/startSession.js.map +1 -1
  478. package/dist/tools/suite.js +391 -310
  479. package/dist/tools/suite.js.map +1 -1
  480. package/dist/tools/testSuite.js +112 -36
  481. package/dist/tools/testSuite.js.map +1 -1
  482. package/dist/tools/testThis.js +35 -802
  483. package/dist/tools/testThis.js.map +1 -1
  484. package/dist/tools/visual.js +698 -116
  485. package/dist/tools/visual.js.map +1 -1
  486. package/dist/tools/wait.js +9 -23
  487. package/dist/tools/wait.js.map +1 -1
  488. package/dist/tools/wda.js +312 -54
  489. package/dist/tools/wda.js.map +1 -1
  490. package/dist/version.js +37 -64
  491. package/dist/version.js.map +1 -1
  492. package/dist/visual/ocr.js +65 -8
  493. package/dist/visual/ocr.js.map +1 -1
  494. package/dist/visual/provider.js +87 -15
  495. package/dist/visual/provider.js.map +1 -1
  496. package/docs/README.md +24 -10
  497. package/docs/ci-reports.md +328 -0
  498. package/docs/concepts.md +227 -0
  499. package/docs/flows.md +153 -0
  500. package/docs/mcp-server.md +210 -104
  501. package/docs/physical-devices.md +84 -0
  502. package/docs/tools.md +808 -217
  503. package/package.json +16 -6
  504. package/dist/appMap/diff.js +0 -60
  505. package/dist/appMap/diff.js.map +0 -1
  506. package/dist/automation/maestroIr.js +0 -282
  507. package/dist/automation/maestroIr.js.map +0 -1
  508. package/dist/interop/maestro.js +0 -151
  509. package/dist/interop/maestro.js.map +0 -1
  510. package/dist/tools/assertVisual.js +0 -74
  511. package/dist/tools/assertVisual.js.map +0 -1
  512. package/dist/tools/automationGenerate.js +0 -394
  513. package/dist/tools/automationGenerate.js.map +0 -1
  514. package/dist/tools/capabilities.js +0 -209
  515. package/dist/tools/capabilities.js.map +0 -1
  516. package/dist/tools/detectContext.js +0 -41
  517. package/dist/tools/detectContext.js.map +0 -1
  518. package/dist/tools/flowGenerate.js +0 -55
  519. package/dist/tools/flowGenerate.js.map +0 -1
  520. package/dist/tools/history.js +0 -56
  521. package/dist/tools/history.js.map +0 -1
  522. package/dist/tools/idling.js +0 -104
  523. package/dist/tools/idling.js.map +0 -1
  524. package/dist/tools/inputCapabilities.js +0 -30
  525. package/dist/tools/inputCapabilities.js.map +0 -1
  526. package/dist/tools/locator.js +0 -57
  527. package/dist/tools/locator.js.map +0 -1
  528. package/dist/tools/maestro.js +0 -78
  529. package/dist/tools/maestro.js.map +0 -1
  530. package/dist/tools/permissions.js +0 -149
  531. package/dist/tools/permissions.js.map +0 -1
  532. package/dist/tools/plan.js +0 -49
  533. package/dist/tools/plan.js.map +0 -1
  534. package/dist/tools/screenInfo.js +0 -70
  535. package/dist/tools/screenInfo.js.map +0 -1
  536. package/dist/tools/seed.js +0 -101
  537. package/dist/tools/seed.js.map +0 -1
  538. package/dist/tools/state.js +0 -170
  539. package/dist/tools/state.js.map +0 -1
  540. package/dist/tools/visualText.js +0 -61
  541. package/dist/tools/visualText.js.map +0 -1
@@ -1,15 +1,20 @@
1
- // qa_flow_check + qa_flow_run (PHASE3-PLAN §3.4) — turn exploration into repeatable QA.
1
+ // qa_flow_check + qa_flow_run: turn exploration into repeatable QA.
2
2
  // Flows live as .swipium/flows/*.yaml. check = parse + static validation (no device).
3
- // run = the orchestrator (src/flows/run.ts), reporting the exact failing step + evidence.
3
+ // run (mode:"run", default) = the orchestrator (src/flows/run.ts), reporting the exact failing
4
+ // step + evidence. run (mode:"plan", 1.5.0 consolidation) = read-only execution preview:
5
+ // compile the flow against backend capabilities without touching a device.
4
6
  import { z } from 'zod';
5
7
  import { createHash } from 'node:crypto';
6
8
  import { existsSync, readFileSync } from 'node:fs';
7
9
  import { join, isAbsolute } from 'node:path';
8
- import { qaOk, qaError, qaStop } from '../lib/result.js';
9
- import { parseFlow } from '../flows/schema.js';
10
+ import { qaOk, qaError, qaStop, qaAnnotate, unknownSessionError } from '../lib/result.js';
11
+ import { FLOW_ENV_PREFIX, isMutatingFlowStep, lookupFlowVar, parseFlow, SECRET_VAR_NAME, } from '../flows/schema.js';
12
+ import { seedExactCommand } from '../flows/seedExec.js';
13
+ import { withinRootOrNull } from '../flows/paths.js';
14
+ import { makeRedactor } from '../lib/redact.js';
10
15
  import { lintFlowObjectWithOptions } from '../flows/lint.js';
11
16
  import { runFlow } from '../flows/run.js';
12
- import { getDriver } from '../session/attach.js';
17
+ import { blockedDeviceResult, getDriver } from '../session/attach.js';
13
18
  import { loadPolicy } from '../report/policy.js';
14
19
  import { classifyFlakeResults } from '../report/flake.js';
15
20
  import { validateCiMutationPolicy, validateCiVariables } from '../ci/preflight.js';
@@ -21,14 +26,15 @@ import { resolveMaskProvider, resolveVisualProvider } from '../visual/provider.j
21
26
  import { backendCapabilities, backendForDriverKind } from '../automation/capabilities.js';
22
27
  import { compileAutomationPlan } from '../automation/plan.js';
23
28
  import { buildReadiness } from '../automation/report.js';
29
+ import { resolveProjectRoot } from '../context/projectRoot.js';
24
30
  const ALL_BACKENDS = ['android-direct', 'ios-raw-simulator', 'ios-wda', 'appium-uiautomator2', 'appium-xcuitest'];
25
31
  const STATUS_ICON = { ready: '✅', candidate: '🟡', blocked: '⛔' };
26
32
  /** Resolve a flow's YAML from explicit text, an absolute/relative path, or a name under .swipium/flows. */
27
- function loadFlowSource(root, flow, flowYaml) {
33
+ export function loadFlowSource(root, flow, flowYaml) {
28
34
  if (flowYaml && flowYaml.trim())
29
35
  return { yamlText: flowYaml, source: 'inline' };
30
36
  if (!flow)
31
- return { error: 'Provide a flow name, a path, or flowYaml.' };
37
+ return { error: 'Provide a flow name, a path, or flowYaml.', errorCode: 'INVALID_ARGUMENT' };
32
38
  const candidates = [];
33
39
  if (isAbsolute(flow))
34
40
  candidates.push(flow);
@@ -43,23 +49,52 @@ function loadFlowSource(root, flow, flowYaml) {
43
49
  return { yamlText: readFileSync(p, 'utf8'), source: p };
44
50
  }
45
51
  catch (e) {
46
- return { error: `Could not read ${p}: ${String(e)}` };
52
+ return { error: `Could not read ${p}: ${String(e)}`, errorCode: 'INVALID_FLOW' };
47
53
  }
48
54
  }
49
55
  }
50
- return { error: `Flow not found. Looked for: ${candidates.join(', ') || '(no project root)'}` };
56
+ return {
57
+ error: `Flow not found. Looked for: ${candidates.join(', ') || '(no project root; pass projectRoot or set SWIPIUM_PROJECT_ROOT)'}`,
58
+ errorCode: 'FLOW_NOT_FOUND',
59
+ };
60
+ }
61
+ /**
62
+ * The project root a flow tool reads from: explicit projectRoot arg > the session's root >
63
+ * resolveProjectRoot (MCP roots > SWIPIUM_PROJECT_ROOT/CLAUDE_PROJECT_DIR > cwd marker), like
64
+ * every other root-aware tool. Undefined only when nothing resolves (inline flowYaml still works).
65
+ */
66
+ async function flowRoot(server, sessionRoot, projectRoot) {
67
+ if (!projectRoot && sessionRoot)
68
+ return sessionRoot;
69
+ const resolved = await resolveProjectRoot(server, projectRoot);
70
+ return resolved.root ?? sessionRoot;
71
+ }
72
+ function flowSourceError(src) {
73
+ return qaError({
74
+ what: src.error ?? 'Flow could not be loaded',
75
+ changedState: false,
76
+ retrySafe: true,
77
+ failureCode: src.errorCode ?? 'FLOW_NOT_FOUND',
78
+ nextSteps: [
79
+ src.errorCode === 'INVALID_ARGUMENT'
80
+ ? 'Pass flow="<name>" (under .swipium/flows), a .yaml path, or inline flowYaml.'
81
+ : 'Pass an existing flow name/path or inline flowYaml; pass projectRoot (or set SWIPIUM_PROJECT_ROOT) if the flow lives in another project.',
82
+ ],
83
+ });
51
84
  }
52
- const MUTATING_FLOW_STEPS = new Set(['networkOffline', 'networkOnline', 'seed', 'restartApp']);
53
- const EXTERNAL_PROVIDER_FLOW_STEPS = new Set(['tapOcrText', 'assertOcrText']);
54
85
  function stable(value) {
55
86
  if (Array.isArray(value))
56
87
  return value.map(stable);
57
88
  if (value && typeof value === 'object')
58
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => [k, stable(v)]));
89
+ return Object.fromEntries(Object.entries(value)
90
+ .sort(([a], [b]) => a.localeCompare(b))
91
+ .map(([k, v]) => [k, stable(v)]));
59
92
  return value;
60
93
  }
61
94
  function fullHash(value) {
62
- return createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
95
+ return createHash('sha256')
96
+ .update(JSON.stringify(stable(value)))
97
+ .digest('hex');
63
98
  }
64
99
  function hash(value) {
65
100
  return fullHash(value).slice(0, 16);
@@ -67,15 +102,41 @@ function hash(value) {
67
102
  function allFlowSteps(flow) {
68
103
  return [...flow.setup, ...flow.steps, ...flow.teardown];
69
104
  }
70
- function flowMutationAffects(flow, session, source, repeat, externalProviders = []) {
105
+ /** Where a seed's command came from, stated in consent: fixtures are loaded from the repo's
106
+ * .swipium/fixtures.json (or qa_start_session fixtures). Swipium never reviewed them. */
107
+ const SEED_ORIGIN = 'declared fixture (.swipium/fixtures.json or qa_start_session fixtures), repo-supplied, UNREVIEWED';
108
+ /** `${VAR}` URL preview for consent: the URL as it will be opened, with credential-like variable
109
+ * values and registered session secrets masked, plus where each variable is read from. */
110
+ function openUrlPreview(url, vars, secrets) {
111
+ const variables = [];
112
+ const resolved = url.replace(/\$\{([^}]+)\}/g, (_, name) => {
113
+ const v = lookupFlowVar(name, vars);
114
+ variables.push({ name, source: v == null ? 'missing' : vars[name] != null ? 'variables' : 'env' });
115
+ if (v == null)
116
+ return `\${${name}}`;
117
+ return SECRET_VAR_NAME.test(name) ? `«${name}»` : v;
118
+ });
119
+ return { url, resolvedUrl: makeRedactor(secrets)(resolved) ?? resolved, variables };
120
+ }
121
+ function flowMutationAffects(flow, session, source, repeat, externalProviders = [], vars = {}) {
71
122
  const mutations = allFlowSteps(flow)
72
123
  .map((step, index) => {
73
- if (!MUTATING_FLOW_STEPS.has(step.kind))
124
+ if (!isMutatingFlowStep(step))
74
125
  return null;
75
126
  if (step.kind === 'seed') {
76
- const seedType = session.fixtures.find((f) => f.name === step.fixture)?.seed?.type ?? 'unknown';
77
- return { step: index + 1, kind: step.kind, fixture: step.fixture, seedType };
127
+ const seed = session.fixtures.find((f) => f.name === step.fixture)?.seed;
128
+ return {
129
+ step: index + 1,
130
+ kind: step.kind,
131
+ fixture: step.fixture,
132
+ seedType: seed?.type ?? 'unknown',
133
+ // The EXACT argv / URL the seed will run. Consent must show what executes.
134
+ command: seed ? seedExactCommand(seed) : null,
135
+ origin: SEED_ORIGIN,
136
+ };
78
137
  }
138
+ if (step.kind === 'openUrl')
139
+ return { step: index + 1, kind: step.kind, ...openUrlPreview(step.url, vars, session.secrets ?? []) };
79
140
  return { step: index + 1, kind: step.kind };
80
141
  })
81
142
  .filter((x) => !!x);
@@ -89,10 +150,21 @@ function flowMutationAffects(flow, session, source, repeat, externalProviders =
89
150
  externalProviders,
90
151
  };
91
152
  }
153
+ /** One line per mutating step for the consent's exactCommand (seeds show their argv/URL). */
154
+ function mutationCommandLines(affects) {
155
+ return affects.mutations.map((m) => {
156
+ if (m.kind === 'seed' && 'fixture' in m)
157
+ return `${m.step}:seed ${m.fixture} [${m.seedType}] > ${m.command ?? '(no seed spec, step will fail)'} (repo-supplied, unreviewed)`;
158
+ if (m.kind === 'openUrl' && 'resolvedUrl' in m)
159
+ return `${m.step}:openUrl ${m.resolvedUrl} (variables: ${m.variables.map((v) => `${v.name}←${v.source}`).join(', ')})`;
160
+ return `${m.step}:${m.kind}`;
161
+ });
162
+ }
92
163
  function flowMutationRisk(affects) {
93
164
  if (affects.mutations.some((m) => m.kind === 'seed' && m.seedType === 'script'))
94
165
  return 'high';
95
- if (affects.externalProviders.length || affects.mutations.some((m) => m.kind === 'networkOffline' || m.kind === 'networkOnline' || m.kind === 'seed'))
166
+ if (affects.externalProviders.length ||
167
+ affects.mutations.some((m) => m.kind === 'networkOffline' || m.kind === 'networkOnline' || m.kind === 'seed' || m.kind === 'openUrl'))
96
168
  return 'medium';
97
169
  return 'low';
98
170
  }
@@ -105,35 +177,58 @@ function ocrSteps(flow) {
105
177
  });
106
178
  return out;
107
179
  }
180
+ const LOCATOR_FAILURES = new Set(['ELEMENT_NOT_FOUND', 'AMBIGUOUS_SELECTOR', 'STALE_REF', 'INVALID_SELECTOR', 'ASSERTION_FAILED']);
181
+ /** nextSteps for a failed qa_flow_run: point at qa_flow_repair for the failing step. */
182
+ export function failedFlowNextSteps(name, flowArg, failedAtStep, failureCode) {
183
+ const out = [];
184
+ // A missing variable is not locator drift: qa_flow_repair cannot fix it (and a cancelled run is
185
+ // not a failure to repair).
186
+ if (failedAtStep != null && failureCode !== 'MISSING_FIXTURE' && failureCode !== 'CANCELLED') {
187
+ const target = flowArg ? `flow:"${flowArg}"` : 'flowYaml:<same YAML>';
188
+ out.push(`${failureCode && !LOCATOR_FAILURES.has(failureCode) ? 'If this is locator drift (renamed/moved control), ' : ''}call qa_flow_repair { ${target}, failedStep:${failedAtStep} } for a reviewable locator fix (review before apply).`);
189
+ }
190
+ if (failureCode === 'MISSING_FIXTURE')
191
+ out.push(`Pass missing values via qa_flow_run { variables } (flows read process.env only for ${FLOW_ENV_PREFIX}* names).`);
192
+ out.push(`If the app itself is wrong, record it with qa_note { workflow:"${name}", outcome:"fail" } with the step evidence.`);
193
+ return out;
194
+ }
108
195
  export function registerFlow(server, sessions) {
109
196
  server.registerTool('qa_flow_check', {
110
197
  title: 'Check a flow',
111
- description: 'Parse and statically validate a Swipium flow (a .swipium/flows/*.yaml authored as name + steps) WITHOUT running it. Reports syntax/schema errors with the offending step, plus warnings (e.g. selector resolvability is only known at run time). Provide `flow` (a name under .swipium/flows, or a path) or `flowYaml` (inline). A sessionId lets it resolve names against that session\'s project root.',
198
+ description: 'Statically validate a flow (.swipium/flows/*.yaml: name + steps) without running it: syntax/schema errors with the offending ' +
199
+ 'step, plus warnings. Pass flow (name or path) or flowYaml. For an execution preview per backend use qa_flow_run mode:"plan".',
112
200
  inputSchema: {
113
201
  sessionId: z.string().optional(),
202
+ projectRoot: z.string().optional().describe('Absolute app root (default: session root > MCP roots > env > cwd).'),
114
203
  flow: z.string().optional().describe('Flow name under .swipium/flows, or a path to a .yaml file.'),
115
204
  flowYaml: z.string().optional().describe('Inline flow YAML (instead of a file).'),
116
- platform: z.enum(['android', 'ios', 'cross-platform']).optional().describe('Optional authoring target for platform-aware warnings.'),
117
- ci: z.boolean().optional().describe('When true, add CI preflight warnings such as missing variables and mutating steps not allowed by policy.'),
205
+ platform: z.enum(['android', 'ios', 'cross-platform']).optional().describe('Authoring target for platform-aware warnings.'),
206
+ ci: z.boolean().optional().describe('Add CI preflight warnings (missing variables, mutating steps policy forbids).'),
118
207
  },
119
- }, async ({ sessionId, flow, flowYaml, platform, ci }) => {
120
- const root = sessionId ? sessions.get(sessionId)?.root : undefined;
208
+ }, async ({ sessionId, projectRoot, flow, flowYaml, platform, ci }) => {
209
+ const root = await flowRoot(server, sessionId ? sessions.get(sessionId)?.root : undefined, projectRoot);
121
210
  const src = loadFlowSource(root, flow, flowYaml);
122
211
  if (src.error)
123
- return qaError({ what: src.error, changedState: false, retrySafe: true, nextSteps: ['Pass an existing flow name/path or inline flowYaml.'] });
212
+ return flowSourceError(src);
124
213
  const { flow: parsed, errors } = parseFlow(src.yamlText);
125
214
  if (errors.length || !parsed) {
126
- return qaError({ what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'})`, changedState: false, retrySafe: true, nextSteps: ['Fix the listed errors and re-check.'] }, { source: src.source, errors });
215
+ return qaError({
216
+ what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'})`,
217
+ changedState: false,
218
+ retrySafe: true,
219
+ failureCode: 'INVALID_FLOW',
220
+ nextSteps: ['Fix the listed errors and re-check.'],
221
+ }, { source: src.source, errors });
127
222
  }
128
223
  const allSteps = [...parsed.setup, ...parsed.steps, ...parsed.teardown];
129
224
  const STRUCTURED_KINDS = new Set(['tap', 'assertVisible', 'assertNotVisible', 'scrollTo', 'waitForVisible', 'inputText']);
130
225
  const warnings = [];
131
226
  if (!parsed.appId)
132
- warnings.push('No appId — a prepareTarget step will rely on the session\'s prepared appId.');
227
+ warnings.push("No appId, so a prepareTarget step will rely on the session's prepared appId.");
133
228
  if (allSteps.some((s) => s.kind === 'tap' && s.selector.startsWith('@')))
134
- warnings.push('Uses @ref selectors — refs are run-time only; prefer text/id selectors for durable flows.');
229
+ warnings.push('Uses @ref selectors. Refs are run-time only; prefer text/id selectors for durable flows.');
135
230
  if (src.yamlText.includes('${'))
136
- warnings.push('Uses ${VARIABLES} — provide them via qa_flow_run { variables } or the environment.');
231
+ warnings.push(`Uses \${VARIABLES}. Provide them via qa_flow_run { variables }, stored session inputs, or ${FLOW_ENV_PREFIX}* environment variables (other env names are never read).`);
137
232
  const flowOcrSteps = ocrSteps(parsed);
138
233
  if (flowOcrSteps.length) {
139
234
  warnings.push(`Uses OCR visual-provider steps (${flowOcrSteps.map((s) => `${s.step}:${s.kind}`).join(', ')}); qa_flow_run will require provider consent and a configured ocrCommand.`);
@@ -145,20 +240,27 @@ export function registerFlow(server, sessions) {
145
240
  const inferredPlatform = platform ?? (driverKind === 'simulator' || driverKind === 'wda' ? 'ios' : driverKind === 'direct' ? 'android' : undefined);
146
241
  const usesStructured = allSteps.some((s) => STRUCTURED_KINDS.has(s.kind));
147
242
  if (driverKind === 'simulator' && parsed.mode === 'structured') {
148
- warnings.push('mode:structured on the iOS simulator backend — tap/assertVisible/inputText need a UI tree (unavailable). Use mode: visual with image/visual steps.');
243
+ warnings.push('mode:structured on the iOS simulator backend. tap/assertVisible/inputText need a UI tree (unavailable). Use mode: visual with image/visual steps.');
149
244
  }
150
245
  if (parsed.mode === 'visual' && usesStructured) {
151
- warnings.push('mode:visual but the flow uses structured steps (tap/assertVisible/…) — those need a UI tree and will fail on a visual-only screen.');
246
+ warnings.push('mode:visual but the flow uses structured steps (tap/assertVisible/…). Those need a UI tree and will fail on a visual-only screen.');
152
247
  }
153
248
  for (const s of allSteps) {
154
- const text = s.kind === 'tap' ? s.selector : s.kind === 'inputText' ? s.into ?? s.value : 'query' in s && typeof s.query === 'string' ? s.query : '';
249
+ const text = s.kind === 'tap'
250
+ ? s.selector
251
+ : s.kind === 'inputText'
252
+ ? (s.into ?? s.value)
253
+ : 'query' in s && typeof s.query === 'string'
254
+ ? s.query
255
+ : '';
155
256
  if (!text)
156
257
  continue;
157
258
  if (inferredPlatform === 'ios' && /resource-id=|uiautomator|android\./i.test(text))
158
259
  warnings.push(`Android-specific locator in iOS flow: "${text}". Prefer accessibilityIdentifier/name/label on iOS.`);
159
260
  if (inferredPlatform === 'android' && /class chain|predicate string|XCUIElementType|accessibility id=/i.test(text))
160
261
  warnings.push(`iOS-specific locator in Android flow: "${text}". Prefer resource-id/content-desc/text on Android.`);
161
- if (inferredPlatform === 'cross-platform' && /(resource-id=|uiautomator|android\.|accessibility id=|class chain|predicate string|XCUIElementType|xpath=|^\/\/)/i.test(text)) {
262
+ if (inferredPlatform === 'cross-platform' &&
263
+ /(resource-id=|uiautomator|android\.|accessibility id=|class chain|predicate string|XCUIElementType|xpath=|^\/\/)/i.test(text)) {
162
264
  warnings.push(`Platform-specific or brittle locator in cross-platform flow: "${text}". Prefer shared text/testID/accessibility labels.`);
163
265
  }
164
266
  }
@@ -171,7 +273,10 @@ export function registerFlow(server, sessions) {
171
273
  for (const v of missingVars)
172
274
  warnings.push(`CI variable: ${v.variable} is required by ${v.flow} step ${v.step} but is not set in the environment.`);
173
275
  }
174
- const lintFindings = lintFlowObjectWithOptions(src.source ?? 'inline', parsed, { platform, policy: ci ? (root ? loadPolicy(root) : null) : undefined });
276
+ const lintFindings = lintFlowObjectWithOptions(src.source ?? 'inline', parsed, {
277
+ platform,
278
+ policy: ci ? (root ? loadPolicy(root) : null) : undefined,
279
+ });
175
280
  for (const f of lintFindings) {
176
281
  const prefix = f.severity === 'error' ? 'Lint error' : 'Lint warning';
177
282
  warnings.push(`${prefix} ${f.code}${f.step ? ` step ${f.step}` : ''}: ${f.message}`);
@@ -179,23 +284,57 @@ export function registerFlow(server, sessions) {
179
284
  // Image-template steps: verify the referenced files exist (best-effort, when a root is known).
180
285
  if (root) {
181
286
  for (const s of allSteps) {
182
- if ((s.kind === 'tapImage' || s.kind === 'assertImage')) {
183
- const p = isAbsolute(s.template) ? s.template : join(root, s.template);
184
- if (!existsSync(p))
287
+ if (s.kind === 'tapImage' || s.kind === 'assertImage') {
288
+ const p = withinRootOrNull(root, s.template);
289
+ if (!p)
290
+ warnings.push(`image template outside the project root: ${s.template}; qa_flow_run will refuse it.`);
291
+ else if (!existsSync(p))
185
292
  warnings.push(`image template not found: ${s.template} (resolve relative to the project root).`);
186
293
  }
187
294
  }
188
295
  }
189
- return qaOk({ valid: true, source: src.source, name: parsed.name, appId: parsed.appId ?? null, mode: parsed.mode, budgetProfile: parsed.budgetProfile ?? null, stepCount: parsed.steps.length, setupCount: parsed.setup.length, teardownCount: parsed.teardown.length, fixtures: parsed.fixtures, warnings, lintFindings }, `✅ flow "${parsed.name}" is valid — ${parsed.steps.length} steps (mode=${parsed.mode}${parsed.setup.length ? `, ${parsed.setup.length} setup` : ''}${parsed.teardown.length ? `, ${parsed.teardown.length} teardown` : ''})${warnings.length ? `\nwarnings:\n - ${warnings.join('\n - ')}` : ''}`);
296
+ return qaOk({
297
+ valid: true,
298
+ source: src.source,
299
+ name: parsed.name,
300
+ appId: parsed.appId ?? null,
301
+ mode: parsed.mode,
302
+ budgetProfile: parsed.budgetProfile ?? null,
303
+ stepCount: parsed.steps.length,
304
+ setupCount: parsed.setup.length,
305
+ teardownCount: parsed.teardown.length,
306
+ fixtures: parsed.fixtures,
307
+ warnings,
308
+ lintFindings,
309
+ }, `✅ flow "${parsed.name}" is valid: ${parsed.steps.length} steps (mode=${parsed.mode}${parsed.setup.length ? `, ${parsed.setup.length} setup` : ''}${parsed.teardown.length ? `, ${parsed.teardown.length} teardown` : ''})${warnings.length ? `\nwarnings:\n - ${warnings.join('\n - ')}` : ''}`);
190
310
  });
191
- server.registerTool('qa_flow_plan', {
192
- title: 'Plan a flow for a backend',
193
- description: 'Compile a Swipium flow into a deterministic, backend-specific automation plan WITHOUT touching a device (Automation Kernel V2). For each backend it answers: can this flow run, which exact selectors/gestures/waits will it use, and what to fix if it cannot. Each step is classified native | supported_with_fallback | visual_only | unsupported, with the exact missing capability and a concrete next step named for anything blocked. Returns structured data plus a natural-language message a coding agent can repeat to the user (ready | candidate | blocked). Provide `flow` (name/path) or `flowYaml`. Without `backend`, it plans across Android DirectDriver, iOS raw simulator, iOS WDA, Appium UiAutomator2, and Appium XCUITest; with a sessionId it also reports the attached backend.',
311
+ server.registerTool('qa_flow_run', {
312
+ title: 'Run a flow (or preview its execution plan)',
313
+ description: 'Run a flow on the prepared app (mode:"run", default) or preview it without a device (mode:"plan"). run executes steps ' +
314
+ 'server-side with setup/teardown, fail-fast, no auto-retry of mutating steps (consent-gated, like OCR steps); a failure ' +
315
+ 'returns the step, screenshot, failureCode, and health. plan reports, per backend (Android, iOS simulator, iOS WDA, ' +
316
+ 'Appium), whether each step is native / fallback / visual_only / unsupported. Pass flow (name/path) or flowYaml, plus ' +
317
+ 'variables for ${VAR}. Steps: docs/tools.md#flow-steps.',
194
318
  inputSchema: {
195
- sessionId: z.string().optional(),
196
- flow: z.string().optional().describe('Flow name under .swipium/flows, or a path to a .yaml file.'),
197
- flowYaml: z.string().optional().describe('Inline flow YAML (instead of a file).'),
198
- backend: z.enum(['android-direct', 'ios-raw-simulator', 'ios-wda', 'appium-uiautomator2', 'appium-xcuitest']).optional().describe('Plan only for this backend. Omit to plan across all five (the pre-run "can this run on Android and iOS?" answer).'),
319
+ mode: z.enum(['plan', 'run']).optional(),
320
+ sessionId: z.string().optional().describe('Required for run; plan: resolves names + marks the attached backend.'),
321
+ projectRoot: z
322
+ .string()
323
+ .optional()
324
+ .describe('plan: absolute app root for flow names (default: session root > MCP roots > env > cwd).'),
325
+ flow: z.string().optional().describe('Flow name under .swipium/flows, or a .yaml path.'),
326
+ flowYaml: z.string().optional().describe('Inline flow YAML.'),
327
+ variables: z
328
+ .record(z.string())
329
+ .optional()
330
+ .describe('run: ${VAR} values (win over stored session inputs, then SWIPIUM_* env only); credential-like names redacted.'),
331
+ repeat: z.number().int().min(1).max(10).optional().describe('run: repeat N times for flake classification (default 1).'),
332
+ consentId: z.string().optional().describe('run: consent for mutating / OCR steps.'),
333
+ approve: z.boolean().optional(),
334
+ backend: z
335
+ .enum(['android-direct', 'ios-raw-simulator', 'ios-wda', 'appium-uiautomator2', 'appium-xcuitest'])
336
+ .optional()
337
+ .describe('plan: only this backend (default all five).'),
199
338
  appium: z
200
339
  .object({
201
340
  automationName: z.string().optional(),
@@ -203,113 +342,153 @@ export function registerFlow(server, sessions) {
203
342
  webviewContextsAvailable: z.boolean().optional(),
204
343
  })
205
344
  .optional()
206
- .describe('Appium session hints that refine the Appium backend capability picture (driver family, WebView contexts).'),
345
+ .describe('plan: Appium session hints.'),
207
346
  },
208
- }, async ({ sessionId, flow, flowYaml, backend, appium }) => {
209
- const session = sessionId ? sessions.get(sessionId) : undefined;
210
- const root = session?.root;
211
- const src = loadFlowSource(root, flow, flowYaml);
212
- if (src.error)
213
- return qaError({ what: src.error, changedState: false, retrySafe: true, nextSteps: ['Pass an existing flow name/path or inline flowYaml.'] });
214
- const { flow: parsed, errors } = parseFlow(src.yamlText);
215
- if (errors.length || !parsed) {
216
- return qaError({ what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'}) — run qa_flow_check`, changedState: false, retrySafe: true, nextSteps: ['Fix the listed errors and re-plan.'] }, { source: src.source, errors });
347
+ }, async ({ mode, sessionId, projectRoot, flow, flowYaml, variables, repeat, consentId, approve, backend, appium }) => {
348
+ const effectiveMode = mode ?? 'run';
349
+ const notes = [];
350
+ // ---- mode:"plan": read-only execution preview (merged twin, 1.5.0). ----
351
+ if (effectiveMode === 'plan') {
352
+ const ignored = [
353
+ variables !== undefined && 'variables',
354
+ repeat !== undefined && 'repeat',
355
+ consentId !== undefined && 'consentId',
356
+ approve !== undefined && 'approve',
357
+ ].filter((x) => !!x);
358
+ if (ignored.length)
359
+ notes.push(`ignored parameter(s) not applicable to mode:"plan": ${ignored.join(', ')}; re-run with mode:"run" to execute`);
360
+ const session = sessionId ? sessions.get(sessionId) : undefined;
361
+ const root = await flowRoot(server, session?.root, projectRoot);
362
+ const src = loadFlowSource(root, flow, flowYaml);
363
+ if (src.error)
364
+ return qaAnnotate(flowSourceError(src), notes);
365
+ const { flow: parsed, errors } = parseFlow(src.yamlText);
366
+ if (errors.length || !parsed) {
367
+ return qaAnnotate(qaError({
368
+ what: `Flow is invalid (${errors.length} error${errors.length === 1 ? '' : 's'}), run qa_flow_check`,
369
+ changedState: false,
370
+ retrySafe: true,
371
+ failureCode: 'INVALID_FLOW',
372
+ nextSteps: ['Fix the listed errors and re-plan.'],
373
+ }, { source: src.source, errors }), notes);
374
+ }
375
+ const appiumHints = appium ? { ...appium } : undefined;
376
+ const attachedBackend = session?.driver?.kind ? backendForDriverKind(session.driver.kind, appiumHints) : undefined;
377
+ // Which backends to plan for: an explicit one, else all concrete backends (so the answer covers
378
+ // Android + iOS even with no device attached: the pre-run "can this run?" product question).
379
+ const targets = backend ? [backend] : [...ALL_BACKENDS];
380
+ if (attachedBackend && attachedBackend !== 'unknown' && !targets.includes(attachedBackend))
381
+ targets.unshift(attachedBackend);
382
+ const plans = targets.map((b) => {
383
+ const caps = backendCapabilities(b, appiumHints);
384
+ const plan = compileAutomationPlan(parsed, caps);
385
+ const readiness = buildReadiness(plan);
386
+ return {
387
+ backend: b,
388
+ attached: b === attachedBackend,
389
+ status: readiness.status,
390
+ headline: readiness.headline,
391
+ executable: plan.executable,
392
+ supportedSteps: readiness.supportedSteps,
393
+ unsupportedSteps: readiness.unsupportedSteps,
394
+ fallbackUsed: readiness.fallbackUsed,
395
+ agentMessage: readiness.agentMessage,
396
+ developerMessage: readiness.developerMessage,
397
+ blockers: readiness.blockers,
398
+ warnings: readiness.warnings,
399
+ steps: plan.steps.map((s) => ({
400
+ index: s.index,
401
+ kind: s.action.kind,
402
+ target: s.action.note,
403
+ support: s.support,
404
+ reason: s.reason,
405
+ requiredCapability: s.requiredCapability,
406
+ })),
407
+ };
408
+ });
409
+ const summaryLines = plans.map((p) => {
410
+ const head = `${STATUS_ICON[p.status] ?? '•'} ${p.backend}${p.attached ? ' (attached)' : ''}: ${p.status}, ${p.supportedSteps} supported, ${p.unsupportedSteps} unsupported`;
411
+ return `${head}\n ${p.agentMessage}`;
412
+ });
413
+ const summary = `flow "${parsed.name}": automation plan across ${plans.length} backend(s) (mode:"plan", read-only, nothing executed):\n${summaryLines.join('\n')}`;
414
+ return qaAnnotate(qaOk({ flow: parsed.name, appId: parsed.appId ?? null, mode: parsed.mode, source: src.source, plans }, summary), notes);
217
415
  }
218
- const appiumHints = appium ? { ...appium } : undefined;
219
- const attachedBackend = session?.driver?.kind ? backendForDriverKind(session.driver.kind, appiumHints) : undefined;
220
- // Which backends to plan for: an explicit one, else all concrete backends (so the answer covers
221
- // Android + iOS even with no device attached — the pre-run "can this run?" product question).
222
- const targets = backend ? [backend] : [...ALL_BACKENDS];
223
- if (attachedBackend && attachedBackend !== 'unknown' && !targets.includes(attachedBackend))
224
- targets.unshift(attachedBackend);
225
- const plans = targets.map((b) => {
226
- const caps = backendCapabilities(b, appiumHints);
227
- const plan = compileAutomationPlan(parsed, caps);
228
- const readiness = buildReadiness(plan);
229
- return {
230
- backend: b,
231
- attached: b === attachedBackend,
232
- status: readiness.status,
233
- headline: readiness.headline,
234
- executable: plan.executable,
235
- supportedSteps: readiness.supportedSteps,
236
- unsupportedSteps: readiness.unsupportedSteps,
237
- fallbackUsed: readiness.fallbackUsed,
238
- agentMessage: readiness.agentMessage,
239
- developerMessage: readiness.developerMessage,
240
- blockers: readiness.blockers,
241
- warnings: readiness.warnings,
242
- steps: plan.steps.map((s) => ({ index: s.index, kind: s.action.kind, target: s.action.note, support: s.support, reason: s.reason, requiredCapability: s.requiredCapability })),
243
- };
244
- });
245
- const summaryLines = plans.map((p) => {
246
- const head = `${STATUS_ICON[p.status] ?? '•'} ${p.backend}${p.attached ? ' (attached)' : ''}: ${p.status} — ${p.supportedSteps} supported, ${p.unsupportedSteps} unsupported`;
247
- return `${head}\n ${p.agentMessage}`;
248
- });
249
- const summary = `flow "${parsed.name}" — automation plan across ${plans.length} backend(s):\n${summaryLines.join('\n')}`;
250
- return qaOk({ flow: parsed.name, appId: parsed.appId ?? null, mode: parsed.mode, source: src.source, plans }, summary);
251
- });
252
- server.registerTool('qa_flow_run', {
253
- title: 'Run a flow',
254
- description: 'Run a Swipium flow against the prepared app and report the result. Drives steps server-side: tap/tapAt/tapImage, inputText (incl. { into, text } to focus a named field), assertVisible/assertNotVisible/assertImage/assertVisual/assertDiff, swipe (device-relative { direction, area, distance }), scrollTo, press, openUrl, wait/waitForIdle/waitForVisible, clearOverlay, networkOffline/Online, restartApp, seed, note, screenshot — with optional setup/teardown (teardown always runs) and mode structured|visual|auto. Fail-fast, NO auto-retry of mutating steps; on failure returns the exact failing step + screenshot + typed failureCode + health. Provide `flow` (name/path) or `flowYaml`, plus `variables` for any ${VAR}.',
255
- inputSchema: {
256
- sessionId: z.string(),
257
- flow: z.string().optional(),
258
- flowYaml: z.string().optional(),
259
- variables: z.record(z.string()).optional().describe('Values for ${VAR} placeholders (merged over process.env). Credential-looking names are auto-redacted.'),
260
- repeat: z.number().int().min(1).max(10).optional().describe('Run the flow N times for flake detection (default 1). Reports pass-rate + a deterministic-pass | deterministic-fail | flaky classification.'),
261
- consentId: z.string().optional().describe('Consent token returned when the flow contains mutating steps or external visual-provider steps such as OCR.'),
262
- approve: z.boolean().optional().describe('Set true with consentId to execute after reviewing the exact flow hash, mutation list, and external provider command if present.'),
263
- },
264
- }, async ({ sessionId, flow, flowYaml, variables, repeat, consentId, approve }) => {
265
- const session = sessions.get(sessionId);
266
- const { driver } = session ? await getDriver(session) : { driver: undefined };
416
+ // ---- mode:"run": execute on the prepared device. ----
417
+ const runIgnored = [
418
+ backend !== undefined && 'backend',
419
+ appium !== undefined && 'appium',
420
+ projectRoot !== undefined && 'projectRoot',
421
+ ].filter((x) => !!x);
422
+ if (runIgnored.length)
423
+ notes.push(`ignored parameter(s) not applicable to mode:"run": ${runIgnored.join(', ')}; they refine the mode:"plan" preview`);
424
+ const session = sessionId ? sessions.get(sessionId) : undefined;
425
+ if (sessionId && !session)
426
+ return qaAnnotate(unknownSessionError(sessionId), notes);
427
+ const { driver, blocked } = session ? await getDriver(session) : { driver: undefined, blocked: undefined };
267
428
  if (!session || !driver) {
268
- return qaError({ what: 'No device attached to this session', changedState: false, retrySafe: true, nextSteps: ['Call qa_prepare_target / qa_ios boot first, then qa_flow_run.'] });
429
+ const refused = blockedDeviceResult(blocked);
430
+ if (refused)
431
+ return qaAnnotate(refused, notes);
432
+ return qaAnnotate(qaError({
433
+ what: sessionId
434
+ ? 'No device attached to this session'
435
+ : 'qa_flow_run mode:"run" needs a sessionId with a prepared device (mode:"plan" works without one)',
436
+ changedState: false,
437
+ retrySafe: true,
438
+ nextSteps: ['Call qa_prepare_target / qa_ios boot first, then qa_flow_run.'],
439
+ }), notes);
269
440
  }
270
441
  const src = loadFlowSource(session.root, flow, flowYaml);
271
442
  if (src.error)
272
- return qaError({ what: src.error, changedState: false, retrySafe: true, nextSteps: ['Pass an existing flow name/path or inline flowYaml.'] });
443
+ return qaAnnotate(flowSourceError(src), notes);
273
444
  const { flow: parsed, errors } = parseFlow(src.yamlText);
274
445
  if (errors.length || !parsed) {
275
- return qaError({ what: `Flow is invalid — run qa_flow_check first`, changedState: false, retrySafe: true, nextSteps: ['Fix the flow and re-run.'] }, { errors });
446
+ return qaAnnotate(qaError({
447
+ what: `Flow is invalid. Run qa_flow_check first`,
448
+ changedState: false,
449
+ retrySafe: true,
450
+ failureCode: 'INVALID_FLOW',
451
+ nextSteps: ['Fix the flow and re-run.'],
452
+ }, { errors }), notes);
276
453
  }
277
- // Backend/mode gate (NEXT-PLAN: catch unsupported combos before running). A structured-mode
454
+ // Backend/mode gate (catch unsupported combos before running). A structured-mode
278
455
  // flow needs a UI tree, which the iOS simulator and a visual-fallback session don't have.
279
456
  if (parsed.mode === 'structured' && (driver.kind === 'simulator' || session.mode === 'visual-fallback')) {
280
- return qaError({
457
+ return qaAnnotate(qaError({
281
458
  what: `This flow is mode:structured but ${driver.kind === 'simulator' ? 'the iOS simulator backend has no UI tree' : 'the session is in visual-fallback'}`,
282
459
  changedState: false,
283
460
  retrySafe: false,
284
461
  failureCode: 'BACKEND_UNSUPPORTED',
285
- nextSteps: ['Author the flow with `mode: visual` (or auto) and use image/visual steps (tapImage/assertImage/assertDiff/assertVisual).'],
286
- });
462
+ nextSteps: [
463
+ 'Author the flow with `mode: visual` (or auto) and use image/visual steps (tapImage/assertImage/assertDiff/assertVisual).',
464
+ ],
465
+ }), notes);
287
466
  }
288
467
  const budget = sessions.budgetStop(session);
289
468
  if (budget)
290
- return qaStop(budget, { counters: session.counters });
469
+ return qaAnnotate(qaStop(budget, { counters: session.counters }), notes);
291
470
  const runs = repeat ?? 1;
292
471
  const externalProviderSteps = ocrSteps(parsed);
293
472
  const externalProviders = [];
294
473
  if (externalProviderSteps.length) {
295
474
  if (session.sensitive) {
296
- return qaError({
475
+ return qaAnnotate(qaError({
297
476
  what: 'Sensitive mode refuses OCR flow steps because they would pass screenshots to an external provider',
298
477
  changedState: false,
299
478
  retrySafe: false,
300
479
  failureCode: 'UNSAFE_ACTION_REFUSED',
301
480
  nextSteps: ['Disable sensitive mode only for a safe, masked QA screen, or remove tapOcrText/assertOcrText from the flow.'],
302
- });
481
+ }), notes);
303
482
  }
304
483
  const command = configuredOcrCommand(session.root);
305
484
  if (!command) {
306
- return qaError({
485
+ return qaAnnotate(qaError({
307
486
  what: 'Flow uses OCR text steps, but OCR is not configured',
308
487
  changedState: false,
309
488
  retrySafe: true,
310
489
  failureCode: 'VISUAL_ONLY_SCREEN',
311
490
  nextSteps: ['Configure ocrCommand in .swipium/config.json or remove tapOcrText/assertOcrText from the flow.'],
312
- });
491
+ }), notes);
313
492
  }
314
493
  try {
315
494
  const preview = resolveVisualProvider(command, { image: '<screenshot>' }, 30000);
@@ -325,18 +504,31 @@ export function registerFlow(server, sessions) {
325
504
  });
326
505
  }
327
506
  catch (e) {
328
- return qaError({
507
+ return qaAnnotate(qaError({
329
508
  what: e instanceof GitScopeForbiddenError ? e.message : `Invalid OCR command template: ${String(e)}`,
330
509
  changedState: false,
331
510
  retrySafe: !(e instanceof GitScopeForbiddenError),
332
511
  failureCode: e instanceof GitScopeForbiddenError ? 'GIT_SCOPE_FORBIDDEN' : 'INVALID_FLOW',
333
- nextSteps: e instanceof GitScopeForbiddenError ? ['Run Git yourself outside Swipium; configure ocrCommand to use a non-Git executable.'] : ['Use an argv array in .swipium/config.json, e.g. ["node","ocr.js","{image}"].'],
334
- });
512
+ nextSteps: e instanceof GitScopeForbiddenError
513
+ ? ['Run Git yourself outside Swipium; configure ocrCommand to use a non-Git executable.']
514
+ : ['Use an argv array in .swipium/config.json, e.g. ["node","ocr.js","{image}"].'],
515
+ }), notes);
335
516
  }
336
517
  }
337
- const affects = flowMutationAffects(parsed, session, src.source, runs, externalProviders);
518
+ // Stored session inputs (qa_agent needs_input / first-run credentials) fill ${VAR}s so a
519
+ // generated flow replays without the agent re-sending raw secrets; explicit args win.
520
+ const sessionInputs = sessions.inputVariables(session);
521
+ const runVariables = { ...sessionInputs, ...(variables ?? {}) };
522
+ const usedSessionInputs = Object.keys(sessionInputs).filter((k) => variables?.[k] == null);
523
+ if (usedSessionInputs.length)
524
+ notes.push(`using stored session input(s): ${usedSessionInputs.join(', ')} (values not shown)`);
525
+ const affects = flowMutationAffects(parsed, session, src.source, runs, externalProviders, runVariables);
338
526
  const mutationRisk = flowMutationRisk(affects);
339
- let mutationConsent = { required: false, approved: true, payloadHash: affects.flowHash };
527
+ let mutationConsent = {
528
+ required: false,
529
+ approved: true,
530
+ payloadHash: affects.flowHash,
531
+ };
340
532
  const privileged = affects.mutations.length > 0 || affects.externalProviders.length > 0;
341
533
  if (privileged) {
342
534
  const payloadHash = fullHash(affects);
@@ -353,15 +545,21 @@ export function registerFlow(server, sessions) {
353
545
  const externalCommand = affects.externalProviders.length
354
546
  ? `; external provider: ${displayArgv(affects.externalProviders[0].argv ?? [])}`
355
547
  : '';
356
- return requireConsent({
548
+ return qaAnnotate(requireConsent({
357
549
  action: 'flow_mutation_run',
358
550
  risk: mutationRisk,
359
- exactCommand: `qa_flow_run ${parsed.name} (${[...affects.mutations.map((m) => `${m.step}:${m.kind}`), ...externalProviderSteps.map((s) => `${s.step}:${s.kind}`)].join(', ')})${externalCommand}`,
551
+ exactCommand: `qa_flow_run ${parsed.name} (${[...affects.mutations.map((m) => `${m.step}:${m.kind}`), ...externalProviderSteps.map((s) => `${s.step}:${s.kind}`)].join(', ')})${externalCommand}${mutationCommandLines(affects).length ? `\n${mutationCommandLines(affects).join('\n')}` : ''}`,
360
552
  affects,
361
- explain: affects.externalProviders.length
553
+ explain: (affects.externalProviders.length
362
554
  ? `Run flow "${parsed.name}" with external visual provider steps? OCR steps pass ${runs * externalProviderSteps.length} screenshot(s) to the configured provider and any mutating steps can change app/device/test state.`
363
- : `Run mutating flow "${parsed.name}"? Mutating steps can change app/device/test state and will be recorded in the mutation ledger.`,
364
- });
555
+ : `Run mutating flow "${parsed.name}"? Mutating steps can change app/device/test state and will be recorded in the mutation ledger.`) +
556
+ (affects.mutations.some((m) => m.kind === 'seed')
557
+ ? " Seed steps run the exact command/URL shown, taken from the repo's fixture declarations. These are repo-supplied and UNREVIEWED by Swipium; approve only if you trust them."
558
+ : '') +
559
+ (affects.mutations.some((m) => m.kind === 'openUrl')
560
+ ? ' openUrl steps interpolate variables into a URL that leaves the test harness, so check the destination.'
561
+ : ''),
562
+ }), notes);
365
563
  }
366
564
  mutationConsent = { required: true, consentId, approved: true, payloadHash };
367
565
  sessions.recordMutation(session, {
@@ -380,18 +578,35 @@ export function registerFlow(server, sessions) {
380
578
  const b = sessions.budgetStop(session);
381
579
  if (b)
382
580
  break;
383
- results.push(await runFlow(sessions, session, driver, parsed, { variables, mutationConsent }));
581
+ results.push(await runFlow(sessions, session, driver, parsed, { variables: runVariables, mutationConsent }));
384
582
  }
385
583
  const flake = classifyFlakeResults(results, runs);
386
- return qaOk({ flow: parsed.name, runs: results.length, passes: flake.passed, fails: flake.failed, passRate: flake.passRate, classification: flake.classification, triage: flake.triage, results: results.map((r) => ({ passed: r.passed, failedAtStep: r.failedAtStep, reason: r.reason, failureCode: r.failureCode })) }, `flow "${parsed.name}" × ${results.length}: ${flake.passed} passed, ${flake.failed} failed → ${flake.classification === 'flaky' ? '⚠ FLAKY' : flake.classification === 'deterministic-pass' ? '✅ deterministic-pass' : '❌ deterministic-fail'} (${flake.passRate}% pass-rate); triage=${flake.triage.likelyCause}`);
584
+ return qaAnnotate(qaOk({
585
+ flow: parsed.name,
586
+ runs: results.length,
587
+ passes: flake.passed,
588
+ fails: flake.failed,
589
+ passRate: flake.passRate,
590
+ classification: flake.classification,
591
+ triage: flake.triage,
592
+ results: results.map((r) => ({
593
+ passed: r.passed,
594
+ failedAtStep: r.failedAtStep,
595
+ reason: r.reason,
596
+ failureCode: r.failureCode,
597
+ })),
598
+ }, `flow "${parsed.name}" × ${results.length}: ${flake.passed} passed, ${flake.failed} failed > ${flake.classification === 'flaky' ? '⚠ FLAKY' : flake.classification === 'deterministic-pass' ? '✅ deterministic-pass' : '❌ deterministic-fail'} (${flake.passRate}% pass-rate); triage=${flake.triage.likelyCause}`), notes);
387
599
  }
388
- const result = await runFlow(sessions, session, driver, parsed, { variables, mutationConsent });
600
+ const result = await runFlow(sessions, session, driver, parsed, { variables: runVariables, mutationConsent });
389
601
  const head = `flow "${result.name}" ${result.passed ? '✅ PASSED' : `❌ FAILED at step ${result.failedAtStep} (${result.reason})`} ` +
390
- `— ${result.steps.filter((s) => s.ok).length}/${result.steps.length} steps in ${result.durationSec}s` +
602
+ `: ${result.steps.filter((s) => s.ok).length}/${result.steps.length} steps in ${Math.round(result.durationMs / 100) / 10}s` +
391
603
  (result.appHealth ? `\nhealth: native=${result.nativeHealth} app=${result.appHealth}` : '');
392
- const stepLines = result.steps.map((s) => `${s.ok ? '✓' : '✗'} ${s.index}. ${s.summary}${s.detail ? ` — ${s.detail}` : ''}${s.screenshotUri ? `\n evidence: ${s.screenshotUri}` : ''}`).join('\n');
393
- // A failed flow is a structured result, not a protocol error — the agent should record it.
394
- return qaOk({ ...result }, `${head}\n${stepLines}`);
604
+ const stepLines = result.steps
605
+ .map((s) => `${s.ok ? '✓' : '✗'} ${s.index}. ${s.summary}${s.detail ? `: ${s.detail}` : ''}${s.screenshotUri ? `\n evidence: ${s.screenshotUri}` : ''}`)
606
+ .join('\n');
607
+ // A failed flow is a structured result, not a protocol error. The agent should record it.
608
+ const nextSteps = result.passed ? undefined : failedFlowNextSteps(parsed.name, flow, result.failedAtStep, result.failureCode);
609
+ return qaAnnotate(qaOk({ ...result, ...(nextSteps ? { nextSteps } : {}) }, `${head}\n${stepLines}${nextSteps ? `\nnext:\n - ${nextSteps.join('\n - ')}` : ''}`), notes);
395
610
  });
396
611
  }
397
612
  //# sourceMappingURL=flow.js.map