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.
- mobile_agent/__init__.py +4 -0
- mobile_agent/__main__.py +598 -0
- mobile_agent/accounts/__init__.py +1 -0
- mobile_agent/accounts/context.py +80 -0
- mobile_agent/accounts/mcp.py +55 -0
- mobile_agent/accounts/pacing.py +318 -0
- mobile_agent/accounts/register.py +78 -0
- mobile_agent/accounts/routes.py +212 -0
- mobile_agent/accounts/routing.py +69 -0
- mobile_agent/accounts/screen.py +273 -0
- mobile_agent/accounts/stats.py +54 -0
- mobile_agent/accounts/store.py +394 -0
- mobile_agent/agent_hooks.py +123 -0
- mobile_agent/ai_plans/__init__.py +7 -0
- mobile_agent/ai_plans/agents.py +96 -0
- mobile_agent/ai_plans/claude_code.py +208 -0
- mobile_agent/ai_plans/cli.py +400 -0
- mobile_agent/ai_plans/codex.py +258 -0
- mobile_agent/ai_plans/compat_chat.py +307 -0
- mobile_agent/ai_plans/device_login.py +164 -0
- mobile_agent/ai_plans/draft_client.py +68 -0
- mobile_agent/ai_plans/gemini.py +137 -0
- mobile_agent/ai_plans/instructions.py +65 -0
- mobile_agent/ai_plans/mcp_http.py +160 -0
- mobile_agent/ai_plans/phone_tools.py +892 -0
- mobile_agent/ai_plans/providers.py +235 -0
- mobile_agent/ai_plans/routes.py +160 -0
- mobile_agent/ai_plans/runner.py +400 -0
- mobile_agent/ai_plans/vault.py +114 -0
- mobile_agent/alerts.py +337 -0
- mobile_agent/android/__init__.py +18 -0
- mobile_agent/android/adb.py +268 -0
- mobile_agent/android/conditions.py +128 -0
- mobile_agent/android/doctor.py +54 -0
- mobile_agent/android/driver.py +137 -0
- mobile_agent/android/emulator.py +216 -0
- mobile_agent/android/logs.py +85 -0
- mobile_agent/android/manager.py +225 -0
- mobile_agent/android/records.py +31 -0
- mobile_agent/android/tree.py +96 -0
- mobile_agent/answer_shapes.py +230 -0
- mobile_agent/api_devices.py +333 -0
- mobile_agent/api_errors.py +17 -0
- mobile_agent/api_get.py +249 -0
- mobile_agent/api_http.py +467 -0
- mobile_agent/api_post.py +453 -0
- mobile_agent/api_routes.py +173 -0
- mobile_agent/app_api/__init__.py +4 -0
- mobile_agent/app_api/cli.py +151 -0
- mobile_agent/app_api/endpoints.py +193 -0
- mobile_agent/app_api/mcp.py +54 -0
- mobile_agent/app_api/openapi.py +146 -0
- mobile_agent/app_api/reading.py +172 -0
- mobile_agent/app_api/register.py +10 -0
- mobile_agent/app_api/screen.py +216 -0
- mobile_agent/app_api/service.py +382 -0
- mobile_agent/app_api/sessions.py +404 -0
- mobile_agent/app_api/tokens.py +167 -0
- mobile_agent/app_choice.py +131 -0
- mobile_agent/app_icons.py +183 -0
- mobile_agent/app_killer.py +69 -0
- mobile_agent/artifacts.py +606 -0
- mobile_agent/attach/__init__.py +13 -0
- mobile_agent/attach/cli.py +46 -0
- mobile_agent/attach/flutter.py +28 -0
- mobile_agent/attach/score.py +119 -0
- mobile_agent/attach/web.py +13 -0
- mobile_agent/attachments/__init__.py +1 -0
- mobile_agent/attachments/agent.py +418 -0
- mobile_agent/attachments/extract.py +217 -0
- mobile_agent/attachments/mcp.py +104 -0
- mobile_agent/attachments/register.py +73 -0
- mobile_agent/attachments/routes.py +326 -0
- mobile_agent/attachments/service.py +170 -0
- mobile_agent/attachments/sniff.py +152 -0
- mobile_agent/attachments/store.py +441 -0
- mobile_agent/audit_verify.py +436 -0
- mobile_agent/bench/README.md +410 -0
- mobile_agent/bench/__init__.py +0 -0
- mobile_agent/bench/__main__.py +394 -0
- mobile_agent/bench/ab.py +168 -0
- mobile_agent/bench/actuation.py +368 -0
- mobile_agent/bench/agents/__init__.py +9 -0
- mobile_agent/bench/agents/base.py +105 -0
- mobile_agent/bench/agents/plan_clients.py +245 -0
- mobile_agent/bench/agents/smart.py +533 -0
- mobile_agent/bench/checks.py +358 -0
- mobile_agent/bench/decide_gate.py +214 -0
- mobile_agent/bench/diagnose.py +181 -0
- mobile_agent/bench/evals/__init__.py +14 -0
- mobile_agent/bench/evals/build.py +565 -0
- mobile_agent/bench/evals/checkpoint.py +214 -0
- mobile_agent/bench/evals/irt.py +219 -0
- mobile_agent/bench/evals/l0.py +647 -0
- mobile_agent/bench/evals/l1.py +866 -0
- mobile_agent/bench/evals/models.py +124 -0
- mobile_agent/bench/evals/pairwise.py +189 -0
- mobile_agent/bench/evals/proxy.py +90 -0
- mobile_agent/bench/evals/sets.py +149 -0
- mobile_agent/bench/evals/traces.py +446 -0
- mobile_agent/bench/fixtures.py +176 -0
- mobile_agent/bench/guard_replay.py +129 -0
- mobile_agent/bench/h2-manifest.json +20 -0
- mobile_agent/bench/h3-manifest.json +20 -0
- mobile_agent/bench/heldout-manifest.json +32 -0
- mobile_agent/bench/heldout.py +375 -0
- mobile_agent/bench/iosworld.py +901 -0
- mobile_agent/bench/iosworld_stats.py +510 -0
- mobile_agent/bench/llm_units.py +116 -0
- mobile_agent/bench/manifest.json +93 -0
- mobile_agent/bench/metrics.py +66 -0
- mobile_agent/bench/phone.py +44 -0
- mobile_agent/bench/replay.py +172 -0
- mobile_agent/bench/report.py +207 -0
- mobile_agent/bench/runner.py +456 -0
- mobile_agent/bench/safety.py +310 -0
- mobile_agent/bench/stream_timing.py +213 -0
- mobile_agent/bench/suite.py +444 -0
- mobile_agent/bench/sweep.py +176 -0
- mobile_agent/bench/tape.py +798 -0
- mobile_agent/bench/tape_replay.py +88 -0
- mobile_agent/bench/trace_evals.py +128 -0
- mobile_agent/bench/trace_view.py +81 -0
- mobile_agent/bench/truth/iphone15pro.json +138 -0
- mobile_agent/bench/truth.py +340 -0
- mobile_agent/bench/tuning.py +543 -0
- mobile_agent/bench/tuning_analysis.py +656 -0
- mobile_agent/bench/vague-heldout-manifest.json +12 -0
- mobile_agent/bench/vague_rubric.py +135 -0
- mobile_agent/bench/vague_run.py +243 -0
- mobile_agent/brand.py +454 -0
- mobile_agent/brand_fonts.py +756 -0
- mobile_agent/build_identity.py +21 -0
- mobile_agent/cable_bt.py +623 -0
- mobile_agent/cable_bt_runtime.py +128 -0
- mobile_agent/catalog.py +69 -0
- mobile_agent/chart_svg.py +415 -0
- mobile_agent/chat_here.py +189 -0
- mobile_agent/cli_constants.py +5 -0
- mobile_agent/cli_parser.py +611 -0
- mobile_agent/cli_run.py +115 -0
- mobile_agent/compiled_read/__init__.py +21 -0
- mobile_agent/compiled_read/delta.py +89 -0
- mobile_agent/compiled_read/extractors.py +202 -0
- mobile_agent/compiled_read/routes.py +56 -0
- mobile_agent/completion.py +267 -0
- mobile_agent/compose.py +48 -0
- mobile_agent/config.py +114 -0
- mobile_agent/connectors/__init__.py +1 -0
- mobile_agent/connectors/api_session.py +76 -0
- mobile_agent/connectors/callback_page.py +146 -0
- mobile_agent/connectors/client.py +582 -0
- mobile_agent/connectors/github_app.py +55 -0
- mobile_agent/connectors/graph_api.py +396 -0
- mobile_agent/connectors/imports.py +170 -0
- mobile_agent/connectors/issue.py +66 -0
- mobile_agent/connectors/keystore.py +85 -0
- mobile_agent/connectors/marks.py +222 -0
- mobile_agent/connectors/oauth.py +476 -0
- mobile_agent/connectors/picker.py +228 -0
- mobile_agent/connectors/presets.py +591 -0
- mobile_agent/connectors/register.py +48 -0
- mobile_agent/connectors/routes.py +153 -0
- mobile_agent/connectors/service.py +966 -0
- mobile_agent/connectors/shared.py +136 -0
- mobile_agent/connectors/skill.py +446 -0
- mobile_agent/connectors/slack_api.py +269 -0
- mobile_agent/connectors/steps.py +92 -0
- mobile_agent/connectors/store.py +144 -0
- mobile_agent/connectors/vault.py +72 -0
- mobile_agent/console.py +178 -0
- mobile_agent/content_queue/__init__.py +1 -0
- mobile_agent/content_queue/client.py +263 -0
- mobile_agent/content_queue/importing.py +153 -0
- mobile_agent/content_queue/media_check.py +224 -0
- mobile_agent/content_queue/prompt.py +64 -0
- mobile_agent/content_queue/register.py +91 -0
- mobile_agent/content_queue/routes.py +370 -0
- mobile_agent/content_queue/runner.py +475 -0
- mobile_agent/content_queue/store.py +521 -0
- mobile_agent/contract.py +996 -0
- mobile_agent/contract_asks.py +248 -0
- mobile_agent/contract_prompt.py +45 -0
- mobile_agent/contract_screen.py +981 -0
- mobile_agent/contract_view.py +84 -0
- mobile_agent/corpus/__init__.py +26 -0
- mobile_agent/corpus/cli.py +63 -0
- mobile_agent/corpus/consent.py +55 -0
- mobile_agent/corpus/listener.py +76 -0
- mobile_agent/corpus/register.py +9 -0
- mobile_agent/corpus/skeleton.py +72 -0
- mobile_agent/corpus/store.py +143 -0
- mobile_agent/costs.py +161 -0
- mobile_agent/decider_scope.py +40 -0
- mobile_agent/decision_provider.py +189 -0
- mobile_agent/deeplinks.py +352 -0
- mobile_agent/device_info.py +116 -0
- mobile_agent/device_manager.py +978 -0
- mobile_agent/device_status.py +195 -0
- mobile_agent/device_targets.py +217 -0
- mobile_agent/devices.py +676 -0
- mobile_agent/devkit/__init__.py +64 -0
- mobile_agent/devkit/account_cli.py +90 -0
- mobile_agent/devkit/asc.py +562 -0
- mobile_agent/devkit/asc_cli.py +214 -0
- mobile_agent/devkit/ci.py +737 -0
- mobile_agent/devkit/ci_cli.py +196 -0
- mobile_agent/devkit/ci_hosts.py +194 -0
- mobile_agent/devkit/es256.py +197 -0
- mobile_agent/devkit/expo.py +440 -0
- mobile_agent/devkit/expo_cli.py +405 -0
- mobile_agent/devkit/expo_dev.py +203 -0
- mobile_agent/devkit/expo_routes.py +90 -0
- mobile_agent/devkit/fixtures_cli.py +133 -0
- mobile_agent/devkit/fixtures_routes.py +82 -0
- mobile_agent/devkit/links.py +375 -0
- mobile_agent/devkit/links_cli.py +123 -0
- mobile_agent/devkit/links_routes.py +83 -0
- mobile_agent/devkit/nightly.py +152 -0
- mobile_agent/devkit/shots.py +203 -0
- mobile_agent/devkit/testflight.py +155 -0
- mobile_agent/devkit/testflight_cli.py +127 -0
- mobile_agent/devkit/testflight_routes.py +111 -0
- mobile_agent/devkit/xcode.py +236 -0
- mobile_agent/devkit/xcode_cli.py +101 -0
- mobile_agent/devtools.py +419 -0
- mobile_agent/doctor.py +509 -0
- mobile_agent/doctor_fix.py +467 -0
- mobile_agent/downloads.py +55 -0
- mobile_agent/drivers.py +793 -0
- mobile_agent/dry_run.py +102 -0
- mobile_agent/effect_ledger.py +83 -0
- mobile_agent/engine_health.py +378 -0
- mobile_agent/engine_hooks.py +96 -0
- mobile_agent/engines.py +408 -0
- mobile_agent/env_file.py +63 -0
- mobile_agent/env_secrets.py +149 -0
- mobile_agent/errors.py +15 -0
- mobile_agent/evals/__init__.py +3 -0
- mobile_agent/evals/app_survey.py +149 -0
- mobile_agent/evals/development/__init__.py +1 -0
- mobile_agent/evals/development/score.py +276 -0
- mobile_agent/evals/latency_report.py +312 -0
- mobile_agent/evals/offline_report.py +374 -0
- mobile_agent/evals/oracles.py +298 -0
- mobile_agent/evals/redteam/__init__.py +15 -0
- mobile_agent/evals/redteam/__main__.py +72 -0
- mobile_agent/evals/redteam/cases.py +730 -0
- mobile_agent/evals/redteam/harness.py +604 -0
- mobile_agent/evals/redteam/live.py +139 -0
- mobile_agent/evals/redteam/plain.py +85 -0
- mobile_agent/evals/vision/__init__.py +0 -0
- mobile_agent/evals/vision/images/dog_001.jpg +0 -0
- mobile_agent/evals/vision/images/dog_002.jpg +0 -0
- mobile_agent/evals/vision/images/dog_003.jpg +0 -0
- mobile_agent/evals/vision/images/dog_004.jpg +0 -0
- mobile_agent/evals/vision/images/dog_005.jpg +0 -0
- mobile_agent/evals/vision/images/dog_006.jpg +0 -0
- mobile_agent/evals/vision/images/dog_007.jpg +0 -0
- mobile_agent/evals/vision/images/dog_010.jpg +0 -0
- mobile_agent/evals/vision/images/dog_011.jpg +0 -0
- mobile_agent/evals/vision/images/dog_013.jpg +0 -0
- mobile_agent/evals/vision/images/dog_014.jpg +0 -0
- mobile_agent/evals/vision/images/dog_015.jpg +0 -0
- mobile_agent/evals/vision/images/dog_016.jpg +0 -0
- mobile_agent/evals/vision/images/dog_017.jpg +0 -0
- mobile_agent/evals/vision/images/dog_019.jpg +0 -0
- mobile_agent/evals/vision/images/dog_020.jpg +0 -0
- mobile_agent/evals/vision/images/dog_021.jpg +0 -0
- mobile_agent/evals/vision/images/dog_022.jpg +0 -0
- mobile_agent/evals/vision/images/dog_023.jpg +0 -0
- mobile_agent/evals/vision/images/dog_024.jpg +0 -0
- mobile_agent/evals/vision/images/dog_026.jpg +0 -0
- mobile_agent/evals/vision/images/dog_027.jpg +0 -0
- mobile_agent/evals/vision/images/dog_034.jpg +0 -0
- mobile_agent/evals/vision/images/dog_035.jpg +0 -0
- mobile_agent/evals/vision/images/dog_038.jpg +0 -0
- mobile_agent/evals/vision/images/dog_042.jpg +0 -0
- mobile_agent/evals/vision/images/dog_047.jpg +0 -0
- mobile_agent/evals/vision/images/dog_052.jpg +0 -0
- mobile_agent/evals/vision/images/dog_058.jpg +0 -0
- mobile_agent/evals/vision/images/dog_062.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
- mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
- mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
- mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
- mobile_agent/evals/vision/manifest.json +2355 -0
- mobile_agent/eventbus.py +110 -0
- mobile_agent/explain.py +34 -0
- mobile_agent/failure_line.py +197 -0
- mobile_agent/fanout.py +663 -0
- mobile_agent/feedback/__init__.py +2 -0
- mobile_agent/feedback/bundle.py +204 -0
- mobile_agent/feedback/outbox.py +196 -0
- mobile_agent/feedback/register.py +9 -0
- mobile_agent/feedback/routes.py +177 -0
- mobile_agent/fleet.py +841 -0
- mobile_agent/fleet_health.py +729 -0
- mobile_agent/fleet_phone.py +186 -0
- mobile_agent/frontier/__init__.py +79 -0
- mobile_agent/frontier/act.py +380 -0
- mobile_agent/frontier/actions.py +70 -0
- mobile_agent/frontier/agent.py +256 -0
- mobile_agent/frontier/approvals.py +366 -0
- mobile_agent/frontier/hooks.py +199 -0
- mobile_agent/frontier/interrupts.py +125 -0
- mobile_agent/frontier/observe.py +331 -0
- mobile_agent/frontier/phases.py +381 -0
- mobile_agent/frontier/pipeline.py +61 -0
- mobile_agent/frontier/prompt.py +384 -0
- mobile_agent/frontier/protocol.py +91 -0
- mobile_agent/frontier/safety.py +467 -0
- mobile_agent/frontier/saved.py +375 -0
- mobile_agent/frontier/screen.py +419 -0
- mobile_agent/frontier/skills.py +147 -0
- mobile_agent/frontier/stages/__init__.py +1 -0
- mobile_agent/frontier/stages/after.py +192 -0
- mobile_agent/frontier/stages/approve.py +138 -0
- mobile_agent/frontier/stages/controls.py +115 -0
- mobile_agent/frontier/stages/ends.py +102 -0
- mobile_agent/frontier/stages/gate.py +92 -0
- mobile_agent/frontier/stages/guards.py +132 -0
- mobile_agent/frontier/stages/points.py +155 -0
- mobile_agent/frontier/stages/send.py +177 -0
- mobile_agent/frontier/turn.py +151 -0
- mobile_agent/full_disk_access.py +83 -0
- mobile_agent/gemini.py +69 -0
- mobile_agent/genuine.py +120 -0
- mobile_agent/graph/__init__.py +4 -0
- mobile_agent/graph/catalog.py +789 -0
- mobile_agent/graph/chain.py +306 -0
- mobile_agent/graph/dispatch.py +98 -0
- mobile_agent/graph/draft.py +373 -0
- mobile_agent/graph/edit.py +76 -0
- mobile_agent/graph/estimate.py +189 -0
- mobile_agent/graph/expr.py +562 -0
- mobile_agent/graph/functions.py +271 -0
- mobile_agent/graph/infer.py +144 -0
- mobile_agent/graph/nodes.py +936 -0
- mobile_agent/graph/phone_steps.py +178 -0
- mobile_agent/graph/rerun.py +327 -0
- mobile_agent/graph/run.py +899 -0
- mobile_agent/graph/schema.py +842 -0
- mobile_agent/graph/state.py +161 -0
- mobile_agent/graph/values.py +198 -0
- mobile_agent/harness/__init__.py +2 -0
- mobile_agent/harness/checkpoints.py +468 -0
- mobile_agent/harness/control.py +101 -0
- mobile_agent/harness/prewarm.py +213 -0
- mobile_agent/harness/register.py +43 -0
- mobile_agent/harness/routes.py +114 -0
- mobile_agent/harness/settings.py +66 -0
- mobile_agent/harness/tools/__init__.py +67 -0
- mobile_agent/harness/tools/ask_user.py +102 -0
- mobile_agent/harness/tools/watch.py +120 -0
- mobile_agent/harness_api.py +815 -0
- mobile_agent/hedge.py +203 -0
- mobile_agent/help_topics.py +305 -0
- mobile_agent/hints.py +248 -0
- mobile_agent/history.py +69 -0
- mobile_agent/home.py +214 -0
- mobile_agent/http_pool.py +175 -0
- mobile_agent/image_policy.py +139 -0
- mobile_agent/inbox.py +148 -0
- mobile_agent/installed_apps.py +344 -0
- mobile_agent/integrations/__init__.py +1 -0
- mobile_agent/integrations/cli.py +472 -0
- mobile_agent/integrations/client_files.py +65 -0
- mobile_agent/integrations/clients.py +998 -0
- mobile_agent/integrations/doctor.py +185 -0
- mobile_agent/integrations/jsonc.py +383 -0
- mobile_agent/integrations/routes.py +135 -0
- mobile_agent/integrations/rules.py +112 -0
- mobile_agent/integrations/server_options.py +46 -0
- mobile_agent/integrations/skill.py +152 -0
- mobile_agent/integrations/skill_files.py +10 -0
- mobile_agent/integrations/textedit.py +309 -0
- mobile_agent/intents/__init__.py +17 -0
- mobile_agent/intents/atlas.py +216 -0
- mobile_agent/intents/callback.py +44 -0
- mobile_agent/intents/cli.py +55 -0
- mobile_agent/intents/harvest.py +91 -0
- mobile_agent/intents/plan.py +93 -0
- mobile_agent/ios/__init__.py +1 -0
- mobile_agent/ios/guide.py +16 -0
- mobile_agent/ios/kb.py +212 -0
- mobile_agent/ios/kb_data.py +1588 -0
- mobile_agent/ios/mcp.py +44 -0
- mobile_agent/ios/register.py +8 -0
- mobile_agent/ios/system.py +369 -0
- mobile_agent/journal.py +531 -0
- mobile_agent/keychain_acl.py +374 -0
- mobile_agent/keys.py +395 -0
- mobile_agent/latency_overlap.py +36 -0
- mobile_agent/latency_trace.py +170 -0
- mobile_agent/learned/__init__.py +15 -0
- mobile_agent/learned/app_path.py +160 -0
- mobile_agent/learned/code_read.py +38 -0
- mobile_agent/learned/parts.py +546 -0
- mobile_agent/learned/programs.py +691 -0
- mobile_agent/learned/register.py +171 -0
- mobile_agent/learned/routes.py +61 -0
- mobile_agent/learned/taught.py +329 -0
- mobile_agent/learned/teach.py +538 -0
- mobile_agent/learned/workflows.py +647 -0
- mobile_agent/learned/zero.py +41 -0
- mobile_agent/links.py +37 -0
- mobile_agent/listen.py +607 -0
- mobile_agent/lock_spike.py +278 -0
- mobile_agent/lockscreen.py +526 -0
- mobile_agent/login.py +354 -0
- mobile_agent/longform/__init__.py +15 -0
- mobile_agent/longform/ads.py +191 -0
- mobile_agent/longform/blocks.py +117 -0
- mobile_agent/longform/budget.py +265 -0
- mobile_agent/longform/context.py +285 -0
- mobile_agent/longform/costs.py +93 -0
- mobile_agent/longform/digest.py +145 -0
- mobile_agent/longform/extract.py +508 -0
- mobile_agent/longform/feed.py +505 -0
- mobile_agent/longform/followup.py +168 -0
- mobile_agent/longform/frames.py +82 -0
- mobile_agent/longform/identity.py +87 -0
- mobile_agent/longform/inbox.py +221 -0
- mobile_agent/longform/items.py +425 -0
- mobile_agent/longform/models.py +115 -0
- mobile_agent/longform/notebook.py +329 -0
- mobile_agent/longform/plan.py +614 -0
- mobile_agent/longform/recall.py +195 -0
- mobile_agent/longform/receipts.py +81 -0
- mobile_agent/longform/register.py +124 -0
- mobile_agent/longform/report.py +343 -0
- mobile_agent/longform/routes.py +102 -0
- mobile_agent/longform/session.py +644 -0
- mobile_agent/longform/store.py +146 -0
- mobile_agent/longform/synthesis.py +347 -0
- mobile_agent/mac_tools.py +223 -0
- mobile_agent/manual_control.py +143 -0
- mobile_agent/maps.py +149 -0
- mobile_agent/marks.py +92 -0
- mobile_agent/mcp_server/__init__.py +3 -0
- mobile_agent/mcp_server/__main__.py +29 -0
- mobile_agent/mcp_server/action_journal.py +24 -0
- mobile_agent/mcp_server/action_tools.py +638 -0
- mobile_agent/mcp_server/approvals.py +219 -0
- mobile_agent/mcp_server/catalog.py +92 -0
- mobile_agent/mcp_server/cli.py +194 -0
- mobile_agent/mcp_server/codes.py +147 -0
- mobile_agent/mcp_server/device_tools.py +306 -0
- mobile_agent/mcp_server/direct.py +272 -0
- mobile_agent/mcp_server/expo_tools.py +105 -0
- mobile_agent/mcp_server/phone_tools.py +510 -0
- mobile_agent/mcp_server/protocol.py +364 -0
- mobile_agent/mcp_server/registry.py +55 -0
- mobile_agent/mcp_server/run_tools.py +531 -0
- mobile_agent/mcp_server/schemas.py +425 -0
- mobile_agent/mcp_server/screen_tools.py +168 -0
- mobile_agent/mcp_server/session.py +469 -0
- mobile_agent/mcp_server/support.py +145 -0
- mobile_agent/mcp_server/tools.py +493 -0
- mobile_agent/memory/__init__.py +9 -0
- mobile_agent/memory/cli.py +207 -0
- mobile_agent/memory/extract.py +191 -0
- mobile_agent/memory/listener.py +100 -0
- mobile_agent/memory/model.py +144 -0
- mobile_agent/memory/project.py +96 -0
- mobile_agent/memory/provider.py +127 -0
- mobile_agent/memory/register.py +21 -0
- mobile_agent/memory/retrieval.py +172 -0
- mobile_agent/memory/routes.py +216 -0
- mobile_agent/memory/runs.py +124 -0
- mobile_agent/memory/store.py +806 -0
- mobile_agent/memory/writer.py +278 -0
- mobile_agent/mirroring.py +585 -0
- mobile_agent/mirroring_runtime.py +221 -0
- mobile_agent/model_clients.py +604 -0
- mobile_agent/narrate.py +813 -0
- mobile_agent/native_helpers.py +84 -0
- mobile_agent/netxray/__init__.py +1 -0
- mobile_agent/netxray/analyze.py +322 -0
- mobile_agent/netxray/attribution.py +62 -0
- mobile_agent/netxray/capture.py +88 -0
- mobile_agent/netxray/cli.py +142 -0
- mobile_agent/netxray/job.py +219 -0
- mobile_agent/netxray/mobster-pcap.c +560 -0
- mobile_agent/netxray/packets.py +410 -0
- mobile_agent/netxray/pcapng.py +223 -0
- mobile_agent/netxray/register.py +93 -0
- mobile_agent/netxray/report.py +214 -0
- mobile_agent/netxray/store.py +48 -0
- mobile_agent/netxray/summary.py +157 -0
- mobile_agent/netxray/target.py +67 -0
- mobile_agent/netxray/tool.py +66 -0
- mobile_agent/netxray/trackers.py +194 -0
- mobile_agent/notify_triggers/__init__.py +1 -0
- mobile_agent/notify_triggers/rules.py +39 -0
- mobile_agent/notify_triggers/sources.py +315 -0
- mobile_agent/notify_triggers/watch.py +135 -0
- mobile_agent/offers.py +86 -0
- mobile_agent/orchestrate.py +215 -0
- mobile_agent/outlets/__init__.py +1 -0
- mobile_agent/outlets/connect.py +249 -0
- mobile_agent/outlets/folder.py +180 -0
- mobile_agent/outlets/hooks.py +453 -0
- mobile_agent/outlets/inbound.py +472 -0
- mobile_agent/outlets/inbound_reply.py +57 -0
- mobile_agent/outlets/inbound_routes.py +81 -0
- mobile_agent/outlets/kinds.py +308 -0
- mobile_agent/outlets/mail.py +110 -0
- mobile_agent/outlets/net.py +86 -0
- mobile_agent/outlets/notion.py +244 -0
- mobile_agent/outlets/register.py +56 -0
- mobile_agent/outlets/routes.py +135 -0
- mobile_agent/outlets/service.py +580 -0
- mobile_agent/outlets/sheets.py +169 -0
- mobile_agent/outlets/slack.py +177 -0
- mobile_agent/outlets/store.py +194 -0
- mobile_agent/outlets/vault.py +80 -0
- mobile_agent/outlets/webhook.py +316 -0
- mobile_agent/output_contract.py +141 -0
- mobile_agent/owner_presence.py +211 -0
- mobile_agent/ownership.py +219 -0
- mobile_agent/packs/__init__.py +1 -0
- mobile_agent/packs/calendar.py +243 -0
- mobile_agent/packs/context.py +68 -0
- mobile_agent/packs/core.py +757 -0
- mobile_agent/packs/front_door.py +316 -0
- mobile_agent/packs/heal.py +214 -0
- mobile_agent/packs/health.py +122 -0
- mobile_agent/packs/insights.py +76 -0
- mobile_agent/packs/instagram.py +640 -0
- mobile_agent/packs/linkedin.py +106 -0
- mobile_agent/packs/maps.py +196 -0
- mobile_agent/packs/mcp.py +149 -0
- mobile_agent/packs/messages.py +270 -0
- mobile_agent/packs/notes.py +226 -0
- mobile_agent/packs/photos.py +166 -0
- mobile_agent/packs/pins.py +47 -0
- mobile_agent/packs/reddit.py +110 -0
- mobile_agent/packs/register.py +276 -0
- mobile_agent/packs/rows.py +108 -0
- mobile_agent/packs/safari.py +143 -0
- mobile_agent/packs/settings.py +253 -0
- mobile_agent/packs/social.py +300 -0
- mobile_agent/packs/threads.py +97 -0
- mobile_agent/packs/tiktok.py +455 -0
- mobile_agent/packs/tools.py +290 -0
- mobile_agent/packs/x.py +127 -0
- mobile_agent/packs/youtube.py +199 -0
- mobile_agent/parity.py +270 -0
- mobile_agent/paths.py +99 -0
- mobile_agent/perception.py +470 -0
- mobile_agent/permissions/__init__.py +32 -0
- mobile_agent/permissions/consequence.py +535 -0
- mobile_agent/permissions/exceptions.py +99 -0
- mobile_agent/permissions/gate.py +316 -0
- mobile_agent/permissions/outside.py +316 -0
- mobile_agent/permissions/policy.py +288 -0
- mobile_agent/permissions/project_policy.py +411 -0
- mobile_agent/permissions/register.py +148 -0
- mobile_agent/permissions/rules.py +291 -0
- mobile_agent/phone_io/__init__.py +146 -0
- mobile_agent/phone_io/cli.py +427 -0
- mobile_agent/phone_io/clipboard.py +78 -0
- mobile_agent/phone_io/controls.py +78 -0
- mobile_agent/phone_io/files.py +289 -0
- mobile_agent/phone_io/handoff.py +123 -0
- mobile_agent/phone_io/icloud.py +118 -0
- mobile_agent/phone_io/install.py +188 -0
- mobile_agent/phone_io/network_capture.py +188 -0
- mobile_agent/phone_io/network_cli.py +138 -0
- mobile_agent/phone_io/network_profiles.py +138 -0
- mobile_agent/phone_io/photos.py +512 -0
- mobile_agent/phone_io/recording.py +115 -0
- mobile_agent/phone_io/secrets.py +146 -0
- mobile_agent/phone_io/transfer.py +895 -0
- mobile_agent/phone_io/tunnel.py +129 -0
- mobile_agent/phone_ready.py +91 -0
- mobile_agent/pinning.py +158 -0
- mobile_agent/plan_gate.py +807 -0
- mobile_agent/plugins/__init__.py +3 -0
- mobile_agent/plugins/install.py +163 -0
- mobile_agent/plugins/manifest.py +242 -0
- mobile_agent/plugins/register.py +15 -0
- mobile_agent/plugins/routes.py +82 -0
- mobile_agent/plugins/service.py +322 -0
- mobile_agent/plugins/skills.py +76 -0
- mobile_agent/plugins/store.py +79 -0
- mobile_agent/pool.py +285 -0
- mobile_agent/power_watch.py +275 -0
- mobile_agent/premium_model.py +22 -0
- mobile_agent/project_env.py +291 -0
- mobile_agent/project_env_cli.py +74 -0
- mobile_agent/project_env_routes.py +55 -0
- mobile_agent/projects/__init__.py +27 -0
- mobile_agent/projects/ci.py +332 -0
- mobile_agent/projects/clone.py +339 -0
- mobile_agent/projects/drafts.py +158 -0
- mobile_agent/projects/folder.py +436 -0
- mobile_agent/projects/github.py +131 -0
- mobile_agent/projects/register.py +31 -0
- mobile_agent/projects/revisions.py +253 -0
- mobile_agent/projects/routes.py +234 -0
- mobile_agent/projects/runs.py +297 -0
- mobile_agent/projects/service.py +695 -0
- mobile_agent/projects/setup.py +84 -0
- mobile_agent/projects/store.py +379 -0
- mobile_agent/projects/sync.py +380 -0
- mobile_agent/projects/workflow_files.py +270 -0
- mobile_agent/replay_core/__init__.py +26 -0
- mobile_agent/replay_core/checks.py +594 -0
- mobile_agent/replay_core/engine.py +596 -0
- mobile_agent/replay_core/heal.py +364 -0
- mobile_agent/replay_core/path.py +240 -0
- mobile_agent/replay_core/recorder.py +206 -0
- mobile_agent/replay_core/store.py +171 -0
- mobile_agent/result_blocks.py +941 -0
- mobile_agent/run.py +642 -0
- mobile_agent/run_delivery.py +123 -0
- mobile_agent/run_executor.py +200 -0
- mobile_agent/run_export.py +284 -0
- mobile_agent/run_gif.py +891 -0
- mobile_agent/runner_hints.py +86 -0
- mobile_agent/runner_renewal.py +205 -0
- mobile_agent/runtime_create.py +549 -0
- mobile_agent/runtime_driver.py +458 -0
- mobile_agent/runtime_limits.py +319 -0
- mobile_agent/runtime_queue.py +394 -0
- mobile_agent/runtime_runs.py +383 -0
- mobile_agent/runtime_status.py +181 -0
- mobile_agent/runtime_teach.py +295 -0
- mobile_agent/runtime_work.py +599 -0
- mobile_agent/screen_text.py +101 -0
- mobile_agent/secret_filter.py +113 -0
- mobile_agent/serve.py +160 -0
- mobile_agent/server.py +363 -0
- mobile_agent/service_support.py +42 -0
- mobile_agent/settle/__init__.py +1 -0
- mobile_agent/settle/predict.py +102 -0
- mobile_agent/settle/replay.py +92 -0
- mobile_agent/settle/store.py +93 -0
- mobile_agent/settle/stream.py +43 -0
- mobile_agent/setup_cli.py +225 -0
- mobile_agent/setup_errors.py +87 -0
- mobile_agent/setup_service.py +527 -0
- mobile_agent/share.py +104 -0
- mobile_agent/signing.py +184 -0
- mobile_agent/sim/__init__.py +6 -0
- mobile_agent/sim/__main__.py +26 -0
- mobile_agent/sim/api.py +952 -0
- mobile_agent/sim/apps.py +172 -0
- mobile_agent/sim/biometrics.py +84 -0
- mobile_agent/sim/cli.py +271 -0
- mobile_agent/sim/doctor.py +293 -0
- mobile_agent/sim/governor.py +90 -0
- mobile_agent/sim/holders.py +88 -0
- mobile_agent/sim/models.py +62 -0
- mobile_agent/sim/registry.py +161 -0
- mobile_agent/sim/simctl.py +269 -0
- mobile_agent/sim/wda.py +618 -0
- mobile_agent/sim/xcode.py +74 -0
- mobile_agent/skills/__init__.py +12 -0
- mobile_agent/skills/codes.py +597 -0
- mobile_agent/skills/notifications.py +281 -0
- mobile_agent/skills/open_url.py +127 -0
- mobile_agent/smart_events.py +588 -0
- mobile_agent/smart_result.py +490 -0
- mobile_agent/smart_run.py +413 -0
- mobile_agent/state.py +662 -0
- mobile_agent/states.py +217 -0
- mobile_agent/steering.py +70 -0
- mobile_agent/style.py +178 -0
- mobile_agent/supervised.py +234 -0
- mobile_agent/swift/helper_bt_hid.swift +534 -0
- mobile_agent/swift/helper_doc_text.swift +184 -0
- mobile_agent/swift/helper_draft_reply.swift +170 -0
- mobile_agent/swift/helper_map.swift +110 -0
- mobile_agent/swift/helper_mirroring.swift +1099 -0
- mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
- mobile_agent/swift/helper_pdf.swift +100 -0
- mobile_agent/swift/helper_phone_audio.swift +993 -0
- mobile_agent/swift/helper_phone_screen.swift +416 -0
- mobile_agent/swift/helper_screen_text.swift +24 -0
- mobile_agent/swift/helper_transfer.swift +246 -0
- mobile_agent/swift/ocr.swift +22 -0
- mobile_agent/task_policy.py +337 -0
- mobile_agent/team_signing/__init__.py +34 -0
- mobile_agent/team_signing/apple_ca.py +37 -0
- mobile_agent/team_signing/bundle.py +136 -0
- mobile_agent/team_signing/cli.py +208 -0
- mobile_agent/team_signing/identity.py +248 -0
- mobile_agent/team_signing/ledger.py +105 -0
- mobile_agent/team_signing/prebuilt.py +61 -0
- mobile_agent/team_signing/resign.py +256 -0
- mobile_agent/team_signing/service.py +382 -0
- mobile_agent/team_signing/tooling.py +243 -0
- mobile_agent/team_signing/writes.py +268 -0
- mobile_agent/terminal_image.py +81 -0
- mobile_agent/testkit/__init__.py +1 -0
- mobile_agent/testkit/a11y_baseline.py +132 -0
- mobile_agent/testkit/api.py +403 -0
- mobile_agent/testkit/capsule.py +400 -0
- mobile_agent/testkit/capsule_cli.py +57 -0
- mobile_agent/testkit/cli.py +948 -0
- mobile_agent/testkit/cli_arguments.py +193 -0
- mobile_agent/testkit/comparison.py +288 -0
- mobile_agent/testkit/comparison_cli.py +41 -0
- mobile_agent/testkit/discover.py +257 -0
- mobile_agent/testkit/editor.py +243 -0
- mobile_agent/testkit/execute.py +515 -0
- mobile_agent/testkit/github_checks.py +248 -0
- mobile_agent/testkit/gitref.py +66 -0
- mobile_agent/testkit/heal_review.py +176 -0
- mobile_agent/testkit/health.py +480 -0
- mobile_agent/testkit/history.py +255 -0
- mobile_agent/testkit/hosted_results.py +321 -0
- mobile_agent/testkit/input_files.py +173 -0
- mobile_agent/testkit/jobs.py +285 -0
- mobile_agent/testkit/junit_verdict.py +33 -0
- mobile_agent/testkit/maestro.py +802 -0
- mobile_agent/testkit/mcp.py +562 -0
- mobile_agent/testkit/mcp_comparison.py +88 -0
- mobile_agent/testkit/minimize.py +471 -0
- mobile_agent/testkit/minimizer_cli.py +48 -0
- mobile_agent/testkit/notify.py +212 -0
- mobile_agent/testkit/phone_video.py +191 -0
- mobile_agent/testkit/pr.py +491 -0
- mobile_agent/testkit/pr_report.py +562 -0
- mobile_agent/testkit/prbuild.py +148 -0
- mobile_agent/testkit/proof_report.py +55 -0
- mobile_agent/testkit/provenance.py +119 -0
- mobile_agent/testkit/prrun.py +244 -0
- mobile_agent/testkit/recent.py +166 -0
- mobile_agent/testkit/recipes.py +146 -0
- mobile_agent/testkit/record.py +204 -0
- mobile_agent/testkit/register.py +288 -0
- mobile_agent/testkit/reports.py +998 -0
- mobile_agent/testkit/repro.py +432 -0
- mobile_agent/testkit/scenario.py +440 -0
- mobile_agent/testkit/scenario_cli.py +37 -0
- mobile_agent/testkit/snapshots_cli.py +60 -0
- mobile_agent/testkit/suite.py +670 -0
- mobile_agent/testkit/targets.py +497 -0
- mobile_agent/testkit/xctest.py +378 -0
- mobile_agent/threads/__init__.py +1 -0
- mobile_agent/threads/chat_once.py +264 -0
- mobile_agent/threads/cli.py +740 -0
- mobile_agent/threads/client.py +326 -0
- mobile_agent/threads/context.py +246 -0
- mobile_agent/threads/mcp.py +739 -0
- mobile_agent/threads/recent.py +34 -0
- mobile_agent/threads/register.py +54 -0
- mobile_agent/threads/routes.py +188 -0
- mobile_agent/threads/service.py +975 -0
- mobile_agent/threads/store.py +361 -0
- mobile_agent/threads/summary.py +117 -0
- mobile_agent/threads/term.py +101 -0
- mobile_agent/threads/workflow_cli.py +104 -0
- mobile_agent/tls.py +36 -0
- mobile_agent/traces.py +271 -0
- mobile_agent/tracks.py +235 -0
- mobile_agent/transport.py +271 -0
- mobile_agent/triggers/__init__.py +1 -0
- mobile_agent/triggers/actions.py +204 -0
- mobile_agent/triggers/model.py +146 -0
- mobile_agent/triggers/register.py +29 -0
- mobile_agent/triggers/routes.py +101 -0
- mobile_agent/triggers/service.py +387 -0
- mobile_agent/triggers/store.py +142 -0
- mobile_agent/tui/__init__.py +89 -0
- mobile_agent/tui/app.py +582 -0
- mobile_agent/tui/chrome.py +321 -0
- mobile_agent/tui/commands.py +375 -0
- mobile_agent/tui/demo_phone.py +369 -0
- mobile_agent/tui/images.py +29 -0
- mobile_agent/tui/keys.py +64 -0
- mobile_agent/tui/prompt.py +195 -0
- mobile_agent/tui/render.py +395 -0
- mobile_agent/tui/screens.py +180 -0
- mobile_agent/tui/session.py +308 -0
- mobile_agent/tui/tasks.py +287 -0
- mobile_agent/tui/theme.py +118 -0
- mobile_agent/tui/widgets.py +272 -0
- mobile_agent/update_check.py +213 -0
- mobile_agent/usage.py +184 -0
- mobile_agent/verify/__init__.py +0 -0
- mobile_agent/verify/__main__.py +41 -0
- mobile_agent/verify/a11y.py +189 -0
- mobile_agent/verify/actions.py +290 -0
- mobile_agent/verify/assertions.py +570 -0
- mobile_agent/verify/audit.py +312 -0
- mobile_agent/verify/business.py +221 -0
- mobile_agent/verify/business_run.py +131 -0
- mobile_agent/verify/business_state.py +168 -0
- mobile_agent/verify/check_schema.py +398 -0
- mobile_agent/verify/checks.py +614 -0
- mobile_agent/verify/cli.py +423 -0
- mobile_agent/verify/conditions.py +483 -0
- mobile_agent/verify/crashes.py +321 -0
- mobile_agent/verify/device_audit.py +246 -0
- mobile_agent/verify/dialogs.py +287 -0
- mobile_agent/verify/evidence.py +705 -0
- mobile_agent/verify/fixtures.py +305 -0
- mobile_agent/verify/flow.py +213 -0
- mobile_agent/verify/l10n.py +141 -0
- mobile_agent/verify/netcerts.py +67 -0
- mobile_agent/verify/netproxy.py +356 -0
- mobile_agent/verify/netrun.py +81 -0
- mobile_agent/verify/netshim.m +28 -0
- mobile_agent/verify/netshim.py +48 -0
- mobile_agent/verify/netstubs.py +132 -0
- mobile_agent/verify/overlay.py +174 -0
- mobile_agent/verify/perf.py +239 -0
- mobile_agent/verify/proof.py +192 -0
- mobile_agent/verify/replay_admission.py +48 -0
- mobile_agent/verify/report.py +952 -0
- mobile_agent/verify/run_cleanup.py +67 -0
- mobile_agent/verify/run_links.py +48 -0
- mobile_agent/verify/run_perf.py +44 -0
- mobile_agent/verify/run_secrets.py +39 -0
- mobile_agent/verify/runner.py +976 -0
- mobile_agent/verify/runs.py +57 -0
- mobile_agent/verify/screen_coverage.py +181 -0
- mobile_agent/verify/smart.py +106 -0
- mobile_agent/verify/snapshot_run.py +114 -0
- mobile_agent/verify/snapshots.py +306 -0
- mobile_agent/verify/sound.py +335 -0
- mobile_agent/verify/source_observation.py +130 -0
- mobile_agent/verify/system_surfaces/__init__.py +7 -0
- mobile_agent/verify/system_surfaces/play.py +106 -0
- mobile_agent/verify/system_surfaces/spec.py +139 -0
- mobile_agent/verify/system_surfaces/stage.py +138 -0
- mobile_agent/verify/system_surfaces/surfaces.py +121 -0
- mobile_agent/verify/tree.py +223 -0
- mobile_agent/verify/verdict.py +377 -0
- mobile_agent/verify/wire.py +392 -0
- mobile_agent/verify/wirestore.py +176 -0
- mobile_agent/waiting/__init__.py +1 -0
- mobile_agent/waiting/contacts.py +73 -0
- mobile_agent/waiting/drafts.py +136 -0
- mobile_agent/waiting/job.py +145 -0
- mobile_agent/waiting/messages.py +174 -0
- mobile_agent/waiting/rank.py +115 -0
- mobile_agent/waiting/register.py +26 -0
- mobile_agent/watch.py +502 -0
- mobile_agent/wda/__init__.py +2 -0
- mobile_agent/wda/frame_clock.py +646 -0
- mobile_agent/wda/frame_source.py +325 -0
- mobile_agent/wda/frame_store.py +115 -0
- mobile_agent/wda/gate.py +163 -0
- mobile_agent/wda/gestures.py +291 -0
- mobile_agent/wda/guards.py +94 -0
- mobile_agent/wda/identity.py +55 -0
- mobile_agent/wda/observe.py +884 -0
- mobile_agent/wda/patches.py +856 -0
- mobile_agent/wda/point_gestures.py +150 -0
- mobile_agent/wda/source.py +32 -0
- mobile_agent/wda/system_dialogs.py +571 -0
- mobile_agent/wda/text.py +694 -0
- mobile_agent/wda/timeouts.py +77 -0
- mobile_agent/wda/video.py +505 -0
- mobile_agent/wda/watchdog.py +243 -0
- mobile_agent/wireless/__init__.py +15 -0
- mobile_agent/wireless/api.py +354 -0
- mobile_agent/wireless/cli.py +287 -0
- mobile_agent/wireless/devicectl.py +229 -0
- mobile_agent/wireless/exposure.py +33 -0
- mobile_agent/wireless/mobster-wifi-pair.c +122 -0
- mobile_agent/wireless/monitor.py +332 -0
- mobile_agent/wireless/network.py +334 -0
- mobile_agent/wireless/onboarding.py +293 -0
- mobile_agent/wireless/pair.py +124 -0
- mobile_agent/wireless/probe.py +322 -0
- mobile_agent/wireless/register.py +8 -0
- mobile_agent/wireless/relay.py +179 -0
- mobile_agent/wireless/result.py +87 -0
- mobile_agent/wireless/store.py +85 -0
- mobile_agent/wireless/switch.py +77 -0
- mobile_agent/wireless/transport.py +118 -0
- mobile_agent/wireless/words.py +81 -0
- mobile_agent/wireless/xctestrun.py +83 -0
- mobile_agent/workflow_tools.py +59 -0
- mobile_agent/workflows/__init__.py +2 -0
- mobile_agent/workflows/brief.py +201 -0
- mobile_agent/workflows/cron.py +110 -0
- mobile_agent/workflows/health.py +257 -0
- mobile_agent/workflows/history.py +126 -0
- mobile_agent/workflows/records.py +442 -0
- mobile_agent/workflows/runs.py +1000 -0
- mobile_agent/workflows/scheduler.py +134 -0
- mobile_agent/workflows/service.py +973 -0
- mobile_agent/workflows/starters.py +285 -0
- mobile_agent/workflows/triggers.py +248 -0
- mobile_agent/xray/__init__.py +1 -0
- mobile_agent/xray/app_cli.py +116 -0
- mobile_agent/xray/cli.py +21 -0
- mobile_agent/xray/crawl.py +754 -0
- mobile_agent/xray/diff.py +171 -0
- mobile_agent/xray/diff_cli.py +43 -0
- mobile_agent/xray/job.py +184 -0
- mobile_agent/xray/paywall.py +188 -0
- mobile_agent/xray/policy.py +305 -0
- mobile_agent/xray/register.py +43 -0
- mobile_agent/xray/report.py +283 -0
- mobile_agent/xray/screens.py +117 -0
- mobster_cli-0.4.3.dist-info/METADATA +93 -0
- mobster_cli-0.4.3.dist-info/RECORD +1009 -0
- mobster_cli-0.4.3.dist-info/WHEEL +5 -0
- mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
- mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
- mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
mobile_agent/contract.py
ADDED
|
@@ -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()]}
|