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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""The deterministic front door: a request that asks only for what a pack reader shows, or for a Settings page, is
|
|
2
|
+
answered by code before any model call. It never acts: a request that holds an act word is never routed."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import time
|
|
7
|
+
from dataclasses import dataclass, replace
|
|
8
|
+
|
|
9
|
+
from .. import harness_api
|
|
10
|
+
from . import core, health, pins, settings, tools
|
|
11
|
+
|
|
12
|
+
ORDER = 2 # after harness and long-form pick-up (0, 1), before the packs' own run fields (3)
|
|
13
|
+
MAX_GOAL = 200
|
|
14
|
+
MAX_N = 40
|
|
15
|
+
# Run fields a routed run may carry: a conversation's id, who asked, the permission mode. Anything else (a workflow's
|
|
16
|
+
# input, a destination, an account, a trigger, attachments, a replay mode) is a run the table wasn't written for.
|
|
17
|
+
SAFE_EXTRAS = frozenset({"threadId", "via", "permissionMode"})
|
|
18
|
+
# An app's name that is also what a request asks for ("messages"): the app is named by an alias, or started in.
|
|
19
|
+
NOUNS = frozenset({"messages"})
|
|
20
|
+
ALIASES = {"ig": "instagram", "insta": "instagram", "twitter": "x", "imessage": "messages",
|
|
21
|
+
"imessages": "messages"}
|
|
22
|
+
|
|
23
|
+
# Words a request may hold besides an ask's own: politeness, question frames, determiners, "latest".
|
|
24
|
+
STOP = frozenset("""a an the my of on in for to at up me please show tell give get read list check what whats what's
|
|
25
|
+
is are do does i have how much many now currently right latest last newest recent recently most top first this that
|
|
26
|
+
app""".split())
|
|
27
|
+
# Any of these in a request, and nothing is routed. Wider than the table needs: a word here only costs a model call.
|
|
28
|
+
ACTS = frozenset("""send sending sent post posting repost share buy purchase pay order book delete remove erase reset
|
|
29
|
+
like unlike follow unfollow block report mute restrict reply respond comment subscribe accept decline approve confirm
|
|
30
|
+
turn toggle enable disable change set update install uninstall clear add create write type call dial save unsend
|
|
31
|
+
submit cancel forward archive move rename edit join leave sign log unlock restart shut allow deny schedule invite
|
|
32
|
+
tag pin hide unhide""".split())
|
|
33
|
+
OPEN_VERBS = frozenset({"open", "go", "launch", "navigate", "take"})
|
|
34
|
+
OPEN_FILLER = frozenset({"to", "the", "my", "me", "app", "please", "up", "in", "and"})
|
|
35
|
+
SEPARATORS = re.compile(r"\s*(?:›|»|→|->|=>|>|/)\s*")
|
|
36
|
+
POLITE = re.compile(r"^(?:(?:hey|hi|ok|okay)[ ,]+)?(?:please[ ,]+)?"
|
|
37
|
+
r"(?:(?:can|could|would|will) you[ ,]+(?:please[ ,]+)?)?"
|
|
38
|
+
r"|[ ,.]*please[ .!?]*$", re.I)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def phrases(*sets):
|
|
42
|
+
return tuple(frozenset(s.split()) for s in sets)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def with_phone(*sets):
|
|
46
|
+
"""Each set as is, and with the phone named ("what iOS version is this iPhone on")."""
|
|
47
|
+
out = []
|
|
48
|
+
for base in sets:
|
|
49
|
+
out.append(base)
|
|
50
|
+
out += [f"{base} {referent}" for referent in ("phone", "iphone", "device")]
|
|
51
|
+
return phrases(*out)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Ask:
|
|
56
|
+
pack: str # the pack's key
|
|
57
|
+
reader: str # one of the pack's readers
|
|
58
|
+
words: tuple # the content words the request may hold, as frozensets: one of these exactly
|
|
59
|
+
pick: tuple = () # (column, regex): answer only with the rows whose column matches
|
|
60
|
+
counted: bool = False # "my last 10 DMs": a count is part of the ask
|
|
61
|
+
needs_app: bool = True # the app must be named, started in, or in front (False: Settings' own words)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
ASKS = (
|
|
65
|
+
Ask("settings", "about", with_phone("ios version", "software version", "version ios"),
|
|
66
|
+
pick=("setting", r"^iOS Version$"), needs_app=False),
|
|
67
|
+
Ask("settings", "about", with_phone("model", "model name"), pick=("setting", r"^Model Name$"), needs_app=False),
|
|
68
|
+
Ask("settings", "storage", with_phone("storage", "free storage", "storage left", "storage space", "space left",
|
|
69
|
+
"free space", "available storage", "storage available",
|
|
70
|
+
"storage remaining", "free storage space"),
|
|
71
|
+
pick=("item", r"^(Used|Capacity|Free)$"), needs_app=False),
|
|
72
|
+
Ask("settings", "battery", with_phone("battery", "battery level", "battery percentage", "battery percent"),
|
|
73
|
+
pick=("setting", r"^Level$"), needs_app=False),
|
|
74
|
+
Ask("settings", "battery", with_phone("battery health"), pick=("setting", r"^Battery Health$"), needs_app=False),
|
|
75
|
+
Ask("instagram", "dms", phrases("dms", "dm", "direct messages", "messages"), counted=True),
|
|
76
|
+
Ask("tiktok", "dms", phrases("dms", "dm", "inbox", "messages"), counted=True),
|
|
77
|
+
Ask("x", "messages", phrases("dms", "dm", "messages"), counted=True),
|
|
78
|
+
Ask("messages", "inbox", phrases("conversations", "messages", "texts", "chats"), counted=True),
|
|
79
|
+
Ask("messages", "unread", phrases("unread messages", "unread texts", "unread conversations", "unread"),
|
|
80
|
+
counted=True),
|
|
81
|
+
Ask("calendar", "events", phrases("events", "upcoming events", "upcoming"), counted=True),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Settings pages a request may open, as Settings names them, and the one parent a nested path must name. A page
|
|
85
|
+
# nobody listed here (and a path nobody checked) goes to the model.
|
|
86
|
+
SETTINGS_PAGES = {name.casefold(): name for name in settings.PAGES}
|
|
87
|
+
PARENTS = {"about": "General", "iphone storage": "General", "software update": "General"}
|
|
88
|
+
OWN_ROUTES = {"About": "about", "iPhone Storage": "storage", "Battery": "battery"}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class Plan:
|
|
93
|
+
pack: core.Pack
|
|
94
|
+
kind: str # "read" or "open"
|
|
95
|
+
what: str # the reader or the route
|
|
96
|
+
n: int = 10
|
|
97
|
+
arg: str = None
|
|
98
|
+
ask: Ask = None
|
|
99
|
+
page: str = None # an open's page, as Settings names it
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def tokens(text):
|
|
103
|
+
return re.findall(r"[a-z0-9&']+", str(text or "").casefold().replace("’", "'"))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def packs_by_word():
|
|
107
|
+
"""{word: pack} for every pack's name and key and the aliases people use."""
|
|
108
|
+
out = {}
|
|
109
|
+
for pack in registered():
|
|
110
|
+
for word in {pack.key, pack.name.casefold()} - NOUNS:
|
|
111
|
+
out[word] = pack
|
|
112
|
+
for alias, key in ALIASES.items():
|
|
113
|
+
pack = next((p for p in registered() if p.key == key), None)
|
|
114
|
+
if pack is not None:
|
|
115
|
+
out[alias] = pack
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def registered():
|
|
120
|
+
from . import register
|
|
121
|
+
return register.all_packs()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def reads_an_act(goal, words):
|
|
125
|
+
"""Whether the request holds an act: an act word, or what the safety gate reads as one."""
|
|
126
|
+
from .. import harness_api as api
|
|
127
|
+
return (any(word in ACTS for word in words) or bool(core.NEVER.search(goal))
|
|
128
|
+
or api.commit_act("", goal, None, None) is not None)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def parse(goal, *, app_bundle=None, front_bundle=None):
|
|
132
|
+
"""The ``Plan`` for a request that is exactly an ask in the table or an open of a Settings page, else None."""
|
|
133
|
+
text = POLITE.sub("", str(goal or "").strip())
|
|
134
|
+
if not text or len(text) > MAX_GOAL or "\n" in text:
|
|
135
|
+
return None
|
|
136
|
+
words = tokens(text)
|
|
137
|
+
if not words or reads_an_act(text, words):
|
|
138
|
+
return None
|
|
139
|
+
if words[0] in OPEN_VERBS:
|
|
140
|
+
return parse_open(text)
|
|
141
|
+
return parse_read(words, app_bundle, front_bundle)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def parse_read(words, app_bundle, front_bundle):
|
|
145
|
+
by_word = packs_by_word()
|
|
146
|
+
named = {by_word[w].key: by_word[w] for w in words if w in by_word}
|
|
147
|
+
if len(named) > 1:
|
|
148
|
+
return None # two apps in one request: the model decides what is meant
|
|
149
|
+
numbers = [w for w in words if w.isdigit()]
|
|
150
|
+
if len(numbers) > 1 or (numbers and not 1 <= int(numbers[0]) <= MAX_N):
|
|
151
|
+
return None
|
|
152
|
+
content = frozenset(w for w in words if w not in STOP and not w.isdigit() and w not in by_word)
|
|
153
|
+
matches = []
|
|
154
|
+
for ask in ASKS:
|
|
155
|
+
pack = next((p for p in registered() if p.key == ask.pack), None)
|
|
156
|
+
if pack is None or ask.reader not in pack.readers or (named and ask.pack not in named):
|
|
157
|
+
continue
|
|
158
|
+
if content not in ask.words or (numbers and not ask.counted):
|
|
159
|
+
continue
|
|
160
|
+
if ask.needs_app and not (ask.pack in named or pack.bundle in (app_bundle, front_bundle)):
|
|
161
|
+
continue
|
|
162
|
+
matches.append((pack, ask))
|
|
163
|
+
if len(matches) != 1:
|
|
164
|
+
return None
|
|
165
|
+
pack, ask = matches[0]
|
|
166
|
+
n = int(numbers[0]) if numbers else pack.read_default.get(ask.reader, 10)
|
|
167
|
+
return Plan(pack, "read", ask.reader, n, None, ask)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def parse_open(text):
|
|
171
|
+
""""open Settings", "go to Settings › General › About", "open settings > battery": Settings' own pages."""
|
|
172
|
+
parts = [p for p in SEPARATORS.split(text) if p.strip()]
|
|
173
|
+
first = [w for w in tokens(parts[0]) if w not in OPEN_VERBS and w not in OPEN_FILLER]
|
|
174
|
+
rest = [" ".join(tokens(p)) for p in parts[1:]]
|
|
175
|
+
if len(parts) == 1 and first and first[0] == "settings":
|
|
176
|
+
rest, first = [" ".join(first[1:])] if len(first) > 1 else [], ["settings"]
|
|
177
|
+
if first != ["settings"] or len(rest) > 2:
|
|
178
|
+
return None
|
|
179
|
+
pack = next((p for p in registered() if p.key == "settings"), None)
|
|
180
|
+
if pack is None:
|
|
181
|
+
return None
|
|
182
|
+
if not rest:
|
|
183
|
+
return Plan(pack, "open", "root")
|
|
184
|
+
pages = [SETTINGS_PAGES.get(name) for name in rest]
|
|
185
|
+
if None in pages:
|
|
186
|
+
return None
|
|
187
|
+
page = pages[-1]
|
|
188
|
+
if len(pages) == 2 and PARENTS.get(page.casefold()) != pages[0]:
|
|
189
|
+
return None # a path nobody checked: not claimed
|
|
190
|
+
route = OWN_ROUTES.get(page, "find")
|
|
191
|
+
return Plan(pack, "open", route, arg=page if route == "find" else None, page=page)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def enabled():
|
|
195
|
+
return os.environ.get("MOBSTER_FRONT_DOOR", "").strip().lower() not in ("off", "0", "false", "no")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def eligible(ctx):
|
|
199
|
+
if not enabled() or getattr(ctx, "engine", None) != "smart" or not getattr(ctx, "goal", None):
|
|
200
|
+
return False
|
|
201
|
+
if set(ctx.extras or {}) - SAFE_EXTRAS:
|
|
202
|
+
return False
|
|
203
|
+
run = (getattr(getattr(ctx, "runtime", None), "runs", None) or {}).get(ctx.run_id)
|
|
204
|
+
if run is not None and (getattr(run, "output_schema", None) is not None
|
|
205
|
+
or getattr(run, "output_format", None) in _structured()
|
|
206
|
+
or getattr(run, "longform", None) or getattr(run, "pinned_apps", None)):
|
|
207
|
+
return False # these need the model's structuring or its plan
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _structured():
|
|
212
|
+
from .. import smart_result
|
|
213
|
+
return smart_result.AUTOMATIC_FORMATS
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def front_app(driver):
|
|
217
|
+
"""The bundle id in front on the phone when the connection can say, else None."""
|
|
218
|
+
reader = getattr(driver, "active_app", None)
|
|
219
|
+
if callable(reader) and getattr(driver, "reads_front_app", True):
|
|
220
|
+
try:
|
|
221
|
+
bundle = reader()
|
|
222
|
+
except Exception: # noqa: BLE001 -- can't tell: not named
|
|
223
|
+
return None
|
|
224
|
+
return bundle if isinstance(bundle, str) else None
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def hook(ctx, driver, approve):
|
|
229
|
+
"""harness_api pre-run hook: answer the request by code, or None and the run goes on as it would."""
|
|
230
|
+
if driver is None or not eligible(ctx):
|
|
231
|
+
return None
|
|
232
|
+
words = tokens(ctx.goal)
|
|
233
|
+
# The phone is asked which app is in front only for a request that could use it (a bare "my DMs").
|
|
234
|
+
plan = parse(ctx.goal, app_bundle=ctx.app_bundle) or (
|
|
235
|
+
parse(ctx.goal, app_bundle=ctx.app_bundle, front_bundle=front_app(driver)) if words else None)
|
|
236
|
+
if plan is None:
|
|
237
|
+
return None
|
|
238
|
+
pinned = pins.aside(plan.pack)
|
|
239
|
+
if pinned:
|
|
240
|
+
ctx.emit({"event": "front_door", "routed": False, "pack": plan.pack.key, "why": pinned})
|
|
241
|
+
return None # the general agent does it
|
|
242
|
+
gate = tools.run_gate(ctx.run_id, ctx.data_dir)
|
|
243
|
+
version = core.app_version(ctx.runtime, plan.pack.bundle)
|
|
244
|
+
trusted, why = gate.trust("pack", plan.pack.bundle, plan.what, None, version)
|
|
245
|
+
if not trusted:
|
|
246
|
+
ctx.emit({"event": "front_door", "routed": False, "pack": plan.pack.key, "why": why})
|
|
247
|
+
return None
|
|
248
|
+
from .register import _io
|
|
249
|
+
pack, started = plan.pack, time.monotonic()
|
|
250
|
+
ctx.emit({"event": "skill_started", "op": f"{pack.prefix}_{'READ' if plan.kind == 'read' else 'OPEN'}"})
|
|
251
|
+
io = _io(ctx, driver, pack)
|
|
252
|
+
try:
|
|
253
|
+
answer = _read(pack, io, plan) if plan.kind == "read" else _open(pack, io, plan)
|
|
254
|
+
except core.Lost as lost:
|
|
255
|
+
answer, why = None, str(lost).rstrip(".")
|
|
256
|
+
ok = answer is not None
|
|
257
|
+
gate.note(ok, "pack", pack.bundle)
|
|
258
|
+
event = {"pack": pack.key, "op": f"{pack.prefix}_{plan.kind.upper()}", "step": plan.what, "ok": ok, "door": True,
|
|
259
|
+
"ms": round((time.monotonic() - started) * 1000), "taps": io.taps, "version": version,
|
|
260
|
+
"how": "code", **({} if ok else {"why": why})}
|
|
261
|
+
ctx.emit({"event": "app_pack", **event})
|
|
262
|
+
health.record(ctx.data_dir,
|
|
263
|
+
{k: v for k, v in event.items() if k != "why"} | ({} if ok else {"why": tools._quiet(why)}))
|
|
264
|
+
if not ok:
|
|
265
|
+
ctx.emit({"event": "front_door", "routed": False, "pack": pack.key, "why": why})
|
|
266
|
+
return None # the model takes the phone where the code left it
|
|
267
|
+
return harness_api.PreRun(result=_result(ctx, answer, started))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _read(pack, io, plan):
|
|
271
|
+
result = pack.read(io, plan.what, plan.n, plan.arg)
|
|
272
|
+
rows = list(result.rows)
|
|
273
|
+
if plan.ask.pick:
|
|
274
|
+
column, pattern = plan.ask.pick
|
|
275
|
+
rows = [row for row in rows if re.search(pattern, str(row.get(column) or ""), re.I)]
|
|
276
|
+
if not rows:
|
|
277
|
+
raise core.Lost(f"{pack.read_titles.get(plan.what, plan.what)} showed nothing it could name")
|
|
278
|
+
columns = result.columns
|
|
279
|
+
keep = rows[:tools.FEEDBACK_ROWS]
|
|
280
|
+
lines = "\n".join(_pair(row, columns) if plan.ask.pick else
|
|
281
|
+
tools.row_line(i + 1, row, columns, pack.required.get(plan.what, ()))
|
|
282
|
+
for i, row in enumerate(keep))
|
|
283
|
+
note = f" It read {len(rows)} of the {plan.n} you asked for: {result.stopped}." if (
|
|
284
|
+
plan.ask.counted and len(rows) < plan.n and result.stopped) else ""
|
|
285
|
+
block = None if plan.ask.pick else tools.block_for(pack, replace(result, rows=rows))
|
|
286
|
+
return {"text": f"{pack.name}, {result.title}, as the screen shows it:\n{lines}{note}", "rows": rows,
|
|
287
|
+
"columns": columns, "title": result.title, "stopped": result.stopped, "block": block}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _pair(row, columns):
|
|
291
|
+
"""One picked row as "iOS Version: 26.0.1" (its first two columns)."""
|
|
292
|
+
keys = [key for key, _label, _kind in columns][:2]
|
|
293
|
+
return ": ".join(core.plain(row.get(key)) for key in keys if row.get(key) not in (None, ""))
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _open(pack, io, plan):
|
|
297
|
+
landing = core.navigate(pack, io, plan.what, plan.arg)
|
|
298
|
+
if not landing.ok:
|
|
299
|
+
raise core.Lost(landing.why)
|
|
300
|
+
page = plan.page or "Settings"
|
|
301
|
+
return {"text": f"Opened {page} in {pack.name}. The screen is {landing.seen.title}.", "rows": [], "columns": [],
|
|
302
|
+
"title": page, "stopped": None, "block": None}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _result(ctx, answer, started):
|
|
306
|
+
from .. import result_blocks as rb
|
|
307
|
+
scalars = (str, int, float, bool, type(None))
|
|
308
|
+
rows = [{k: v for k, v in row.items() if isinstance(v, scalars)} for row in answer["rows"]]
|
|
309
|
+
columns = [{"key": k, "label": label, "type": kind} for k, label, kind in answer["columns"]]
|
|
310
|
+
data = {"rows": rows, "columns": columns, "title": answer["title"], "stopped": answer["stopped"] or None} \
|
|
311
|
+
if rows else None
|
|
312
|
+
blocks = [answer["block"]] if answer["block"] else []
|
|
313
|
+
return {"event": "result", "status": "completed", "engine": "smart", "outcome": "done", "answer": answer["text"],
|
|
314
|
+
"reason": None, "data": data, "actions": 1, "decisions": 0, "costUsd": 0.0, "model": None,
|
|
315
|
+
"elapsed_ms": round((time.monotonic() - started) * 1000), "blocks": rb.envelope(blocks),
|
|
316
|
+
"independently_verified": True, "proof": []}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Packs that survive an app's redesigns and A/B tests: drift is logged, the general agent takes over at once, and a
|
|
2
|
+
screen the pack missed can become a learned variant, used only after it passes a replay check."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from . import core
|
|
12
|
+
|
|
13
|
+
KIND = "variant" # the heal file's lines this module owns
|
|
14
|
+
MIN_SIGNALS = 3
|
|
15
|
+
MAX_SIGNALS = 6
|
|
16
|
+
MAX_VARIANTS = 12 # per pack, version and screen
|
|
17
|
+
MAX_LABEL = 28
|
|
18
|
+
CHROME_TOP, CHROME_BOTTOM = .3, .85
|
|
19
|
+
# A name, a handle, a number, a time or a link: never kept as a signal.
|
|
20
|
+
PERSONAL = re.compile(r"(@|https?://|\d{2,}|\b\d+\s?[smhdw]\b|\bago\b|[\w.]+\.(com|net|org)\b|"
|
|
21
|
+
r"^[A-Za-z0-9]+[._][A-Za-z0-9._]+$|^[A-Z][a-z]+ [A-Z][a-z]+$)", re.I)
|
|
22
|
+
_stores = {}
|
|
23
|
+
_lock = threading.Lock()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def fingerprint(snapshot):
|
|
27
|
+
"""The screen's structural hash (replay_core): which roles and label shapes, no text. None when unknown."""
|
|
28
|
+
try:
|
|
29
|
+
from ..replay_core.path import screen_hashes
|
|
30
|
+
hashed = screen_hashes(snapshot)
|
|
31
|
+
return hashed["hash"] if hashed else None
|
|
32
|
+
except Exception: # noqa: BLE001
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def chrome(snapshot):
|
|
37
|
+
"""The app's own words on ``snapshot``: short labels in its top part (titles, filters, the search field), the
|
|
38
|
+
current tab, and named buttons in between, with no name, handle, number, time or link in them."""
|
|
39
|
+
picked, seen = [], set()
|
|
40
|
+
for element in getattr(snapshot, "elements", ()) or ():
|
|
41
|
+
label = core.plain(element.label)
|
|
42
|
+
if not label or len(label) > MAX_LABEL or PERSONAL.search(label) or label.casefold() in seen:
|
|
43
|
+
continue
|
|
44
|
+
y = core.centre_y(element)
|
|
45
|
+
if y > CHROME_BOTTOM:
|
|
46
|
+
if not core.is_selected(element):
|
|
47
|
+
continue
|
|
48
|
+
elif y >= CHROME_TOP and element.role != "Button":
|
|
49
|
+
continue
|
|
50
|
+
seen.add(label.casefold())
|
|
51
|
+
picked.append((label, element.role, round(y, 2)))
|
|
52
|
+
picked.sort(key=lambda item: item[2])
|
|
53
|
+
return picked
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class Variant:
|
|
58
|
+
id: str
|
|
59
|
+
pack: str
|
|
60
|
+
version: str
|
|
61
|
+
screen: str
|
|
62
|
+
fingerprint: str
|
|
63
|
+
signals: tuple # ({"pattern", "role", "region"}, ...)
|
|
64
|
+
pictured: bool
|
|
65
|
+
status: str = "proposed" # proposed | confirmed | trusted
|
|
66
|
+
runs: tuple = ()
|
|
67
|
+
at: float = 0.0
|
|
68
|
+
checked_at: float = 0.0
|
|
69
|
+
|
|
70
|
+
def spec(self, pack):
|
|
71
|
+
signals = tuple(core.Signal(re.compile(s["pattern"], re.I), s.get("role"), tuple(s["region"])
|
|
72
|
+
if s.get("region") else None) for s in self.signals)
|
|
73
|
+
title = pack.screen_title(self.screen)
|
|
74
|
+
if self.pictured:
|
|
75
|
+
return core.ScreenSpec(self.screen, title, (), "learned", ocr=signals)
|
|
76
|
+
return core.ScreenSpec(self.screen, title, signals, "learned")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Learned:
|
|
80
|
+
"""The variants this Mac learned, by pack, app version and screen (a JSON file beside the journal)."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, path, clock=time.time):
|
|
83
|
+
self.path = Path(path) if path else None
|
|
84
|
+
self.clock = clock
|
|
85
|
+
self.lock = threading.Lock()
|
|
86
|
+
self.version_on_phone = {} # pack key -> the app version the phone has, when known
|
|
87
|
+
newest = {}
|
|
88
|
+
if self.path is not None and self.path.exists():
|
|
89
|
+
try:
|
|
90
|
+
for line in self.path.read_text(encoding="utf-8").splitlines():
|
|
91
|
+
try:
|
|
92
|
+
item = json.loads(line)
|
|
93
|
+
except ValueError:
|
|
94
|
+
continue
|
|
95
|
+
if item.get("kind") != KIND:
|
|
96
|
+
continue
|
|
97
|
+
item = {k: v for k, v in item.items() if k in Variant.__dataclass_fields__}
|
|
98
|
+
newest[item["id"]] = Variant(**{**item, "signals": tuple(item.get("signals") or ()),
|
|
99
|
+
"runs": tuple(item.get("runs") or ())})
|
|
100
|
+
except (OSError, TypeError, KeyError):
|
|
101
|
+
newest = {}
|
|
102
|
+
self.variants = list(newest.values())
|
|
103
|
+
|
|
104
|
+
def _write(self, variant):
|
|
105
|
+
"""One line for ``variant``'s new state in the heal file (its newest line is its state)."""
|
|
106
|
+
if self.path is None:
|
|
107
|
+
return
|
|
108
|
+
from .. import hints
|
|
109
|
+
hints._append(self.path, {"kind": KIND, "source": "pack", **variant.__dict__, "signals": list(variant.signals),
|
|
110
|
+
"runs": list(variant.runs)})
|
|
111
|
+
|
|
112
|
+
def _replace(self, old, new):
|
|
113
|
+
self.variants = [new if v is old else v for v in self.variants]
|
|
114
|
+
self._write(new)
|
|
115
|
+
|
|
116
|
+
def propose(self, pack, snapshot, screen, version, run_id):
|
|
117
|
+
"""A candidate variant of ``screen`` from ``snapshot`` (the pack missed it): its id, or None when the screen
|
|
118
|
+
shows too few of the app's own words to learn from."""
|
|
119
|
+
words = chrome(snapshot)[:MAX_SIGNALS]
|
|
120
|
+
if len(words) < MIN_SIGNALS:
|
|
121
|
+
return None
|
|
122
|
+
pictured = core.image_only(snapshot)
|
|
123
|
+
signals = tuple({"pattern": "^" + re.escape(label) + "$", "role": None if pictured else role,
|
|
124
|
+
"region": [max(0.0, round(y - .06, 2)), min(1.0, round(y + .06, 2))]}
|
|
125
|
+
for label, role, y in words)
|
|
126
|
+
mark = fingerprint(snapshot) or ""
|
|
127
|
+
with self.lock:
|
|
128
|
+
same = next((v for v in self.variants if (v.pack, v.version, v.screen) == (pack.key, version or "", screen)
|
|
129
|
+
and (v.fingerprint == mark and mark or v.signals == signals)), None)
|
|
130
|
+
if same is not None:
|
|
131
|
+
if run_id and run_id not in same.runs:
|
|
132
|
+
self._replace(same, Variant(**{**same.__dict__, "runs": (*same.runs, run_id)[-8:]}))
|
|
133
|
+
return same.id
|
|
134
|
+
kept = [v for v in self.variants if (v.pack, v.version, v.screen) == (pack.key, version or "", screen)]
|
|
135
|
+
if len(kept) >= MAX_VARIANTS:
|
|
136
|
+
return None
|
|
137
|
+
variant = Variant(f"{pack.key}-{screen}-{int(self.clock() * 1000) % 10**10}", pack.key, version or "",
|
|
138
|
+
screen, mark, signals, pictured, "proposed", (run_id,) if run_id else (), self.clock())
|
|
139
|
+
self.variants.append(variant)
|
|
140
|
+
self._write(variant)
|
|
141
|
+
return variant.id
|
|
142
|
+
|
|
143
|
+
def confirm_run(self, run_id):
|
|
144
|
+
"""The run finished completed: the general agent finished from the screens the pack missed in it."""
|
|
145
|
+
changed = 0
|
|
146
|
+
with self.lock:
|
|
147
|
+
for variant in list(self.variants):
|
|
148
|
+
if variant.status == "proposed" and run_id in variant.runs:
|
|
149
|
+
self._replace(variant, Variant(**{**variant.__dict__, "status": "confirmed"}))
|
|
150
|
+
changed += 1
|
|
151
|
+
return changed
|
|
152
|
+
|
|
153
|
+
def check(self, pack, snapshot, screen, version):
|
|
154
|
+
"""The replay check, on a fresh screen the pack missed again: a confirmed variant of ``screen`` that names it,
|
|
155
|
+
while no built-in screen of another name does, becomes trusted."""
|
|
156
|
+
trusted = []
|
|
157
|
+
with self.lock:
|
|
158
|
+
others = [spec for spec in pack.screens if spec.name != screen]
|
|
159
|
+
rival = max((core.score(spec, snapshot)[0] for spec in others), default=0.0)
|
|
160
|
+
for variant in list(self.variants):
|
|
161
|
+
if (variant.pack, variant.version, variant.screen, variant.status) != (pack.key, version or "", screen,
|
|
162
|
+
"confirmed"):
|
|
163
|
+
continue
|
|
164
|
+
if core.image_only(snapshot) != variant.pictured:
|
|
165
|
+
continue
|
|
166
|
+
value = core.score(variant.spec(pack), snapshot)[0]
|
|
167
|
+
if value >= core.SURE and rival < core.SURE:
|
|
168
|
+
new = Variant(**{**variant.__dict__, "status": "trusted", "checked_at": self.clock()})
|
|
169
|
+
self._replace(variant, new)
|
|
170
|
+
trusted.append(new.id)
|
|
171
|
+
return trusted
|
|
172
|
+
|
|
173
|
+
def specs(self, pack):
|
|
174
|
+
"""The trusted variants of ``pack`` as ScreenSpecs, for the app version on the phone (all, when unknown). An
|
|
175
|
+
act's control is never found through one: core.control_for reads the pack's own patterns."""
|
|
176
|
+
version = self.version_on_phone.get(pack.key)
|
|
177
|
+
return tuple(v.spec(pack) for v in self.variants if v.pack == pack.key and v.status == "trusted"
|
|
178
|
+
and (not version or v.version in (version, "")))
|
|
179
|
+
|
|
180
|
+
def summary(self, pack_key):
|
|
181
|
+
counts = {}
|
|
182
|
+
for variant in self.variants:
|
|
183
|
+
if variant.pack == pack_key:
|
|
184
|
+
counts[variant.status] = counts.get(variant.status, 0) + 1
|
|
185
|
+
return counts
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def store_for(data_dir):
|
|
189
|
+
"""The one Learned store of a data folder. Without one (a benchmark, a test), a fresh store that keeps nothing."""
|
|
190
|
+
if not data_dir:
|
|
191
|
+
return Learned(None)
|
|
192
|
+
from .. import hints
|
|
193
|
+
key = str(Path(data_dir) / hints.HEALS)
|
|
194
|
+
with _lock:
|
|
195
|
+
if key not in _stores:
|
|
196
|
+
_stores[key] = Learned(key)
|
|
197
|
+
return _stores[key]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def confirm_run(run_id):
|
|
201
|
+
"""A run that finished completed: confirm, in every store, the variants proposed from screens it missed."""
|
|
202
|
+
with _lock:
|
|
203
|
+
stores = list(_stores.values())
|
|
204
|
+
for store in stores:
|
|
205
|
+
store.confirm_run(run_id)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def attach(packs, data_dir):
|
|
209
|
+
"""Give ``packs`` the store of ``data_dir``: core.recognize then uses its trusted variants."""
|
|
210
|
+
store = store_for(data_dir)
|
|
211
|
+
if data_dir:
|
|
212
|
+
for pack in packs:
|
|
213
|
+
pack.learned = store
|
|
214
|
+
return store
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Pack health: which app version a pack was checked on, how its steps go on this Mac, and when it steps aside."""
|
|
2
|
+
|
|
3
|
+
RECENT = 200
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def bundle_of(pack_key):
|
|
7
|
+
"""The bundle id of the pack named ``pack_key`` (the ledger's ``app``), or the key when no pack has it."""
|
|
8
|
+
try:
|
|
9
|
+
from .register import all_packs
|
|
10
|
+
return next((pack.bundle for pack in all_packs() if pack.key == pack_key), pack_key)
|
|
11
|
+
except Exception: # noqa: BLE001
|
|
12
|
+
return pack_key
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def record(data_dir, entry):
|
|
16
|
+
"""Append one pack step (``entry``, no screen text) to the hint ledger."""
|
|
17
|
+
from .. import hints
|
|
18
|
+
hints.record(data_dir, {**entry, "kind": "use", "source": "pack", "app": bundle_of(entry.get("pack")),
|
|
19
|
+
"key": entry.get("step"), "ok": bool(entry.get("ok"))})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def recent(data_dir, pack_key=None, limit=RECENT):
|
|
23
|
+
"""The newest ``limit`` pack steps (of one pack), oldest first."""
|
|
24
|
+
from .. import hints
|
|
25
|
+
lines = [e for e in hints.recent(data_dir, source="pack", limit=10 ** 6)
|
|
26
|
+
if e.get("kind", "use") == "use" and (pack_key is None or e.get("pack") == pack_key)]
|
|
27
|
+
return lines[-limit:]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def status(pack, installed=None, entries=()):
|
|
31
|
+
"""One pack's health: what it was checked on, what the phone has, and how its recent steps went."""
|
|
32
|
+
verified = pack.verified
|
|
33
|
+
steps = [e for e in entries if e.get("pack") == pack.key]
|
|
34
|
+
landed = sum(1 for e in steps if e.get("ok"))
|
|
35
|
+
aside = [e for e in steps if not e.get("ok")]
|
|
36
|
+
state = "unknown"
|
|
37
|
+
if steps:
|
|
38
|
+
state = "healthy" if not aside or landed / len(steps) >= .8 else "degraded"
|
|
39
|
+
changed = changed_screens(steps)
|
|
40
|
+
version = installed or (verified.version if verified else None)
|
|
41
|
+
if not steps:
|
|
42
|
+
line = f"{pack.name} pack: not used on this Mac yet"
|
|
43
|
+
elif changed:
|
|
44
|
+
line = (f"{pack.name} pack: working on {version}; {len(changed)} screen{'s' if len(changed) != 1 else ''} "
|
|
45
|
+
"changed")
|
|
46
|
+
else:
|
|
47
|
+
line = f"{pack.name} pack: working on {version}"
|
|
48
|
+
learned = (getattr(pack, "learned", None).summary(pack.key) if getattr(pack, "learned", None) is not None else {})
|
|
49
|
+
return {"pack": pack.key, "name": pack.name, "bundleId": pack.bundle, "summary": line,
|
|
50
|
+
"changedScreens": [{"screen": name, "title": pack.screen_title(name), "since": at, "version": v}
|
|
51
|
+
for name, (at, v) in changed.items()],
|
|
52
|
+
"learned": learned,
|
|
53
|
+
"verified": ({"version": verified.version, "date": verified.date, "device": verified.device or None}
|
|
54
|
+
if verified else None),
|
|
55
|
+
"pin": _pin(pack),
|
|
56
|
+
"installed": installed, "sameVersion": (installed == verified.version) if installed and verified else None,
|
|
57
|
+
"screens": [{"name": s.name, "title": s.title, "evidence": s.evidence} for s in pack.screens],
|
|
58
|
+
"steps": len(steps), "landed": landed, "steppedAside": len(aside), "state": state,
|
|
59
|
+
"lastProblem": ({"step": aside[-1].get("step"), "why": aside[-1].get("why"), "at": aside[-1].get("at"),
|
|
60
|
+
"version": aside[-1].get("version")} if aside else None)}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _pin(pack):
|
|
64
|
+
"""What the team pinned this pack to ("off" or an app version), and whether it steps aside; None with no pin."""
|
|
65
|
+
from . import pins
|
|
66
|
+
to = pins.current().get(pack.key)
|
|
67
|
+
return None if to is None else {"to": to, "aside": pins.aside(pack) is not None}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def changed_screens(steps):
|
|
71
|
+
"""{screen: (since, version)} for each screen a pack last failed to recognize (``unsure_screen``) with no
|
|
72
|
+
recognition of it since, in log order: the screens the app changed under the pack."""
|
|
73
|
+
missed = {}
|
|
74
|
+
for entry in steps:
|
|
75
|
+
if entry.get("unsure_screen"):
|
|
76
|
+
missed[entry["unsure_screen"]] = (entry.get("at") or 0, entry.get("version"))
|
|
77
|
+
elif entry.get("ok") and entry.get("screen"):
|
|
78
|
+
missed.pop(entry["screen"], None)
|
|
79
|
+
return missed
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def tab_routes(pack):
|
|
83
|
+
"""The routes a health run visits: each reaches a screen from the tab bar by taps alone (no argument, no link
|
|
84
|
+
needed,
|
|
85
|
+
no act)."""
|
|
86
|
+
acts = {spec.route for spec in pack.controls.values()}
|
|
87
|
+
return [route for route in pack.routes.values()
|
|
88
|
+
if route.from_tabs and not route.arg and route.name not in acts and route.steps
|
|
89
|
+
and all(type(step).__name__ == "Tap" for step in route.steps)]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def check(pack, io):
|
|
93
|
+
"""Visit each tab route and say whether the pack named the screen it reached: [{route, screen, landed, confidence,
|
|
94
|
+
evidence, why}]."""
|
|
95
|
+
from . import core
|
|
96
|
+
rows = []
|
|
97
|
+
for route in tab_routes(pack):
|
|
98
|
+
landing = core.navigate(pack, io, route.name)
|
|
99
|
+
rows.append({"route": route.name, "title": route.title, "screen": landing.seen.screen, "landed": landing.ok,
|
|
100
|
+
"confidence": landing.seen.confidence, "evidence": landing.seen.evidence or None,
|
|
101
|
+
"why": landing.why or None})
|
|
102
|
+
if io.cancelled():
|
|
103
|
+
break
|
|
104
|
+
return rows
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def check_result(pack, rows, version, seconds):
|
|
108
|
+
"""A health run's result, as a task's: the answer line and a checklist card."""
|
|
109
|
+
from .. import result_blocks as rb
|
|
110
|
+
landed = sum(1 for row in rows if row["landed"])
|
|
111
|
+
items = [{"id": f"s{i + 1}", "label": row["title"][:1].upper() + row["title"][1:],
|
|
112
|
+
"status": "pass" if row["landed"] else "fail",
|
|
113
|
+
"expected": f"{pack.name} shows {row['title']}",
|
|
114
|
+
"observed": (f"recognized ({row['confidence']:.2f})" if row["landed"] else (row["why"] or "not named"))}
|
|
115
|
+
for i, row in enumerate(rows)]
|
|
116
|
+
changed = len(rows) - landed
|
|
117
|
+
answer = (f"{pack.name} pack: working on {version or 'this version'}"
|
|
118
|
+
+ (f"; {changed} screen{'s' if changed != 1 else ''} changed." if changed else "."))
|
|
119
|
+
return {"event": "result", "status": "completed", "engine": "smart", "outcome": "done" if not changed else "check",
|
|
120
|
+
"answer": answer, "actions": len(rows), "decisions": 0, "elapsed_ms": round(seconds * 1000),
|
|
121
|
+
"costUsd": 0.0, "model": None,
|
|
122
|
+
"blocks": rb.envelope([rb.checklist(items, title=f"{pack.name} pack health", id=f"{pack.key}-health")])}
|