mobile-debug-mcp 0.32.4 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +14 -0
  3. package/.cursor-plugin/marketplace.json +16 -0
  4. package/.github/copilot-instructions.md +7 -0
  5. package/AGENTS.md +21 -10
  6. package/README.md +62 -4
  7. package/agent-plugin/.claude-plugin/plugin.json +20 -0
  8. package/agent-plugin/.codex-plugin/plugin.json +34 -0
  9. package/agent-plugin/.cursor-plugin/plugin.json +23 -0
  10. package/agent-plugin/.mcp.json +14 -0
  11. package/agent-plugin/README.md +5 -3
  12. package/agent-plugin/assets/mobile-debug-tools.svg +28 -0
  13. package/agent-plugin/mcp.json +1 -1
  14. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +28 -0
  15. package/dist/interact/android.js +10 -4
  16. package/dist/interact/conditional-action.js +68 -0
  17. package/dist/interact/index.js +54 -14
  18. package/dist/interact/ios.js +14 -8
  19. package/dist/manage/android.js +4 -3
  20. package/dist/manage/ios.js +3 -2
  21. package/dist/observe/android.js +25 -4
  22. package/dist/observe/evidence.js +273 -0
  23. package/dist/observe/index.js +33 -6
  24. package/dist/observe/ios.js +25 -6
  25. package/dist/observe/readiness-service.js +115 -0
  26. package/dist/observe/readiness.js +81 -0
  27. package/dist/observe/recording.js +165 -0
  28. package/dist/observe/snapshot-metadata.js +10 -2
  29. package/dist/server/common.js +9 -0
  30. package/dist/server/condition-schema.js +32 -0
  31. package/dist/server/journey-schema.js +3 -2
  32. package/dist/server/readiness-schema.js +12 -0
  33. package/dist/server/tool-definitions.js +46 -0
  34. package/dist/server/tool-handlers.js +99 -17
  35. package/dist/server-core.js +1 -1
  36. package/dist/utils/ios/utils.js +4 -4
  37. package/dist/utils/operation-budget.js +24 -2
  38. package/dist/utils/resolve-device.js +7 -0
  39. package/docs/CHANGELOG.md +11 -0
  40. package/docs/specs/020-conditional-actions/intent.md +62 -0
  41. package/docs/specs/020-conditional-actions/plan.md +214 -0
  42. package/docs/specs/020-conditional-actions/spec.md +301 -0
  43. package/docs/specs/021-app-readiness-signals/intent.md +40 -0
  44. package/docs/specs/021-app-readiness-signals/plan.md +102 -0
  45. package/docs/specs/021-app-readiness-signals/spec.md +144 -0
  46. package/docs/specs/022-correlated-evidence/device-evidence.json +60 -0
  47. package/docs/specs/022-correlated-evidence/intent.md +37 -0
  48. package/docs/specs/022-correlated-evidence/modul8-evidence.json +1062 -0
  49. package/docs/specs/022-correlated-evidence/modul8-selector-fix-evidence.json +336 -0
  50. package/docs/specs/022-correlated-evidence/plan.md +171 -0
  51. package/docs/specs/022-correlated-evidence/spec.md +123 -0
  52. package/docs/specs/process.md +213 -0
  53. package/docs/tools/TOOLS.md +3 -0
  54. package/docs/tools/evidence.md +79 -0
  55. package/docs/tools/interact.md +125 -2
  56. package/docs/tools/observe.md +63 -0
  57. package/package.json +1 -1
  58. package/scripts/agent-plugin.mjs +68 -8
  59. package/src/interact/android.ts +10 -4
  60. package/src/interact/conditional-action.ts +75 -0
  61. package/src/interact/index.ts +42 -14
  62. package/src/interact/ios.ts +14 -8
  63. package/src/manage/android.ts +4 -3
  64. package/src/manage/ios.ts +3 -2
  65. package/src/observe/android.ts +33 -6
  66. package/src/observe/evidence.ts +255 -0
  67. package/src/observe/index.ts +32 -8
  68. package/src/observe/ios.ts +32 -8
  69. package/src/observe/readiness-service.ts +89 -0
  70. package/src/observe/readiness.ts +78 -0
  71. package/src/observe/snapshot-metadata.ts +12 -1
  72. package/src/server/common.ts +7 -0
  73. package/src/server/condition-schema.ts +32 -0
  74. package/src/server/journey-schema.ts +3 -2
  75. package/src/server/readiness-schema.ts +12 -0
  76. package/src/server/tool-definitions.ts +44 -0
  77. package/src/server/tool-handlers.ts +93 -17
  78. package/src/server-core.ts +1 -1
  79. package/src/types.ts +30 -1
  80. package/src/utils/ios/utils.ts +4 -4
  81. package/src/utils/operation-budget.ts +22 -2
  82. package/src/utils/resolve-device.ts +8 -0
  83. package/test/device/automated/observe/correlated-evidence.smoke.ts +70 -0
  84. package/test/device/manual/interact/conditional_actions.manual.ts +95 -0
  85. package/test/device/manual/interact/modul8_selector_tap.manual.ts +63 -0
  86. package/test/device/manual/interact/readiness.manual.ts +99 -0
  87. package/test/fixtures/readiness-app/MainActivity.java +23 -0
  88. package/test/fixtures/readiness-app/README.md +12 -0
  89. package/test/fixtures/readiness-app/build.ts +37 -0
  90. package/test/fixtures/readiness-app/main.m +27 -0
  91. package/test/unit/observe/android_tree_acquisition.test.ts +2 -0
  92. package/test/unit/observe/capture_debug_snapshot.test.ts +42 -32
  93. package/test/unit/observe/compact_tree_error.test.ts +24 -0
  94. package/test/unit/observe/conditional_snapshots.test.ts +33 -0
  95. package/test/unit/observe/correlated-evidence.test.ts +128 -0
  96. package/test/unit/observe/ios_tree_acquisition.test.ts +2 -0
  97. package/test/unit/observe/readiness.test.ts +39 -0
  98. package/test/unit/observe/readiness_native.test.ts +68 -0
  99. package/test/unit/server/conditional_actions.test.ts +299 -0
  100. package/test/unit/server/conditional_schema.test.ts +59 -0
  101. package/test/unit/server/correlated-evidence.test.ts +194 -0
  102. package/test/unit/server/readiness.test.ts +170 -0
  103. package/test/unit/server/selector_tap_target.test.ts +79 -0
  104. package/test/unit/server/tap_selector.test.ts +2 -0
  105. package/skills/mobile-debug-tools-usage/SKILL.md +0 -96
  106. /package/{skills → .agents/skills}/README.md +0 -0
  107. /package/{skills → .agents/skills}/mcp-builder/SKILL.md +0 -0
  108. /package/{skills → .agents/skills}/mcp-builder/references/build-flags.md +0 -0
  109. /package/{skills → .agents/skills}/mcp-builder/references/diagnostics-schema.md +0 -0
  110. /package/{skills → .agents/skills}/mcp-builder/references/toolchain-details.md +0 -0
  111. /package/{skills → .agents/skills}/spec-review/SKILL.md +0 -0
  112. /package/{skills → .agents/skills}/spec-review/references/spec-review-checklist.md +0 -0
  113. /package/{skills → .agents/skills}/spec-review/references/spec-review-template.md +0 -0
  114. /package/{skills → .agents/skills}/test-authoring/SKILL.md +0 -0
  115. /package/{skills → .agents/skills}/test-authoring/references/repo-test-layout.md +0 -0
  116. /package/{skills → .agents/skills}/test-authoring/references/test-authoring-workflow.md +0 -0
  117. /package/{skills → .agents/skills}/test-authoring/references/test-quality-checklist.md +0 -0
@@ -0,0 +1,214 @@
1
+ # Plan: conditional actions
2
+
3
+ Date: 2026-10-29. Status: complete — implementation and required verification passed.
4
+ Intent: [intent.md](intent.md). Specification: [spec.md](spec.md).
5
+ Both approved by the originator's separate “yes” replies in this conversation;
6
+ the latest approval authorises implementation of spec 020.
7
+
8
+ ## Implementation sequence
9
+
10
+ - [x] Inspect action wrapping, native input, observation/history, budgets, tests.
11
+ - [x] Add session metadata and retained successful-baseline lookup without changing
12
+ existing revision semantics or growing history.
13
+ - [x] Add strict condition schema, supported-tool validation, and journey tap wiring.
14
+ - [x] Add a request-scoped asynchronous pre-dispatch guard, pin the device, and
15
+ isolate the observation budget from the action budget (respecting outer budgets).
16
+ - [x] Integrate at Android/iOS input spawn boundaries; retain failure evidence
17
+ through adapter errors; defer guarded network-window reset until dispatch.
18
+ - [x] Preserve condition evidence and non-delivery semantics in compact/debug
19
+ responses, including journey stop and not-checked cases.
20
+ - [x] Add deterministic unit/adapter regression coverage and device journeys.
21
+ - [x] Update public docs/changelog and packaged agent usage guidance; sync/check plugin.
22
+ - [x] Run focused checks, maintainer verification, device suite, and focused device journeys.
23
+ - [x] Review changed code/contracts/callers and record results and limits.
24
+
25
+ ## Expected affected files
26
+
27
+ - `src/observe/snapshot-metadata.ts`, `src/observe/index.ts`, `src/types.ts`.
28
+ - New shared condition schema/guard modules under `src/` as responsibility requires.
29
+ - `src/utils/operation-budget.ts`, Android/iOS input adapters.
30
+ - `src/server/tool-definitions.ts`, `tool-handlers.ts`, `journey-schema.ts`, `common.ts`.
31
+ - Unit tests under `test/unit/server/`, `test/unit/observe/`, and adapter fixtures.
32
+ - Focused reusable device helper under `test/device/manual/` if explicit simulator
33
+ selection/state setup makes it unsuitable for automatic generic suite execution.
34
+ - `docs/tools/interact.md`, `observe.md`, `docs/CHANGELOG.md`, `README.md`, and
35
+ `agent-plugin/skills/mobile-debug-mcp/SKILL.md`.
36
+
37
+ ## Verification mapping
38
+
39
+ | Spec criteria | Verification |
40
+ | --- | --- |
41
+ | 1, 5, 8 | Real Android/iOS adapter calls using fake binaries; all supported actions, verification modes and response modes; unguarded compatibility tests |
42
+ | 2, 3 | Mutable hierarchy fixture changes text/bounds/enabled/state and changes during target resolution/adapter preparation; assert zero native input and exact final tree |
43
+ | 4 | Session, history expiry/eviction, device mismatch, error/malformed observation, timeout including late completion and child cleanup |
44
+ | 6 | Journey with earlier passed step, blocked conditional tap, later not-run step; both response modes |
45
+ | 7 | Runtime invalid-input matrix and advertised schemas; unsupported tools/steps rejected before any side effects |
46
+ | 9 | Overlapping guarded/unguarded calls and network-window reset spy; condition budget distinct from input budget; outer-budget expiry |
47
+ | 10 | Documentation and focused review against actual implementation |
48
+
49
+ Commands: focused test files via existing `tsx` runtime, then `npm run verify`,
50
+ `npm run test:device`, and focused device helper against built `dist/` modules.
51
+ Runtime: repository supports Node >=18; record actual Node/toolchain with results.
52
+ No new dependency, migration, helper APK, publication, or service is needed.
53
+
54
+ ## Device routes and readiness
55
+
56
+ Available during spec reconnaissance: Android 16 `emulator-5554` and booted iOS
57
+ 26.4 iPhone Air `F72108FF-C8D6-4C1E-980E-1AE5E5CCD27B`. The physical Pixel is
58
+ excluded from mutation checks. Tool access and fresh trees work on both simulators.
59
+ Before device runs, inspect current state and choose safe system controls; avoid
60
+ interacting with the open Modul8 subscription flow. Use an already-installed
61
+ system Settings app on Android/iOS where possible; no accounts or entitlements.
62
+
63
+ For each platform on the newly built server:
64
+
65
+ 1. Open system Settings, observe its tree and session/revision, repeat to check a
66
+ stable baseline, then perform a guarded safe navigation tap and assert destination.
67
+ 2. Observe the baseline, navigate to another Settings screen, then send a guarded
68
+ action with the old expectation; assert non-delivery and returned current tree.
69
+ Confirm the blocked action has no effect using a fresh state assertion.
70
+ 3. Run a journey with an earlier successful wait/assert, an old-revision guarded
71
+ tap, and a later action. Confirm the tap fails, later step is not run, and state
72
+ stays unchanged. Repeat relevant compact/debug evidence checks.
73
+
74
+ Exact selectors depend on the current simulator OS tree and will be recorded
75
+ with results. Restore original foreground apps where practical; do not alter
76
+ settings, app data, or subscriptions. Owner of local prerequisites: current task.
77
+ Unavailable/timeout and last-moment preparation races use controlled fake binaries;
78
+ no forced device disconnect or privileged settings change is needed. This is
79
+ reported as simulated error-path evidence, separate from real-device passes.
80
+
81
+ ## Results
82
+
83
+ Implementation is present. Node.js v23.11.0 was used locally (the supported
84
+ minimum is >=18; CI's Node 18 run has not been executed locally).
85
+
86
+ ### Automated checks
87
+
88
+ - Initial `npm run verify`: PASS, including lint, build, unit tests, plugin check.
89
+ - Focused conditional action regression: PASS using production Android/iOS input
90
+ adapters and controlled fake native binaries. Coverage includes all supported
91
+ actions, each verification/response mode, late target-preparation changes,
92
+ mismatches, unavailable/malformed observations, session/history/device rejection,
93
+ request isolation, native input failure, and journey stop. Complete native
94
+ observation/input paths also pass with fake ADB/idb, including child-close
95
+ evidence for condition timeouts. This is simulated platform evidence.
96
+ - Focused schema parity and retention tests: PASS. Advertised JSON schemas are
97
+ exercised using the existing MCP SDK's validator, alongside runtime schemas.
98
+ All elements contribute to revision checks beyond the bounded delta history.
99
+ - `npm run sync:agent-plugin`: PASS; no generated manifest differences.
100
+ - Final `npm run verify`: PASS (lint, build, 59 unit-test files, plugin consistency).
101
+ Log: `/tmp/mobile-debug-conditional-verify-final.log`.
102
+ - Initial `npm run test:device`: FAIL at iOS UI-tree smoke (`Invalid UI tree resolution`) while the original simulator was booting.
103
+ Android screenshot/log/tree wrappers passed. iOS screenshot wrapper passed;
104
+ the iOS log wrapper also reported pass but its underlying log retrieval printed
105
+ `getpwuid_r` / unavailable log-store errors, so this is not evidence of usable
106
+ iOS logs. These wrappers use automatic device selection; focused tests below
107
+ pin their simulator explicitly. Full log: `/tmp/mobile-debug-conditional-device-suite.log`.
108
+
109
+ - Final `npm run test:device`: PASS after the replacement iPhone 17 Pro finished
110
+ booting. All six Android/iOS screenshot, log, and tree smoke wrappers passed,
111
+ without the earlier iOS log-store diagnostics. Log:
112
+ `/tmp/mobile-debug-conditional-device-suite-final.log`.
113
+ - Final documentation relative-link checks and `git diff --check`: PASS.
114
+ - Final lint after the device helper's selector/start-state correction: PASS.
115
+
116
+ ### Focused Android emulator evidence
117
+
118
+ Command (against the built server over MCP stdio, no mocked tool handlers):
119
+
120
+ ```sh
121
+ node --import tsx test/device/manual/interact/conditional_actions.manual.ts \
122
+ --platform android --id emulator-5554 \
123
+ --target-text 'Network & internet' --destination-text Internet \
124
+ --output /tmp/mobile-debug-conditional-android.json
125
+ ```
126
+
127
+ Device: Android 16 sdk_gphone64_arm64, `emulator-5554`.
128
+ Build: local `dist/server.js`, handshake `mobile-debug-mcp` 0.32.4, with
129
+ `expectedState` confirmed in its advertised tool schema. This is the working-tree
130
+ build, not proof of a published release.
131
+ Starting state: system Settings root, with Network & internet visible.
132
+
133
+ | Journey | Actions / expected outcome | Observed outcome | Result |
134
+ | --- | --- | --- | --- |
135
+ | Matching snapshot | Observe Settings; conditional coordinate tap Network & internet | `matched`, delivered once; fresh tree contains Internet | PASS |
136
+ | Stale snapshot, compact | On network page, attempt safe back-navigation tap with root expectation | `PRECONDITION_FAILED`, not delivered; compact tree contains Internet; fresh tree confirms network page remains | PASS |
137
+ | Stale snapshot, debug | Repeat guarded back navigation with root expectation | Same non-delivery and unchanged destination, full debug evidence | PASS |
138
+ | Journey stop, compact | Successful wait for Internet, conditional back tap with root expectation, later tap | Earlier wait passed, guarded step failed with fresh tree, later step `not_run`; destination remained | PASS |
139
+ | Journey stop, debug | Same sequence in debug mode | Same stop and preserved evidence | PASS |
140
+
141
+ Full structured evidence: `/tmp/mobile-debug-conditional-android.json`.
142
+ The five scenarios were rerun successfully against the final built server after
143
+ the compact-error and metadata-validity fixes and the helper selector correction.
144
+ The helper is reproducible and requires a deliberately prepared safe system
145
+ Settings screen. No setting or subscription was changed. The original Modul8
146
+ dev app was brought back to the foreground after checks.
147
+
148
+ ### Focused iOS simulator evidence
149
+
150
+ The original iPhone Air simulator stalled during boot and was closed/rebooted by
151
+ the user. The replacement iPhone 17 Pro became responsive; read-only screenshot
152
+ and UI-tree checks confirmed Home and then Settings before running input checks.
153
+ No boot/restart/reset commands were issued by the agent.
154
+
155
+ Command:
156
+
157
+ ```sh
158
+ node --import tsx test/device/manual/interact/conditional_actions.manual.ts \
159
+ --platform ios --id F9243BAF-62A8-4990-8A99-17F527F56FDA \
160
+ --target-text General --destination-text About \
161
+ --output /tmp/mobile-debug-conditional-ios.json
162
+ ```
163
+
164
+ Device: iOS 26.4 iPhone 17 Pro simulator,
165
+ `F9243BAF-62A8-4990-8A99-17F527F56FDA`.
166
+ Build: the same local `dist/server.js` 0.32.4 build and MCP stdio route as Android,
167
+ with condition support confirmed in tool discovery.
168
+ Starting state: Settings root with General visible, About absent.
169
+
170
+ | Journey | Actions / expected outcome | Observed outcome | Result |
171
+ | --- | --- | --- | --- |
172
+ | Matching snapshot | Observe Settings; conditional coordinate tap General | `matched`, delivered; fresh tree contains About | PASS |
173
+ | Stale snapshot, compact | Attempt safe back-navigation tap with Settings-root expectation | `PRECONDITION_FAILED`, not delivered; compact fresh tree contains About; new observation confirms General stays visible | PASS |
174
+ | Stale snapshot, debug | Same guarded back navigation | Same non-delivery and preserved full condition tree | PASS |
175
+ | Journey stop, compact | Successful wait for About, stale conditional tap on `resource_id: BackButton`, later back tap | Wait passed; condition failed; later step `not_run`; General remained visible | PASS |
176
+ | Journey stop, debug | Same sequence in debug mode | Same stop and fresh condition evidence | PASS |
177
+
178
+ Structured evidence: `/tmp/mobile-debug-conditional-ios.json`.
179
+ An initial helper attempt selected the application container sharing the Settings
180
+ label and correctly stopped with `ELEMENT_NOT_INTERACTABLE` / `not_checked`;
181
+ this was not counted as a conditional-journey pass. The helper now prefers the
182
+ unique native resource ID and requires the destination to be absent from its
183
+ starting screen. A subsequent incorrect-start attempt was also discarded.
184
+ The final five-pass run above starts at the confirmed root and uses BackButton.
185
+
186
+ Cleanup: only navigation was exercised. No setting, permission, account, or app
187
+ data was changed. Home is restored after the smoke-suite rerun.
188
+
189
+ ### Focused quality review
190
+
191
+ Reviewed the shared condition schema and advertised definitions, action wrapper
192
+ and journey dispatch, Android/iOS native input boundaries, observation metadata
193
+ and history, budget ancestors, compact serialization, error/recovery mapping,
194
+ and associated tests/callers. The guard runs after adapter preparation, and
195
+ blocked conditions replace generic adapter failures without inventing an execute
196
+ trace or allowing recovery. Condition observation budgets pause only the current
197
+ action; enclosing deadlines still apply. Tests check no late input after timeout
198
+ and native observation child closure. No new framework or dependency was added.
199
+
200
+ Review findings fixed:
201
+
202
+ - Journey light-verification response overlay must preserve `not_requested` when
203
+ its conditional tap did not dispatch.
204
+ - Compact tree responses must retain observation errors instead of looking like
205
+ successful deltas carrying an old revision/session. Added a regression test.
206
+ - Repeated successful observations refresh retained revision timestamps, so a
207
+ recently observed stable screen does not yield an immediately unusable baseline.
208
+
209
+ Limits: live simulator checks cover conditional taps and journey stops; conditional
210
+ swipes and Android typing/back use production-adapter tests with fake native
211
+ tools. Physical devices were not used for mutation checks. UI normalization, the observation-to-OS-input race, and concurrent external
212
+ input remain explicit spec limits. No mutation testing was performed. No tests
213
+ or validation rules were weakened. Existing uncommitted process/skill relocation
214
+ work was preserved. No package was published and nothing was committed/pushed.
@@ -0,0 +1,301 @@
1
+ # Spec 020: conditional actions
2
+
3
+ Date: 2026-10-29.
4
+ Status: approved — implementation authorised.
5
+ Intent: [intent.md](intent.md), approved in the current conversation by the
6
+ originator's “yes” in response to the captured intent.
7
+
8
+ ## Outcome
9
+
10
+ A client can request a supported UI action only if the observed UI snapshot still
11
+ matches its expectation. The server obtains a new hierarchy immediately before
12
+ native input dispatch. A mismatch prevents input and returns the fresh hierarchy
13
+ so the client can decide what to do next.
14
+
15
+ This is a precondition check, not a guarantee that the UI remains unchanged until
16
+ the OS delivers the input or that the action achieves its intended outcome.
17
+
18
+ ## Current code and observations
19
+
20
+ - `src/server/tool-handlers.ts` wraps action controls, delivery status, response
21
+ modes, and journeys. Existing assertions are separate from native input.
22
+ - `ToolsInteract.tapElementHandler` revalidates a handle but does not compare a
23
+ client-provided expected snapshot. Selector resolution can also precede input.
24
+ - `src/observe/snapshot-metadata.ts` computes a signature from the full element
25
+ list, screen, and resolution, and assigns revisions per resolved device.
26
+ Revisions remain stable for consecutive identical signatures. State, bounds,
27
+ identities, and actionability contribute; screen fingerprints omit some of
28
+ these and are unsuitable as the only condition.
29
+ - Revisions are process-local integers. Revision numbers alone cannot safely
30
+ distinguish server restarts. Error trees may carry a previous revision, so a
31
+ revision on an error response is not proof of a successful observation.
32
+ - Android and iOS input adapters mark dispatch immediately before spawning input.
33
+ The new guard must run there, after metadata/capability checks and resolution,
34
+ rather than only at entry to the MCP handler.
35
+
36
+ ### Device reconnaissance (existing behaviour, not feature verification)
37
+
38
+ Read-only `list_devices` and `get_ui_tree` calls through the connected mobile-debug
39
+ MCP succeeded on 2026-10-29:
40
+
41
+ | Platform | Device | Observed state | Evidence |
42
+ | --- | --- | --- | --- |
43
+ | Android 16 | `emulator-5554`, sdk_gphone64_arm64 | Modul8 Subscription screen | 18 elements, revision 1, 1280 × 2856, tree call about 4.1 seconds |
44
+ | iOS 26.4 | `F72108FF-C8D6-4C1E-980E-1AE5E5CCD27B`, iPhone Air simulator | Home screen | 14 elements, revision 2, 420 × 912, tree call about 8.4 seconds |
45
+
46
+ These are observations from the connected server; its exact build was not exposed
47
+ by these calls. The local checkout declares version 0.32.4. No inputs, installs,
48
+ or settings changes were made. Verification must use a build from the changed
49
+ checkout; these reads do not establish conditional-action behaviour.
50
+
51
+ ## Initial scope
52
+
53
+ | Tool | Platforms | Condition support |
54
+ | --- | --- | --- |
55
+ | `tap` | Android, iOS | Optional expected snapshot |
56
+ | `tap_element` (selector or handle) | Android, iOS | Optional expected snapshot |
57
+ | `swipe` | Android, iOS | Optional expected snapshot |
58
+ | `type_text` | Android | Optional expected snapshot |
59
+ | `press_back` | Android | Optional expected snapshot |
60
+ | `run_journey` | Android, iOS | Optional condition on each `tap` step |
61
+
62
+ No platform gains an input capability it does not already support.
63
+
64
+ Out of scope: lifecycle/build/install/reset tools, `scroll_to_element`,
65
+ `adjust_control`, other multi-input operations, arbitrary predicate expressions,
66
+ per-element-only conditions, automatic retries/replanning, rollback, screenshot
67
+ comparison, and changing post-action verification semantics.
68
+
69
+ A supplied `expectedState` on any unsupported tool or journey step is rejected
70
+ as `INVALID_ARGUMENT` before any operation. It must never be silently ignored.
71
+ Conditions cannot be journey defaults or a condition on the journey as a whole.
72
+
73
+ ## Input contract
74
+
75
+ Add this optional field to each supported tool and journey `tap` step:
76
+
77
+ ```ts
78
+ expectedState?: {
79
+ snapshotSessionId: string;
80
+ snapshotRevision: number;
81
+ timeoutMs?: number; // default 15000; integer, 100–60000 ms
82
+ }
83
+ ```
84
+
85
+ The object is strict. The session ID is nonempty; revision is a positive safe
86
+ integer. Invalid types, null, missing required members, extra members, and
87
+ out-of-range values return `INVALID_ARGUMENT` before observation or input.
88
+ Validate both the advertised JSON schema and actual runtime arguments.
89
+
90
+ Guarded standalone calls require explicit `platform` and concrete `deviceId`.
91
+ The ID must identify the same resolved device as the baseline; aliases such as
92
+ `booted` and `default` are not valid guarded targets. Journey taps inherit the
93
+ journey's once-resolved concrete platform/device. Unguarded calls retain their
94
+ existing device selection and argument behaviour.
95
+
96
+ ### Obtaining an expectation
97
+
98
+ Successful UI-tree observations add `snapshot_session_id`, a random ID generated
99
+ once per server process. Keep existing revision and timestamp fields intact.
100
+ Carry the session ID through debug and compact `get_ui_tree` responses and nested
101
+ UI-tree evidence. This field does not imply that an error response is usable.
102
+
103
+ The client takes `snapshot_session_id` and `snapshot_revision` from a successful
104
+ `get_ui_tree` on the same device and supplies them as the expectation:
105
+
106
+ ```json
107
+ {
108
+ "platform": "android",
109
+ "deviceId": "emulator-5554",
110
+ "x": 100,
111
+ "y": 200,
112
+ "expectedState": {
113
+ "snapshotSessionId": "<session from get_ui_tree>",
114
+ "snapshotRevision": 12
115
+ },
116
+ "responseMode": "compact",
117
+ "verificationMode": "none"
118
+ }
119
+ ```
120
+
121
+ Require a retained successful baseline for this session, device, and revision.
122
+ Reuse the existing bounded snapshot history (up to eight revisions, ten-minute
123
+ retention); no new unbounded history. Expired, evicted, other-device, or
124
+ previous-session baselines cannot authorise input. Clients must observe again.
125
+ Check baseline availability before the fresh read and retain its identity for
126
+ that check, so the fresh read itself cannot evict the baseline being checked.
127
+
128
+ ## Meaning of expected state
129
+
130
+ Match means the fresh successful observation's revision equals the retained
131
+ expected revision within the same server session and device. Use the existing
132
+ snapshot signature and revision rules; do not substitute screen fingerprints,
133
+ partial delta history, or target identity alone.
134
+
135
+ This checks the normalized accessibility snapshot, not every app or OS state.
136
+ The signature includes all elements even though retained delta records are
137
+ bounded. Existing normalization trims/lowercases string fields, includes state
138
+ and bounds, and does not prove the foreground package/activity (current tree
139
+ `screen` is empty on both adapters). Pixel-only changes, inaccessible controls,
140
+ backend state, and changes hidden by normalization are outside the guarantee.
141
+ An intervening observed change followed by a return to the old screen produces
142
+ a different revision and is conservatively rejected. An unobserved transient
143
+ change cannot be detected. Document these limits in tool usage guidance.
144
+
145
+ ## Dispatch rules
146
+
147
+ 1. Validate the entire condition, supported tool/step, and guarded targeting
148
+ before starting the operation. Resolve and pin the concrete device once.
149
+ 2. Resolve the requested target and perform existing actionability/capability
150
+ checks. Existing target failures still prevent input normally; an unperformed
151
+ input does not imply its condition was checked.
152
+ 3. Immediately before the input subprocess would spawn, validate the retained
153
+ baseline and obtain a new, uncached UI hierarchy on the pinned device. Do not
154
+ use a selector-resolution tree, previous assertion, compact delta, or an
155
+ earlier pre-action fingerprint as the final check.
156
+ 4. Reject error/malformed trees, missing revision/session metadata, or a response
157
+ identifying another device. An old revision on an error tree never passes.
158
+ 5. Compare the expectation once. No polling until it becomes true, automatic
159
+ rebasing, or retry of input. Existing internal acquisition retries remain
160
+ bounded by the condition timeout; they do not authorise a stale read.
161
+ 6. On match, record the successful condition result, check the remaining action
162
+ budget, mark dispatch, and spawn native input. No further observation,
163
+ target resolution, metadata lookup, intentional delay, or asynchronous
164
+ capability probe may occur between the final check and input spawn.
165
+ 7. On failure, preserve the structured condition result across adapter catches.
166
+ Do not mark dispatch or reset the network action window. Skip post-input
167
+ verification and recovery, and return immediately.
168
+
169
+ The condition observation has its own `timeoutMs` budget. Its elapsed time does
170
+ not consume the existing action-dispatch budget; target resolution and input
171
+ retain existing action controls. Check a nested budget's outer deadline too
172
+ where applicable. An expired condition read cannot permit later background input.
173
+ Condition checking is mandatory even with `verificationMode: "none"` or `"light"`.
174
+
175
+ The guard must be scoped to the request using existing async-context conventions;
176
+ it must not leak into another call or recursively guard its own observation.
177
+ There is no cross-client transaction or device lock in this initial version.
178
+ Concurrent actions, external input, and app updates may still change the UI
179
+ after observation; clients should sequence actions on a device. Do not claim
180
+ atomicity, exclusivity, or rollback.
181
+
182
+ ## Response contract
183
+
184
+ Preserve existing action fields. Guarded calls add:
185
+
186
+ ```ts
187
+ condition: {
188
+ status: 'matched' | 'mismatch' | 'unavailable' | 'not_checked';
189
+ expected: { snapshot_session_id: string; snapshot_revision: number };
190
+ observed: GetUITreeResponse | null;
191
+ elapsed_ms: number;
192
+ reason?: string;
193
+ }
194
+ ```
195
+
196
+ Stable `reason` values for unavailable checks: `BASELINE_UNAVAILABLE`,
197
+ `SESSION_MISMATCH`, `DEVICE_MISMATCH`, `OBSERVATION_FAILED`,
198
+ `OBSERVATION_INVALID`, and `CONDITION_TIMEOUT`. `not_checked` uses
199
+ `ACTION_NOT_REACHED` when target/capability failure precedes the guard.
200
+ `elapsed_ms` measures the condition check, or zero if it was not reached.
201
+
202
+ | Outcome | Action result | Delivery fields | Condition evidence |
203
+ | --- | --- | --- | --- |
204
+ | Match, then input | Existing action outcome | Existing delivery semantics | `matched`, fresh observed tree |
205
+ | State differs | `success: false`, `failure_code: PRECONDITION_FAILED`, `retryable: false` | `dispatch_started: false`, `delivery_status: not_delivered`, `lifecycle_state: failed` | `mismatch`, exact fresh observed tree |
206
+ | Check cannot be completed | `success: false`, `failure_code: PRECONDITION_UNAVAILABLE`, `retryable: false` | Same non-delivery fields | `unavailable`, reason; valid fresh tree if available, otherwise null |
207
+ | Target/capability failure before check | Existing failure code | No input dispatch | `not_checked`, observed null |
208
+
209
+ For blocked input, `verification.status` is `not_requested`; this is a failed
210
+ precondition, not a failed post-action assertion. If input fails after a match,
211
+ retain `matched` alongside the actual input failure; a matching condition is not
212
+ proof of delivery or outcome correctness. Add failure codes to shared types and
213
+ recovery handling without enabling automatic retry.
214
+
215
+ Debug mode includes full condition evidence. Compact mode always keeps condition
216
+ status, expected values, elapsed time, and reason. On mismatch/unavailable it also
217
+ keeps `observed`, including the full tree when available: this is a documented
218
+ exception to compact mode's usual omission of device IDs and tree contents.
219
+ On match, compact mode keeps only observed snapshot session/revision and capture
220
+ time instead of the full tree. `not_checked` keeps observed null.
221
+
222
+ The returned mismatch tree is the actual observation that blocked input; do not
223
+ replace it with a later snapshot. No automatic screenshot or log capture is
224
+ required. Apply existing response-data handling; UI trees can contain private
225
+ text. Do not log expectation payloads, input text, or full trees by default.
226
+
227
+ ## Journey behaviour
228
+
229
+ A conditional tap uses the same guard as a standalone `tap_element`. Conditions
230
+ are evaluated at that step's dispatch boundary, after any earlier waits/asserts.
231
+ A mismatch or unavailable condition fails that step and sets
232
+ `stopped_at_step_id`; subsequent steps are `not_run`. Earlier completed steps
233
+ remain completed. Preserve condition evidence in both journey response modes,
234
+ regardless of `captureOnFailure`. Existing optional debug failure snapshots must
235
+ not overwrite the guard evidence. There is no rollback or implicit baseline
236
+ refresh from an earlier step.
237
+
238
+ ## Acceptance criteria
239
+
240
+ 1. Matching expectations dispatch exactly once on every supported tool/platform;
241
+ existing post-action controls still apply.
242
+ 2. A changed text, bounds, enabled flag, or exposed element state blocks input
243
+ even when the target still exists; return the blocking observation.
244
+ 3. Detect a change after selector resolution and during adapter preparation by
245
+ checking at the final dispatch boundary, not only at handler entry.
246
+ 4. Old sessions, expired/evicted baselines, another-device baselines, failed
247
+ observations, malformed metadata, and condition timeout never dispatch input.
248
+ 5. Conditions are enforced in none/light/full verification and compact/debug
249
+ response modes; mismatch evidence survives compact serialization.
250
+ 6. A failed journey condition stops subsequent steps and preserves earlier results.
251
+ 7. Unsupported/malformed conditions fail validation before side effects.
252
+ 8. Unguarded calls preserve existing contracts and behaviour.
253
+ 9. Concurrent requests do not share guard state; blocked input neither resets
254
+ the network window nor triggers retry/recovery. No late dispatch after timeout.
255
+ 10. Documentation explains snapshot normalization, history/session limits,
256
+ additional observation latency, data exposure in mismatch responses, and the
257
+ observation-to-input race.
258
+
259
+ ## Affected surfaces and verification expectations
260
+
261
+ Implementation is expected in tool definitions and runtime validation, action
262
+ wrapping/serialization and journey schema, snapshot metadata/history and types,
263
+ and the Android/iOS native input boundaries. Reuse existing async contexts,
264
+ budgets, and device resolution. Keep platform subprocess logic in its adapters.
265
+ Update interaction/observation docs, README usage, changelog, and packaged agent
266
+ skill guidance if it describes the new workflow; use the existing plugin sync
267
+ command when changing its source.
268
+
269
+ Use the repository's self-running `tsx` unit tests to prove dispatch prevention,
270
+ freshness ordering, error propagation, exact evidence, isolation, timeout, and
271
+ schema parity. Exercise real adapter paths with controlled subprocess fixtures;
272
+ a test that mocks away the guard boundary is insufficient.
273
+
274
+ Run `npm run verify` and `npm run test:device` after implementation, plus focused
275
+ Android and iOS simulator journeys using the newly built server. Use an isolated
276
+ test app or safe system controls, avoiding changes to the currently open Modul8
277
+ subscription flow. Verify an unchanged-state action, deliberate intervening
278
+ state change with non-delivery, and journey stop. Use controlled failures for
279
+ unavailable/timeout and race ordering; report simulated and device evidence
280
+ separately. Record exact test routes and cleanup in the implementation plan.
281
+
282
+ ## Risks and decisions
283
+
284
+ - Whole-tree changes may conservatively block an action due to unrelated dynamic
285
+ UI; this initial version favours explicit rejection. Per-element predicates
286
+ can be specified later if needed.
287
+ - Hierarchy acquisition adds seconds to guarded input; the separate condition
288
+ budget is intentional and must not silently weaken freshness.
289
+ - Returning a fresh hierarchy in compact failures increases payload size and
290
+ exposes the same UI text as `get_ui_tree`; callers must handle it accordingly.
291
+ - Session metadata is an additive contract change. Older servers cannot accept
292
+ guarded calls; clients must obtain expectations from this supporting server.
293
+ Local tests are not proof of an installed or published version's behaviour.
294
+ - No data migration, new dependency, helper APK, external service, or automatic
295
+ release is required. Package publication remains a separate authorised action.
296
+
297
+ ## Approval
298
+
299
+ Approved by the originator on 2026-10-29 in the current conversation: “yes”,
300
+ in direct response to the linked complete spec and the listed first-version
301
+ scope. Implementation is authorised within that scope.
@@ -0,0 +1,40 @@
1
+ # Intent: app readiness signals
2
+
3
+ Originator: repository maintainer (current conversation).
4
+ Date: 2026-10-29. Status: approved.
5
+ Source: [GitHub issue #65: App readiness signals](https://github.com/clivejefferies/mobile-debug-tools/issues/65), roadmap priority 2 of 9.
6
+
7
+ ## Problem
8
+
9
+ Clients can mistake an observation captured during loading or a transition for
10
+ settled app content. They also need to distinguish an unresponsive app from an
11
+ app paused by a debugger, where evidence permits.
12
+
13
+ ## Desired outcome
14
+
15
+ Android/iOS observations and bounded waits expose evidence-backed readiness:
16
+ loading, transitioning, settled, unresponsive, and debugger-paused. Results carry
17
+ the supporting observations and uncertainty so clients can decide whether to act,
18
+ wait, or investigate. A screenshot alone does not establish readiness.
19
+
20
+ ## Scope and success criteria
21
+
22
+ - Build on existing observation, loading-state, snapshot, and wait contracts.
23
+ - Distinguish readiness states only when available evidence supports them;
24
+ otherwise report uncertainty rather than guess.
25
+ - Bound waits and distinguish timeout/observation failure from proven app
26
+ unresponsiveness.
27
+ - Coordinate with debugger sessions when available; do not invent a current
28
+ debugger integration.
29
+ - Preserve existing tool behaviour unless the approved spec explicitly changes it.
30
+
31
+ ## Exclusions and questions
32
+
33
+ No runtime debugger implementation, automatic recovery/restart, or performance
34
+ profiling. The spec must define state evidence, sampling/stability rules,
35
+ unsupported signals, API placement, and how long observations remain current.
36
+
37
+ ## Approval
38
+
39
+ Approved on 2026-10-29 by the originator’s “yes” in direct response to the
40
+ complete intent summary and linked artifact in this conversation.
@@ -0,0 +1,102 @@
1
+ # Plan: app readiness signals
2
+
3
+ Date: 2026-10-29. Status: complete — implementation and required verification passed.
4
+ [Intent](intent.md) and [spec](spec.md) separately approved in this conversation;
5
+ “yep” authorises implementation of spec 021. Preserve existing uncommitted work.
6
+
7
+ ## Implementation
8
+
9
+ - [x] Add a request-local classifier and typed, bounded evidence summaries in
10
+ `src/observe/readiness.ts` and shared types.
11
+ - [x] Add summaries to tree observations without extra reads; expose strict new
12
+ tool schemas/handlers using pinned devices and existing operation budgets.
13
+ - [x] Add opt-in readiness to existing UI/changed-UI waits, sharing their samples
14
+ and one deadline; retain default semantics.
15
+ - [x] Add deterministic classifier, schema, wait, native acquisition/error/timeout
16
+ tests in `test/unit/`; reuse existing self-running `tsx` conventions.
17
+ - [x] Update tool docs, README/changelog, and packaged agent usage guidance.
18
+ - [x] Verify, review the changed contracts, and record results below.
19
+
20
+ ## Verification
21
+
22
+ | Criteria | Checks |
23
+ | --- | --- |
24
+ | State precedence and uncertainty | Explicit expected classifier outputs with controlled monotonic times; native/label loaders, changes, stable UI, empty/error/old-session/wrong-device samples |
25
+ | Bounded sampling and isolation | Production observation adapters with fake binaries, timeout/child-close evidence, outer budget, concurrent requests, no late success |
26
+ | Schemas and compatibility | SDK JSON-schema validation against runtime validation; compact/debug tree responses; default and opt-in waits; no extra reads |
27
+ | Real device behaviour | Updated server over MCP stdio on Android emulator and iOS simulator: stable UI, controlled loader → settled, bounded loading timeout, predicate present during loader, and changed-UI wait |
28
+
29
+ Commands: focused tests with the existing `tsx` runtime, `npm run verify`,
30
+ `npm run test:device`, and a reproducible focused device helper under
31
+ `test/device/manual/`. Native fixture apps provide a timed loader without
32
+ accounts, network services, settings changes, or inspection helper services.
33
+ Use the existing Android fixture build conventions and installed iOS SDK tools.
34
+ Known targets: Android `emulator-5554`; iOS
35
+ `F9243BAF-62A8-4990-8A99-17F527F56FDA` (reconfirm availability).
36
+ Fixture builds/access are local prerequisites owned by this task. Record exact
37
+ routes, artifacts, build identity and cleanup; restore original foreground apps.
38
+ Unsupported unresponsive/debugger pause signals must remain explicit, not tested
39
+ as implemented capabilities. No deployment or package publication is required.
40
+
41
+ ## Results and review
42
+
43
+ Local build/version: working-tree `dist/server.js`, MCP handshake 0.32.4,
44
+ Node v23.11.0. This is local evidence, not a published package or Node 18 CI run.
45
+
46
+ | Check | Result / evidence |
47
+ | --- | --- |
48
+ | Classifier, schemas, opt-in waits | PASS: state precedence, monotonic stability, invalid/error continuity, no extra predicate reads, explicit unsupported states, timeout uncertainty |
49
+ | Production Android/iOS observation adapters with fake binaries | PASS: two-sample stability, native loaders, timeout child closure, and killing a hung iOS metadata probe; simulated error evidence |
50
+ | Maintainer checks | PASS: final `npm run verify` (lint, build, 62 unit-test files, plugin consistency); `/tmp/mobile-readiness-verify-complete.log` |
51
+ | Existing `npm run test:device` | PASS: all six Android/iOS screenshot, log, and tree smoke wrappers; `/tmp/mobile-readiness-device-suite.log` |
52
+ | Native fixture journeys through built MCP stdio | PASS: six on each simulator; evidence linked below |
53
+
54
+ Focused device commands (each builds, installs, and uninstalls its own offline
55
+ native fixture; temporary artifacts are removed):
56
+
57
+ ```sh
58
+ node --import tsx test/device/manual/interact/readiness.manual.ts \
59
+ --platform android --id emulator-5554 --output /tmp/mobile-readiness-android.json
60
+ node --import tsx test/device/manual/interact/readiness.manual.ts \
61
+ --platform ios --id F9243BAF-62A8-4990-8A99-17F527F56FDA \
62
+ --output /tmp/mobile-readiness-ios.json
63
+ ```
64
+
65
+ Android 16 sdk_gphone64_arm64 and iOS 26.4 iPhone 17 Pro were explicitly pinned.
66
+ Starting state: newly launched fixture, Ready visible, loader hidden. Show loader
67
+ starts a 12-second loader while Ready stays visible; completion displays Done.
68
+
69
+ | Scenario | Android | iOS | Observed proof |
70
+ | --- | --- | --- | --- |
71
+ | Stable content | PASS | PASS | Two fresh samples report settled |
72
+ | Controlled loader | PASS | PASS | Loading; Android native indicator, iOS accessible label marked heuristic |
73
+ | Bounded wait | PASS | PASS | 100 ms deadline rejects readiness; unknown plus deadline limitation, no invented unresponsive state |
74
+ | Predicate + readiness | PASS | PASS | Ready remains visible during loading; success only after settled, Done independently observed |
75
+ | Changed-UI wait | PASS | PASS | Loader completion changes hierarchy; opt-in wait returns success with settled evidence |
76
+ | Readiness wait success | PASS | PASS | Bounded wait establishes settled content |
77
+
78
+ All twelve device scenarios passed. Both fixture apps were uninstalled, build
79
+ folders removed, Android’s original foreground app restored, and iOS returned
80
+ to Home. No settings, permissions, accounts, or production app data were changed.
81
+ Structured evidence: `/tmp/mobile-readiness-android.json` and
82
+ `/tmp/mobile-readiness-ios.json`.
83
+
84
+ Review covered classification/normalization, schema parity, serialization,
85
+ request-local state, ancestor budgets, native acquisition and cleanup, existing
86
+ wait callers, conditional-action compatibility, and evidence privacy. Findings
87
+ fixed: discovery budget expiry must not look like a proven missing device;
88
+ iOS probes/metadata must respect the active budget; timeout envelopes must retain
89
+ an already-observed predicate instead of pretending its target was absent.
90
+ The Android helper uses the stable accessibility ID, avoiding button-case
91
+ variation. Initial fixture attempts exposed these issues and were not counted
92
+ as passes; the later runs above passed without weakening assertions.
93
+
94
+ Limits: unresponsive/debugger-paused detection remains unsupported. Stability
95
+ covers normalized sampled UI, not app identity, responsiveness, backend outcome,
96
+ or atomic input readiness. Transport cancellation still relies on deadlines.
97
+ No mutation testing or physical-device input checks were performed. The prior
98
+ conditional work was committed externally during this task and was preserved;
99
+ this task did not commit, push, or publish readiness changes.
100
+
101
+ Final source/diagnostic correction was covered by the completion verification run.
102
+ Documentation relative links and `git diff --check` also passed.