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,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,127 +22,333 @@ 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
- return { tool: 'qa_job_status', why: `job ${lastJob.jobId} (${lastJob.kind}) is still running`, args: { sessionId: sid, jobId: lastJob.jobId } };
109
+ return {
110
+ tool: 'qa_job_status',
111
+ why: `job ${lastJob.jobId} (${lastJob.kind}) is still running`,
112
+ args: { sessionId: sid, jobId: lastJob.jobId },
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.
25
150
  if (!s.device)
26
- return { tool: 'qa_test_this', why: goal ? `no device/app prepared yet — run the autopilot for goal "${goal}"` : 'no device/app prepared yet — orchestrate setup', args: { sessionId: sid, mode: 'execute', ...(goal ? { goal } : {}) } };
151
+ return {
152
+ tool: 'qa_test_this',
153
+ why: goal ? `no device/app prepared yet, run the autopilot for goal "${goal}"` : 'no device/app prepared yet, orchestrate setup',
154
+ args: { sessionId: sid, mode: 'execute', ...(goal ? { goal } : {}) },
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).
27
158
  if (!s.appId)
28
- return { tool: 'qa_prepare_target', why: 'device bound but no app launched', args: { sessionId: sid } };
29
- if (s.recordedActions.length === 0)
30
- return { tool: 'qa_smoke', why: 'app is up but nothing exercised yet — run a smoke pass', args: { sessionId: sid } };
31
- if (s.findings.length > 0)
32
- return { tool: 'qa_report', why: `${s.findings.length} finding(s) recorded — summarize with evidence`, args: { sessionId: sid } };
33
- if (s.recordedActions.length > 0)
34
- return { tool: 'qa_suite_generate', why: 'actions recorded — turn the run into a durable POM suite', args: { sessionId: sid } };
35
- return { tool: 'qa_report', why: 'wrap up and report', args: { sessionId: sid } };
36
- }
37
- /** The compact, current-version instruction set for coding agents (Milestone A). Pure + deterministic
38
- * so it is snapshot-testable; it references the live SWIPIUM_VERSION / tool / prompt counts. */
39
- export function agentBrief() {
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))
176
+ return {
177
+ tool: 'qa_generate',
178
+ why: 'actions recorded, turn the run into a durable POM suite',
179
+ args: { sessionId: sid, target: 'suite' },
180
+ };
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
+ };
196
+ }
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) {
40
221
  return {
41
- version: SWIPIUM_VERSION,
222
+ orientation: true,
223
+ swipiumVersion: SWIPIUM_VERSION,
42
224
  tools: TOOL_COUNT,
43
225
  prompts: PROMPT_COUNT,
44
- purpose: 'Turn a coding agent into a mobile QA tester. For a zero-context "test this" request, call the macro tool first.',
45
226
  firstCall: {
46
227
  tool: 'qa_test_this',
47
- args: { mode: 'execute' },
48
- 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.',
49
230
  },
50
231
  polling: {
51
232
  tool: 'qa_job_status',
52
- 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)',
53
235
  terminalStates: ['completed', 'blocked', 'unsafe', 'needs_input'],
54
236
  },
55
- report: {
56
- tool: 'qa_get_artifact',
57
- args: { uri: '<reportUri from the terminal result>' },
58
- rule: 'Fetch reportUri instead of calling more tools, unless nextRecommendedAction says otherwise.',
59
- },
237
+ report: { tool: 'qa_get_artifact', args: { uri: '<reportUri from the terminal result>' } },
60
238
  goals: TEST_GOALS,
61
- needsInputRule: 'Relay exactly the one returned question (with its fields/secret flags). Do not invent extra questions. Resume with the response\'s `resume` call.',
62
- blockerRule: 'Relay failureCode, owner, what Swipium tried (attempted/workaroundsAttempted), and how to fix it. A build failure is not a test failure.',
63
- 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.',
64
- continueRule: 'For completed/blocked/unsafe states, summarize from the structured envelope and the report — no extra Swipium calls are required.',
65
- 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.',
66
- macroTools: ['qa_test_this', 'qa_smoke', 'qa_explore', 'qa_report', 'qa_app_map_read'],
67
- escapeHatches: ['qa_doctor', 'qa_start_session', 'qa_detect_context', 'qa_plan', 'qa_prepare_target', 'qa_prepare_ios_target', 'qa_ios', 'qa_wda', 'qa_act', 'qa_snapshot'],
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
+ },
68
253
  };
69
254
  }
70
255
  export function registerAgentTools(server, sessions) {
71
- // ---- qa_agent_brief ----
72
- server.registerTool('qa_agent_brief', {
73
- title: 'Agent brief (how to drive Swipium)',
74
- 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.',
75
- inputSchema: {},
76
- }, async () => {
77
- const b = agentBrief();
78
- const summary = `Swipium v${b.version} (${b.tools} tools, ${b.prompts} prompts) — agent brief\n` +
79
- `1. ${b.firstCall.tool} ${JSON.stringify(b.firstCall.args)}\n` +
80
- `2. ${b.polling.tool} until state ∈ {${b.polling.terminalStates.join(', ')}}\n` +
81
- `3. ${b.report.tool} { uri: reportUri }\n` +
82
- 'needs_input -> ask the one returned question; otherwise continue.';
83
- return qaOk(b, summary);
84
- });
85
256
  // ---- qa_status ----
86
257
  server.registerTool('qa_status', {
87
- title: 'Session status (compact)',
88
- 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.',
89
- inputSchema: { sessionId: z.string() },
90
- }, 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
+ }
91
280
  const s = sessions.get(sessionId);
92
281
  if (!s)
93
- return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first.'] });
282
+ return unknownSessionError(sessionId, [
283
+ 'Call qa_status without sessionId for orientation, or qa_start_session / qa_test_this to create a session.',
284
+ ]);
94
285
  const remaining = budgetRemaining(s);
95
286
  const lastJob = [...s.jobs.values()].sort((a, b) => b.startedAt - a.startedAt)[0];
96
- const next = nextBestAction(s);
287
+ const next = nextBestAction(s, goal);
97
288
  const status = {
98
- sessionId: s.id, root: s.root, device: s.device ?? null, appId: s.appId ?? null, mode: s.mode,
99
- budgetRemaining: remaining, counters: s.counters,
100
- recordedActions: s.recordedActions.length, findings: s.findings.length, notes: s.notes.length,
101
- workarounds: s.workarounds, inputsProvided: s.inputs.map((i) => i.varName),
289
+ sessionId: s.id,
290
+ root: s.root,
291
+ device: s.device ?? null,
292
+ appId: s.appId ?? null,
293
+ mode: effectiveMode(s),
294
+ budgetRemaining: remaining,
295
+ counters: s.counters,
296
+ recordedActions: s.recordedActions.length,
297
+ findings: s.findings.length,
298
+ notes: s.notes.length,
299
+ workarounds: s.workarounds,
300
+ inputsProvided: s.inputs.map((i) => i.varName),
102
301
  readiness: readinessForSession(s),
103
- lastJob: lastJob ? { jobId: lastJob.jobId, kind: lastJob.kind, status: lastJob.status, progress: lastJob.progress, progressDetail: lastJob.progressDetail ?? null } : null,
302
+ lastJob: lastJob
303
+ ? {
304
+ jobId: lastJob.jobId,
305
+ kind: lastJob.kind,
306
+ status: lastJob.status,
307
+ progress: lastJob.progress,
308
+ progressDetail: lastJob.progressDetail ?? null,
309
+ }
310
+ : null,
104
311
  nextBestAction: next,
105
312
  };
106
313
  const progLine = progressLine(lastJob?.progressDetail);
107
- 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` +
108
315
  `budget left: ${remaining.minutes}m / ${remaining.actions} actions / ${remaining.screenshots} shots\n` +
109
316
  `recorded=${s.recordedActions.length} findings=${s.findings.length} notes=${s.notes.length}` +
110
- (lastJob ? `\nlast job: ${lastJob.kind} [${lastJob.status}]${progLine ? `\n ${progLine}` : lastJob.progress ? ` ${lastJob.progress}` : ''}` : '') +
317
+ (lastJob
318
+ ? `\nlast job: ${lastJob.kind} [${lastJob.status}]${progLine ? `\n ${progLine}` : lastJob.progress ? ` ${lastJob.progress}` : ''}`
319
+ : '') +
111
320
  (s.workarounds.length ? `\nworkarounds: ${s.workarounds.length}` : '') +
112
- `\n→ next: ${next.tool} — ${next.why}`;
321
+ `\nnext: ${next.tool} ${JSON.stringify(next.args)} (${next.why})`;
113
322
  return qaOk(status, summary);
114
323
  });
115
- // ---- qa_next_best_action ----
116
- server.registerTool('qa_next_best_action', {
117
- title: 'What should I call next?',
118
- 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.',
119
- inputSchema: { sessionId: z.string(), goal: z.enum(['smoke', 'explore', 'create_automation_suite', 'release_gate', 'test_login', 'reproduce_bug']).optional() },
120
- }, async ({ sessionId, goal }) => {
121
- const s = sessions.get(sessionId);
122
- if (!s)
123
- return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first.'] });
124
- const next = nextBestAction(s, goal);
125
- return qaOk({ nextBestAction: next }, `→ ${next.tool} ${JSON.stringify(next.args)} — ${next.why}`);
126
- });
127
324
  // ---- qa_explain_blocker ----
128
325
  server.registerTool('qa_explain_blocker', {
129
326
  title: 'Explain a blocker',
130
- 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.',
131
329
  inputSchema: {
132
- 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.'),
133
331
  context: z.string().optional(),
332
+ sessionId: z.string().optional().describe('The blocked session (qa_status then moves past the blocker).'),
134
333
  },
135
- }, async ({ failureCode, context }) => {
334
+ }, async ({ failureCode, context, sessionId }) => {
335
+ const sess = sessionId ? sessions.get(sessionId) : undefined;
336
+ if (sessionId && !sess)
337
+ return unknownSessionError(sessionId);
136
338
  const code = failureCode;
137
339
  const info = FAILURES[code];
138
340
  if (!info) {
139
- return qaError({ what: `Unknown failure code "${failureCode}"`, changedState: false, retrySafe: true, nextSteps: ['Pass a code surfaced by a Swipium tool (the `failureCode` field of an error).'] });
341
+ return qaError({
342
+ what: `Unknown failure code "${failureCode}"`,
343
+ changedState: false,
344
+ retrySafe: true,
345
+ nextSteps: ['Pass a code surfaced by a Swipium tool (the `failureCode` field of an error).'],
346
+ });
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);
140
352
  }
141
353
  const owner = failureOwner(code);
142
354
  const ownerText = {
@@ -146,12 +358,18 @@ export function registerAgentTools(server, sessions) {
146
358
  user: 'you (provide input/approval/test data)',
147
359
  };
148
360
  const explanation = {
149
- failureCode: code, bucket: info.bucket, owner, severity: info.severity,
150
- retrySafe: info.retrySafe, canSwipiumFix: isSelfFixable(code),
151
- whatItMeans: info.summary, whoFixesIt: ownerText[owner], howToFix: info.recovery,
361
+ failureCode: code,
362
+ bucket: info.bucket,
363
+ owner,
364
+ severity: info.severity,
365
+ retrySafe: info.retrySafe,
366
+ canSwipiumFix: isSelfFixable(code),
367
+ whatItMeans: info.summary,
368
+ whoFixesIt: ownerText[owner],
369
+ howToFix: info.recovery,
152
370
  context: context ?? null,
153
371
  };
154
- const summary = `${code} — ${info.summary}\n` +
372
+ const summary = `${code}: ${info.summary}\n` +
155
373
  `owner: ${ownerText[owner]}; retry-safe: ${info.retrySafe}; Swipium can fix: ${isSelfFixable(code)}\n` +
156
374
  `fix: ${info.recovery}`;
157
375
  return qaOk(explanation, summary);
@@ -159,66 +377,257 @@ export function registerAgentTools(server, sessions) {
159
377
  // ---- qa_continue_from_blocker ----
160
378
  server.registerTool('qa_continue_from_blocker', {
161
379
  title: 'Resume after providing input',
162
- 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.',
163
384
  inputSchema: {
164
385
  sessionId: z.string(),
165
- kind: z.string().describe('The NeedsInput kind being answered (e.g. credentials, preferred_platform, artifact_outside_root).'),
166
- values: z.record(z.union([z.string(), z.boolean()])).optional().describe('Field name → value. Secret fields are redacted on receipt.'),
167
- secretFields: z.array(z.string()).optional().describe('Which value keys are secret (default: password/otp/token-like names).'),
386
+ kind: z.string().describe('The needs_input kind (e.g. credentials, monorepo_target).'),
387
+ values: z
388
+ .record(z.union([z.string(), z.boolean()]))
389
+ .optional()
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).'),
168
392
  },
169
393
  }, async ({ sessionId, kind, values, secretFields }) => {
170
394
  const s = sessions.get(sessionId);
171
395
  if (!s)
172
- return qaError({ what: `Unknown sessionId ${sessionId}`, changedState: false, retrySafe: true, nextSteps: ['Call qa_start_session first.'] });
173
- const vals = values ?? {};
174
- const secretRe = /pass|secret|token|otp|pin|cvv|key/i;
175
- const isSecret = (k) => (secretFields?.includes(k)) || secretRe.test(k);
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);
405
+ const isSecret = (k) => secretFields?.includes(k) || secretRe.test(k);
176
406
  // Register inputs into the SECURE STORE (P0.5): each maps to a flow variable; secret values
177
407
  // join the redaction set and are NEVER echoed back. Generated flows reference the var name.
178
408
  const accepted = [];
179
409
  const storedVars = [];
180
- const reInvokeArgs = { sessionId: s.id };
410
+ const choices = {};
181
411
  for (const [k, v] of Object.entries(vals)) {
182
412
  const secret = isSecret(k);
183
413
  if (typeof v === 'string' && (secret || /email|user|account|otp|code|token|pin/i.test(k))) {
184
414
  const varName = inputVarName(k);
185
415
  sessions.setInput(s, varName, v, secret, `needs_input:${kind}`);
186
416
  storedVars.push(varName);
187
- accepted.push(`${k} → \${${varName}}${secret ? ' (redacted)' : ''}`);
417
+ accepted.push(`${k} > \${${varName}}${secret ? ' (redacted)' : ''}`);
188
418
  }
189
419
  else {
190
- accepted.push(`${k}=${v}`);
191
- if (k === 'platform')
192
- reInvokeArgs.platform = v;
193
- if (k === 'allowOutsideRoot')
194
- reInvokeArgs.allowOutsideRoot = v;
195
- if (k === 'target' || k === 'device')
196
- reInvokeArgs.device = v;
197
- if (k === 'developmentTeam' || k === 'provisioningProfile' || k === 'serviceEndpoint')
198
- reInvokeArgs[k] = v;
420
+ choices[k] = v;
199
421
  }
200
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();
201
450
  sessions.persist(s);
202
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);
203
456
  // Decide the resume action by kind.
204
457
  const reInvokeKinds = new Set([
205
- 'artifact_outside_root', 'preferred_platform', 'monorepo_target',
458
+ 'artifact_outside_root',
459
+ 'preferred_platform',
460
+ 'monorepo_target',
206
461
  'signing_team',
207
- 'destructive_exploration_approval', 'external_service_required',
462
+ 'destructive_exploration_approval',
463
+ 'external_service_required',
208
464
  ]);
209
465
  // Every resume is a DIRECTLY executable call. Credentials/OTP are now registered as secure
210
466
  // inputs, so re-invoking the autopilot drives the authenticated flows with them (the macro
211
- // tool resolves the already-prepared session/device and continues) — no bare qa_act guess.
212
- const resume = kind === 'credentials' || kind === 'otp_or_manual_verification'
213
- ? { tool: 'qa_test_this', why: 'credentials registered (redacted) — re-run the autopilot to drive authenticated flows with them', args: { sessionId: s.id, mode: 'execute', stopOnNeedsInput: false } }
214
- : reInvokeKinds.has(kind)
215
- ? { tool: 'qa_test_this', why: 're-run orchestration with your choice applied', args: { mode: 'execute', ...reInvokeArgs } }
216
- : { tool: 'qa_test_this', why: 'resume orchestration', args: { mode: 'execute', ...reInvokeArgs } };
217
- return qaOk({ kind, accepted, storedVariables: storedVars, nextAction: resume, secretsRegistered: accepted.filter((a) => a.includes('redacted')).length }, `Accepted ${accepted.length} field(s)${accepted.some((a) => a.includes('redacted')) ? ' (secrets redacted)' : ''}.` +
467
+ // tool resolves the already-prepared session/device and continues), not a bare qa_act guess.
468
+ const resume = declined
469
+ ? {
470
+ tool: 'qa_test_this',
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 },
475
+ }
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 } };
499
+ return qaOk({
500
+ kind,
501
+ accepted,
502
+ ...(declined ? { loginOutOfScope: true } : {}),
503
+ storedVariables: storedVars,
504
+ ignored: mapped.ignored,
505
+ ...(mapped.projectRoot ? { projectRoot: mapped.projectRoot } : {}),
506
+ nextAction: resume,
507
+ secretsRegistered: accepted.filter((a) => a.includes('redacted')).length,
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)' : ''}.` +
218
510
  (storedVars.length ? `\nstored for replay: ${storedVars.join(', ')}` : '') +
219
- `\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})`);
220
513
  });
221
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
+ }
222
631
  /** Map a NeedsInput field name to its canonical Swipium flow variable (P0.5). */
223
632
  function inputVarName(field) {
224
633
  const f = field.toLowerCase();