swipium 1.5.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 (498) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +173 -280
  3. package/THREAT_MODEL.md +223 -47
  4. package/dist/appMap/automationLink.js +30 -27
  5. package/dist/appMap/automationLink.js.map +1 -1
  6. package/dist/appMap/build.js +47 -21
  7. package/dist/appMap/build.js.map +1 -1
  8. package/dist/appMap/codeIndex.js +2 -2
  9. package/dist/appMap/codeIndex.js.map +1 -1
  10. package/dist/appMap/featureIndex.js +3 -3
  11. package/dist/appMap/featureIndex.js.map +1 -1
  12. package/dist/appMap/featureModel.js +2 -2
  13. package/dist/appMap/featureModel.js.map +1 -1
  14. package/dist/appMap/firstRunApply.js +2 -2
  15. package/dist/appMap/firstRunApply.js.map +1 -1
  16. package/dist/appMap/issues.js +10 -10
  17. package/dist/appMap/issues.js.map +1 -1
  18. package/dist/appMap/migrations.js +5 -5
  19. package/dist/appMap/migrations.js.map +1 -1
  20. package/dist/appMap/prelaunch.js +2 -2
  21. package/dist/appMap/prelaunch.js.map +1 -1
  22. package/dist/appMap/projectRegistry.js +77 -11
  23. package/dist/appMap/projectRegistry.js.map +1 -1
  24. package/dist/appMap/provenance.js +2 -2
  25. package/dist/appMap/provenance.js.map +1 -1
  26. package/dist/appMap/query.js +2 -2
  27. package/dist/appMap/query.js.map +1 -1
  28. package/dist/appMap/runtimeMerge.js +3 -3
  29. package/dist/appMap/runtimeMerge.js.map +1 -1
  30. package/dist/appMap/schema.js +1 -1
  31. package/dist/appMap/schema.js.map +1 -1
  32. package/dist/appMap/screenMatch.js +1 -1
  33. package/dist/appMap/screenMatch.js.map +1 -1
  34. package/dist/appMap/staticScan.js +17 -17
  35. package/dist/appMap/staticScan.js.map +1 -1
  36. package/dist/appMap/store.js +55 -7
  37. package/dist/appMap/store.js.map +1 -1
  38. package/dist/appMap/tsAstScan.js +3 -3
  39. package/dist/appMap/tsAstScan.js.map +1 -1
  40. package/dist/artifacts/bundletool.js +5 -5
  41. package/dist/artifacts/bundletool.js.map +1 -1
  42. package/dist/artifacts/resolve.js +10 -10
  43. package/dist/artifacts/resolve.js.map +1 -1
  44. package/dist/automation/capabilities.js +3 -3
  45. package/dist/automation/capabilities.js.map +1 -1
  46. package/dist/automation/gestures.js +1 -1
  47. package/dist/automation/gestures.js.map +1 -1
  48. package/dist/automation/plan.js +7 -7
  49. package/dist/automation/plan.js.map +1 -1
  50. package/dist/automation/report.js +3 -3
  51. package/dist/automation/report.js.map +1 -1
  52. package/dist/automation/selectors.js +14 -25
  53. package/dist/automation/selectors.js.map +1 -1
  54. package/dist/automation/types.js +1 -1
  55. package/dist/automation/types.js.map +1 -1
  56. package/dist/automation/waits.js +5 -5
  57. package/dist/automation/waits.js.map +1 -1
  58. package/dist/automation/webview.js +2 -2
  59. package/dist/automation/webview.js.map +1 -1
  60. package/dist/automationGen/appiumModel.js +13 -7
  61. package/dist/automationGen/appiumModel.js.map +1 -1
  62. package/dist/automationGen/ciEmitter.js +5 -5
  63. package/dist/automationGen/ciEmitter.js.map +1 -1
  64. package/dist/automationGen/identifiers.js +224 -0
  65. package/dist/automationGen/identifiers.js.map +1 -0
  66. package/dist/automationGen/jsEmitter.js +265 -106
  67. package/dist/automationGen/jsEmitter.js.map +1 -1
  68. package/dist/automationGen/packagePatch.js +2 -2
  69. package/dist/automationGen/packagePatch.js.map +1 -1
  70. package/dist/automationGen/platformResolve.js +36 -0
  71. package/dist/automationGen/platformResolve.js.map +1 -0
  72. package/dist/automationGen/projectProfile.js +71 -26
  73. package/dist/automationGen/projectProfile.js.map +1 -1
  74. package/dist/automationGen/pythonEmitter.js +276 -105
  75. package/dist/automationGen/pythonEmitter.js.map +1 -1
  76. package/dist/automationGen/readmeEmitter.js +7 -6
  77. package/dist/automationGen/readmeEmitter.js.map +1 -1
  78. package/dist/{tools/automationGenerate.js → automationGen/run.js} +72 -113
  79. package/dist/automationGen/run.js.map +1 -0
  80. package/dist/automationGen/suitePlan.js +6 -4
  81. package/dist/automationGen/suitePlan.js.map +1 -1
  82. package/dist/automationGen/validation.js +20 -8
  83. package/dist/automationGen/validation.js.map +1 -1
  84. package/dist/build/parseBuildLog.js +2 -2
  85. package/dist/build/parseBuildLog.js.map +1 -1
  86. package/dist/build/plan.js +7 -7
  87. package/dist/build/plan.js.map +1 -1
  88. package/dist/ci/preflight.js +13 -4
  89. package/dist/ci/preflight.js.map +1 -1
  90. package/dist/cli/gc.js +74 -0
  91. package/dist/cli/gc.js.map +1 -0
  92. package/dist/cli/init.js +217 -26
  93. package/dist/cli/init.js.map +1 -1
  94. package/dist/cli/main.js +50 -0
  95. package/dist/cli/main.js.map +1 -0
  96. package/dist/cli/report.js +218 -0
  97. package/dist/cli/report.js.map +1 -0
  98. package/dist/cli/scan.js +35 -14
  99. package/dist/cli/scan.js.map +1 -1
  100. package/dist/cli/suite.js +6 -6
  101. package/dist/cli/suite.js.map +1 -1
  102. package/dist/cli/verify.js +7 -7
  103. package/dist/cli/verify.js.map +1 -1
  104. package/dist/consent/consent.js +162 -5
  105. package/dist/consent/consent.js.map +1 -1
  106. package/dist/context/detect.js +45 -14
  107. package/dist/context/detect.js.map +1 -1
  108. package/dist/context/findApps.js +6 -6
  109. package/dist/context/findApps.js.map +1 -1
  110. package/dist/context/projectRoot.js +174 -19
  111. package/dist/context/projectRoot.js.map +1 -1
  112. package/dist/context/scan.js +1 -1
  113. package/dist/context/scan.js.map +1 -1
  114. package/dist/core/capabilityGroups.js +90 -0
  115. package/dist/core/capabilityGroups.js.map +1 -0
  116. package/dist/core/target.js +3 -3
  117. package/dist/core/target.js.map +1 -1
  118. package/dist/core/targetPlan.js +65 -62
  119. package/dist/core/targetPlan.js.map +1 -1
  120. package/dist/drivers/DirectDriver.js +300 -36
  121. package/dist/drivers/DirectDriver.js.map +1 -1
  122. package/dist/drivers/SimctlDriver.js +120 -7
  123. package/dist/drivers/SimctlDriver.js.map +1 -1
  124. package/dist/drivers/WdaDriver.js +350 -50
  125. package/dist/drivers/WdaDriver.js.map +1 -1
  126. package/dist/explore/candidates.js +2 -2
  127. package/dist/explore/candidates.js.map +1 -1
  128. package/dist/explore/graph.js +5 -5
  129. package/dist/explore/graph.js.map +1 -1
  130. package/dist/explore/policy.js +5 -5
  131. package/dist/explore/policy.js.map +1 -1
  132. package/dist/explore/runner.js +301 -284
  133. package/dist/explore/runner.js.map +1 -1
  134. package/dist/explore/signatures.js +1 -1
  135. package/dist/featureTesting/executionBootstrap.js +29 -30
  136. package/dist/featureTesting/executionBootstrap.js.map +1 -1
  137. package/dist/featureTesting/featureMap.js +2 -2
  138. package/dist/featureTesting/featureMap.js.map +1 -1
  139. package/dist/featureTesting/featureScope.js +12 -12
  140. package/dist/featureTesting/featureScope.js.map +1 -1
  141. package/dist/featureTesting/mapFeatureScope.js +7 -7
  142. package/dist/featureTesting/mapFeatureScope.js.map +1 -1
  143. package/dist/featureTesting/objectiveModel.js +10 -10
  144. package/dist/featureTesting/objectiveModel.js.map +1 -1
  145. package/dist/featureTesting/resultMerge.js +6 -6
  146. package/dist/featureTesting/resultMerge.js.map +1 -1
  147. package/dist/featureTesting/sources.js +4 -4
  148. package/dist/featureTesting/sources.js.map +1 -1
  149. package/dist/featureTesting/suiteBridge.js +1 -1
  150. package/dist/featureTesting/suiteBridge.js.map +1 -1
  151. package/dist/featureTesting/synonyms.js +1 -1
  152. package/dist/featureTesting/synonyms.js.map +1 -1
  153. package/dist/featureTesting/testCaseFactory.js +11 -11
  154. package/dist/featureTesting/testCaseFactory.js.map +1 -1
  155. package/dist/featureTesting/testPlan.js +3 -3
  156. package/dist/featureTesting/testPlan.js.map +1 -1
  157. package/dist/firstRun/authStateMachine.js +1 -1
  158. package/dist/firstRun/authStateMachine.js.map +1 -1
  159. package/dist/firstRun/classifyScreen.js +5 -5
  160. package/dist/firstRun/classifyScreen.js.map +1 -1
  161. package/dist/firstRun/firstRunPlanner.js +11 -11
  162. package/dist/firstRun/firstRunPlanner.js.map +1 -1
  163. package/dist/firstRun/firstRunRunner.js +8 -8
  164. package/dist/firstRun/firstRunRunner.js.map +1 -1
  165. package/dist/firstRun/generatedDataPolicy.js +9 -9
  166. package/dist/firstRun/generatedDataPolicy.js.map +1 -1
  167. package/dist/firstRun/inputPlanner.js +5 -5
  168. package/dist/firstRun/inputPlanner.js.map +1 -1
  169. package/dist/firstRun/onboardingStateMachine.js +1 -1
  170. package/dist/firstRun/onboardingStateMachine.js.map +1 -1
  171. package/dist/firstRun/paywallPolicy.js +3 -3
  172. package/dist/firstRun/paywallPolicy.js.map +1 -1
  173. package/dist/firstRun/types.js +1 -1
  174. package/dist/firstRun/types.js.map +1 -1
  175. package/dist/fixtures/catalog.js +33 -3
  176. package/dist/fixtures/catalog.js.map +1 -1
  177. package/dist/fixtures/load.js +50 -0
  178. package/dist/fixtures/load.js.map +1 -0
  179. package/dist/flows/discover.js +2 -2
  180. package/dist/flows/discover.js.map +1 -1
  181. package/dist/flows/generate.js +20 -10
  182. package/dist/flows/generate.js.map +1 -1
  183. package/dist/flows/pack.js +3 -3
  184. package/dist/flows/pack.js.map +1 -1
  185. package/dist/flows/paths.js +57 -0
  186. package/dist/flows/paths.js.map +1 -0
  187. package/dist/flows/repair.js +111 -23
  188. package/dist/flows/repair.js.map +1 -1
  189. package/dist/flows/run.js +153 -57
  190. package/dist/flows/run.js.map +1 -1
  191. package/dist/flows/schema.js +50 -5
  192. package/dist/flows/schema.js.map +1 -1
  193. package/dist/flows/seedExec.js +4 -4
  194. package/dist/flows/seedExec.js.map +1 -1
  195. package/dist/flows/templates.js +5 -5
  196. package/dist/index.js +50 -8
  197. package/dist/index.js.map +1 -1
  198. package/dist/issues/classify.js +16 -16
  199. package/dist/issues/classify.js.map +1 -1
  200. package/dist/issues/fingerprint.js +24 -4
  201. package/dist/issues/fingerprint.js.map +1 -1
  202. package/dist/issues/index.js +94 -31
  203. package/dist/issues/index.js.map +1 -1
  204. package/dist/issues/metrics.js +4 -4
  205. package/dist/issues/metrics.js.map +1 -1
  206. package/dist/issues/recurrence.js +35 -8
  207. package/dist/issues/recurrence.js.map +1 -1
  208. package/dist/issues/report.js +4 -4
  209. package/dist/issues/report.js.map +1 -1
  210. package/dist/issues/reportBridge.js +4 -4
  211. package/dist/issues/reportBridge.js.map +1 -1
  212. package/dist/issues/schema.js +2 -2
  213. package/dist/issues/schema.js.map +1 -1
  214. package/dist/issues/sourceRevision.js +2 -2
  215. package/dist/issues/sourceRevision.js.map +1 -1
  216. package/dist/issues/store.js +110 -32
  217. package/dist/issues/store.js.map +1 -1
  218. package/dist/lib/abortScope.js +48 -0
  219. package/dist/lib/abortScope.js.map +1 -0
  220. package/dist/lib/android.js +75 -7
  221. package/dist/lib/android.js.map +1 -1
  222. package/dist/lib/coordSpace.js +1 -1
  223. package/dist/lib/coordSpace.js.map +1 -1
  224. package/dist/lib/device.js +32 -7
  225. package/dist/lib/device.js.map +1 -1
  226. package/dist/lib/gestures.js +141 -0
  227. package/dist/lib/gestures.js.map +1 -0
  228. package/dist/lib/gitignore.js +4 -3
  229. package/dist/lib/gitignore.js.map +1 -1
  230. package/dist/lib/image.js +1 -1
  231. package/dist/lib/image.js.map +1 -1
  232. package/dist/lib/lockfile.js +370 -31
  233. package/dist/lib/lockfile.js.map +1 -1
  234. package/dist/lib/needsInput.js +3 -3
  235. package/dist/lib/needsInput.js.map +1 -1
  236. package/dist/lib/png.js +2 -2
  237. package/dist/lib/png.js.map +1 -1
  238. package/dist/lib/redact.js +170 -9
  239. package/dist/lib/redact.js.map +1 -1
  240. package/dist/lib/result.js +79 -10
  241. package/dist/lib/result.js.map +1 -1
  242. package/dist/lib/schemaHash.js +3 -3
  243. package/dist/lib/schemaHash.js.map +1 -1
  244. package/dist/lib/sensitive.js +3 -2
  245. package/dist/lib/sensitive.js.map +1 -1
  246. package/dist/lib/simctl.js +3 -3
  247. package/dist/lib/simctl.js.map +1 -1
  248. package/dist/lib/spawn.js +52 -10
  249. package/dist/lib/spawn.js.map +1 -1
  250. package/dist/lib/toolAnnotations.js +114 -0
  251. package/dist/lib/toolAnnotations.js.map +1 -0
  252. package/dist/lib/wda.js +294 -15
  253. package/dist/lib/wda.js.map +1 -1
  254. package/dist/mobileAudit/checks.js +12 -12
  255. package/dist/mobileAudit/checks.js.map +1 -1
  256. package/dist/mobileAudit/evidence.js +4 -4
  257. package/dist/mobileAudit/evidence.js.map +1 -1
  258. package/dist/mobileAudit/profiles.js +1 -1
  259. package/dist/mobileAudit/profiles.js.map +1 -1
  260. package/dist/mobileAudit/results.js +3 -3
  261. package/dist/mobileAudit/results.js.map +1 -1
  262. package/dist/mobileAudit/runner.js +72 -18
  263. package/dist/mobileAudit/runner.js.map +1 -1
  264. package/dist/oracle/auth.js +1 -1
  265. package/dist/oracle/auth.js.map +1 -1
  266. package/dist/oracle/failures.js +219 -36
  267. package/dist/oracle/failures.js.map +1 -1
  268. package/dist/oracle/health.js +63 -17
  269. package/dist/oracle/health.js.map +1 -1
  270. package/dist/oracle/locator.js +13 -13
  271. package/dist/oracle/locator.js.map +1 -1
  272. package/dist/oracle/record.js +3 -3
  273. package/dist/oracle/record.js.map +1 -1
  274. package/dist/orchestration/envelope.js +2 -2
  275. package/dist/orchestration/envelope.js.map +1 -1
  276. package/dist/orchestration/goal.js +4 -4
  277. package/dist/orchestration/goal.js.map +1 -1
  278. package/dist/orchestration/testThis/execute.js +62 -21
  279. package/dist/orchestration/testThis/execute.js.map +1 -1
  280. package/dist/orchestration/testThis/pipeline.js +119 -30
  281. package/dist/orchestration/testThis/pipeline.js.map +1 -1
  282. package/dist/orchestration/testThis/plan.js +162 -73
  283. package/dist/orchestration/testThis/plan.js.map +1 -1
  284. package/dist/orchestration/testThis/sessionIntent.js +72 -0
  285. package/dist/orchestration/testThis/sessionIntent.js.map +1 -0
  286. package/dist/orchestration/testThis/terminal.js +34 -16
  287. package/dist/orchestration/testThis/terminal.js.map +1 -1
  288. package/dist/orchestration/testThis/types.js +1 -1
  289. package/dist/orchestration/testThis/types.js.map +1 -1
  290. package/dist/plan/plan.js +9 -9
  291. package/dist/plan/plan.js.map +1 -1
  292. package/dist/prompts/index.js +22 -22
  293. package/dist/prompts/index.js.map +1 -1
  294. package/dist/report/export.js +392 -37
  295. package/dist/report/export.js.map +1 -1
  296. package/dist/report/findingsDedupe.js +41 -0
  297. package/dist/report/findingsDedupe.js.map +1 -0
  298. package/dist/report/policy.js +7 -4
  299. package/dist/report/policy.js.map +1 -1
  300. package/dist/report/qaLevel.js +8 -8
  301. package/dist/report/qaLevel.js.map +1 -1
  302. package/dist/report/sarifSources.js +160 -0
  303. package/dist/report/sarifSources.js.map +1 -0
  304. package/dist/report/summary.js +2 -2
  305. package/dist/report/summary.js.map +1 -1
  306. package/dist/report/testCatalog.js +4 -4
  307. package/dist/report/testCatalog.js.map +1 -1
  308. package/dist/report/toolHealth.js +124 -0
  309. package/dist/report/toolHealth.js.map +1 -0
  310. package/dist/server.js +472 -45
  311. package/dist/server.js.map +1 -1
  312. package/dist/services/automationGenerate.js +8 -2
  313. package/dist/services/automationGenerate.js.map +1 -1
  314. package/dist/services/build.js +2 -2
  315. package/dist/services/build.js.map +1 -1
  316. package/dist/{tools → services}/flowGenerate.js +29 -13
  317. package/dist/services/flowGenerate.js.map +1 -0
  318. package/dist/services/preflight.js +4 -4
  319. package/dist/services/preflight.js.map +1 -1
  320. package/dist/services/prepareAndroid.js +46 -8
  321. package/dist/services/prepareAndroid.js.map +1 -1
  322. package/dist/services/prepareIos.js +24 -9
  323. package/dist/services/prepareIos.js.map +1 -1
  324. package/dist/services/report.js +100 -45
  325. package/dist/services/report.js.map +1 -1
  326. package/dist/services/smoke.js +32 -16
  327. package/dist/services/smoke.js.map +1 -1
  328. package/dist/services/suiteGenerate.js +28 -3
  329. package/dist/services/suiteGenerate.js.map +1 -1
  330. package/dist/services/testSuiteKnowledge.js +40 -27
  331. package/dist/services/testSuiteKnowledge.js.map +1 -1
  332. package/dist/session/attach.js +270 -6
  333. package/dist/session/attach.js.map +1 -1
  334. package/dist/session/processRegistry.js +317 -48
  335. package/dist/session/processRegistry.js.map +1 -1
  336. package/dist/session/progress.js +1 -1
  337. package/dist/session/retention.js +201 -0
  338. package/dist/session/retention.js.map +1 -0
  339. package/dist/session/store.js +275 -48
  340. package/dist/session/store.js.map +1 -1
  341. package/dist/snapshot/overlays.js +53 -8
  342. package/dist/snapshot/overlays.js.map +1 -1
  343. package/dist/snapshot/parse.js +28 -4
  344. package/dist/snapshot/parse.js.map +1 -1
  345. package/dist/snapshot/present.js +11 -5
  346. package/dist/snapshot/present.js.map +1 -1
  347. package/dist/snapshot/settle.js +39 -9
  348. package/dist/snapshot/settle.js.map +1 -1
  349. package/dist/state/consent.js +99 -0
  350. package/dist/state/consent.js.map +1 -0
  351. package/dist/state/profile.js +46 -2
  352. package/dist/state/profile.js.map +1 -1
  353. package/dist/suite/compile.js +9 -5
  354. package/dist/suite/compile.js.map +1 -1
  355. package/dist/suite/lint.js +4 -4
  356. package/dist/suite/lint.js.map +1 -1
  357. package/dist/suite/pom.js +50 -41
  358. package/dist/suite/pom.js.map +1 -1
  359. package/dist/suite/secretGuard.js +266 -0
  360. package/dist/suite/secretGuard.js.map +1 -0
  361. package/dist/suite/testcase.js +9 -7
  362. package/dist/suite/testcase.js.map +1 -1
  363. package/dist/testSuite/exporter.js +4 -4
  364. package/dist/testSuite/exporter.js.map +1 -1
  365. package/dist/testSuite/generator.js +13 -9
  366. package/dist/testSuite/generator.js.map +1 -1
  367. package/dist/testSuite/history.js +1 -1
  368. package/dist/testSuite/history.js.map +1 -1
  369. package/dist/testSuite/issueLinks.js +3 -3
  370. package/dist/testSuite/issueLinks.js.map +1 -1
  371. package/dist/testSuite/lint.js +4 -4
  372. package/dist/testSuite/lint.js.map +1 -1
  373. package/dist/testSuite/merge.js +5 -5
  374. package/dist/testSuite/merge.js.map +1 -1
  375. package/dist/testSuite/schema.js +5 -5
  376. package/dist/testSuite/schema.js.map +1 -1
  377. package/dist/testSuite/store.js +3 -3
  378. package/dist/testSuite/store.js.map +1 -1
  379. package/dist/testSuite/traceability.js +2 -2
  380. package/dist/testSuite/traceability.js.map +1 -1
  381. package/dist/tools/act.js +794 -164
  382. package/dist/tools/act.js.map +1 -1
  383. package/dist/tools/agent.js +451 -128
  384. package/dist/tools/agent.js.map +1 -1
  385. package/dist/tools/appControl.js +69 -19
  386. package/dist/tools/appControl.js.map +1 -1
  387. package/dist/tools/appMap.js +188 -130
  388. package/dist/tools/appMap.js.map +1 -1
  389. package/dist/tools/build.js +32 -42
  390. package/dist/tools/build.js.map +1 -1
  391. package/dist/tools/bundletool.js +21 -31
  392. package/dist/tools/bundletool.js.map +1 -1
  393. package/dist/tools/clearOverlay.js +55 -12
  394. package/dist/tools/clearOverlay.js.map +1 -1
  395. package/dist/tools/device.js +76 -24
  396. package/dist/tools/device.js.map +1 -1
  397. package/dist/tools/doctor.js +68 -28
  398. package/dist/tools/doctor.js.map +1 -1
  399. package/dist/tools/explore.js +41 -47
  400. package/dist/tools/explore.js.map +1 -1
  401. package/dist/tools/featureTesting.js +31 -48
  402. package/dist/tools/featureTesting.js.map +1 -1
  403. package/dist/tools/firstRun.js +34 -41
  404. package/dist/tools/firstRun.js.map +1 -1
  405. package/dist/tools/flow.js +183 -103
  406. package/dist/tools/flow.js.map +1 -1
  407. package/dist/tools/flowRepair.js +25 -13
  408. package/dist/tools/flowRepair.js.map +1 -1
  409. package/dist/tools/generate.js +52 -89
  410. package/dist/tools/generate.js.map +1 -1
  411. package/dist/tools/getArtifact.js +16 -5
  412. package/dist/tools/getArtifact.js.map +1 -1
  413. package/dist/tools/health.js +15 -12
  414. package/dist/tools/health.js.map +1 -1
  415. package/dist/tools/ios.js +45 -223
  416. package/dist/tools/ios.js.map +1 -1
  417. package/dist/tools/issues.js +251 -35
  418. package/dist/tools/issues.js.map +1 -1
  419. package/dist/tools/jobs.js +41 -16
  420. package/dist/tools/jobs.js.map +1 -1
  421. package/dist/tools/metro.js +36 -35
  422. package/dist/tools/metro.js.map +1 -1
  423. package/dist/tools/mobileAudit.js +50 -45
  424. package/dist/tools/mobileAudit.js.map +1 -1
  425. package/dist/tools/network.js +31 -14
  426. package/dist/tools/network.js.map +1 -1
  427. package/dist/tools/note.js +26 -26
  428. package/dist/tools/note.js.map +1 -1
  429. package/dist/tools/prepareIosTarget.js +10 -16
  430. package/dist/tools/prepareIosTarget.js.map +1 -1
  431. package/dist/tools/prepareTarget.js +117 -50
  432. package/dist/tools/prepareTarget.js.map +1 -1
  433. package/dist/tools/report.js +11 -22
  434. package/dist/tools/report.js.map +1 -1
  435. package/dist/tools/resolveArtifact.js +11 -15
  436. package/dist/tools/resolveArtifact.js.map +1 -1
  437. package/dist/tools/resolveTarget.js +78 -24
  438. package/dist/tools/resolveTarget.js.map +1 -1
  439. package/dist/tools/screenRecord.js +23 -21
  440. package/dist/tools/screenRecord.js.map +1 -1
  441. package/dist/tools/screenshot.js +23 -16
  442. package/dist/tools/screenshot.js.map +1 -1
  443. package/dist/tools/smoke.js +20 -16
  444. package/dist/tools/smoke.js.map +1 -1
  445. package/dist/tools/snapshot.js +155 -118
  446. package/dist/tools/snapshot.js.map +1 -1
  447. package/dist/tools/startSession.js +104 -116
  448. package/dist/tools/startSession.js.map +1 -1
  449. package/dist/tools/suite.js +72 -43
  450. package/dist/tools/suite.js.map +1 -1
  451. package/dist/tools/testSuite.js +30 -20
  452. package/dist/tools/testSuite.js.map +1 -1
  453. package/dist/tools/testThis.js +24 -28
  454. package/dist/tools/testThis.js.map +1 -1
  455. package/dist/tools/visual.js +686 -210
  456. package/dist/tools/visual.js.map +1 -1
  457. package/dist/tools/wait.js +9 -38
  458. package/dist/tools/wait.js.map +1 -1
  459. package/dist/tools/wda.js +187 -57
  460. package/dist/tools/wda.js.map +1 -1
  461. package/dist/version.js +31 -22
  462. package/dist/version.js.map +1 -1
  463. package/dist/visual/ocr.js +53 -6
  464. package/dist/visual/ocr.js.map +1 -1
  465. package/dist/visual/provider.js +80 -12
  466. package/dist/visual/provider.js.map +1 -1
  467. package/docs/README.md +24 -10
  468. package/docs/ci-reports.md +328 -0
  469. package/docs/concepts.md +227 -0
  470. package/docs/flows.md +153 -0
  471. package/docs/mcp-server.md +194 -128
  472. package/docs/physical-devices.md +84 -0
  473. package/docs/tools.md +812 -152
  474. package/package.json +3 -2
  475. package/dist/tools/assertVisual.js +0 -84
  476. package/dist/tools/assertVisual.js.map +0 -1
  477. package/dist/tools/automationGenerate.js.map +0 -1
  478. package/dist/tools/capabilities.js +0 -179
  479. package/dist/tools/capabilities.js.map +0 -1
  480. package/dist/tools/detectContext.js +0 -41
  481. package/dist/tools/detectContext.js.map +0 -1
  482. package/dist/tools/flowGenerate.js.map +0 -1
  483. package/dist/tools/history.js +0 -86
  484. package/dist/tools/history.js.map +0 -1
  485. package/dist/tools/locator.js +0 -85
  486. package/dist/tools/locator.js.map +0 -1
  487. package/dist/tools/permissions.js +0 -174
  488. package/dist/tools/permissions.js.map +0 -1
  489. package/dist/tools/plan.js +0 -52
  490. package/dist/tools/plan.js.map +0 -1
  491. package/dist/tools/screenInfo.js +0 -76
  492. package/dist/tools/screenInfo.js.map +0 -1
  493. package/dist/tools/seed.js +0 -133
  494. package/dist/tools/seed.js.map +0 -1
  495. package/dist/tools/state.js +0 -217
  496. package/dist/tools/state.js.map +0 -1
  497. package/dist/tools/visualText.js +0 -106
  498. package/dist/tools/visualText.js.map +0 -1
@@ -1,13 +1,19 @@
1
1
  // Agent-efficiency layer. Compact, deterministic helpers so an MCP client spends fewer turns:
2
- // a one-glance session status, a blocker explainer, and a resume-from-blocker entry that consumes
2
+ // the server instructions, a one-glance status (orientation without a session; state + next best
3
+ // action with one), a blocker explainer, and a resume-from-blocker entry that consumes
3
4
  // user-provided input safely.
5
+ import { realpathSync, statSync } from 'node:fs';
6
+ import { isAbsolute, resolve as resolvePath, sep } from 'node:path';
4
7
  import { z } from 'zod';
5
- import { qaOk, qaError } from '../lib/result.js';
8
+ import { qaOk, qaError, unknownSessionError } from '../lib/result.js';
6
9
  import { FAILURES, failureOwner, isSelfFixable } from '../oracle/failures.js';
7
10
  import { progressLine } from '../session/progress.js';
8
11
  import { readinessForSession } from '../report/readiness.js';
9
12
  import { SWIPIUM_VERSION, TOOL_COUNT, PROMPT_COUNT } from '../version.js';
10
13
  import { TEST_GOALS } from '../orchestration/goal.js';
14
+ import { CAPABILITY_GROUPS } from '../core/capabilityGroups.js';
15
+ import { recallTestThisIntent, markLoginDeclined } from '../orchestration/testThis/sessionIntent.js';
16
+ import { SECRET_VAR_NAME } from '../flows/schema.js';
11
17
  function budgetRemaining(s) {
12
18
  const elapsedMin = (Date.now() - s.createdAt) / 60000;
13
19
  return {
@@ -16,9 +22,88 @@ function budgetRemaining(s) {
16
22
  screenshots: Math.max(0, s.budget.maxScreenshots - s.counters.screenshots),
17
23
  };
18
24
  }
19
- /** Deterministic "what next" given the session's observed state (optionally goal-aware). */
20
- function nextBestAction(s, goal) {
25
+ /** Has this session already produced a durable generated asset (suite/flow/POM/test cases)?
26
+ * Both generation paths leave a workaround-trail entry on success: the qa_test_this pipeline
27
+ * records "generated a POM suite (…)" and qa_generate records "generated <target> test asset(s)
28
+ * from recorded actions (qa_generate)". */
29
+ function hasGeneratedAssets(s) {
30
+ return s.workarounds.some((w) => /^generated .*(suite|flow|pom|test)/i.test(w));
31
+ }
32
+ /** The mode qa_status reports (JSON and text alike). A WDA-less iOS simulator (SimctlDriver, kind
33
+ * 'simulator') is visual-only regardless of the stored mode, which only flips on a failed UI dump. */
34
+ export function effectiveMode(s) {
35
+ // After a restart there is no live driver, but the persisted driverKind still tells the transport.
36
+ return (s.driver?.kind ?? s.driverKind) === 'simulator' ? 'visual-only' : s.mode;
37
+ }
38
+ const SIMULATOR_UDID_RE = /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
39
+ /** Which platform the session's bound device is on. The live driver kind is authoritative
40
+ * (simctl/WDA ⇒ iOS); a rehydrated session has no driver, so fall back to the device id shape:
41
+ * iOS simulators are UUIDs, adb serials never are. */
42
+ export function sessionPlatform(s) {
43
+ const kind = s.driver?.kind ?? s.driverKind;
44
+ if (kind === 'simulator' || kind === 'wda')
45
+ return 'ios';
46
+ if (kind === 'direct')
47
+ return 'android';
48
+ if (!s.device)
49
+ return undefined;
50
+ return SIMULATOR_UDID_RE.test(s.device) ? 'ios' : 'android';
51
+ }
52
+ /** Has a smoke pass run in this session? Persisted signals only: the qa_test_this pipeline's
53
+ * `smoke_completed` milestone, or the launch_smoke note every smoke run records (qa_smoke too). */
54
+ function smokeRan(s) {
55
+ return s.milestones?.smoke_completed != null || s.notes.some((n) => n.workflow === 'launch_smoke');
56
+ }
57
+ /** Latest time anything was exercised/observed (notes, findings, recorded actions). */
58
+ function lastActivityAt(s) {
59
+ let t = 0;
60
+ for (const n of s.notes)
61
+ t = Math.max(t, n.at ?? 0);
62
+ for (const f of s.findings)
63
+ t = Math.max(t, f.at ?? 0);
64
+ for (const a of s.recordedActions)
65
+ t = Math.max(t, a.at ?? 0);
66
+ return t;
67
+ }
68
+ /** Milestone keys stamped by the agent-layer tools so qa_status can tell a terminal job's
69
+ * recommended action was carried out (each overwritten with the latest time). */
70
+ export const BLOCKER_ANSWERED_MILESTONE = 'blocker_answered';
71
+ export const BLOCKER_EXPLAINED_MILESTONE = 'blocker_explained';
72
+ /** Latest time the user answered a needs_input question: a stored input or a
73
+ * qa_continue_from_blocker call (choices-only answers store no input). */
74
+ function lastAnsweredAt(s) {
75
+ let t = s.milestones?.[BLOCKER_ANSWERED_MILESTONE] ?? 0;
76
+ for (const i of s.inputs ?? [])
77
+ t = Math.max(t, i.at ?? 0);
78
+ return t;
79
+ }
80
+ /** The newest report artifact, if one exists. */
81
+ function latestReport(s) {
82
+ return s.artifacts.filter((a) => a.kind === 'report' && /\/report\/report-/.test(a.uri)).sort((a, b) => b.createdAt - a.createdAt)[0];
83
+ }
84
+ /** Deterministic "what next" given the session's observed state (optionally goal-aware).
85
+ * An explicit state ladder, evaluated top-down, the first matching state wins. Every rung keys
86
+ * on state its recommended tool CHANGES, so following the advice always advances the ladder:
87
+ * 1. a job is running > qa_job_status (poll it)
88
+ * 2. the last qa_test_this job ended > its nextRecommendedAction (report / explain blocker /
89
+ * and nothing happened since answer the needs_input question), but only until that
90
+ * action is observably done: a report newer than the job,
91
+ * an answer (stored input / resume call) newer than the job
92
+ * (> re-run the autopilot with it), or a qa_explain_blocker
93
+ * {sessionId} call newer than the job. Replaying a done
94
+ * action would loop, so the ladder moves on.
95
+ * 3. no device bound > qa_test_this (orchestrate setup)
96
+ * 4. device but no app > qa_prepare_target (Android) / qa_prepare_ios_target (iOS sim)
97
+ * 5. no smoke yet and nothing recorded > qa_smoke (records the smoke milestone)
98
+ * 6. findings, no report since > qa_report
99
+ * 7. clean run, actions, no assets yet > qa_generate (make the run durable)
100
+ * 8. no report since the last activity > qa_report (wrap up)
101
+ * 9. a fresh report exists > qa_get_artifact (read it, done; TERMINAL: the only
102
+ * rung whose advice changes nothing, by design)
103
+ * Exported for unit tests (test/nextBestAction.test.ts, test/orchStatusLadder.test.ts). */
104
+ export function nextBestAction(s, goal) {
21
105
  const sid = s.id;
106
+ // 1. A job is still running: poll it before anything else.
22
107
  const lastJob = [...s.jobs.values()].sort((a, b) => b.startedAt - a.startedAt)[0];
23
108
  if (lastJob?.status === 'running')
24
109
  return {
@@ -26,108 +111,186 @@ function nextBestAction(s, goal) {
26
111
  why: `job ${lastJob.jobId} (${lastJob.kind}) is still running`,
27
112
  args: { sessionId: sid, jobId: lastJob.jobId },
28
113
  };
114
+ // 2. The last autopilot job is terminal and nothing was exercised after it: its own
115
+ // nextRecommendedAction is authoritative (a finished run must not be re-smoked).
116
+ const result = lastJob?.result;
117
+ const jobEnd = lastJob ? (lastJob.endedAt ?? lastJob.startedAt) : 0;
118
+ if (lastJob && result?.state && jobEnd >= lastActivityAt(s)) {
119
+ const nra = result.nextRecommendedAction;
120
+ const blocked = result.state === 'blocked' || result.state === 'unsafe';
121
+ const explained = (s.milestones?.[BLOCKER_EXPLAINED_MILESTONE] ?? 0) > jobEnd;
122
+ const answered = result.state === 'needs_input' && lastAnsweredAt(s) > jobEnd;
123
+ const reportedSince = (latestReport(s)?.createdAt ?? 0) > jobEnd;
124
+ if (blocked && !explained)
125
+ return {
126
+ tool: 'qa_explain_blocker',
127
+ why: `last job ${lastJob.jobId} ended ${result.state} (${result.failureCode ?? 'UNKNOWN'}): explain the blocker and relay the fix`,
128
+ args: { failureCode: result.failureCode ?? 'UNKNOWN', sessionId: sid },
129
+ };
130
+ // The question was answered (qa_continue_from_blocker stored the input): resume the autopilot
131
+ // with it. Starting that job is the state change; replaying the resume call would loop.
132
+ if (answered)
133
+ return {
134
+ tool: 'qa_test_this',
135
+ why: `the needs_input question of job ${lastJob.jobId} was answered; re-run the autopilot with the answer`,
136
+ args: { mode: 'execute', ...recallTestThisIntent(s), sessionId: sid, stopOnNeedsInput: false },
137
+ };
138
+ // A qa_report recommendation is satisfied by any report newer than the job.
139
+ const satisfied = blocked || (nra?.tool === 'qa_report' && reportedSince);
140
+ if (nra?.tool && !satisfied)
141
+ return {
142
+ tool: nra.tool,
143
+ why: result.state === 'needs_input'
144
+ ? `last job ${lastJob.jobId} is waiting on one question: ${nra.why ?? 'answer it and resume'}`
145
+ : `last job ${lastJob.jobId} ${result.state}: ${nra.why ?? 'follow its recommendation'}; no further calls needed after that`,
146
+ args: { ...(nra.args ?? {}) },
147
+ };
148
+ }
149
+ // 3. No device bound: orchestrate setup end-to-end.
29
150
  if (!s.device)
30
151
  return {
31
152
  tool: 'qa_test_this',
32
- why: goal ? `no device/app prepared yet — run the autopilot for goal "${goal}"` : 'no device/app prepared yet — orchestrate setup',
153
+ why: goal ? `no device/app prepared yet, run the autopilot for goal "${goal}"` : 'no device/app prepared yet, orchestrate setup',
33
154
  args: { sessionId: sid, mode: 'execute', ...(goal ? { goal } : {}) },
34
155
  };
156
+ // 4. Device bound but no app launched: route to the platform's prepare tool (H9: after
157
+ // `qa_ios boot` the bound device is a simulator; the Android-only qa_prepare_target would fail).
35
158
  if (!s.appId)
36
- return { tool: 'qa_prepare_target', why: 'device bound but no app launched', args: { sessionId: sid } };
37
- if (s.recordedActions.length === 0)
38
- return { tool: 'qa_smoke', why: 'app is up but nothing exercised yet — run a smoke pass', args: { sessionId: sid } };
39
- if (s.findings.length > 0)
40
- return { tool: 'qa_report', why: `${s.findings.length} finding(s) recorded — summarize with evidence`, args: { sessionId: sid } };
41
- if (s.recordedActions.length > 0)
159
+ return sessionPlatform(s) === 'ios'
160
+ ? {
161
+ tool: 'qa_prepare_ios_target',
162
+ why: 'iOS simulator bound but no app launched',
163
+ args: { sessionId: sid, device: s.device },
164
+ }
165
+ : { tool: 'qa_prepare_target', why: 'device bound but no app launched', args: { sessionId: sid } };
166
+ // 5. App is up but nothing has been exercised (a smoke pass is a milestone, not an action count).
167
+ if (!smokeRan(s) && s.recordedActions.length === 0)
168
+ return { tool: 'qa_smoke', why: 'app is up but nothing exercised yet, run a smoke pass', args: { sessionId: sid } };
169
+ const report = latestReport(s);
170
+ const reportFresh = !!report && report.createdAt >= lastActivityAt(s);
171
+ // 6. Findings recorded: reporting them beats generating more assets.
172
+ if (s.findings.length > 0 && !reportFresh)
173
+ return { tool: 'qa_report', why: `${s.findings.length} finding(s) recorded, summarize with evidence`, args: { sessionId: sid } };
174
+ // 7. Clean run with recorded actions but no durable asset yet: make the run reusable.
175
+ if (s.findings.length === 0 && s.recordedActions.length > 0 && !hasGeneratedAssets(s))
42
176
  return {
43
177
  tool: 'qa_generate',
44
- why: 'actions recorded — turn the run into a durable POM suite',
178
+ why: 'actions recorded, turn the run into a durable POM suite',
45
179
  args: { sessionId: sid, target: 'suite' },
46
180
  };
47
- return { tool: 'qa_report', why: 'wrap up and report', args: { sessionId: sid } };
181
+ // 8. Wrap up with a report covering everything done so far.
182
+ if (!reportFresh)
183
+ return {
184
+ tool: 'qa_report',
185
+ why: hasGeneratedAssets(s)
186
+ ? 'clean run and test assets already generated, wrap up and report'
187
+ : 'smoke done, wrap up and report what was covered',
188
+ args: { sessionId: sid },
189
+ };
190
+ // 9. Terminal: a report newer than any activity exists. Read it; nothing else to do.
191
+ return {
192
+ tool: 'qa_get_artifact',
193
+ why: 'the run is reported. Read the report; no further Swipium calls are needed',
194
+ args: { uri: report.uri },
195
+ };
48
196
  }
49
- /** The compact, current-version instruction set for coding agents (Milestone A). Pure + deterministic
50
- * so it is snapshot-testable; it references the live SWIPIUM_VERSION / tool / prompt counts. */
51
- export function agentBrief() {
197
+ /** Server `instructions` (MCP InitializeResult.instructions): the operating manual clients may put
198
+ * in the model's system prompt. Kept short on purpose; per-tool detail lives in tool descriptions
199
+ * and docs/tools.md; qa_status (no sessionId) returns the same rules plus capability groups. */
200
+ export const SERVER_INSTRUCTIONS = [
201
+ 'Swipium runs mobile QA on local Android Emulators and iOS Simulators (physical devices are out of scope).',
202
+ '',
203
+ 'First call: qa_test_this {mode:"execute"} (add goal, e.g. "release_gate"). It finds or builds the app, prepares an emulator/simulator, tests, and reports in a background job. It usually returns requiresConsent first (boot/install/build, see Consent), then {sessionId, jobId, state:"running"}.',
204
+ 'Polling: qa_job_status {sessionId, jobId, waitMs:60000} until status is no longer "running". Then result.state is completed | blocked | unsafe | needs_input (none if cancelled), with reportUri. On completed, read it (qa_get_artifact {uri: reportUri}) and stop unless the user asks for more.',
205
+ 'Orientation: qa_status without sessionId returns these rules + tool groups; with sessionId, session state and nextBestAction (goal biases it).',
206
+ '',
207
+ 'needs_input: relay exactly the one returned question (fields, secret flags) to the user, then make the returned `resume` call (qa_continue_from_blocker) with their answer and follow its nextAction. Never invent extra questions.',
208
+ 'blocked / unsafe: relay failureCode, owner, what Swipium tried, and the fix (qa_explain_blocker {failureCode, sessionId}). A build failure is not a test failure.',
209
+ 'Ask the user only on needs_input or a consent request; otherwise keep going.',
210
+ 'Consent: clients with MCP elicitation prompt the user directly. Otherwise show a requiresConsent result to the user and re-call with consentId + approve:true only after they agree. CONSENT_DECLINED / CONSENT_CANCELLED / CONSENT_REFUSED mean nothing ran - do not retry without asking.',
211
+ '',
212
+ 'Project root: projectRoot arg, else MCP roots, else SWIPIUM_PROJECT_ROOT, else CLAUDE_PROJECT_DIR, else the server cwd if it is not / or $HOME and contains a project marker (package.json, app.json, pubspec.yaml, Gradle/Xcode files, Podfile, android/, ios/). On PROJECT_ROOT_UNRESOLVED pass an absolute projectRoot.',
213
+ 'iOS Simulator without WebDriverAgent is visual-only: qa_snapshot/qa_act return BACKEND_UNSUPPORTED; use qa_screenshot + qa_visual, or attach WebDriverAgent via qa_wda. qa_test_this then runs a visual-only smoke.',
214
+ 'Unknown arguments fail with INVALID_ARGUMENT (nothing runs). STALE_CLIENT means the client runs an outdated server: restart the client.',
215
+ 'Feature work: read the app map first (qa_app_map_read / qa_app_map_query / qa_app_map_feature_scope); qa_test_feature tests one feature.',
216
+ 'Low-level escape hatches: qa_start_session, qa_prepare_target / qa_prepare_ios_target, qa_snapshot, qa_act, qa_visual, qa_flow_run.',
217
+ ].join('\n');
218
+ /** qa_status without a sessionId: first-call orientation (the operating rules + capability groups
219
+ * by name). Pure + deterministic so it is snapshot-testable. */
220
+ export function orientation(goal) {
52
221
  return {
53
- version: SWIPIUM_VERSION,
222
+ orientation: true,
223
+ swipiumVersion: SWIPIUM_VERSION,
54
224
  tools: TOOL_COUNT,
55
225
  prompts: PROMPT_COUNT,
56
- purpose: 'Turn a coding agent into a mobile QA tester. For a zero-context "test this" request, call the macro tool first.',
57
226
  firstCall: {
58
227
  tool: 'qa_test_this',
59
- args: { mode: 'execute' },
60
- note: 'Pass projectRoot if the client did not expose a workspace root. mode:"execute" runs the whole path as a background job.',
228
+ args: { mode: 'execute', ...(goal ? { goal } : {}) },
229
+ note: 'Pass projectRoot="/abs/path" if the client exposes no workspace root. Usually returns requiresConsent first (boot/install/build); after approval it runs as a background job.',
61
230
  },
62
231
  polling: {
63
232
  tool: 'qa_job_status',
64
- args: { sessionId: '<from firstCall>', jobId: '<from firstCall>' },
233
+ args: { sessionId: '<from firstCall>', jobId: '<from firstCall>', waitMs: 60000 },
234
+ until: 'status is no longer "running" (done | failed | cancelled; a cancelled job has no result)',
65
235
  terminalStates: ['completed', 'blocked', 'unsafe', 'needs_input'],
66
236
  },
67
- report: {
68
- tool: 'qa_get_artifact',
69
- args: { uri: '<reportUri from the terminal result>' },
70
- rule: 'Fetch reportUri instead of calling more tools, unless nextRecommendedAction says otherwise.',
71
- },
237
+ report: { tool: 'qa_get_artifact', args: { uri: '<reportUri from the terminal result>' } },
72
238
  goals: TEST_GOALS,
73
- needsInputRule: "Relay exactly the one returned question (with its fields/secret flags). Do not invent extra questions. Resume with the response's `resume` call.",
74
- blockerRule: 'Relay failureCode, owner, what Swipium tried (attempted/workaroundsAttempted), and how to fix it. A build failure is not a test failure.',
75
- stopRule: 'Stop and ask the user only on a needs_input state (credentials, monorepo target, destructive approval, signing, external service). Otherwise continue without user input.',
76
- continueRule: 'For completed/blocked/unsafe states, summarize from the structured envelope and the report — no extra Swipium calls are required.',
77
- mapRule: 'Before feature-focused testing, consult the durable App Knowledge Map: qa_app_map_read, qa_app_map_query, or qa_app_map_feature_scope to reuse what Swipium already learned instead of rediscovering the app. qa_test_this and qa_explore update the map automatically.',
78
- macroTools: ['qa_test_this', 'qa_smoke', 'qa_explore', 'qa_report', 'qa_app_map_read'],
79
- escapeHatches: [
80
- 'qa_doctor',
81
- 'qa_start_session',
82
- 'qa_detect_context',
83
- 'qa_plan',
84
- 'qa_prepare_target',
85
- 'qa_prepare_ios_target',
86
- 'qa_ios',
87
- 'qa_wda',
88
- 'qa_act',
89
- 'qa_snapshot',
90
- ],
239
+ rules: {
240
+ needsInput: 'Relay exactly the one returned question; make its `resume` call (qa_continue_from_blocker) with the answer, then follow nextAction.',
241
+ blocker: 'Relay failureCode, owner, what was tried, and the fix (qa_explain_blocker {failureCode, sessionId}). A build failure is not a test failure.',
242
+ consent: 'Clients with MCP elicitation prompt the user directly. Otherwise show requiresConsent to the user; re-call with consentId + approve:true only after they agree. CONSENT_DECLINED / CANCELLED / REFUSED: nothing ran, do not retry without asking.',
243
+ stop: 'Ask the user only on needs_input or a consent request; otherwise continue. After a completed job, read the report and stop.',
244
+ iosVisualOnly: 'An iOS Simulator without WebDriverAgent is visual-only: use qa_screenshot + qa_visual, or attach WebDriverAgent via qa_wda.',
245
+ appMap: 'Before feature testing, read the app map (qa_app_map_read / qa_app_map_query / qa_app_map_feature_scope).',
246
+ },
247
+ capabilityGroups: CAPABILITY_GROUPS.map((g) => ({ group: g.group, purpose: g.purpose, tools: [...g.tools] })),
248
+ nextBestAction: {
249
+ tool: 'qa_test_this',
250
+ why: goal ? `no session yet, run the autopilot for goal "${goal}"` : 'no session yet, start the autopilot',
251
+ args: { mode: 'execute', ...(goal ? { goal } : {}) },
252
+ },
91
253
  };
92
254
  }
93
255
  export function registerAgentTools(server, sessions) {
94
- // ---- qa_agent_brief ----
95
- server.registerTool('qa_agent_brief', {
96
- title: 'Agent brief (how to drive Swipium)',
97
- description: 'Read-only, cheap, deterministic. Returns the current-version instruction set for coding agents: the first call for low-context QA (qa_test_this), the polling + report-fetch flow, the NeedsInput/blocker rules, when to stop vs. continue, and which tools are macros vs. low-level escape hatches. Call this once at the start instead of inspecting the source tree. Orientation: this is the FIRST call / how-to; qa_capabilities is the full tool catalog by group; qa_status is the current session/device state; qa_next_best_action is the single next tool to call.',
98
- inputSchema: {},
99
- }, async () => {
100
- const b = agentBrief();
101
- const summary = `Swipium v${b.version} (${b.tools} tools, ${b.prompts} prompts) — agent brief\n` +
102
- `1. ${b.firstCall.tool} ${JSON.stringify(b.firstCall.args)}\n` +
103
- `2. ${b.polling.tool} until state ∈ {${b.polling.terminalStates.join(', ')}}\n` +
104
- `3. ${b.report.tool} { uri: reportUri }\n` +
105
- 'needs_input -> ask the one returned question; otherwise continue.';
106
- return qaOk(b, summary);
107
- });
108
256
  // ---- qa_status ----
109
257
  server.registerTool('qa_status', {
110
- title: 'Session status (compact)',
111
- description: 'A concise snapshot of a session for an agent: project root/framework, bound device + app, budget remaining, counters, recorded actions, findings/notes, last job, and the safe fallbacks taken (workarounds). Read-only, cheap — call between steps instead of re-deriving state. Orientation: this is the current session/device STATE; qa_agent_brief is the first-call how-to; qa_capabilities is the tool catalog by group; qa_next_best_action is the single next tool to call.',
112
- inputSchema: { sessionId: z.string() },
113
- }, async ({ sessionId }) => {
258
+ title: 'Status, orientation, and next step',
259
+ description: 'Without sessionId: first-call orientation: how to drive Swipium (first call, polling, stop rules) plus the tool groups. ' +
260
+ 'With sessionId: compact session state (device, app, budget left, counters, findings, last job, workarounds, readiness) and ' +
261
+ 'nextBestAction: the single next tool to call, with args and why. Pass goal to bias the recommendation. Cheap; call between steps.',
262
+ inputSchema: {
263
+ sessionId: z.string().optional().describe('Omit for orientation; pass to get that session state.'),
264
+ goal: z
265
+ .enum(TEST_GOALS)
266
+ .optional()
267
+ .describe('Bias nextBestAction toward this autopilot goal.'),
268
+ },
269
+ }, async ({ sessionId, goal }) => {
270
+ if (!sessionId) {
271
+ const o = orientation(goal);
272
+ const summary = `Swipium v${o.swipiumVersion} (${o.tools} tools, ${o.prompts} prompts)\n` +
273
+ `1. ${o.firstCall.tool} ${JSON.stringify(o.firstCall.args)}\n` +
274
+ `2. ${o.polling.tool} {sessionId, jobId, waitMs} until status ≠ running; result.state ∈ {${o.polling.terminalStates.join(', ')}}\n` +
275
+ `3. ${o.report.tool} { uri: reportUri }, then stop\n` +
276
+ 'requiresConsent > show it to the user; needs_input > ask the one returned question; otherwise continue.\n' +
277
+ o.capabilityGroups.map((g) => `[${g.group}] ${g.tools.join(', ')}`).join('\n');
278
+ return qaOk(o, summary);
279
+ }
114
280
  const s = sessions.get(sessionId);
115
281
  if (!s)
116
- return qaError({
117
- what: `Unknown sessionId ${sessionId}`,
118
- changedState: false,
119
- retrySafe: true,
120
- nextSteps: ['Call qa_start_session first.'],
121
- });
282
+ return unknownSessionError(sessionId, [
283
+ 'Call qa_status without sessionId for orientation, or qa_start_session / qa_test_this to create a session.',
284
+ ]);
122
285
  const remaining = budgetRemaining(s);
123
286
  const lastJob = [...s.jobs.values()].sort((a, b) => b.startedAt - a.startedAt)[0];
124
- const next = nextBestAction(s);
287
+ const next = nextBestAction(s, goal);
125
288
  const status = {
126
289
  sessionId: s.id,
127
290
  root: s.root,
128
291
  device: s.device ?? null,
129
292
  appId: s.appId ?? null,
130
- mode: s.mode,
293
+ mode: effectiveMode(s),
131
294
  budgetRemaining: remaining,
132
295
  counters: s.counters,
133
296
  recordedActions: s.recordedActions.length,
@@ -148,45 +311,30 @@ export function registerAgentTools(server, sessions) {
148
311
  nextBestAction: next,
149
312
  };
150
313
  const progLine = progressLine(lastJob?.progressDetail);
151
- const summary = `session ${s.id} — ${s.device ?? 'no device'}${s.appId ? ` / ${s.appId}` : ''} (mode=${s.mode})\n` +
314
+ const summary = `session ${s.id}: ${s.device ?? 'no device'}${s.appId ? ` / ${s.appId}` : ''} (mode=${status.mode})\n` +
152
315
  `budget left: ${remaining.minutes}m / ${remaining.actions} actions / ${remaining.screenshots} shots\n` +
153
316
  `recorded=${s.recordedActions.length} findings=${s.findings.length} notes=${s.notes.length}` +
154
317
  (lastJob
155
318
  ? `\nlast job: ${lastJob.kind} [${lastJob.status}]${progLine ? `\n ${progLine}` : lastJob.progress ? ` ${lastJob.progress}` : ''}`
156
319
  : '') +
157
320
  (s.workarounds.length ? `\nworkarounds: ${s.workarounds.length}` : '') +
158
- `\n→ next: ${next.tool} — ${next.why}`;
321
+ `\nnext: ${next.tool} ${JSON.stringify(next.args)} (${next.why})`;
159
322
  return qaOk(status, summary);
160
323
  });
161
- // ---- qa_next_best_action ----
162
- server.registerTool('qa_next_best_action', {
163
- title: 'What should I call next?',
164
- description: 'Given the current session state, returns the single best next tool to call (with args) and why — so an agent does not have to reason out the orchestration sequence. Deterministic, read-only. Pass `goal` to bias the first call toward a specific autopilot intent. Orientation: this answers "what do I do NOW"; qa_agent_brief is the first-call how-to; qa_capabilities is the tool catalog by group; qa_status is the current session/device state.',
165
- inputSchema: {
166
- sessionId: z.string(),
167
- goal: z.enum(['smoke', 'explore', 'create_automation_suite', 'release_gate', 'test_login', 'reproduce_bug']).optional(),
168
- },
169
- }, async ({ sessionId, goal }) => {
170
- const s = sessions.get(sessionId);
171
- if (!s)
172
- return qaError({
173
- what: `Unknown sessionId ${sessionId}`,
174
- changedState: false,
175
- retrySafe: true,
176
- nextSteps: ['Call qa_start_session first.'],
177
- });
178
- const next = nextBestAction(s, goal);
179
- return qaOk({ nextBestAction: next }, `→ ${next.tool} ${JSON.stringify(next.args)} — ${next.why}`);
180
- });
181
324
  // ---- qa_explain_blocker ----
182
325
  server.registerTool('qa_explain_blocker', {
183
326
  title: 'Explain a blocker',
184
- description: 'Turn a typed failure code (e.g. AAB_NEEDS_BUNDLETOOL, WDA_SIGNING_FAILED, NO_BUILD_ARTIFACT) into a user-friendly explanation: what it means, who owns the fix (app / environment / Swipium / user), whether it is retry-safe, whether Swipium can fix it itself, and the exact recovery step. Use to relay a blocker to the user in plain language.',
327
+ description: 'Explain a failureCode in plain language: what it means, who owns the fix (app / environment / Swipium / user), whether ' +
328
+ 'it is retry-safe or Swipium can fix it, and the recovery step.',
185
329
  inputSchema: {
186
- failureCode: z.string().describe('A Swipium failure code (from a tool that returned isError with failureCode).'),
330
+ failureCode: z.string().describe('failureCode from a Swipium error.'),
187
331
  context: z.string().optional(),
332
+ sessionId: z.string().optional().describe('The blocked session (qa_status then moves past the blocker).'),
188
333
  },
189
- }, async ({ failureCode, context }) => {
334
+ }, async ({ failureCode, context, sessionId }) => {
335
+ const sess = sessionId ? sessions.get(sessionId) : undefined;
336
+ if (sessionId && !sess)
337
+ return unknownSessionError(sessionId);
190
338
  const code = failureCode;
191
339
  const info = FAILURES[code];
192
340
  if (!info) {
@@ -197,6 +345,11 @@ export function registerAgentTools(server, sessions) {
197
345
  nextSteps: ['Pass a code surfaced by a Swipium tool (the `failureCode` field of an error).'],
198
346
  });
199
347
  }
348
+ // Mark the blocker explained so qa_status's ladder stops recommending this call.
349
+ if (sess) {
350
+ sess.milestones[BLOCKER_EXPLAINED_MILESTONE] = Date.now();
351
+ sessions.persist(sess);
352
+ }
200
353
  const owner = failureOwner(code);
201
354
  const ownerText = {
202
355
  app: 'the app developer (fix the app)',
@@ -216,7 +369,7 @@ export function registerAgentTools(server, sessions) {
216
369
  howToFix: info.recovery,
217
370
  context: context ?? null,
218
371
  };
219
- const summary = `${code} — ${info.summary}\n` +
372
+ const summary = `${code}: ${info.summary}\n` +
220
373
  `owner: ${ownerText[owner]}; retry-safe: ${info.retrySafe}; Swipium can fix: ${isSelfFixable(code)}\n` +
221
374
  `fix: ${info.recovery}`;
222
375
  return qaOk(explanation, summary);
@@ -224,55 +377,82 @@ export function registerAgentTools(server, sessions) {
224
377
  // ---- qa_continue_from_blocker ----
225
378
  server.registerTool('qa_continue_from_blocker', {
226
379
  title: 'Resume after providing input',
227
- description: 'Consume user-provided input for a NeedsInput question and return the next action. Secret values (passwords/OTP/tokens) are added to the session redaction set immediately (never stored in plaintext, never logged). Non-secret choices (platform, allowOutsideRoot, target) are echoed back as the args to re-invoke qa_test_this / qa_prepare_target with.',
380
+ description: 'Answer a needs_input question and get the next call. Secret values (passwords/OTP/tokens) join the redaction set at ' +
381
+ 'once and are never echoed or logged (flows reference them as ${VAR}); non-secret choices (platform, device, target, allowOutsideRoot) map onto ' +
382
+ 'the re-invocation args. Returns accepted, ignored[] (with how to apply), projectRoot for monorepo_target, and ' +
383
+ 'nextAction.',
228
384
  inputSchema: {
229
385
  sessionId: z.string(),
230
- kind: z.string().describe('The NeedsInput kind being answered (e.g. credentials, preferred_platform, artifact_outside_root).'),
386
+ kind: z.string().describe('The needs_input kind (e.g. credentials, monorepo_target).'),
231
387
  values: z
232
388
  .record(z.union([z.string(), z.boolean()]))
233
389
  .optional()
234
- .describe('Field name → value. Secret fields are redacted on receipt.'),
235
- secretFields: z.array(z.string()).optional().describe('Which value keys are secret (default: password/otp/token-like names).'),
390
+ .describe('Field name > value. Secret fields are redacted on receipt.'),
391
+ secretFields: z.array(z.string()).optional().describe('Secret keys (default: password/otp/token-like names).'),
236
392
  },
237
393
  }, async ({ sessionId, kind, values, secretFields }) => {
238
394
  const s = sessions.get(sessionId);
239
395
  if (!s)
240
- return qaError({
241
- what: `Unknown sessionId ${sessionId}`,
242
- changedState: false,
243
- retrySafe: true,
244
- nextSteps: ['Call qa_start_session first.'],
245
- });
246
- const vals = values ?? {};
247
- const secretRe = /pass|secret|token|otp|pin|cvv|key/i;
396
+ return unknownSessionError(sessionId);
397
+ const vals = { ...(values ?? {}) };
398
+ // Shared secret-name rule (flows/schema.ts); `code` covers OTP fields named code / verificationCode.
399
+ const secretRe = SECRET_VAR_NAME;
400
+ // "test pre-login only" (the credentials question's own fallback) is a DECISION, not an input:
401
+ // mark login out of scope for the session instead of reporting it as an unknown field.
402
+ const declined = (kind === 'credentials' || kind === 'otp_or_manual_verification') && extractDecline(vals);
403
+ if (declined)
404
+ markLoginDeclined(sessions, s);
248
405
  const isSecret = (k) => secretFields?.includes(k) || secretRe.test(k);
249
406
  // Register inputs into the SECURE STORE (P0.5): each maps to a flow variable; secret values
250
407
  // join the redaction set and are NEVER echoed back. Generated flows reference the var name.
251
408
  const accepted = [];
252
409
  const storedVars = [];
253
- const reInvokeArgs = { sessionId: s.id };
410
+ const choices = {};
254
411
  for (const [k, v] of Object.entries(vals)) {
255
412
  const secret = isSecret(k);
256
413
  if (typeof v === 'string' && (secret || /email|user|account|otp|code|token|pin/i.test(k))) {
257
414
  const varName = inputVarName(k);
258
415
  sessions.setInput(s, varName, v, secret, `needs_input:${kind}`);
259
416
  storedVars.push(varName);
260
- accepted.push(`${k} → \${${varName}}${secret ? ' (redacted)' : ''}`);
417
+ accepted.push(`${k} > \${${varName}}${secret ? ' (redacted)' : ''}`);
261
418
  }
262
419
  else {
263
- accepted.push(`${k}=${v}`);
264
- if (k === 'platform')
265
- reInvokeArgs.platform = v;
266
- if (k === 'allowOutsideRoot')
267
- reInvokeArgs.allowOutsideRoot = v;
268
- if (k === 'target' || k === 'device')
269
- reInvokeArgs.device = v;
270
- if (k === 'developmentTeam' || k === 'provisioningProfile' || k === 'serviceEndpoint')
271
- reInvokeArgs[k] = v;
420
+ choices[k] = v;
272
421
  }
273
422
  }
423
+ // H1: map non-secret choices onto args the re-invoked tool ACTUALLY accepts; anything it
424
+ // doesn't consume is reported back as `ignored` (with how to apply it), never silently dropped.
425
+ const mapped = mapBlockerChoices(kind, choices, s.root);
426
+ if (mapped.error) {
427
+ sessions.persist(s);
428
+ return qaError({
429
+ failureCode: 'INVALID_ARGUMENT',
430
+ what: mapped.error,
431
+ changedState: storedVars.length > 0,
432
+ retrySafe: true,
433
+ nextSteps: [
434
+ 'Re-call qa_continue_from_blocker with target set to one of the offered candidate paths (absolute, or relative to the project root).',
435
+ ],
436
+ });
437
+ }
438
+ for (const [k, v] of Object.entries(choices))
439
+ if (!mapped.ignored.some((i) => i.field === k))
440
+ accepted.push(`${k}=${v}`);
441
+ if (mapped.projectRoot) {
442
+ // qa_test_this resolves an existing session's root from the SESSION (projectRoot is only
443
+ // read when creating one), so adopt the chosen app directory as the effective root; the
444
+ // same thing the single-candidate discovery path does.
445
+ s.root = mapped.projectRoot;
446
+ s.chosenTarget = mapped.projectRoot; // qa_test_this skips the monorepo question for this root
447
+ }
448
+ // Persisted answer marker: qa_status stops replaying the resume call once answered.
449
+ s.milestones[BLOCKER_ANSWERED_MILESTONE] = Date.now();
274
450
  sessions.persist(s);
275
451
  sessions.addWorkaround(s, `resumed from "${kind}" blocker with: ${accepted.join(', ') || '(no values)'}`);
452
+ const reInvokeArgs = { sessionId: s.id, ...mapped.args };
453
+ // Replay the ORIGINAL qa_test_this intent (goal/goalText/flags). A resume that drops it
454
+ // silently downgrades e.g. release_gate to the default smoke.
455
+ const intent = recallTestThisIntent(s);
276
456
  // Decide the resume action by kind.
277
457
  const reInvokeKinds = new Set([
278
458
  'artifact_outside_root',
@@ -284,27 +464,170 @@ export function registerAgentTools(server, sessions) {
284
464
  ]);
285
465
  // Every resume is a DIRECTLY executable call. Credentials/OTP are now registered as secure
286
466
  // inputs, so re-invoking the autopilot drives the authenticated flows with them (the macro
287
- // tool resolves the already-prepared session/device and continues) — no bare qa_act guess.
288
- const resume = kind === 'credentials' || kind === 'otp_or_manual_verification'
467
+ // tool resolves the already-prepared session/device and continues), not a bare qa_act guess.
468
+ const resume = declined
289
469
  ? {
290
470
  tool: 'qa_test_this',
291
- why: 'credentials registered (redacted) — re-run the autopilot to drive authenticated flows with them',
292
- args: { sessionId: s.id, mode: 'execute', stopOnNeedsInput: false },
471
+ why: kind === 'credentials'
472
+ ? 'login marked out of scope for this session. Continue with pre-login coverage only (authenticated flows are reported as blocked, not failed)'
473
+ : 'verification marked out of scope; flows behind it are skipped and reported as blocked',
474
+ args: { mode: 'execute', ...intent, sessionId: s.id, stopOnNeedsInput: false },
293
475
  }
294
- : reInvokeKinds.has(kind)
295
- ? { tool: 'qa_test_this', why: 're-run orchestration with your choice applied', args: { mode: 'execute', ...reInvokeArgs } }
296
- : { tool: 'qa_test_this', why: 'resume orchestration', args: { mode: 'execute', ...reInvokeArgs } };
476
+ : kind === 'credentials' || kind === 'otp_or_manual_verification'
477
+ ? {
478
+ tool: 'qa_test_this',
479
+ why: storedVars.length
480
+ ? 'credentials registered (redacted). Re-run the autopilot to drive authenticated flows with them'
481
+ : 'no credentials were provided. Re-run the autopilot (pre-login coverage)',
482
+ args: { mode: 'execute', ...intent, sessionId: s.id, stopOnNeedsInput: false },
483
+ }
484
+ : kind === 'destructive_exploration_approval' && mapped.approveDestructive
485
+ ? {
486
+ // qa_test_this has no destructive switch; the supported path is qa_explore's
487
+ // candidate-bound approval: discover candidates first, then approve one exactly.
488
+ tool: 'qa_explore',
489
+ why: 'destructive exploration approved. Discover exact destructive candidates first (dry run), then approve one with safeMode:"approved_destructive_candidate"',
490
+ args: { sessionId: s.id, safeMode: 'dry_run_destructive' },
491
+ }
492
+ : reInvokeKinds.has(kind)
493
+ ? {
494
+ tool: 'qa_test_this',
495
+ why: 're-run orchestration with your choice applied',
496
+ args: { mode: 'execute', ...intent, ...reInvokeArgs },
497
+ }
498
+ : { tool: 'qa_test_this', why: 'resume orchestration', args: { mode: 'execute', ...intent, ...reInvokeArgs } };
297
499
  return qaOk({
298
500
  kind,
299
501
  accepted,
502
+ ...(declined ? { loginOutOfScope: true } : {}),
300
503
  storedVariables: storedVars,
504
+ ignored: mapped.ignored,
505
+ ...(mapped.projectRoot ? { projectRoot: mapped.projectRoot } : {}),
301
506
  nextAction: resume,
302
507
  secretsRegistered: accepted.filter((a) => a.includes('redacted')).length,
303
- }, `Accepted ${accepted.length} field(s)${accepted.some((a) => a.includes('redacted')) ? ' (secrets redacted)' : ''}.` +
508
+ }, (declined ? 'Login marked out of scope for this session. Testing continues pre-login only.\n' : '') +
509
+ `Accepted ${accepted.length} field(s)${accepted.some((a) => a.includes('redacted')) ? ' (secrets redacted)' : ''}.` +
304
510
  (storedVars.length ? `\nstored for replay: ${storedVars.join(', ')}` : '') +
305
- `\n→ next: ${resume.tool} ${JSON.stringify(resume.args)} — ${resume.why}`);
511
+ (mapped.ignored.length ? `\nnot applied: ${mapped.ignored.map((i) => `${i.field} (${i.howToApply})`).join('; ')}` : '') +
512
+ `\nnext: ${resume.tool} ${JSON.stringify(resume.args)} (${resume.why})`);
306
513
  });
307
514
  }
515
+ /** The qa_test_this input keys a blocker resume may set (mirrors its zod schema in
516
+ * src/tools/testThis.ts; test/blockerResume.test.ts asserts they stay in lockstep). */
517
+ export const TEST_THIS_RESUME_KEYS = ['projectRoot', 'platform', 'device', 'allowOutsideRoot'];
518
+ function realOrSelf(p) {
519
+ try {
520
+ return realpathSync(p);
521
+ }
522
+ catch {
523
+ return resolvePath(p);
524
+ }
525
+ }
526
+ function truthy(v) {
527
+ return v === true || (typeof v === 'string' && /^(true|yes|1)$/i.test(v.trim()));
528
+ }
529
+ /** Map a NeedsInput answer's non-secret choices onto re-invocation args (H1). Pure except for the
530
+ * existence check on a monorepo target. Exported for tests. */
531
+ export function mapBlockerChoices(kind, choices, sessionRoot) {
532
+ const out = { args: {}, ignored: [] };
533
+ const ignore = (field, value, howToApply) => out.ignored.push({ field, value, howToApply });
534
+ for (const [k, v] of Object.entries(choices)) {
535
+ switch (k) {
536
+ case 'platform':
537
+ if (v === 'android' || v === 'ios')
538
+ out.args.platform = v;
539
+ else
540
+ ignore(k, v, 'platform must be "android" or "ios"');
541
+ break;
542
+ case 'allowOutsideRoot':
543
+ out.args.allowOutsideRoot = truthy(v);
544
+ break;
545
+ case 'device':
546
+ if (typeof v === 'string' && v.trim())
547
+ out.args.device = v.trim();
548
+ else
549
+ ignore(k, v, 'device must be a serial/UDID string');
550
+ break;
551
+ case 'target': {
552
+ if (kind === 'monorepo_target') {
553
+ // The monorepo question offers app DIRECTORIES. This is the project root, not a device.
554
+ if (typeof v !== 'string' || !v.trim()) {
555
+ out.error = 'monorepo target must be an app directory path';
556
+ break;
557
+ }
558
+ const raw = v.trim();
559
+ if (raw === '~' || raw.startsWith('~/') || raw.startsWith('~' + sep)) {
560
+ out.error = `monorepo target "${v}" must be one of the offered app directories inside the project root (no ~ paths)`;
561
+ break;
562
+ }
563
+ const abs = isAbsolute(raw) ? resolvePath(raw) : resolvePath(sessionRoot, raw);
564
+ let isDir = false;
565
+ try {
566
+ isDir = statSync(abs).isDirectory();
567
+ }
568
+ catch {
569
+ isDir = false;
570
+ }
571
+ if (!isDir) {
572
+ out.error = `monorepo target "${v}" is not an existing directory (resolved to ${abs})`;
573
+ break;
574
+ }
575
+ // The offered candidates are always app directories discovered UNDER the project root
576
+ // (findMobileApps), so containment in the (real) root is the constraint: an answer can
577
+ // never re-point the session at `/`, the home dir, or an unrelated tree.
578
+ const realRoot = realOrSelf(sessionRoot);
579
+ const realTarget = realOrSelf(abs);
580
+ if (realTarget !== realRoot && !realTarget.startsWith(realRoot.endsWith(sep) ? realRoot : realRoot + sep)) {
581
+ out.error = `monorepo target "${v}" (${realTarget}) is outside the project root ${realRoot}; pick one of the offered app directories`;
582
+ break;
583
+ }
584
+ out.projectRoot = abs;
585
+ out.args.projectRoot = abs;
586
+ }
587
+ else if (typeof v === 'string' && v.trim()) {
588
+ out.args.device = v.trim();
589
+ }
590
+ else {
591
+ ignore(k, v, 'target must be a device serial/UDID string');
592
+ }
593
+ break;
594
+ }
595
+ case 'approveDestructive':
596
+ out.approveDestructive = truthy(v);
597
+ if (!out.approveDestructive)
598
+ ignore(k, v, 'exploration stays non-destructive (the default)');
599
+ break;
600
+ case 'developmentTeam':
601
+ ignore(k, v, 'qa_test_this does not take a signing team. Set ios.wda.developmentTeam in .swipium/config.json (or DEVELOPMENT_TEAM in the server environment), then resume');
602
+ break;
603
+ case 'provisioningProfile':
604
+ ignore(k, v, 'Swipium builds for the simulator and does not consume a provisioning profile. Configure it in Xcode if your scheme needs one');
605
+ break;
606
+ case 'serviceEndpoint':
607
+ ignore(k, v, 'Swipium cannot route the app to a service endpoint. Configure it in the app build/env (e.g. a staging config), then resume');
608
+ break;
609
+ default:
610
+ ignore(k, v, `not an input qa_test_this accepts (accepted: ${TEST_THIS_RESUME_KEYS.join(', ')})`);
611
+ }
612
+ }
613
+ return out;
614
+ }
615
+ const DECLINE_KEYS = /^(choice|fallback|fallbackOption|option|answer|decision|selected|preLoginOnly|decline|declined|skip)$/i;
616
+ const DECLINE_TEXT = /pre-?login only|stay pre-?login|decline|no credentials|skip (login|flows requiring verification)|^skip$/i;
617
+ /** Detect (and remove from `vals`) a "decline the input" answer: a fallback-option string such as
618
+ * "test pre-login only", or a boolean preLoginOnly/decline flag. Exported for tests. */
619
+ export function extractDecline(vals) {
620
+ let declined = false;
621
+ for (const [k, v] of Object.entries(vals)) {
622
+ if (!DECLINE_KEYS.test(k))
623
+ continue;
624
+ if ((typeof v === 'boolean' && v && /preLoginOnly|decline|skip/i.test(k)) || (typeof v === 'string' && DECLINE_TEXT.test(v.trim()))) {
625
+ declined = true;
626
+ delete vals[k];
627
+ }
628
+ }
629
+ return declined;
630
+ }
308
631
  /** Map a NeedsInput field name to its canonical Swipium flow variable (P0.5). */
309
632
  function inputVarName(field) {
310
633
  const f = field.toLowerCase();