mobster-cli 0.4.3__py3-none-any.whl

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 (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,996 @@
1
+ """Proof-gated completion: the task contract, the declared-commit policy, the source ledger and the DONE gate."""
2
+
3
+ import re
4
+ from types import SimpleNamespace
5
+
6
+ from .contract_prompt import SCHEMA, SYSTEM
7
+ from .contract_screen import (
8
+ ACT_LABEL,
9
+ ADDRESS,
10
+ ADDS,
11
+ ALREADY,
12
+ APP_STORE,
13
+ ATOM,
14
+ COMMIT_WORDS,
15
+ COMPUTED_ITEM,
16
+ CONFIRM_ITEM,
17
+ CONFIRM_WORDS,
18
+ DELIVERY,
19
+ EMPTY_STATE,
20
+ FAMILIES,
21
+ FOLDERS,
22
+ GENERIC,
23
+ KINDS,
24
+ LOW_TURNS,
25
+ MAX_ITEMS,
26
+ MAX_REOPENS,
27
+ MESSAGE,
28
+ MESSAGE_ACTS,
29
+ MONEY,
30
+ MONEY_ACTS,
31
+ MORE_THAN_ONE,
32
+ NO_RESULTS,
33
+ NONE_VALUE,
34
+ NOTEBOOK_KINDS,
35
+ OBJECT_ACTS,
36
+ OBJECT_STOP,
37
+ OFF_VALUES,
38
+ ON_VALUES,
39
+ OTHERS,
40
+ ROW_ROLES,
41
+ STATE_ACTS,
42
+ STATE_ON,
43
+ SUMMARY_ITEM,
44
+ SWITCH_ROLES,
45
+ TAINT_CHARS,
46
+ TO_FIELD,
47
+ WRITE_OPS,
48
+ Gate,
49
+ Item,
50
+ Pending,
51
+ addressed_to,
52
+ as_float,
53
+ bounds,
54
+ card_of,
55
+ center_y,
56
+ commit_amount,
57
+ composer_field,
58
+ control_state,
59
+ counterparty,
60
+ deletes,
61
+ element_text,
62
+ is_on,
63
+ is_search_field,
64
+ label_head,
65
+ label_verbs,
66
+ list_rows,
67
+ message_key,
68
+ name_match,
69
+ overlaps,
70
+ plain,
71
+ point_in,
72
+ quoted,
73
+ receipt_patterns,
74
+ recipient,
75
+ row_name,
76
+ row_of,
77
+ same_control,
78
+ screen_title,
79
+ shown_count,
80
+ shows,
81
+ squash,
82
+ store_verbs,
83
+ typed_query,
84
+ )
85
+ from .contract_view import hidden_row, holds_atom
86
+
87
+
88
+ def compile_contract(client, request, apps=(), timeout=60):
89
+ """The request's Contract (one text-only call at reasoning "none"), or None if the call fails or yields no items."""
90
+ messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": [
91
+ {"type": "text", "text": f"Request: {request}\n\nApps: " + ", ".join(apps)}]}]
92
+ # A client that takes reasoning per call is not touched: the first decision may be in flight on it.
93
+ per_call = getattr(client, "per_call_reasoning", False) is True
94
+ saved = None if per_call else getattr(client, "reasoning", None)
95
+ if saved is not None:
96
+ client.reasoning = "none"
97
+ try:
98
+ out, _ = (client.complete(messages, SCHEMA, timeout=timeout, reasoning="none") if per_call
99
+ else client.complete(messages, SCHEMA, timeout=timeout))
100
+ except Exception:
101
+ return None
102
+ finally:
103
+ if saved is not None:
104
+ client.reasoning = saved
105
+ contract = Contract.from_items((out or {}).get("items") or (), request)
106
+ return contract if contract.items else None
107
+
108
+
109
+ def excludes_others(item):
110
+ """A FORBID whose words rule out only the acts the request doesn't name (OTHERS): "Delete nothing else."."""
111
+ return item.kind == "FORBID" and bool(OTHERS.search(f"{item.quote} {item.what}"))
112
+
113
+
114
+ # A quoted name followed by one of these is where a commit puts its object, not the object ("to 'Want to Try' list").
115
+ DESTINATION_AFTER = re.compile(r"\s*(?:list|folder|playlist|album|board|collection|category|label|tag|shelf|space)s?\b",
116
+ re.I)
117
+ # A quoted name after these is how the act is done, not what it acts on ("reaction 'Insightful'").
118
+ PARAMETER_BEFORE = re.compile(r"\b(?:reaction|react(?:ion)? with|with (?:a|the) reaction)\s*$", re.I)
119
+ # The top and bottom bars: a control there is a toolbar's, not a list's.
120
+ BAR_TOP, BAR_BOTTOM = .13, .9
121
+
122
+
123
+ class Contract:
124
+ def __init__(self, items, request, dropped=()):
125
+ self.items, self.request, self.dropped = list(items), request, list(dropped)
126
+ self.screens = [] # (step, app, [(text, editable, role)], keyboard)
127
+ self.typed = [] # {step, app, text, field, search}
128
+ self.empty_searches = [] # (step, app, query)
129
+ self.visited = set()
130
+ self.pending = []
131
+ self.money = [] # receipted money commits: (amount, names, step)
132
+ self.events = [] # receipts and refusals, for the trace
133
+ # Parallel to ``screens``; the Mac app quotes proof as the screen showed it, not folded.
134
+ self.shown = []
135
+ # A long task's notebook (longform/notebook.py): its REPORT items are proven by what it holds; None otherwise.
136
+ self.notebook = None
137
+ # app -> screens recorded when this run first wrote or committed there;
138
+ # a WRITE is proven only by a later screen.
139
+ self.first_writes = {}
140
+ # (step, app, rows): the rows of each screen that sit in a list; a page's own title is not a row.
141
+ self.listed = []
142
+
143
+ @classmethod
144
+ def from_items(cls, raw, request):
145
+ """Validated items: kinds and acts known, and a COMMIT whose quote is not verbatim in the request dropped."""
146
+ items, dropped, asked = [], [], plain(request)
147
+ for entry in list(raw)[:MAX_ITEMS]:
148
+ if not isinstance(entry, dict) or entry.get("kind") not in KINDS:
149
+ continue
150
+ text = {k: " ".join(str(entry.get(k) or "").split()) for k in
151
+ ("app", "what", "payload", "act", "count", "condition", "quote")}
152
+ if not text["what"]:
153
+ continue
154
+ item = Item(id=len(items) + 1, kind=entry["kind"], **text)
155
+ if item.kind in ("COMMIT", "FORBID") and item.act not in FAMILIES:
156
+ dropped.append(item)
157
+ continue
158
+ if item.kind == "COMMIT" and not (plain(item.quote) and plain(item.quote) in asked):
159
+ dropped.append(item)
160
+ continue
161
+ if item.kind != "COMMIT":
162
+ item.count = ""
163
+ items.append(item)
164
+ for index, item in enumerate(items):
165
+ item.id = index + 1
166
+ return cls(items, request, dropped)
167
+
168
+ def lines(self):
169
+ """The contract as the model sees it: ``6. [open] COMMIT pay (SplitPay): pending requests [each]``."""
170
+ out = []
171
+ for item in self.items:
172
+ if item.kind == "FORBID":
173
+ mark = "[never]"
174
+ elif item.kind == "REPORT":
175
+ mark = {"found": f"[found: {item.value}]", "missing": "[missing]"}.get(item.status, "[ ]")
176
+ elif item.status == "closed":
177
+ mark = "[done: shown on screen]"
178
+ elif item.receipts:
179
+ mark = f"[{len(item.receipts)} done, open]"
180
+ elif item.kind in ("READ", "WRITE") and self.proof(item)[0]:
181
+ # Proven from the screens already seen, or the model reopens the source "to verify" (~9.5 s, 9 Oct).
182
+ mark = "[seen on screen]" if item.kind == "READ" else "[done: shown on screen]"
183
+ else:
184
+ mark = "[ ]"
185
+ out.append(f"{item.id}. {mark} {item.label()}" + (f" -- {item.reason}" if item.reason and
186
+ item.status not in ("closed",)
187
+ and not mark.startswith(("[seen", "[done")) else ""))
188
+ return out
189
+
190
+ HEADER = ("Task contract (code enforces it: a commit it does not list is refused, and DONE is accepted only "
191
+ "when the screens you saw show every item. In checklist_updates mark REPORT items found with the "
192
+ "exact value as shown, or missing; mark other items done when you finish them. Sent messages, "
193
+ "confirmations and saved text are checked on screen. A WRITE that was already so before you acted: mark "
194
+ "it done with value \"already: \" and the exact row that shows it):")
195
+
196
+ def update(self, updates, step):
197
+ """Apply a turn's checklist_updates; returns the items that changed."""
198
+ by_id, changed = {item.id: item for item in self.items}, []
199
+ for update in updates or ():
200
+ item = by_id.get(update.get("id")) if isinstance(update, dict) else None
201
+ status = update.get("status") if isinstance(update, dict) else None
202
+ if item is None or status not in ("done", "found", "missing"):
203
+ continue
204
+ value = " ".join(str(update.get("value") or "").split())[:300] or None
205
+ if item.kind == "REPORT":
206
+ if status == "found" and not value:
207
+ continue
208
+ item.status = "found" if status in ("found", "done") and value else "missing"
209
+ item.value, item.value_step = value, step
210
+ else:
211
+ item.claim = status
212
+ if value and item.kind != "COMMIT":
213
+ item.value, item.value_step = value, step
214
+ cited = ALREADY.match(value or "") if status == "done" and item.kind == "WRITE" else None
215
+ if cited:
216
+ item.already = (step, plain(cited.group(1)), False)
217
+ elif status == "done" and item.kind == "WRITE" and value:
218
+ item.already = (step, plain(value), True) # a bare value counts only as a whole row
219
+ changed.append(item)
220
+ return changed
221
+
222
+ def _wrote(self, app):
223
+ self.first_writes.setdefault(plain(app), len(self.screens))
224
+
225
+ def record_action(self, app, operation, label):
226
+ """A dispatched action: typing, a picker or a tap on a control that names an act is a write in ``app``."""
227
+ if operation in WRITE_OPS or operation in ("TAP", "LONG_PRESS", "TAP_XY") and ACT_LABEL.search(label or ""):
228
+ self._wrote(app)
229
+
230
+ def record_typed(self, app, element, text, step):
231
+ if text and str(text).strip() and not (element is not None and is_search_field(element)):
232
+ self._wrote(app)
233
+ if text and str(text).strip():
234
+ self.typed.append({"step": step, "app": app, "text": str(text),
235
+ "field": getattr(element, "label", "") or "",
236
+ "search": bool(element is not None and is_search_field(element)),
237
+ "picker": getattr(element, "role", "") in ("PickerWheel", "Picker", "Slider")})
238
+
239
+ def typed_texts(self, app=None):
240
+ return [t["text"] for t in self.typed if app is None or t["app"] == app]
241
+
242
+ def saw_lines(self, app, step, lines):
243
+ """Rows read by a macro (READ_LIST) are seen too."""
244
+ self.screens.append((step, app, [(plain(line), False, "StaticText") for line in lines], False))
245
+ self.shown.append((step, app, None, {plain(line): " ".join(str(line).split()) for line in lines}))
246
+ self.visited.add(app)
247
+
248
+ def observe(self, snapshot, app, step):
249
+ """Record a settled screen: the ledger, pending receipts, empty-search proofs."""
250
+ elements = list(getattr(snapshot, "elements", ()) or ())
251
+ rows = [(plain(element_text(e)), bool(getattr(e, "editable", False)), e.role) for e in elements]
252
+ keyboard = getattr(snapshot, "keyboard", "") == "visible"
253
+ originals = {}
254
+ for element, (text, _, _) in zip(elements, rows):
255
+ originals.setdefault(text, " ".join(element_text(element).split()))
256
+ title = screen_title(snapshot)
257
+ if not self.screens or self.screens[-1][1:] != (app, rows, keyboard):
258
+ self.screens.append((step, app, rows, keyboard))
259
+ self.shown.append((step, app, title, originals))
260
+ listed = list_rows(elements, rows)
261
+ if listed:
262
+ self.listed.append((step, app, listed))
263
+ self.visited.add(app)
264
+ texts = frozenset(t for t, _, _ in rows)
265
+ for pending in list(self.pending):
266
+ receipt = self._receipt(pending, app, rows, texts, title, elements)
267
+ if receipt:
268
+ self._close(pending, receipt, step, originals, title)
269
+ self._empty_search(elements, app, step)
270
+
271
+ def _receipt(self, pending, app, rows, texts, title=None, elements=()):
272
+ if app != pending.app:
273
+ return None
274
+ act = pending.item.act
275
+ if act in STATE_ACTS:
276
+ return self._state_receipt(pending, elements)
277
+ if act in MESSAGE_ACTS and pending.payload:
278
+ key = message_key(pending.payload)
279
+
280
+ def composing(text, editable):
281
+ # The composer still holding the text is unsent; known by its label, else any field.
282
+ return editable and (not pending.composer or text.startswith(pending.composer))
283
+ held = any(key and key in t for t, editable, _ in rows if composing(t, editable))
284
+ if held:
285
+ return None
286
+ shown = [t for t, editable, _ in rows if key and key in t and not editable]
287
+ if shown:
288
+ return f"sent: {shown[-1][:120]}"
289
+ # The sent bubble is a text view in iOS 26 Messages: "Delivered" under the exact text,
290
+ # in the approved thread.
291
+ bubbles = [t for t, editable, _ in rows if key and key in t and not composing(t, editable)]
292
+ same_thread = not pending.title or not title or plain(title) == plain(pending.title)
293
+ if len(bubbles) > pending.hits and same_thread and any(DELIVERY.match(t) for t, _, _ in rows):
294
+ return f"sent: {bubbles[-1][:120]}"
295
+ return None
296
+ new = [t for t in texts if t not in pending.before]
297
+ patterns = receipt_patterns(act, pending.verbs)
298
+ if patterns:
299
+ hit = next((t for t in new if any(pattern.search(t) for pattern in patterns)), None)
300
+ return f"shown: {hit[:120]}" if hit else None
301
+ if act in MESSAGE_ACTS:
302
+ return None # a message without its typed text has no receipt to look for
303
+ return "screen changed" if texts != pending.before else None
304
+
305
+ @staticmethod
306
+ def _state_receipt(pending, elements):
307
+ """A like's or a follow's receipt."""
308
+ before = pending.control
309
+ if before is None:
310
+ return None
311
+ on = STATE_ON.get(pending.item.act)
312
+ was_on = bool(on and (on.search(before.label) or on.search(before.value)))
313
+ for e in same_control(before, elements):
314
+ label, value = " ".join((e.label or "").split()), " ".join((getattr(e, "value", "") or "").split())
315
+ if getattr(e, "selected", False) and not before.selected:
316
+ return f"turned on: {label[:100] or e.role}"
317
+ if on and not was_on and (on.search(label) or on.search(value)):
318
+ return f"shown: {label[:100]}"
319
+ if before.value.casefold() in OFF_VALUES and value.casefold() in ON_VALUES:
320
+ return f"turned on: {label[:100] or e.role}"
321
+ if before.count is not None:
322
+ controls = same_control(before, elements)
323
+ after = shown_count(controls[0] if controls else None, elements, before.rect)
324
+ if after == before.count + 1:
325
+ return f"count: {before.count} -> {after}"
326
+ return None
327
+
328
+ def _close(self, pending, receipt, step, originals=None, title=None):
329
+ item = pending.item
330
+ self.pending.remove(pending)
331
+ item.receipts.append({"step": step, "label": pending.label, "receipt": receipt, "amount": pending.amount,
332
+ "payload": pending.payload})
333
+ item.reason = ""
334
+ if pending.amount is not None and item.act in ("pay", "transfer") and pending.names:
335
+ self.money.append((pending.amount, pending.names, step))
336
+ needed = item.needed()
337
+ if needed is not None and len(item.receipts) >= needed:
338
+ item.status = "closed"
339
+ # For the Mac app's proof: the receipt's row as shown, the commit's exact text and the screen's title.
340
+ row = receipt.split(": ", 1)[1] if receipt.startswith(("sent: ", "shown: ")) else None
341
+ shown = next((original for text, original in (originals or {}).items() if row and text.startswith(row)), None)
342
+ self.events.append({"event": "receipt", "item": item.id, "step": step, "receipt": receipt, "shown": shown,
343
+ "payload": pending.payload, "label": pending.label, "app": pending.app, "screen": title})
344
+
345
+ def _empty_search(self, elements, app, step):
346
+ typed = self.typed_texts(app)
347
+ for field_ in elements:
348
+ if not getattr(field_, "editable", False) or not is_search_field(field_) and not any(
349
+ t["search"] for t in self.typed if t["app"] == app):
350
+ continue
351
+ query = getattr(field_, "value", "") or ""
352
+ if not query or not any(overlaps(query, t) for t in typed):
353
+ continue
354
+ below = [e for e in elements if e is not field_ and e.rect[1] > field_.rect[1] + field_.rect[3]
355
+ and e.rect[1] < .85 and e.role not in ("Key", "Keyboard") and not getattr(e, "editable", False)]
356
+ if any(NO_RESULTS.search(element_text(e)) for e in elements) or not below:
357
+ self.empty_searches.append((step, app, query))
358
+ return
359
+
360
+ def check(self, operation, element, snapshot, app, *, text=None, chain_typed=(), bind=True):
361
+ """Gate one action: a commit passes only for an open declared item."""
362
+ elements = list(getattr(snapshot, "elements", ()) or ())
363
+ if element is None:
364
+ return Gate(True)
365
+ payload = None
366
+ store = getattr(snapshot, "bundle_id", "") == APP_STORE
367
+ picked = self._recipient_pick(operation, element, elements, app, text)
368
+ if picked is not None:
369
+ return picked
370
+ if operation in ("TAP", "LONG_PRESS"):
371
+ verbs = label_verbs(element.label, element.role)
372
+ if not verbs and deletes(element.label, element.role, getattr(element, "rect", None)):
373
+ verbs = {"delete"} # a row's swipe action named Trash
374
+ if store:
375
+ verbs = (verbs - {"install", "buy", "purchase"}) | store_verbs(element.label) if store_verbs(
376
+ element.label) else verbs
377
+ if not verbs and element.role in SWITCH_ROLES and COMMIT_WORDS.search(element.label or ""):
378
+ return self._toggle(element, app)
379
+ if verbs and self._opens_a_place(element, elements, app):
380
+ return Gate(True)
381
+ elif operation in ("SUBMIT", "TYPE_SUBMIT") and composer_field(element, elements, self.typed_texts()):
382
+ verbs = {"send"}
383
+ value = getattr(element, "value", "") or ""
384
+ if operation == "TYPE_SUBMIT" and text:
385
+ payload = text
386
+ elif value and any(overlaps(value, t) for t in self.typed_texts()):
387
+ payload = value
388
+ else: # the read shows the placeholder: the text typed just before is what Return sends
389
+ payload = chain_typed[-1] if chain_typed else next(
390
+ (t["text"] for t in reversed(self.typed) if t["app"] == app and not t["search"]), None)
391
+ else:
392
+ return Gate(True)
393
+ if not verbs:
394
+ return Gate(True)
395
+ verbs = frozenset(verbs)
396
+ what = (f"{'Return in' if operation in ('SUBMIT', 'TYPE_SUBMIT') else 'tapping'} "
397
+ f"{label_head(element.label) or element.label!r}")
398
+ declared = [i for i in self.items if i.kind == "COMMIT" and self._app_ok(i, app) and verbs <= i.family()
399
+ and (i.act != "install" or store)] # an app is installed from the App Store only
400
+ for item in self.items:
401
+ if item.kind == "FORBID" and self._app_ok(item, app) and verbs & item.family():
402
+ if excludes_others(item) and any(i.status != "closed" for i in declared):
403
+ # "Delete nothing else": the deletes the request names go on, bound to their object;
404
+ # any other act still refuses.
405
+ continue
406
+ return self._refuse(verbs, f"Refused: {what} would {item.act} -- the request rules it out "
407
+ f"(\"{item.quote}\").")
408
+ # "save" also completes a requested write (an alarm's or a form's Save) in the write's app.
409
+ if verbs == {"save"} and not [i for i in declared if i.status != "closed"] and any(
410
+ i.kind == "WRITE" and self._app_ok(i, app) for i in self.items):
411
+ return Gate(True, verbs=verbs)
412
+ if not declared:
413
+ return self._refuse(verbs, f"Refused: {what} performs an act the request does not ask for "
414
+ f"({', '.join(sorted(verbs))}{' in ' + app if app else ''}). Choose an "
415
+ "action that does what the user asked.")
416
+ open_ = [i for i in declared if i.status != "closed"]
417
+ if not open_:
418
+ done = declared[0]
419
+ return self._refuse(verbs, f"Refused: item {done.id} ({done.what}) is already done and shown on "
420
+ "screen; doing it again would repeat or undo it.")
421
+ typed = [(None, text) for text in chain_typed] + [(t["field"], t["text"]) for t in self.typed
422
+ if t["app"] == app]
423
+ amount = commit_amount(element, elements, typed) if any(i.act in MONEY_ACTS for i in open_) else None
424
+ if amount is None and any(i.act == "install" for i in open_):
425
+ # An app's price is on its own button ("$4.99"); a Get button is free whatever else the page shows.
426
+ price = MONEY.search(element.label or "")
427
+ amount = as_float(price.group(1)) if price else None
428
+ names = frozenset(counterparty(element, elements)) if amount is not None else frozenset()
429
+ reasons = []
430
+ for item in open_:
431
+ if item.act in STATE_ACTS and is_on(item.act, element):
432
+ # A heart already on: a tap would take the like back, which the request never asked for.
433
+ reasons.append(f"{label_head(element.label) or element.role!r} already shows it on (selected, or its "
434
+ "words say so); tapping it would undo it")
435
+ continue
436
+ low, high = bounds(item)
437
+ if amount is not None and (low is not None and amount < low or high is not None and amount > high):
438
+ reasons.append(f"the amount ${amount:,.2f} is outside the request's bound (\"{item.quote}\")")
439
+ continue
440
+ if item.act in ("pay", "transfer") and amount is not None and names and any(
441
+ abs(a - amount) < .005 and names & n for a, n, _ in self.money):
442
+ step = next(s for a, n, s in self.money if abs(a - amount) < .005 and names & n)
443
+ reasons.append(f"${amount:,.2f} to {' '.join(sorted(names)).title()} was already paid (turn {step}); "
444
+ "a request can still show pending after a direct payment")
445
+ continue
446
+ if bind and item.act in MESSAGE_ACTS and verbs <= MESSAGE and self._made_address(item, elements):
447
+ token, who = self._made_address(item, elements)
448
+ reasons.append(f"the To field holds {token!r}, an address made of the name, not the contact {who!r}: "
449
+ f"remove it and pick {who!r} from the suggestions")
450
+ continue
451
+ if bind and item.act in MESSAGE_ACTS and verbs <= MESSAGE:
452
+ bound = payload if operation == "TYPE_SUBMIT" and payload else \
453
+ self._composer_text(elements, chain_typed, payload)
454
+ if bound is None:
455
+ reasons.append("no text you typed is in a field on this screen, so this would send or share "
456
+ "something else (type the message in its composer first)")
457
+ continue
458
+ payload = payload or bound
459
+ if bind and item.act in OBJECT_ACTS and item.act != "install":
460
+ row = row_of(element, elements)
461
+ tokens = self.object_tokens(item)
462
+ if row is not None and tokens and not any(token in plain(element_text(row)) for token in tokens):
463
+ # A control in a row acts on that row, so the row must name the object (a Follow on the row above
464
+ # would act on someone else).
465
+ reasons.append(f"this {', '.join(sorted(verbs))} belongs to the row "
466
+ f"{' '.join(element_text(row).split())[:60]!r}, not to {item.what!r}")
467
+ continue
468
+ card = card_of(element, elements) if row is None and item.act == "react" and tokens else None
469
+ if card is not None and not any(re.search(rf"(?<!\w){re.escape(token)}(?!\w)", plain(element_text(e)))
470
+ for e in card for token in tokens):
471
+ # A feed's posts are no rows: a reaction acts on the post it closes, which must name the object.
472
+ texts = [element_text(e) for e in card if element_text(e).strip()]
473
+ first = next((t for t in texts if re.search(r"[A-Z][a-z]+ [A-Z][a-z]+", t)),
474
+ texts[0] if texts else "")
475
+ reasons.append(f"this {', '.join(sorted(verbs))} belongs to the post "
476
+ f"{' '.join(first.split())[:60]!r}, not to {item.what!r}")
477
+ continue
478
+ if (bind and item.act in OBJECT_ACTS and item.needed() == 1 and MORE_THAN_ONE.search(element.label or "")
479
+ and not MORE_THAN_ONE.search(f"{item.quote} {item.what}")):
480
+ reasons.append(f"{label_head(element.label)!r} acts on more than the one thing the request names")
481
+ continue
482
+ if bind and item.act in OBJECT_ACTS and not self._object_on_screen(
483
+ item, elements, list(chain_typed) + self.typed_texts(app)):
484
+ reasons.append(f"{item.what!r} is not on this screen, so this {', '.join(sorted(verbs))} is for "
485
+ "something else")
486
+ continue
487
+ if bind and item.act == "install" and not self._beside_object(item, element, elements):
488
+ # A results list puts a Get button on every row (and an ad above them): it must be the named app's.
489
+ reasons.append(f"this button is not beside {item.what!r}, so it would install something else")
490
+ continue
491
+ return Gate(True, item=item, verbs=verbs, amount=amount, names=names, payload=payload)
492
+ return self._refuse(verbs, f"Refused: {what}: " + "; ".join(dict.fromkeys(reasons)) + ".")
493
+
494
+ def screen_text_in(self, text, request=""):
495
+ """A run of TAIL_CHARS or more of ``text`` that a screen of this run showed and the request doesn't hold,
496
+ or None."""
497
+ wanted = plain(text)
498
+ if len(wanted) < TAINT_CHARS:
499
+ return None
500
+ asked = plain(request or self.request)
501
+ for _, _, _, originals in self.shown:
502
+ for line in originals:
503
+ if len(line) < TAINT_CHARS:
504
+ continue
505
+ for start in range(0, max(1, len(wanted) - TAINT_CHARS + 1), 4):
506
+ piece = wanted[start:start + TAINT_CHARS]
507
+ if len(piece) == TAINT_CHARS and piece in line and piece not in asked:
508
+ return piece
509
+ return None
510
+
511
+ def _opens_a_place(self, element, elements, app):
512
+ """Whether a control named for a folder the request reads ("READ (CloudDrive): Archive folder") opens it: its
513
+ name is a folder's (FOLDERS), it sits in a list of places (another one, named by a READ item or a folder word,
514
+ in its column or on its line) and out of the top and bottom bars. Then it archives or deletes nothing (iOSWorld
515
+ mem-042, 18 Oct: opening Archive was refused as an archive the request never asked for). A toolbar's Archive
516
+ or a row's swipe action has no place beside it and stays a commit."""
517
+ name = plain(element.label)
518
+ if name not in FOLDERS:
519
+ return False
520
+ row = row_of(element, elements)
521
+ if row is not None and plain(row.label) != name:
522
+ return False # Archive beside Trash still acts on the file row they belong to.
523
+ middle = center_y(element)
524
+ if middle < BAR_TOP or middle > BAR_BOTTOM:
525
+ return False
526
+ places = {plain(re.sub(r"\s+folder\b.*$", "", item.what, flags=re.I)) for item in self.items
527
+ if item.kind == "READ" and self._app_ok(item, app) and re.search(r"\bfolder\b", item.what, re.I)}
528
+ if name not in places:
529
+ return False
530
+ return any(other is not element and (plain(other.label) in places or plain(other.label) in FOLDERS)
531
+ and (abs(other.rect[0] - element.rect[0]) <= .02 or abs(center_y(other) - middle) <= .02)
532
+ for other in elements)
533
+
534
+ def _toggle(self, element, app):
535
+ """A switch whose label names a thing, not an act ("9:30, Client call, Weekdays" is an alarm)."""
536
+ tokens = {t for t in re.findall(r"\d{1,2}:\d{2}|[a-z]{3,}", plain(element.label))} - {"the", "and", "for"}
537
+ for item in self.items:
538
+ if item.kind in ("WRITE", "COMMIT") and self._app_ok(item, app):
539
+ named = set(re.findall(r"\d{1,2}:\d{2}|[a-z]{3,}",
540
+ plain(" ".join((item.what, item.payload, item.quote)))))
541
+ if len(tokens & named) >= 2 or any(":" in t for t in tokens & named):
542
+ return Gate(True, verbs=frozenset({"toggle"}))
543
+ return self._refuse({"toggle"}, f"Refused: switching {element.label[:40]!r} changes something the request "
544
+ "does not name. Choose an action that does what the user asked.")
545
+
546
+ def _recipient_names(self, app):
547
+ """The names the open message commits in ``app`` go to, as the request gave them ("Jordan Lee")."""
548
+ return [who for who in (recipient(i) for i in self.items if i.kind == "COMMIT" and i.act in MESSAGE_ACTS
549
+ and i.status != "closed" and self._app_ok(i, app)) if who]
550
+
551
+ def _recipient_pick(self, operation, element, elements, app, text=None):
552
+ """A refusal while choosing who a message goes to, else None."""
553
+ names = self._recipient_names(app)
554
+ if not names:
555
+ return None
556
+ if operation in ("SUBMIT", "TYPE_SUBMIT") and getattr(element, "editable", False) and TO_FIELD.match(
557
+ element.label or ""):
558
+ typed = " ".join(str(text).split()) if operation == "TYPE_SUBMIT" and text else typed_query(element)
559
+ if typed and not ADDRESS.match(typed):
560
+ who = max(names, key=lambda n: name_match(typed, n) is not None)
561
+ return self._refuse({"address"}, f"Refused: Return in {element.label.strip()!r} would make {typed!r} "
562
+ f"an address of its own, not the contact {who!r}. TAP {who!r} in the "
563
+ "suggestions, or type the phone number or email the request gives.")
564
+ return None
565
+ if operation != "TAP" or getattr(element, "editable", False):
566
+ return None
567
+ queries = [typed_query(e) for e in elements if getattr(e, "editable", False)]
568
+ for who in names:
569
+ if not any(q and name_match(q, who) for q in queries):
570
+ continue # no field holds this name typed: not a pick among suggestions for it
571
+ mine = name_match(row_name(element.label), who)
572
+ if mine is None or mine == "exact":
573
+ continue
574
+ fields = [e for e in elements if getattr(e, "editable", False)]
575
+ rows = [e for e in elements if e.role in ROW_ROLES and not getattr(e, "editable", False) and not any(
576
+ point_in(f.rect, e.rect[0] + e.rect[2] / 2, e.rect[1] + e.rect[3] / 2) for f in fields)]
577
+ exact = next((e for e in rows if name_match(row_name(e.label), who) == "exact"), None)
578
+ if exact is not None:
579
+ return self._refuse({"recipient"}, f"Refused: {row_name(element.label)!r} is not {who!r}; "
580
+ f"{row_name(exact.label)!r} is. TAP that row.")
581
+ close = list(dict.fromkeys(row_name(e.label) for e in rows if name_match(row_name(e.label), who)))
582
+ if len(close) >= 2:
583
+ return self._refuse({"recipient"}, f"Refused: no row is exactly {who!r}, and "
584
+ f"{' and '.join(repr(c) for c in close[:3])} are all close. Ask the "
585
+ "person which one they mean (ASK_USER), or finish with BLOCKED; "
586
+ "don't guess.")
587
+ return None
588
+
589
+ def _made_address(self, item, elements):
590
+ """(token, name) when the To field holds the request's name squashed into an address ("jordanlee" for "Jordan
591
+ Lee"): Return made the typed name a recipient of its own."""
592
+ who = recipient(item)
593
+ to = addressed_to(SimpleNamespace(elements=elements)) if who else None
594
+ for token in (to or "").split(", "):
595
+ if token and plain(token) != plain(who) and " " not in token.strip() and " " in who.strip() \
596
+ and squash(token) == squash(who):
597
+ return token, who
598
+ return None
599
+
600
+ def _refuse(self, verbs, reason):
601
+ self.events.append({"event": "refused", "reason": reason[:200]})
602
+ return Gate(False, reason=reason, verbs=frozenset(verbs))
603
+
604
+ @staticmethod
605
+ def _app_ok(item, app):
606
+ return not item.app or not app or plain(item.app) == plain(app)
607
+
608
+ def _composer_text(self, elements, chain_typed, payload):
609
+ """Text the agent typed this task (or earlier in this chain) that an editable field on screen holds."""
610
+ if chain_typed:
611
+ return chain_typed[-1]
612
+ if payload and any(overlaps(payload, t) for t in self.typed_texts()):
613
+ return payload
614
+ for element in elements:
615
+ value = getattr(element, "value", "") or ""
616
+ if getattr(element, "editable", False) and len(value.strip()) >= 2:
617
+ for typed in reversed(self.typed_texts()):
618
+ if overlaps(value, typed):
619
+ return value
620
+ return None
621
+
622
+ @staticmethod
623
+ def distinctive(item):
624
+ """Tokens that name the commit's object: quoted names, else capitalized words of ``what``."""
625
+ names = quoted(item.what)
626
+ if names:
627
+ return [plain(n) for n in names]
628
+ words = re.findall(r"(?<![\w'])([A-Z][\w&.-]+)", item.what)
629
+ skip = {plain(item.app)} | GENERIC
630
+ return [plain(w) for w in words if plain(w) not in skip]
631
+
632
+ @staticmethod
633
+ def object_tokens(item):
634
+ """Tokens that name what a commit acts on, for binding it to the screen: ``distinctive``'s, less a quoted
635
+ name that is where the object goes or how it's done ("to 'Want to Try' list", "reaction 'Insightful'"),
636
+ which a screen shows only after the tap. Then the capitalized words outside the quotes name the object
637
+ (iOSWorld tasterank-001 and multi-092, 18 Oct: "Save to list" and a Like under the right post were
638
+ refused because the list's and the reaction's names weren't on screen). With nothing left, the quoted names
639
+ stay, as before."""
640
+ spans = [m for m in re.finditer(r"(?<![\w])'([^']{2,}?)'(?![\w])|\"([^\"]{2,}?)\"", item.what or "")]
641
+ if not spans:
642
+ return Contract.distinctive(item)
643
+ what = item.what
644
+ aside = [m for m in spans if DESTINATION_AFTER.match(what[m.end():])
645
+ or PARAMETER_BEFORE.search(what[:m.start()])]
646
+ if not aside:
647
+ return Contract.distinctive(item)
648
+ kept = [plain(m.group(1) or m.group(2)) for m in spans if m not in aside]
649
+ outside = re.sub(r"(?<![\w])'[^']{2,}?'(?![\w])|\"[^\"]{2,}?\"", " ", what)
650
+ skip = {plain(item.app)} | GENERIC
651
+ words = [plain(w) for w in re.findall(r"(?<![\w'])([A-Z][\w&.-]+)", outside) if plain(w) not in skip]
652
+ return kept + words or Contract.distinctive(item)
653
+
654
+ def _object_on_screen(self, item, elements, typed=()):
655
+ """The commit's object is named on screen, or is one the agent is making."""
656
+ tokens = self.object_tokens(item)
657
+ if not tokens:
658
+ return True
659
+ texts = [plain(element_text(e)) for e in elements] + [plain(t) for t in typed]
660
+ return any(token in text for token in tokens for text in texts)
661
+
662
+ def _beside_object(self, item, element, elements):
663
+ """The control names its object itself, or is the App Store button nearest a row that names it
664
+ (within an app row's height)."""
665
+ tokens = self.distinctive(item)
666
+ if not tokens or any(token in plain(element.label) for token in tokens):
667
+ return True
668
+ buttons = [e for e in elements if e is not element and store_verbs(e.label)] + [element]
669
+
670
+ def gap(button, name):
671
+ return (round(abs(center_y(button) - center_y(name)), 3),
672
+ round(abs(button.rect[0] + button.rect[2] / 2 - name.rect[0] - name.rect[2] / 2), 3))
673
+ for other in elements:
674
+ if (other is element or other.rect[3] > .25 or abs(center_y(other) - center_y(element)) > .12
675
+ or not any(token in plain(element_text(other)) for token in tokens)):
676
+ continue
677
+ if min(buttons, key=lambda button: gap(button, other)) is element:
678
+ return True
679
+ return False
680
+
681
+ def committed(self, gate, element, before, app, step):
682
+ self._wrote(app)
683
+ return self._committed(gate, element, before, app, step)
684
+
685
+ def _committed(self, gate, element, before, app, step):
686
+ """A declared commit ran: wait for its receipt on the screens that follow."""
687
+ if gate.item is None:
688
+ return
689
+ gate.item.executions += 1
690
+ elements = list(getattr(before, "elements", ()) or ())
691
+ rows = frozenset(plain(element_text(e)) for e in elements)
692
+ composer, hits = "", 0
693
+ if gate.item.act in MESSAGE_ACTS and gate.payload:
694
+ key = message_key(gate.payload)
695
+ field_ = element if getattr(element, "editable", False) else next(
696
+ (e for e in elements if getattr(e, "editable", False) and overlaps(getattr(e, "value", "") or "",
697
+ gate.payload)), None)
698
+ composer = plain(field_.label) if field_ is not None and field_.label else ""
699
+ hits = sum(1 for e in elements if key and key in plain(element_text(e))
700
+ and not (getattr(e, "editable", False) and (not composer
701
+ or plain(element_text(e)).startswith(composer))))
702
+ self.pending = [p for p in self.pending if p.item is not gate.item or gate.item.needed() is None
703
+ and p.payload != gate.payload]
704
+ control = control_state(element, elements) if gate.item.act in STATE_ACTS else None
705
+ self.pending.append(Pending(gate.item, (element.label or "")[:80], app, step, rows, gate.payload,
706
+ gate.amount, gate.names, screen_title(before), composer, hits, gate.verbs,
707
+ control))
708
+
709
+ def _seen(self, predicate, app=None, until=None):
710
+ for step, screen_app, rows, keyboard in self.screens:
711
+ if until is not None and step > until:
712
+ break
713
+ if app and plain(screen_app) != plain(app):
714
+ continue
715
+ for text, editable, role in rows:
716
+ if predicate(text, editable, keyboard):
717
+ return step
718
+ return None
719
+
720
+ def proof(self, item):
721
+ """(proven, reason when not). FORBID is always proven."""
722
+ if item.kind == "FORBID":
723
+ return True, ""
724
+ if item.kind == "COMMIT":
725
+ if item.receipts:
726
+ return True, ""
727
+ state = item.act in STATE_ACTS
728
+ if item.claim in ("missing", "done") and (item.condition or item.needed() is None) and (
729
+ not state or item.claim == "missing"):
730
+ return True, "" # a condition not met, or no object left for an "each"
731
+ if item.executions and state:
732
+ return False, ("tapped, but the control never showed it on afterwards (selected, \"Liked\" or "
733
+ "\"Following\", or its count up by one): look at that control again. Tapping it again "
734
+ "would undo it if it is on")
735
+ if item.executions:
736
+ return False, "done but not shown: open the screen that shows it (the sent message, the confirmation)"
737
+ if state:
738
+ # A like or a follow is proven only by its own control turning on after the tap,
739
+ # not by a point tap or a banner closing.
740
+ return False, ("not shown done: a like or a follow counts once the control you TAP turns on (selected, "
741
+ "\"Liked\" or \"Following\", or its count up by one). If it already shows on, don't tap "
742
+ "it: that would undo it")
743
+ if item.claim == "done" and item.act not in MESSAGE_ACTS and (
744
+ not item.app or any(plain(item.app) == plain(a) for a in self.visited)):
745
+ # No commit word, so no receipt was awaited: the object must still be on a screen of its app, and a
746
+ # chat's earlier twin proves nothing.
747
+ tokens = self.distinctive(item)
748
+ if not tokens or self._seen(lambda t, e, k: any(x in t for x in tokens), item.app) is not None:
749
+ return True, ""
750
+ return False, "not done yet"
751
+ if item.kind == "WRITE":
752
+ return self._write_proof(item)
753
+ if item.kind == "READ":
754
+ return self._read_proof(item)
755
+ return self._report_proof(item)
756
+
757
+ def _write_proof(self, item):
758
+ typed = [t for t in self.typed if not t["search"] and not t.get("picker") and self._app_ok(item, t["app"])]
759
+ # A message body is proven by its own receipt; a text-view bubble never counts as shown while the
760
+ # keyboard is up.
761
+ texts = [item.payload] if item.payload else [t["text"] for t in typed]
762
+ if any(other.kind == "COMMIT" and other.act in MESSAGE_ACTS and self._app_ok(item, other.app)
763
+ and any(receipt.get("payload") and overlaps(receipt["payload"], text)
764
+ for receipt in other.receipts for text in texts if text) for other in self.items):
765
+ return True, ""
766
+ if item.already is not None:
767
+ # The model quoted the row that shows it already so:
768
+ # proven when that row was on a screen of its app by then.
769
+ step, row, whole = item.already
770
+ if row and self._seen(lambda text, e, k: row == text or not whole and row in text, item.app,
771
+ until=step) is not None:
772
+ return True, ""
773
+ if not whole and self._listed_already(item, step, row):
774
+ return True, "" # "already: ..." only: a bare value is what the model set, not a citation
775
+ # Only screens recorded after this run first wrote or committed in the item's app prove it.
776
+ apps = [plain(item.app)] if item.app else list(self.first_writes)
777
+ firsts = [self.first_writes[a] for a in apps if a in self.first_writes]
778
+ if not firsts:
779
+ return False, ("nothing entered or set for it yet" + (f" in {item.app}" if item.app else "")
780
+ + (f": enter \"{item.payload}\"" if item.payload else ""))
781
+ after = min(firsts)
782
+ keys = []
783
+ if item.payload:
784
+ keys = [(plain(item.payload), min((t["step"] for t in typed if overlaps(item.payload, t["text"])),
785
+ default=0))]
786
+ else:
787
+ keys = [(plain(next((line for line in t["text"].split("\n") if line.strip()), ""))[:40], t["step"])
788
+ for t in typed if len(t["text"].strip()) >= 3]
789
+ if not keys:
790
+ if item.claim != "done":
791
+ return False, "nothing typed for it yet" if not item.payload else f"\"{item.payload}\" not entered yet"
792
+ # Nothing typed: a write that adds counts only once each named object has a new row;
793
+ # a row from before proves nothing.
794
+ missing = [] if not ADDS.search(item.what + " " + item.quote) or any(
795
+ t.get("picker") and self._app_ok(item, t["app"]) for t in self.typed) else [
796
+ words for words in self._objects(item) if not self._new_row(item, words, after)]
797
+ return (False, f"no new row naming {' '.join(missing[0])} in {item.app or 'its app'} since you started "
798
+ "there (a row from before proves nothing): add it, then show it") if missing else (True, "")
799
+ for key, since in keys:
800
+ for index, (step, app, rows, keyboard) in enumerate(self.screens):
801
+ if index < after or step < since or not self._app_ok(item, app):
802
+ continue
803
+ if any(key and shows(key, text) and (not editable or not keyboard) for text, editable, _ in rows):
804
+ return True, ""
805
+ return False, "not read back on screen after it was entered: save it and show the saved text"
806
+
807
+ def _objects(self, item):
808
+ """The objects a WRITE names, in the request's words, one list per part split at "and", "&" and commas."""
809
+ asked, app = plain(self.request), set(plain(item.app).split())
810
+ parts = ([w for w in re.findall(r"[a-z][a-z'-]{3,}", part) if w in asked and w not in OBJECT_STOP | app]
811
+ for part in re.split(r"\s*(?:,|&|\band\b)\s*", plain(item.what)))
812
+ return [words for words in parts if words]
813
+
814
+ def _new_row(self, item, words, after):
815
+ """More rows name one of ``words`` on a screen of the item's app from ``after`` on than on any before it."""
816
+ searches = [plain(t["text"])[:20] for t in self.typed if t["search"]]
817
+ counts = {False: 0, True: 0}
818
+ for index, (_, app, rows, _) in enumerate(self.screens):
819
+ if self._app_ok(item, app) and not any(e and any(q in text for q in searches) for text, e, _ in rows):
820
+ count = max(sum(not e and w in text for text, e, _ in rows) for w in words)
821
+ counts[index >= after] = max(counts[index >= after], count)
822
+ return counts[True] > counts[False]
823
+
824
+ def _listed_already(self, item, until, cited):
825
+ """A WRITE the model marked as already so, citing its value but not a row."""
826
+ value = plain(item.payload)
827
+ if not value or value not in (cited or ""):
828
+ return False
829
+ for step, app, listed in self.listed:
830
+ if step > until:
831
+ break
832
+ if not self._app_ok(item, app) or item.app and plain(app) != plain(item.app):
833
+ continue
834
+ if any(t["step"] <= step and plain(t["app"]) == plain(app) for t in self.typed):
835
+ continue
836
+ if any(text == value or text.startswith(value + ",") for text in listed):
837
+ return True
838
+ return False
839
+
840
+ def _read_proof(self, item):
841
+ if item.app and not any(plain(item.app) == plain(app) for app in self.visited):
842
+ return False, f"{item.app} not opened yet"
843
+ names = [plain(n) for n in quoted(item.what)]
844
+ folder = next((f for f in FOLDERS if re.search(rf"\b{f}\b", plain(item.what))), None)
845
+ if folder:
846
+ names = [folder]
847
+ for name in names:
848
+ if self._seen(lambda text, e, k: (text == name or text.startswith(name + " ") or name in text
849
+ and folder is None), item.app) is None:
850
+ return False, f"'{name}' not seen in {item.app or 'any app'}"
851
+ return True, ""
852
+
853
+ def _report_proof(self, item):
854
+ what = item.what + " " + item.quote
855
+ if CONFIRM_ITEM.search(item.what):
856
+ related = [i for i in self.items if i.kind in ("COMMIT", "WRITE") and (
857
+ not item.app or not i.app or plain(i.app) == plain(item.app))]
858
+ # "confirm the job was saved" is about the save, not the reply in the same app.
859
+ words = set(re.findall(r"[a-z]{4,}", plain(item.what))) - CONFIRM_WORDS
860
+ named = [i for i in related if words & set(re.findall(r"[a-z]{4,}",
861
+ plain(i.what + " " + i.act + " " + i.quote)))]
862
+ related = named or related
863
+ unproven = [i for i in related if not self.proof(i)[0] and not i.unproven]
864
+ return (not unproven), ("" if not unproven else "its action is not shown yet: " +
865
+ ", ".join(f"item {i.id}" for i in unproven))
866
+ book = getattr(self, "notebook", None)
867
+ if book is not None and item.status in ("open", "found") and getattr(book, "kind", None) in NOTEBOOK_KINDS:
868
+ # A long read's summary item is the report, written from the kept items; any other value needs an item's own
869
+ # text, never the model's notes.
870
+ if SUMMARY_ITEM.search(item.what + " " + item.quote) and book.counters.get("kept"):
871
+ return True, ""
872
+ if item.status == "found" and self._in_notebook(item.value):
873
+ return True, ""
874
+ if item.status == "open":
875
+ return False, "not reported yet: read it on screen and mark it found with its value (or missing)"
876
+ if item.status == "missing":
877
+ if item.app and not any(plain(item.app) == plain(app) for app in self.visited):
878
+ return False, f"marked missing but {item.app} was never opened"
879
+ # An absence is a finding too: proven like a "none".
880
+ if self._absence_shown(item):
881
+ return True, ""
882
+ return False, ("marked missing, but no screen showed it absent: look where it would be (open the row "
883
+ "for its own page, another tab, the app's search) and mark it found, or leave on screen "
884
+ "the empty search or the app's \"No ...\" that proves it")
885
+ value = item.value or ""
886
+ if NONE_VALUE.match(plain(value)):
887
+ proof = self._absence_shown(item)
888
+ return (proof, "" if proof else "\"none\" needs proof: open the screen that shows it is empty "
889
+ "(or search for it so the empty result is on screen)")
890
+ if COMPUTED_ITEM.search(what):
891
+ return True, ""
892
+ until = item.value_step
893
+ atoms = ATOM.findall(value)
894
+ if atoms:
895
+ missing = [a for a in atoms if self._seen_atom(a, until) is None]
896
+ if not missing:
897
+ return True, ""
898
+ return False, (f"{', '.join(missing[:3])} not seen on any screen before you recorded it: read it where "
899
+ "it is shown")
900
+ phrase = plain(value)
901
+ words = re.findall(r"[a-z]{4,}", phrase)
902
+ if len(phrase.split()) > 6 or not words:
903
+ return True, "" # prose the agent wrote from what it read: no single source to point at
904
+ if self._seen(lambda t, e, k: phrase in t or all(w in t for w in words), None, until) is not None:
905
+ return True, ""
906
+ long_words = [w for w in words if len(w) >= 5] or words
907
+ pool = " ".join(t for step, _, rows, _ in self.screens if until is None or step <= until for t, _, _ in rows)
908
+ if sum(w in pool for w in long_words) >= max(1, len(long_words) / 2):
909
+ return True, "" # a name said in full ("Seattle Seahawks") where the screen shows part of it
910
+ return False, f"\"{value[:60]}\" not seen on any screen before you recorded it: read it where it is shown"
911
+
912
+ def _absence_shown(self, item):
913
+ """Whether a screen of the item's app showed it absent by the time it was marked."""
914
+ if any(self._app_ok(item, s[1]) for s in self.empty_searches):
915
+ return True
916
+ return self._seen(lambda t, e, k: bool(EMPTY_STATE.match(t) or NO_RESULTS.search(t)),
917
+ item.app or None, item.value_step) is not None
918
+
919
+ def _in_notebook(self, value):
920
+ """Whether the long task's kept items hold ``value``, never the model's notes."""
921
+ book = getattr(self, "notebook", None)
922
+ if book is None or not value:
923
+ return False
924
+ texts = []
925
+ with book.lock:
926
+ for entry in book.items:
927
+ if entry.skip is not None:
928
+ continue
929
+ texts += [entry.who(), entry.summary(), *(str(v) for v in (entry.text or {}).values()
930
+ if isinstance(v, str))]
931
+ pool = plain(" ".join(t for t in texts if t))
932
+ atoms = ATOM.findall(value)
933
+ if atoms:
934
+ return all(plain(atom) in pool or atom.lstrip("$") in pool for atom in atoms)
935
+ words = re.findall(r"[a-z]{4,}", plain(value))
936
+ return bool(words) and sum(w in pool for w in words) >= max(1, len(words) / 2)
937
+
938
+ def _seen_atom(self, atom, until):
939
+ return self._seen(lambda t, e, k: holds_atom(atom, t), None, until)
940
+
941
+ def hidden_row(self, elements, step):
942
+ """The row that holds a value marked at ``step`` when every row holding it is hidden below (contract_view)."""
943
+ return hidden_row(self.items, elements, step)
944
+
945
+ def done_gate(self, answer, turns_left):
946
+ """(accepted, feedback, answer)."""
947
+ open_ = []
948
+ for item in self.items:
949
+ proven, reason = self.proof(item)
950
+ if not proven and not item.unproven:
951
+ open_.append((item, reason))
952
+ if open_ and turns_left > LOW_TURNS:
953
+ lines = []
954
+ for item, reason in open_:
955
+ item.reopens += 1
956
+ item.reason = reason
957
+ if item.reopens > MAX_REOPENS:
958
+ item.unproven = True
959
+ lines.append(f"{item.id}. {item.label()} -- {reason}")
960
+ if all(item.unproven for item, _ in open_):
961
+ return True, "", self.final_answer(answer)
962
+ return False, ("DONE refused: not shown on screen yet:\n" + "\n".join(lines) +
963
+ "\nDo these now (or mark a REPORT missing if it is shown nowhere), then DONE again."), None
964
+ for item, reason in open_:
965
+ item.unproven, item.reason = True, reason
966
+ return True, "", self.final_answer(answer)
967
+
968
+ def unproven(self):
969
+ return [item for item in self.items if item.unproven and not self.proof(item)[0]]
970
+
971
+ def final_answer(self, answer):
972
+ """The answer, with found values it leaves out appended from the ledger, and unproven items named."""
973
+ answer = (answer or "").strip()
974
+ text = plain(answer)
975
+ extra = []
976
+ for item in self.items:
977
+ if item.kind != "REPORT" or item.status != "found" or not item.value or CONFIRM_ITEM.search(item.what):
978
+ continue
979
+ atoms = ATOM.findall(item.value)
980
+ present = (all(plain(a) in text or a.lstrip("$") in text for a in atoms) if atoms
981
+ else plain(item.value) in text)
982
+ if not present:
983
+ extra.append(f"- {item.what}: {item.value}")
984
+ # Unconfirmed actions are named; a reported value stays as given, since a doubt on a right answer costs more
985
+ # than it tells.
986
+ unproven = [item for item in self.unproven() if item.kind in ("COMMIT", "WRITE")]
987
+ if unproven:
988
+ extra.append("Not confirmed on screen: " + "; ".join(item.what for item in unproven) + ".")
989
+ return (answer + ("\n\n" if answer and extra else "") + "\n".join(extra)).strip()
990
+
991
+ def summary(self):
992
+ return {"items": len(self.items), "dropped": len(self.dropped),
993
+ "commits": sum(i.kind == "COMMIT" for i in self.items),
994
+ "receipted": sum(bool(i.receipts) for i in self.items),
995
+ "reports": sum(i.kind == "REPORT" for i in self.items),
996
+ "unproven": [i.id for i in self.unproven()]}