mobile-debug-mcp 0.32.4 → 0.34.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 (170) 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/.github/workflows/ci.yml +21 -0
  6. package/AGENTS.md +21 -10
  7. package/README.md +69 -4
  8. package/agent-plugin/.claude-plugin/plugin.json +20 -0
  9. package/agent-plugin/.codex-plugin/plugin.json +34 -0
  10. package/agent-plugin/.cursor-plugin/plugin.json +23 -0
  11. package/agent-plugin/.mcp.json +14 -0
  12. package/agent-plugin/README.md +9 -3
  13. package/agent-plugin/assets/mobile-debug-tools.svg +28 -0
  14. package/agent-plugin/mcp.json +1 -1
  15. package/agent-plugin/plugin.json +1 -1
  16. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -1
  17. package/dist/debugger/android.js +78 -0
  18. package/dist/debugger/guard.js +30 -0
  19. package/dist/debugger/sessions.js +262 -0
  20. package/dist/debugger/worker.js +165 -0
  21. package/dist/interact/android.js +10 -4
  22. package/dist/interact/conditional-action.js +68 -0
  23. package/dist/interact/index.js +70 -14
  24. package/dist/interact/ios.js +14 -8
  25. package/dist/manage/android.js +4 -3
  26. package/dist/manage/ios.js +3 -2
  27. package/dist/observe/android.js +25 -4
  28. package/dist/observe/evidence.js +280 -0
  29. package/dist/observe/index.js +33 -6
  30. package/dist/observe/ios.js +25 -6
  31. package/dist/observe/readiness-service.js +124 -0
  32. package/dist/observe/readiness.js +88 -0
  33. package/dist/observe/recording.js +165 -0
  34. package/dist/observe/snapshot-metadata.js +10 -2
  35. package/dist/observe/system-trace-android.js +135 -0
  36. package/dist/observe/system-trace.js +285 -0
  37. package/dist/server/common.js +9 -0
  38. package/dist/server/condition-schema.js +32 -0
  39. package/dist/server/debugger-handlers.js +38 -0
  40. package/dist/server/debugger-schema.js +42 -0
  41. package/dist/server/journey-schema.js +4 -2
  42. package/dist/server/readiness-schema.js +12 -0
  43. package/dist/server/system-trace-schema.js +17 -0
  44. package/dist/server/tool-definitions.js +51 -0
  45. package/dist/server/tool-handlers.js +294 -105
  46. package/dist/server-core.js +3 -3
  47. package/dist/server.js +34 -0
  48. package/dist/system/index.js +5 -4
  49. package/dist/utils/exec.js +2 -0
  50. package/dist/utils/ios/utils.js +4 -4
  51. package/dist/utils/java.js +14 -12
  52. package/dist/utils/operation-budget.js +26 -2
  53. package/dist/utils/resolve-device.js +9 -0
  54. package/docs/CHANGELOG.md +15 -0
  55. package/docs/specs/020-conditional-actions/intent.md +62 -0
  56. package/docs/specs/020-conditional-actions/plan.md +214 -0
  57. package/docs/specs/020-conditional-actions/spec.md +301 -0
  58. package/docs/specs/021-app-readiness-signals/intent.md +40 -0
  59. package/docs/specs/021-app-readiness-signals/plan.md +102 -0
  60. package/docs/specs/021-app-readiness-signals/spec.md +144 -0
  61. package/docs/specs/022-correlated-evidence/device-evidence.json +60 -0
  62. package/docs/specs/022-correlated-evidence/intent.md +37 -0
  63. package/docs/specs/022-correlated-evidence/modul8-evidence.json +1062 -0
  64. package/docs/specs/022-correlated-evidence/modul8-selector-fix-evidence.json +336 -0
  65. package/docs/specs/022-correlated-evidence/plan.md +171 -0
  66. package/docs/specs/022-correlated-evidence/spec.md +123 -0
  67. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  68. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  69. package/docs/specs/023-android-system-traces/intent.md +40 -0
  70. package/docs/specs/023-android-system-traces/plan.md +117 -0
  71. package/docs/specs/023-android-system-traces/spec.md +120 -0
  72. package/docs/specs/024-android-heap-dump-capture/intent.md +39 -0
  73. package/docs/specs/025-android-runtime-debugger/intent.md +40 -0
  74. package/docs/specs/025-android-runtime-debugger/plan.md +104 -0
  75. package/docs/specs/025-android-runtime-debugger/spec.md +182 -0
  76. package/docs/specs/process.md +213 -0
  77. package/docs/tools/TOOLS.md +5 -0
  78. package/docs/tools/debugger.md +104 -0
  79. package/docs/tools/evidence.md +82 -0
  80. package/docs/tools/interact.md +125 -2
  81. package/docs/tools/observe.md +70 -0
  82. package/docs/tools/system-traces.md +126 -0
  83. package/package.json +1 -1
  84. package/scripts/agent-plugin.mjs +68 -8
  85. package/src/debugger/android.ts +65 -0
  86. package/src/debugger/guard.ts +26 -0
  87. package/src/debugger/sessions.ts +168 -0
  88. package/src/debugger/worker.ts +122 -0
  89. package/src/interact/android.ts +10 -4
  90. package/src/interact/conditional-action.ts +75 -0
  91. package/src/interact/index.ts +57 -28
  92. package/src/interact/ios.ts +14 -8
  93. package/src/manage/android.ts +4 -3
  94. package/src/manage/ios.ts +3 -2
  95. package/src/observe/android.ts +33 -6
  96. package/src/observe/evidence.ts +262 -0
  97. package/src/observe/index.ts +32 -8
  98. package/src/observe/ios.ts +32 -8
  99. package/src/observe/readiness-service.ts +94 -0
  100. package/src/observe/readiness.ts +84 -0
  101. package/src/observe/snapshot-metadata.ts +12 -1
  102. package/src/observe/system-trace-android.ts +133 -0
  103. package/src/observe/system-trace.ts +218 -0
  104. package/src/server/common.ts +7 -0
  105. package/src/server/condition-schema.ts +32 -0
  106. package/src/server/debugger-handlers.ts +29 -0
  107. package/src/server/debugger-schema.ts +37 -0
  108. package/src/server/journey-schema.ts +4 -2
  109. package/src/server/readiness-schema.ts +12 -0
  110. package/src/server/system-trace-schema.ts +17 -0
  111. package/src/server/tool-definitions.ts +49 -0
  112. package/src/server/tool-handlers.ts +182 -21
  113. package/src/server-core.ts +3 -3
  114. package/src/server.ts +24 -0
  115. package/src/system/index.ts +7 -5
  116. package/src/types.ts +30 -1
  117. package/src/utils/exec.ts +2 -0
  118. package/src/utils/ios/utils.ts +4 -4
  119. package/src/utils/java.ts +13 -12
  120. package/src/utils/operation-budget.ts +24 -2
  121. package/src/utils/resolve-device.ts +10 -0
  122. package/test/device/automated/debugger/runtime.android.test.ts +203 -0
  123. package/test/device/automated/observe/correlated-evidence.smoke.ts +70 -0
  124. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  125. package/test/device/manual/interact/conditional_actions.manual.ts +95 -0
  126. package/test/device/manual/interact/modul8_selector_tap.manual.ts +63 -0
  127. package/test/device/manual/interact/readiness.manual.ts +99 -0
  128. package/test/fixtures/debugger-app/MainActivity.java +46 -0
  129. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  130. package/test/fixtures/debugger-app/README.md +13 -0
  131. package/test/fixtures/debugger-app/build.ts +29 -0
  132. package/test/fixtures/readiness-app/MainActivity.java +23 -0
  133. package/test/fixtures/readiness-app/README.md +12 -0
  134. package/test/fixtures/readiness-app/build.ts +37 -0
  135. package/test/fixtures/readiness-app/main.m +27 -0
  136. package/test/helpers/perfetto.ts +59 -0
  137. package/test/unit/debugger/guards.test.ts +46 -0
  138. package/test/unit/debugger/native.test.ts +33 -0
  139. package/test/unit/debugger/sessions.test.ts +83 -0
  140. package/test/unit/observe/android_tree_acquisition.test.ts +2 -0
  141. package/test/unit/observe/capture_debug_snapshot.test.ts +42 -32
  142. package/test/unit/observe/compact_tree_error.test.ts +24 -0
  143. package/test/unit/observe/conditional_snapshots.test.ts +33 -0
  144. package/test/unit/observe/correlated-evidence.test.ts +128 -0
  145. package/test/unit/observe/ios_tree_acquisition.test.ts +2 -0
  146. package/test/unit/observe/readiness.test.ts +41 -0
  147. package/test/unit/observe/readiness_native.test.ts +68 -0
  148. package/test/unit/observe/system-trace-android.test.ts +52 -0
  149. package/test/unit/observe/system-trace.test.ts +166 -0
  150. package/test/unit/server/conditional_actions.test.ts +299 -0
  151. package/test/unit/server/conditional_schema.test.ts +59 -0
  152. package/test/unit/server/correlated-evidence.test.ts +194 -0
  153. package/test/unit/server/readiness.test.ts +170 -0
  154. package/test/unit/server/selector_tap_target.test.ts +79 -0
  155. package/test/unit/server/system-trace.test.ts +64 -0
  156. package/test/unit/server/tap_selector.test.ts +2 -0
  157. package/worker/DebuggerWorker.java +160 -0
  158. package/skills/mobile-debug-tools-usage/SKILL.md +0 -96
  159. /package/{skills → .agents/skills}/README.md +0 -0
  160. /package/{skills → .agents/skills}/mcp-builder/SKILL.md +0 -0
  161. /package/{skills → .agents/skills}/mcp-builder/references/build-flags.md +0 -0
  162. /package/{skills → .agents/skills}/mcp-builder/references/diagnostics-schema.md +0 -0
  163. /package/{skills → .agents/skills}/mcp-builder/references/toolchain-details.md +0 -0
  164. /package/{skills → .agents/skills}/spec-review/SKILL.md +0 -0
  165. /package/{skills → .agents/skills}/spec-review/references/spec-review-checklist.md +0 -0
  166. /package/{skills → .agents/skills}/spec-review/references/spec-review-template.md +0 -0
  167. /package/{skills → .agents/skills}/test-authoring/SKILL.md +0 -0
  168. /package/{skills → .agents/skills}/test-authoring/references/repo-test-layout.md +0 -0
  169. /package/{skills → .agents/skills}/test-authoring/references/test-authoring-workflow.md +0 -0
  170. /package/{skills → .agents/skills}/test-authoring/references/test-quality-checklist.md +0 -0
@@ -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.
@@ -0,0 +1,144 @@
1
+ # Spec 021: app readiness signals
2
+
3
+ Date: 2026-10-29. Status: approved — implementation authorised.
4
+ Source: [issue #65](https://github.com/clivejefferies/mobile-debug-tools/issues/65).
5
+ [Intent](intent.md) approved by the originator’s “yes” in this conversation.
6
+
7
+ ## Behaviour and scope
8
+
9
+ Report readiness of the observed foreground device UI on Android/iOS. This is
10
+ not proof of a particular app’s identity, backend completion, or safe input.
11
+ Reuse snapshot signatures, loading observations, device resolution, and budgets.
12
+ Do not infer readiness from screenshots or input-dispatch success.
13
+
14
+ Initial supported states are `loading`, `transitioning`, `settled`, and `unknown`.
15
+ Reserve `unresponsive` and `debugger_paused`, but report their capabilities as
16
+ unsupported on both platforms: there is currently no authoritative process-health
17
+ or debugger-session provider. A timeout must never manufacture either state.
18
+ Adding those providers requires a subsequent spec decision; this feature does
19
+ not implement a debugger, recovery, restart, or performance profiling.
20
+
21
+ ## Public contracts
22
+
23
+ Add tools `get_app_readiness` and `wait_for_app_ready`, requiring `platform`
24
+ (`android`/`ios`) and optionally accepting `deviceId`. Resolve and pin one concrete
25
+ device per call. Both accept strict integer controls:
26
+
27
+ | Control | Default | Range |
28
+ | --- | --- | --- |
29
+ | `timeoutMs` | get: 15000; wait: 30000 | 100–60000 |
30
+ | `stabilityMs` | 500 | 100–10000 |
31
+ | `pollIntervalMs` (wait only) | 500 | 100–1000 |
32
+
33
+ Reject invalid/unknown arguments as `INVALID_ARGUMENT` before device work.
34
+ `get_app_readiness` takes at most two fresh samples, separating their acquisitions
35
+ by at least `stabilityMs`. A loading first sample can return immediately. The
36
+ wait tool samples until settled or its deadline. Reads are sequential; never
37
+ start a background poller or overlap hierarchy dumps within a call.
38
+
39
+ Both return `{ success, device, readiness, elapsed_ms, failure_code? }`.
40
+ For get, success means a supported state was established; `unknown` is unsuccessful.
41
+ For wait, only `settled` is successful. Unsuccessful codes are
42
+ `READINESS_UNAVAILABLE` (get), `READINESS_TIMEOUT` (wait deadline), and
43
+ `DEVICE_UNAVAILABLE` (resolution/disconnection prevents continuation).
44
+
45
+ `readiness` contains:
46
+
47
+ - `state`, `scope: device_ui`, and `basis` (`observed`, `heuristic`, or `insufficient`).
48
+ - `captured_at_ms` (latest successful sample, or null), `sample_count`,
49
+ `stable_for_ms`, and nullable `snapshot_session_id`/`snapshot_revision`.
50
+ - `evidence`: bounded records of signal code, source, capture time, and snapshot
51
+ revision; maximum eight, newest retained. No full text, logs, screenshots, or trees.
52
+ - `limitations`: applicable codes from `INSUFFICIENT_SAMPLES`, `OBSERVATION_FAILED`,
53
+ `OBSERVATION_INVALID`, `DEADLINE_EXCEEDED`, `NORMALIZED_UI_ONLY`,
54
+ `FOREGROUND_APP_UNVERIFIED`, `UNRESPONSIVE_SIGNAL_UNSUPPORTED`, and
55
+ `DEBUGGER_SIGNAL_UNSUPPORTED`.
56
+ - `capabilities`: true for loading/transition/stability observation; false for
57
+ unresponsive/debugger-paused detection in this version.
58
+
59
+ Add the same `readiness` summary to debug/compact `get_ui_tree` and nested tree
60
+ observations without additional reads. Single-sample summaries can only report
61
+ loading or unknown; separate calls never silently share a stability window.
62
+ Preserve existing fields, compact error evidence, and delta behaviour.
63
+
64
+ Add opt-in `requireReady: boolean` (default false) to `wait_for_ui` and
65
+ `wait_for_ui_change`. When enabled, success requires the existing predicate and
66
+ settled readiness on the same current samples. Reuse those waits’ samples and
67
+ single existing deadline; do not add another wait budget or unrelated tree reads.
68
+ Return `readiness` alongside the existing envelope. On expiry retain the existing
69
+ timeout/failure shape and include readiness limitations. Default calls retain
70
+ existing predicate semantics; no new journey assertion type is introduced.
71
+
72
+ ## Classification rules
73
+
74
+ | Evidence | State / basis |
75
+ | --- | --- |
76
+ | Visible native `android.widget.ProgressBar`, `ActivityIndicator`, or `ProgressIndicator` | loading / observed |
77
+ | Visible label exactly “Loading” or “Please wait”, ignoring case, whitespace, and trailing full stops/ellipsis | loading / heuristic |
78
+ | Two valid consecutive signatures differ | transitioning / observed |
79
+ | At least two valid identical signatures spanning `stabilityMs`, without loading evidence | settled / observed |
80
+ | Missing, failed, invalid, or insufficient evidence | unknown / insufficient |
81
+
82
+ Loading takes precedence over stability. Do not reuse the broad existing
83
+ `loading_state` keyword heuristic as proof: a button named “Progress” is not an
84
+ active loader. Keep that legacy field unchanged for compatibility.
85
+
86
+ Use full normalized snapshot signatures, including state and bounds, not the
87
+ coarser screen fingerprint or truncated delta. A change resets stability;
88
+ failed reads break consecutive-sample continuity. Validate device/session,
89
+ revision, a finite capture timestamp from this acquisition, nonempty elements,
90
+ and positive finite resolution. Never accept retained metadata as a fresh read.
91
+ Track elapsed/stability intervals with a monotonic clock; expose wall-clock
92
+ capture timestamps only as evidence. Results describe their capture window,
93
+ not a guarantee that the UI stays settled afterward.
94
+
95
+ ## Failure, lifecycle, and privacy
96
+
97
+ Bound acquisition, retries, and delays by the remaining operation budget and any
98
+ outer deadline. No late sample may produce success. Terminate/await owned timed-out
99
+ subprocesses; perform no follow-up snapshot after expiry. Waits can retry transient
100
+ observation failures within budget, with continuity reset; disconnect is terminal.
101
+ Retain the last supported state on wait timeout only as timestamped evidence,
102
+ with `DEADLINE_EXCEEDED`; if the latest read failed, current state is unknown.
103
+ There is no cancellation argument. Existing transport cancellation does not
104
+ currently abort platform reads; those reads remain bounded by their deadlines.
105
+ Do not claim immediate cancellation support or create persistent sessions.
106
+ Do not reset network windows, dispatch input, or retain cross-request histories.
107
+ Do not log UI payloads or input text. No permissions, dependencies, migrations,
108
+ or automatic package release are required.
109
+
110
+ ## Acceptance and verification
111
+
112
+ - Loading outranks identical trees; changing trees transition; consecutive stable
113
+ valid trees settle. Text-only loading is explicitly heuristic.
114
+ - A frozen-looking screenshot, stable tree, slow dump, timeout, or missing loader
115
+ cannot establish responsiveness, debugger pause, or app/backend completion.
116
+ - Error/empty/stale-session/wrong-device samples never settle; failures reset
117
+ continuity. Deadlines prevent late success and background work.
118
+ - New summaries preserve compact/debug evidence and existing defaults; opt-in
119
+ waits require both their predicate and readiness.
120
+ - Every state has evidence and limitations; unsupported capabilities are explicit.
121
+
122
+ Use deterministic native-adapter/clock fixtures for transitions, failures,
123
+ timeouts, isolation, and schema parity. Run `npm run verify`, `npm run test:device`,
124
+ and updated-server Android/iOS journeys for stable content, a controlled loader
125
+ transition, bounded waiting, and opt-in predicate waits. Record simulator/build,
126
+ results, and unsupported coverage in the plan after approval.
127
+
128
+ ## Investigation and approval
129
+
130
+ Existing owners: `src/observe/snapshot-metadata.ts`, `src/observe/index.ts`,
131
+ `src/interact/index.ts`, server schemas/handlers, shared types and operation budgets.
132
+ The legacy idle wait can compare null fingerprints; the new classifier must not
133
+ copy that inference. No debugger/readiness module currently exists.
134
+
135
+ Read-only reconnaissance used the connected server (exact build not reported;
136
+ local checkout 0.32.4). Android 16 `emulator-5554`: two fresh 15-element trees,
137
+ same revision 9, no loading hint, reads about 4.4/3.0 seconds. iOS 26.4 iPhone 17
138
+ Pro `F9243BAF-62A8-4990-8A99-17F527F56FDA`: two fresh 14-element trees, revision
139
+ 10, no loading hint, reads about 3.4/2.0 seconds. These establish available
140
+ observation routes, not feature passes; no inputs or settings were changed.
141
+
142
+ Approved by the originator’s “yep” on 2026-10-29 in direct response to this
143
+ spec and first-version scope, including unsupported unresponsive/debugger-paused
144
+ detection and opt-in existing-wait integration.
@@ -0,0 +1,60 @@
1
+ {
2
+ "verified_on": "2026-10-30",
3
+ "source_base": "3701f3987940a8abc76ed893b575ed370e3fb673",
4
+ "source_state": "working tree with correlated evidence changes",
5
+ "node_version": "23.11.0",
6
+ "build_sha256": {
7
+ "dist/server/tool-handlers.js": "4ae4aef2467518216e3b987112aadb91e5ff560679782fe08aefdb389209cc50",
8
+ "dist/observe/evidence.js": "bd7c6ac046ad4adde08117dcb66f68682e0036a83308c0c84f6d5ce141f8a386"
9
+ },
10
+ "journeys": [
11
+ {
12
+ "status": "PASS",
13
+ "server": {
14
+ "name": "mobile-debug-mcp",
15
+ "version": "0.32.4"
16
+ },
17
+ "platform": "android",
18
+ "device": "emulator-5554",
19
+ "osVersion": "16",
20
+ "startingApp": "com.android.settings",
21
+ "actionId": "5fdfe5a0-cfd3-4182-a12b-c440d855cec5",
22
+ "phases": {
23
+ "dispatchStarted": 1793350325789,
24
+ "dispatchCompleted": 1793350326584,
25
+ "screenshotStarted": 1793350327002,
26
+ "screenshotCompleted": 1793350327334
27
+ },
28
+ "screenshot": "captured",
29
+ "logs": "captured",
30
+ "uiTree": "captured",
31
+ "disabledComponents": "omitted",
32
+ "foreignReference": "unavailable",
33
+ "cleanup": "No settings, app data or streams changed; Settings remains foreground. No content artifacts saved."
34
+ },
35
+ {
36
+ "status": "PASS",
37
+ "server": {
38
+ "name": "mobile-debug-mcp",
39
+ "version": "0.32.4"
40
+ },
41
+ "platform": "ios",
42
+ "device": "F9243BAF-62A8-4990-8A99-17F527F56FDA",
43
+ "osVersion": "iOS 26.4",
44
+ "startingApp": "com.apple.Preferences",
45
+ "actionId": "99a4cdc4-8be1-4194-acab-3d08e7bd4a41",
46
+ "phases": {
47
+ "dispatchStarted": 1793350335131,
48
+ "dispatchCompleted": 1793350335486,
49
+ "screenshotStarted": 1793350335806,
50
+ "screenshotCompleted": 1793350336212
51
+ },
52
+ "screenshot": "captured",
53
+ "logs": "captured",
54
+ "uiTree": "captured",
55
+ "disabledComponents": "omitted",
56
+ "foreignReference": "unavailable",
57
+ "cleanup": "No settings, app data or streams changed; Settings remains foreground. No content artifacts saved."
58
+ }
59
+ ]
60
+ }