mobile-debug-mcp 0.34.0 → 0.35.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 (432) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.github/copilot-instructions.md +14 -36
  11. package/.github/workflows/ci.yml +4 -4
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +156 -129
  15. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  16. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  17. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  18. package/agent-plugin/.mcp.json +1 -1
  19. package/agent-plugin/README.md +16 -6
  20. package/agent-plugin/mcp.json +1 -1
  21. package/agent-plugin/plugin.json +1 -1
  22. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +27 -2
  23. package/dist/contracts/execution-controls.js +24 -0
  24. package/dist/contracts/selector.js +8 -0
  25. package/dist/contracts/target.js +15 -0
  26. package/dist/contracts/ui.js +1 -0
  27. package/dist/debugger/android.js +2 -1
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/protocol.js +33 -0
  30. package/dist/debugger/sessions.js +104 -62
  31. package/dist/debugger/worker.js +53 -42
  32. package/dist/interact/adjustment/adjustment-context.js +41 -0
  33. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  34. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  35. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  36. package/dist/interact/adjustment/adjustment-result.js +19 -0
  37. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  38. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  39. package/dist/interact/adjustment/adjustment.js +52 -0
  40. package/dist/interact/adjustment/control-geometry.js +87 -0
  41. package/dist/interact/adjustment/control-values.js +136 -0
  42. package/dist/interact/adjustment/slider-labels.js +84 -0
  43. package/dist/interact/assertions/screen-assertion.js +37 -0
  44. package/dist/interact/assertions/state-assertion-context.js +35 -0
  45. package/dist/interact/assertions/state-assertion.js +76 -0
  46. package/dist/interact/assertions/state-comparison.js +141 -0
  47. package/dist/interact/assertions/visibility-assertion.js +34 -0
  48. package/dist/interact/conditional-action.js +12 -7
  49. package/dist/interact/index.js +65 -3090
  50. package/dist/interact/input-service.js +1 -0
  51. package/dist/interact/interaction-trace.js +21 -0
  52. package/dist/interact/model.js +1 -0
  53. package/dist/interact/observation.js +1 -0
  54. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  55. package/dist/interact/targeting/candidate-result.js +23 -0
  56. package/dist/interact/targeting/candidate-scoring.js +50 -0
  57. package/dist/interact/targeting/element-helpers.js +216 -0
  58. package/dist/interact/targeting/find-element.js +65 -0
  59. package/dist/interact/targeting/nearby-control.js +56 -0
  60. package/dist/interact/targeting/resolved-elements.js +21 -0
  61. package/dist/interact/targeting/tap-target.js +17 -0
  62. package/dist/interact/targeting/target-scope.js +104 -0
  63. package/dist/interact/waits/legacy-log-wait.js +38 -0
  64. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  65. package/dist/interact/waits/legacy-wait-state.js +34 -0
  66. package/dist/interact/waits/legacy-wait.js +48 -0
  67. package/dist/interact/waits/screen-wait.js +49 -0
  68. package/dist/interact/waits/ui-change-helpers.js +157 -0
  69. package/dist/interact/waits/ui-change-state.js +18 -0
  70. package/dist/interact/waits/ui-change-wait.js +77 -0
  71. package/dist/interact/waits/ui-wait-condition.js +56 -0
  72. package/dist/interact/waits/ui-wait-state.js +38 -0
  73. package/dist/interact/waits/ui-wait.js +106 -0
  74. package/dist/manage/android.js +55 -51
  75. package/dist/manage/index.js +38 -75
  76. package/dist/manage/ios/build-types.js +1 -0
  77. package/dist/manage/ios/build.js +210 -0
  78. package/dist/manage/ios/project-discovery.js +29 -0
  79. package/dist/manage/ios/scheme.js +18 -0
  80. package/dist/manage/ios.js +42 -256
  81. package/dist/observe/android/context.js +1 -0
  82. package/dist/observe/android/current-screen.js +55 -0
  83. package/dist/observe/android/fingerprint.js +13 -0
  84. package/dist/observe/android/log-stream.js +132 -0
  85. package/dist/observe/android/logs.js +107 -0
  86. package/dist/observe/android/screenshot.js +70 -0
  87. package/dist/observe/android/ui-tree.js +107 -0
  88. package/dist/observe/android.js +16 -520
  89. package/dist/observe/evidence.js +114 -75
  90. package/dist/observe/index.js +16 -263
  91. package/dist/observe/ios/context.js +16 -0
  92. package/dist/observe/ios/fingerprint.js +12 -0
  93. package/dist/observe/ios/log-stream.js +105 -0
  94. package/dist/observe/ios/logs.js +168 -0
  95. package/dist/observe/ios/screenshot.js +22 -0
  96. package/dist/observe/ios/ui-elements.js +241 -0
  97. package/dist/observe/ios/ui-tree.js +137 -0
  98. package/dist/observe/ios.js +17 -715
  99. package/dist/observe/logs/model.js +1 -0
  100. package/dist/{network → observe/network}/index.js +2 -2
  101. package/dist/observe/readiness-service.js +4 -2
  102. package/dist/observe/readiness.js +29 -10
  103. package/dist/observe/screenshot-encoding.js +41 -0
  104. package/dist/observe/snapshot-collection.js +79 -0
  105. package/dist/observe/snapshot-logs.js +30 -0
  106. package/dist/observe/snapshot-metadata.js +43 -31
  107. package/dist/observe/snapshot-semantic.js +115 -0
  108. package/dist/observe/system-trace.js +22 -13
  109. package/dist/runtime/action-results.js +270 -0
  110. package/dist/runtime/clock.js +4 -0
  111. package/dist/server/common.js +1 -256
  112. package/dist/server/definitions/assert.js +162 -0
  113. package/dist/server/definitions/diagnostic.js +118 -0
  114. package/dist/server/definitions/input.js +274 -0
  115. package/dist/server/definitions/manage.js +194 -0
  116. package/dist/server/definitions/observe.js +149 -0
  117. package/dist/server/definitions/target.js +138 -0
  118. package/dist/server/definitions/wait.js +105 -0
  119. package/dist/server/handlers/action-execution.js +163 -0
  120. package/dist/server/handlers/correlation.js +23 -0
  121. package/dist/server/{debugger-handlers.js → handlers/debugger-handlers.js} +22 -12
  122. package/dist/server/handlers/debugger-response.js +9 -0
  123. package/dist/server/handlers/diagnostic.js +69 -0
  124. package/dist/server/handlers/handler-types.js +1 -0
  125. package/dist/server/handlers/input.js +154 -0
  126. package/dist/server/handlers/interact.js +113 -0
  127. package/dist/server/handlers/journey-assertions.js +35 -0
  128. package/dist/server/handlers/journey.js +147 -0
  129. package/dist/server/handlers/manage.js +119 -0
  130. package/dist/server/handlers/observe.js +95 -0
  131. package/dist/server/handlers/payload.js +4 -0
  132. package/dist/server/handlers/tool-handlers.js +160 -0
  133. package/dist/server/schemas/condition-schema.js +2 -0
  134. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +11 -6
  135. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  136. package/dist/server/tool-definitions.js +58 -1135
  137. package/dist/server-core.js +2 -2
  138. package/dist/system/android-status.js +101 -0
  139. package/dist/system/android.js +37 -27
  140. package/dist/system/index.js +13 -439
  141. package/dist/system/ios-status.js +132 -0
  142. package/dist/system/ios.js +11 -8
  143. package/dist/system/model.js +1 -0
  144. package/dist/system/probes.js +105 -0
  145. package/dist/system/status-summary.js +43 -0
  146. package/dist/system/unsupported-status.js +52 -0
  147. package/dist/utils/android/commands.js +89 -0
  148. package/dist/utils/android/gradle.js +139 -0
  149. package/dist/utils/android/logs.js +103 -0
  150. package/dist/utils/android/ui-tree.js +233 -0
  151. package/dist/utils/android/utils.js +6 -564
  152. package/dist/utils/cli/idb/idb-helper.js +4 -6
  153. package/dist/utils/cli/idb/install-idb.js +13 -9
  154. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  155. package/dist/utils/ios/utils.js +45 -46
  156. package/dist/utils/java.js +65 -49
  157. package/dist/utils/resolve-device.js +4 -2
  158. package/dist/utils/ui/index.js +26 -26
  159. package/docs/CHANGELOG.md +44 -0
  160. package/docs/README.md +38 -0
  161. package/docs/benchmarks/README.md +18 -0
  162. package/docs/benchmarks/interaction-latency/README.md +14 -0
  163. package/docs/benchmarks/interaction-latency/final.md +4 -1
  164. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  165. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  166. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  167. package/docs/code-structure.md +118 -0
  168. package/docs/specs/001-state-verification/spec.md +16 -14
  169. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  170. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  171. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  172. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  173. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  174. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  175. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  176. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  177. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  178. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  179. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  180. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  181. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  182. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  183. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  184. package/docs/specs/016-environment-tools/spec.md +20 -20
  185. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  186. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  187. package/docs/specs/020-conditional-actions/intent.md +5 -3
  188. package/docs/specs/020-conditional-actions/plan.md +8 -9
  189. package/docs/specs/020-conditional-actions/spec.md +4 -2
  190. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  191. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  192. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  193. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  194. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  195. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  196. package/docs/specs/023-android-system-traces/intent.md +3 -2
  197. package/docs/specs/023-android-system-traces/plan.md +5 -7
  198. package/docs/specs/023-android-system-traces/spec.md +3 -1
  199. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  200. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  201. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  202. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  203. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  204. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  205. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  206. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  207. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  208. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  209. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  210. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  211. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  212. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  213. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  214. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  215. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  216. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  217. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  218. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  219. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  220. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  221. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  222. package/docs/tools/TOOLS.md +31 -18
  223. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  224. package/docs/tools/debugger.md +76 -0
  225. package/docs/tools/interact.md +42 -20
  226. package/docs/tools/manage.md +25 -44
  227. package/docs/tools/observe.md +55 -23
  228. package/docs/tools/system-traces.md +9 -1
  229. package/docs/tools/system.md +12 -5
  230. package/package.json +11 -8
  231. package/scripts/architecture-graph.mjs +69 -0
  232. package/scripts/check-architecture.mjs +47 -0
  233. package/scripts/code-quality-baseline.json +1 -0
  234. package/scripts/code-quality.mjs +161 -0
  235. package/src/contracts/execution-controls.ts +26 -0
  236. package/src/contracts/selector.ts +17 -0
  237. package/src/contracts/target.ts +19 -0
  238. package/src/contracts/ui.ts +94 -0
  239. package/src/debugger/android.ts +2 -1
  240. package/src/debugger/diagnostics.ts +43 -0
  241. package/src/debugger/guard.ts +6 -1
  242. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  243. package/src/debugger/protocol.ts +37 -0
  244. package/src/debugger/sessions.ts +314 -155
  245. package/src/debugger/worker.ts +51 -38
  246. package/src/interact/adjustment/adjustment-context.ts +83 -0
  247. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  248. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  249. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  250. package/src/interact/adjustment/adjustment-result.ts +34 -0
  251. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  252. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  253. package/src/interact/adjustment/adjustment.ts +49 -0
  254. package/src/interact/adjustment/control-geometry.ts +120 -0
  255. package/src/interact/adjustment/control-values.ts +141 -0
  256. package/src/interact/adjustment/slider-labels.ts +84 -0
  257. package/src/interact/assertions/screen-assertion.ts +39 -0
  258. package/src/interact/assertions/state-assertion-context.ts +48 -0
  259. package/src/interact/assertions/state-assertion.ts +64 -0
  260. package/src/interact/assertions/state-comparison.ts +168 -0
  261. package/src/interact/assertions/visibility-assertion.ts +41 -0
  262. package/src/interact/conditional-action.ts +18 -12
  263. package/src/interact/index.ts +76 -3492
  264. package/src/interact/input-service.ts +11 -0
  265. package/src/interact/interaction-trace.ts +36 -0
  266. package/src/interact/model.ts +109 -0
  267. package/src/interact/observation.ts +7 -0
  268. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  269. package/src/interact/targeting/candidate-result.ts +24 -0
  270. package/src/interact/targeting/candidate-scoring.ts +42 -0
  271. package/src/interact/targeting/element-helpers.ts +243 -0
  272. package/src/interact/targeting/find-element.ts +47 -0
  273. package/src/interact/targeting/nearby-control.ts +46 -0
  274. package/src/interact/targeting/resolved-elements.ts +20 -0
  275. package/src/interact/targeting/tap-target.ts +22 -0
  276. package/src/interact/targeting/target-scope.ts +121 -0
  277. package/src/interact/waits/legacy-log-wait.ts +30 -0
  278. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  279. package/src/interact/waits/legacy-wait-state.ts +48 -0
  280. package/src/interact/waits/legacy-wait.ts +34 -0
  281. package/src/interact/waits/screen-wait.ts +37 -0
  282. package/src/interact/waits/ui-change-helpers.ts +184 -0
  283. package/src/interact/waits/ui-change-state.ts +26 -0
  284. package/src/interact/waits/ui-change-wait.ts +59 -0
  285. package/src/interact/waits/ui-wait-condition.ts +52 -0
  286. package/src/interact/waits/ui-wait-state.ts +55 -0
  287. package/src/interact/waits/ui-wait.ts +84 -0
  288. package/src/manage/android.ts +56 -50
  289. package/src/manage/index.ts +63 -93
  290. package/src/manage/ios/build-types.ts +14 -0
  291. package/src/manage/ios/build.ts +230 -0
  292. package/src/manage/ios/project-discovery.ts +26 -0
  293. package/src/manage/ios/scheme.ts +16 -0
  294. package/src/manage/ios.ts +65 -298
  295. package/src/observe/android/context.ts +1 -0
  296. package/src/observe/android/current-screen.ts +63 -0
  297. package/src/observe/android/fingerprint.ts +13 -0
  298. package/src/observe/android/log-stream.ts +127 -0
  299. package/src/observe/android/logs.ts +110 -0
  300. package/src/observe/android/screenshot.ts +79 -0
  301. package/src/observe/android/ui-tree.ts +113 -0
  302. package/src/observe/android.ts +17 -531
  303. package/src/observe/evidence.ts +122 -70
  304. package/src/observe/index.ts +24 -286
  305. package/src/observe/ios/context.ts +22 -0
  306. package/src/observe/ios/fingerprint.ts +12 -0
  307. package/src/observe/ios/log-stream.ts +103 -0
  308. package/src/observe/ios/logs.ts +179 -0
  309. package/src/observe/ios/screenshot.ts +26 -0
  310. package/src/observe/ios/ui-elements.ts +266 -0
  311. package/src/observe/ios/ui-tree.ts +143 -0
  312. package/src/observe/ios.ts +18 -749
  313. package/src/observe/logs/model.ts +11 -0
  314. package/src/{network → observe/network}/index.ts +2 -2
  315. package/src/observe/readiness-service.ts +5 -3
  316. package/src/observe/readiness.ts +39 -18
  317. package/src/observe/screenshot-encoding.ts +32 -0
  318. package/src/observe/snapshot-collection.ts +69 -0
  319. package/src/observe/snapshot-logs.ts +26 -0
  320. package/src/observe/snapshot-metadata.ts +51 -31
  321. package/src/observe/snapshot-semantic.ts +153 -0
  322. package/src/observe/system-trace.ts +24 -15
  323. package/src/runtime/action-results.ts +376 -0
  324. package/src/runtime/clock.ts +9 -0
  325. package/src/server/common.ts +9 -359
  326. package/src/server/definitions/assert.ts +164 -0
  327. package/src/server/definitions/diagnostic.ts +120 -0
  328. package/src/server/definitions/input.ts +276 -0
  329. package/src/server/definitions/manage.ts +196 -0
  330. package/src/server/definitions/observe.ts +151 -0
  331. package/src/server/definitions/target.ts +140 -0
  332. package/src/server/definitions/wait.ts +107 -0
  333. package/src/server/handlers/action-execution.ts +164 -0
  334. package/src/server/handlers/correlation.ts +20 -0
  335. package/src/server/handlers/debugger-handlers.ts +39 -0
  336. package/src/server/handlers/debugger-response.ts +7 -0
  337. package/src/server/handlers/diagnostic.ts +79 -0
  338. package/src/server/handlers/handler-types.ts +7 -0
  339. package/src/server/handlers/input.ts +173 -0
  340. package/src/server/handlers/interact.ts +135 -0
  341. package/src/server/handlers/journey-assertions.ts +39 -0
  342. package/src/server/handlers/journey.ts +132 -0
  343. package/src/server/handlers/manage.ts +141 -0
  344. package/src/server/handlers/observe.ts +116 -0
  345. package/src/server/handlers/payload.ts +36 -0
  346. package/src/server/handlers/tool-handlers.ts +159 -0
  347. package/src/server/schemas/condition-schema.ts +2 -0
  348. package/src/server/{debugger-schema.ts → schemas/debugger-schema.ts} +13 -7
  349. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  350. package/src/server/{system-trace-schema.ts → schemas/system-trace-schema.ts} +3 -3
  351. package/src/server/tool-definitions.ts +67 -1143
  352. package/src/server-core.ts +2 -2
  353. package/src/server.ts +1 -1
  354. package/src/system/android-status.ts +124 -0
  355. package/src/system/android.ts +33 -21
  356. package/src/system/index.ts +21 -525
  357. package/src/system/ios-status.ts +147 -0
  358. package/src/system/ios.ts +14 -10
  359. package/src/system/model.ts +41 -0
  360. package/src/system/probes.ts +114 -0
  361. package/src/system/status-summary.ts +47 -0
  362. package/src/system/unsupported-status.ts +55 -0
  363. package/src/types.ts +16 -106
  364. package/src/utils/android/commands.ts +81 -0
  365. package/src/utils/android/gradle.ts +132 -0
  366. package/src/utils/android/logs.ts +103 -0
  367. package/src/utils/android/ui-tree.ts +239 -0
  368. package/src/utils/android/utils.ts +16 -556
  369. package/src/utils/cli/idb/check-idb.ts +4 -4
  370. package/src/utils/cli/idb/idb-helper.ts +6 -8
  371. package/src/utils/cli/idb/install-idb.ts +15 -9
  372. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  373. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  374. package/src/utils/ios/utils.ts +57 -61
  375. package/src/utils/java.ts +62 -43
  376. package/src/utils/resolve-device.ts +9 -6
  377. package/src/utils/ui/index.ts +28 -28
  378. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  379. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  380. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  381. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  382. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  383. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  384. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  385. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  386. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  387. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  388. package/test/fixtures/coroutine-app/README.md +48 -0
  389. package/test/fixtures/coroutine-app/build.ts +35 -0
  390. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  391. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  392. package/test/fixtures/debugger-app/README.md +52 -0
  393. package/test/fixtures/debugger-app/build.ts +25 -15
  394. package/test/fixtures/latency-journey-app/README.md +2 -0
  395. package/test/fixtures/readiness-app/README.md +2 -0
  396. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  397. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  398. package/test/unit/debugger/diagnostics.test.ts +80 -0
  399. package/test/unit/debugger/guards.test.ts +4 -0
  400. package/test/unit/debugger/preparation.test.ts +32 -0
  401. package/test/unit/debugger/protocol.test.ts +15 -0
  402. package/test/unit/debugger/sessions.test.ts +1 -1
  403. package/test/unit/interact/adjustment_service.test.ts +42 -0
  404. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  405. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  406. package/test/unit/interact/element_helpers.test.ts +19 -0
  407. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  408. package/test/unit/interact/state_comparison.test.ts +20 -0
  409. package/test/unit/interact/target_scope.test.ts +35 -0
  410. package/test/unit/network/get_network_activity.test.ts +1 -1
  411. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  412. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  413. package/test/unit/quality/architecture.test.ts +60 -0
  414. package/test/unit/quality/code-quality.test.ts +58 -0
  415. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  416. package/test/unit/server/conditional_actions.test.ts +1 -1
  417. package/test/unit/server/conditional_schema.test.ts +2 -2
  418. package/test/unit/server/debugger-response.test.ts +63 -0
  419. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  420. package/dist/server/tool-handlers.js +0 -1000
  421. package/docs/ROADMAP.md +0 -646
  422. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  423. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  424. package/docs/specs/baseline-spec-v0.md +0 -312
  425. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  426. package/src/server/debugger-handlers.ts +0 -29
  427. package/src/server/tool-handlers.ts +0 -1008
  428. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  429. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  430. /package/dist/server/{system-trace-schema.js → schemas/system-trace-schema.js} +0 -0
  431. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  432. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -41,7 +41,8 @@ export class AndroidDebugNative {
41
41
  try {
42
42
  jdwpOutput = (await exec(resolveAdbCmd(), ['-s', device, 'jdwp'], { timeout: debuggerRemaining(500), signal: debuggerSignal(), maxBuffer: 1024 * 1024 })).stdout;
43
43
  }
44
- catch (e) {
44
+ catch (error) {
45
+ const e = error;
45
46
  if (debuggerSignal()?.aborted)
46
47
  throw new DebuggerError('DEBUGGER_CANCELLED');
47
48
  debuggerRemaining(1);
@@ -0,0 +1,54 @@
1
+ /** Only catalog values cross the debugger error boundary. Never inspect exception text. */
2
+ export const stages = ['prerequisite', 'compilation', 'worker_start', 'ipc', 'target_resolution', 'forwarding', 'attach', 'command', 'transport', 'cleanup', 'unknown'];
3
+ export const reasons = ['unavailable', 'unsupported', 'spawn_failed', 'process_exited', 'invalid_message', 'message_limit', 'write_failed', 'connection_refused', 'transport_lost', 'timeout', 'cancelled', 'operation_failed', 'unknown'];
4
+ const recovery = {
5
+ prerequisite: 'Check JDK 17+ availability, including java, javac and jdk.jdi.',
6
+ compilation: 'Check the JDK compiler and worker source/cache availability.',
7
+ worker_start: 'Check Java worker launch availability. Inspect session state and cleanup before attaching again.',
8
+ ipc: 'Inspect session state and cleanup. Detach before attempting a fresh attach.',
9
+ target_resolution: 'Check the explicit device and running debuggable app/process.',
10
+ forwarding: 'Check the explicit device and debugger forwarding availability.',
11
+ attach: 'Check the running debuggable target and whether another debugger is attached. Inspect cleanup before a fresh attach.',
12
+ command: 'Inspect debugger session state and cleanup before another state-changing command.',
13
+ transport: 'Inspect session state and cleanup. Detach before attempting a fresh attach; execution state may be unknown.',
14
+ cleanup: 'Retry detach_debugger and inspect cleanup.remaining_resources. Do not kill the app or ADB server.',
15
+ unknown: 'Inspect debugger session state and cleanup; the failure cause is unknown.'
16
+ };
17
+ export function diagnostic(stage, reason) {
18
+ const guidance = reason === 'timeout' || reason === 'cancelled'
19
+ ? 'Execution may be uncertain. Inspect session state and cleanup before another state-changing command.' : recovery[stage];
20
+ return { stage, reason, recovery: guidance };
21
+ }
22
+ export function parseDiagnostic(value) {
23
+ if (!value || typeof value !== 'object')
24
+ throw new Error('INVALID_WORKER_DIAGNOSTIC');
25
+ const { stage, reason } = value;
26
+ if (!stages.includes(stage) || !reasons.includes(reason))
27
+ throw new Error('INVALID_WORKER_DIAGNOSTIC');
28
+ return diagnostic(stage, reason);
29
+ }
30
+ export class DebuggerError extends Error {
31
+ code;
32
+ diagnostic;
33
+ constructor(code, diagnostic) {
34
+ super(code);
35
+ this.code = code;
36
+ this.diagnostic = diagnostic;
37
+ }
38
+ }
39
+ const validationCodes = new Set(['INVALID_ARGUMENT', 'DEBUGGER_BUSY', 'DEBUGGER_NOT_FOUND', 'DEBUGGER_PAUSED', 'DEBUGGER_NOT_PAUSED', 'DEBUGGER_STALE_REFERENCE']);
40
+ export function classify(error, stage, fallback, reason = 'unknown') {
41
+ const existing = error instanceof DebuggerError ? error : new DebuggerError(fallback);
42
+ if (existing.diagnostic || validationCodes.has(existing.code))
43
+ return existing;
44
+ const knownReason = existing.code === 'DEBUGGER_TIMEOUT' ? 'timeout' : existing.code === 'DEBUGGER_CANCELLED' ? 'cancelled' : reason;
45
+ return new DebuggerError(existing.code, diagnostic(stage, knownReason));
46
+ }
47
+ export async function atStage(stage, fallback, operation) {
48
+ try {
49
+ return await operation();
50
+ }
51
+ catch (error) {
52
+ throw classify(error, stage, fallback);
53
+ }
54
+ }
@@ -0,0 +1,33 @@
1
+ import { parseDiagnostic } from './diagnostics.js';
2
+ const workerCodes = new Set(['INVALID_ARGUMENT', 'DEBUGGER_BUSY', 'DEBUGGER_NOT_FOUND', 'DEBUGGER_NOT_PAUSED', 'DEBUGGER_STALE_REFERENCE', 'DEBUGGER_DISCONNECTED', 'DEBUGGER_WORKER_FAILED']);
3
+ function record(value) {
4
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
5
+ }
6
+ export function parseWorkerMessage(value) {
7
+ if (!record(value))
8
+ throw new Error('INVALID_WORKER_MESSAGE');
9
+ if (value.event) {
10
+ if (typeof value.event !== 'string')
11
+ throw new Error('INVALID_WORKER_EVENT');
12
+ for (const field of ['reason', 'stop_id']) {
13
+ if (value[field] !== undefined && typeof value[field] !== 'string')
14
+ throw new Error('INVALID_WORKER_EVENT');
15
+ }
16
+ const event = { ...value };
17
+ if (value.diagnostic !== undefined)
18
+ event.diagnostic = parseDiagnostic(value.diagnostic);
19
+ return { kind: 'event', value: event };
20
+ }
21
+ if (typeof value.id !== 'number' || !Number.isSafeInteger(value.id) || value.id < 1)
22
+ throw new Error('INVALID_WORKER_REPLY');
23
+ if (value.error) {
24
+ if (!record(value.error) || (value.error.code !== undefined && typeof value.error.code !== 'string'))
25
+ throw new Error('INVALID_WORKER_ERROR');
26
+ if (value.error.code !== undefined && !workerCodes.has(value.error.code))
27
+ throw new Error('INVALID_WORKER_ERROR');
28
+ return { kind: 'reply', id: value.id, result: {}, error: { ...(value.error.code === undefined ? {} : { code: value.error.code }), ...(value.error.diagnostic === undefined ? {} : { diagnostic: parseDiagnostic(value.error.diagnostic) }) } };
29
+ }
30
+ if (!record(value.result))
31
+ throw new Error('INVALID_WORKER_RESULT');
32
+ return { kind: 'reply', id: value.id, result: value.result };
33
+ }
@@ -1,3 +1,4 @@
1
+ import { atStage, classify, diagnostic } from './diagnostics.js';
1
2
  import { registerDebuggerPauseProvider } from './guard.js';
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import { AndroidDebugNative } from './android.js';
@@ -28,21 +29,29 @@ export class DebuggerSessions {
28
29
  for (const s of remaining.slice(0, Math.max(0, remaining.length - 100)))
29
30
  this.sessions.delete(s.id);
30
31
  }
31
- get(id) { this.prune(); const s = this.sessions.get(id); if (!s)
32
- throw new DebuggerError('DEBUGGER_NOT_FOUND'); s.activity = Date.now(); return s; }
32
+ get(id) {
33
+ this.prune();
34
+ const s = this.sessions.get(id);
35
+ if (!s)
36
+ throw new DebuggerError('DEBUGGER_NOT_FOUND');
37
+ s.activity = Date.now();
38
+ return s;
39
+ }
33
40
  serial(s, fn) { const out = s.queue.then(fn); s.queue = out.catch(() => { }); return out; }
34
41
  result(s, payload = {}) {
35
42
  const success = payload.success ?? (!['failed', 'disconnected'].includes(s.state) && s.cleanup.status !== 'failed');
36
43
  const code = s.cleanup.status === 'failed' ? 'DEBUGGER_CLEANUP_FAILED' : s.state === 'disconnected' ? 'DEBUGGER_DISCONNECTED' : s.reason?.match(/^[A-Z_]+$/) ? s.reason : 'DEBUGGER_WORKER_FAILED';
37
- return structuredClone({ success, ...(!success ? { error: { code, message: code, retryable: false } } : {}), debug_session_id: s.id,
44
+ return structuredClone({ success, ...(!success ? { error: { code, message: code, retryable: false, diagnostic: s.cleanup.status === 'failed' ? diagnostic('cleanup', 'operation_failed') : s.diagnostic ?? diagnostic('unknown', 'unknown') } } : {}), debug_session_id: s.id,
38
45
  device: { platform: 'android', id: s.device }, target: s.target, state: s.state, captured_at_ms: Date.now(),
39
46
  capabilities: debuggerCapabilities, limitations, cleanup: s.cleanup, reason: s.reason, stop: s.stop, ...payload });
40
47
  }
41
48
  summary(id) { return this.result(this.get(id)); }
42
49
  hasActiveSessions() { return [...this.sessions.values()].some(s => s.cleanup.status !== 'complete'); }
43
50
  paused(device) { const s = [...this.sessions.values()].find(s => s.device === device && s.state === 'paused'); return s ? this.result(s) : null; }
44
- assertRunning(device) { if (this.paused(device))
45
- throw new DebuggerError('DEBUGGER_PAUSED'); }
51
+ assertRunning(device) {
52
+ if (this.paused(device))
53
+ throw new DebuggerError('DEBUGGER_PAUSED');
54
+ }
46
55
  event(s, event) {
47
56
  if (s.cleanup.status === 'complete')
48
57
  return;
@@ -52,6 +61,8 @@ export class DebuggerSessions {
52
61
  s.pausedAt ??= Date.now();
53
62
  }
54
63
  if (event.event === 'terminated' || event.event === 'worker_failed') {
64
+ if (!s.cleaning)
65
+ s.diagnostic ??= event.diagnostic ?? diagnostic(event.event === 'worker_failed' ? 'ipc' : 'transport', event.reason === 'transport_lost' ? 'transport_lost' : 'unknown');
55
66
  if (!s.cleaning && s.state !== 'failed')
56
67
  s.reason = event.reason ?? 'worker_failed';
57
68
  if (!s.cleaning) {
@@ -69,30 +80,35 @@ export class DebuggerSessions {
69
80
  const prerequisites = { jdk: false, device: false, process: false };
70
81
  let candidates = [];
71
82
  const reasons = [];
83
+ const diagnostics = [];
72
84
  try {
73
- await this.prerequisite();
85
+ await atStage('prerequisite', 'DEBUGGER_UNSUPPORTED', () => this.prerequisite());
74
86
  prerequisites.jdk = true;
75
87
  }
76
88
  catch (e) {
77
89
  if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code))
78
90
  throw e;
79
91
  reasons.push('DEBUGGER_UNSUPPORTED');
92
+ diagnostics.push(classify(e, 'prerequisite', 'DEBUGGER_UNSUPPORTED').diagnostic ?? diagnostic('prerequisite', 'unknown'));
80
93
  }
81
94
  try {
82
- candidates = await this.native.candidates(device, app);
95
+ candidates = await atStage('target_resolution', 'DEVICE_UNAVAILABLE', () => this.native.candidates(device, app));
83
96
  prerequisites.device = true;
84
97
  prerequisites.process = candidates.some(t => pid === undefined || pid === t.pid);
85
- if (!prerequisites.process)
98
+ if (!prerequisites.process) {
86
99
  reasons.push('PROCESS_NOT_FOUND');
100
+ diagnostics.push(diagnostic('target_resolution', 'unavailable'));
101
+ }
87
102
  }
88
103
  catch (e) {
89
104
  if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code))
90
105
  throw e;
91
106
  reasons.push(e instanceof DebuggerError ? e.code : 'DEVICE_UNAVAILABLE');
107
+ diagnostics.push(classify(e, 'target_resolution', 'DEVICE_UNAVAILABLE').diagnostic ?? diagnostic('target_resolution', 'unknown'));
92
108
  }
93
109
  return { success: true, debug_session_id: null, device: { platform: 'android', id: device }, target: null, state: null,
94
110
  captured_at_ms: Date.now(), supported: Object.values(prerequisites).every(Boolean), prerequisites, candidates,
95
- capabilities: debuggerCapabilities, reasons, limitations, cleanup: { status: 'complete', remaining_resources: [] } };
111
+ ...(diagnostics.length ? { diagnostics } : {}), capabilities: debuggerCapabilities, reasons, limitations, cleanup: { status: 'complete', remaining_resources: [] } };
96
112
  }
97
113
  async attach(device, app, pid, timeout = 10000) {
98
114
  this.prune();
@@ -103,27 +119,43 @@ export class DebuggerSessions {
103
119
  queue: Promise.resolve(), events: [], sequence: 0, cleanup: { status: 'pending', remaining_resources: [] }, created: Date.now(), activity: Date.now() };
104
120
  this.sessions.set(s.id, s);
105
121
  return this.serial(s, async () => {
122
+ let stage = 'prerequisite';
106
123
  const deadline = Date.now() + timeout;
107
- const remaining = () => { debuggerRemaining(timeout); const ms = deadline - Date.now(); if (ms <= 0)
108
- throw new DebuggerError('DEBUGGER_TIMEOUT'); return ms; };
124
+ const remaining = () => {
125
+ try {
126
+ debuggerRemaining(timeout);
127
+ const ms = deadline - Date.now();
128
+ if (ms <= 0)
129
+ throw new DebuggerError('DEBUGGER_TIMEOUT');
130
+ return ms;
131
+ }
132
+ catch (e) {
133
+ throw classify(e, stage, 'DEBUGGER_ATTACH_FAILED');
134
+ }
135
+ };
109
136
  try {
110
- await this.prerequisite();
137
+ await atStage('prerequisite', 'DEBUGGER_ATTACH_FAILED', () => this.prerequisite());
111
138
  remaining();
112
- const candidates = (await this.native.candidates(device, app)).filter(t => pid === undefined || t.pid === pid);
139
+ stage = 'target_resolution';
140
+ const candidates = (await atStage('target_resolution', 'DEBUGGER_ATTACH_FAILED', () => this.native.candidates(device, app))).filter(t => pid === undefined || t.pid === pid);
113
141
  remaining();
114
142
  if (!candidates.length)
115
- throw new DebuggerError('PROCESS_NOT_FOUND');
143
+ throw new DebuggerError('PROCESS_NOT_FOUND', diagnostic('target_resolution', 'unavailable'));
116
144
  if (candidates.length > 1)
117
145
  throw new DebuggerError('PROCESS_AMBIGUOUS');
118
146
  s.target = candidates[0];
119
- s.port = await this.native.forward(device, s.target.pid);
147
+ stage = 'forwarding';
148
+ s.port = await atStage('forwarding', 'DEBUGGER_ATTACH_FAILED', () => this.native.forward(device, s.target.pid));
120
149
  remaining();
121
150
  s.cleanup.remaining_resources.push(`forward:tcp:${s.port}`);
122
- s.worker = await this.workerFactory(event => this.event(s, event));
151
+ stage = 'worker_start';
152
+ s.worker = await atStage('worker_start', 'DEBUGGER_ATTACH_FAILED', () => this.workerFactory(event => this.event(s, event)));
123
153
  remaining();
124
154
  s.cleanup.remaining_resources.push('jdi_worker');
125
- await s.worker.call('attach', { port: s.port, timeoutMs: remaining() }, remaining());
126
- await this.native.identity(device, s.target);
155
+ stage = 'attach';
156
+ await atStage('attach', 'DEBUGGER_ATTACH_FAILED', () => s.worker.call('attach', { port: s.port, timeoutMs: remaining() }, remaining()));
157
+ stage = 'target_resolution';
158
+ await atStage('target_resolution', 'DEBUGGER_ATTACH_FAILED', () => this.native.identity(device, s.target));
127
159
  remaining();
128
160
  if (s.state === 'attaching')
129
161
  s.state = 'running';
@@ -140,10 +172,11 @@ export class DebuggerSessions {
140
172
  s.timer.unref();
141
173
  }
142
174
  catch (e) {
175
+ s.diagnostic ??= classify(e, stage, 'DEBUGGER_ATTACH_FAILED').diagnostic;
143
176
  s.state = 'failed';
144
177
  s.reason = e instanceof DebuggerError ? e.code : 'DEBUGGER_ATTACH_FAILED';
145
178
  await this.cleanup(s, true);
146
- return this.result(s, { success: false, error: { code: s.reason, message: s.reason, retryable: false } });
179
+ return this.result(s, { success: false, error: { code: s.reason, message: s.reason, retryable: false, diagnostic: s.diagnostic } });
147
180
  }
148
181
  return this.result(s);
149
182
  });
@@ -203,58 +236,67 @@ export class DebuggerSessions {
203
236
  if (after > s.sequence)
204
237
  throw new DebuggerError('INVALID_ARGUMENT');
205
238
  if (args.signal?.aborted)
206
- throw new DebuggerError('DEBUGGER_CANCELLED');
239
+ throw new DebuggerError('DEBUGGER_CANCELLED', diagnostic('command', 'cancelled'));
207
240
  if (s.worker && ['running', 'paused'].includes(s.state))
208
- await s.worker.call('heartbeat', {}, Math.min(timeout, 1000));
241
+ await atStage('command', 'DEBUGGER_WORKER_FAILED', () => s.worker.call('heartbeat', {}, Math.min(timeout, 1000)));
209
242
  while (Date.now() < deadline && !s.events.some(e => e.sequence > after) && s.cleanup.status === 'pending') {
210
243
  if (args.signal?.aborted)
211
- throw new DebuggerError('DEBUGGER_CANCELLED');
244
+ throw new DebuggerError('DEBUGGER_CANCELLED', diagnostic('command', 'cancelled'));
212
245
  await new Promise(resolve => setTimeout(resolve, Math.min(50, deadline - Date.now())));
213
246
  }
214
247
  const events = s.events.filter(e => e.sequence > after).slice(0, 100);
215
248
  return this.result(s, { events, next_cursor: events.at(-1)?.sequence ?? s.sequence, overflow: after < (s.events[0]?.sequence ?? s.sequence + 1) - 1, timed_out: events.length === 0 });
216
249
  }
217
- return this.serial(s, async () => {
218
- if (command === 'detach') {
219
- if (s.cleanup.status !== 'complete')
220
- await this.cleanup(s, true);
221
- return this.result(s, { success: s.cleanup.status === 'complete', execution_state: 'unknown' });
222
- }
223
- if (command === 'state' && (s.cleanup.status === 'complete' || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state)))
250
+ return this.serial(s, () => this.executeCommand(s, command, args, timeout));
251
+ }
252
+ assertCommandTarget(s, command, args) {
253
+ if (!s.worker || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state))
254
+ throw new DebuggerError('DEBUGGER_DISCONNECTED', s.diagnostic ?? diagnostic('command', 'unavailable'));
255
+ if (['stack', 'locals', 'step', 'resume'].includes(command)) {
256
+ if (args.stopId !== s.stop?.stop_id)
257
+ throw new DebuggerError('DEBUGGER_STALE_REFERENCE');
258
+ if (s.state !== 'paused')
259
+ throw new DebuggerError('DEBUGGER_NOT_PAUSED');
260
+ }
261
+ }
262
+ async executeCommand(s, command, args, timeout) {
263
+ if (command === 'detach') {
264
+ if (s.cleanup.status !== 'complete')
265
+ await this.cleanup(s, true);
266
+ return this.result(s, { success: s.cleanup.status === 'complete', execution_state: 'unknown' });
267
+ }
268
+ if (command === 'state' && (s.cleanup.status === 'complete' || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state)))
269
+ return this.result(s);
270
+ this.assertCommandTarget(s, command, args);
271
+ if (command === 'step' || command === 'resume') {
272
+ s.state = 'running';
273
+ s.stop = null;
274
+ s.pausedAt = undefined;
275
+ }
276
+ try {
277
+ const data = await s.worker.call(command, args, timeout);
278
+ return this.result(s, command === 'state' ? { breakpoints: data.breakpoints } : { result: data });
279
+ }
280
+ catch (e) {
281
+ return this.handleCommandError(s, command, classify(e, 'command', 'DEBUGGER_WORKER_FAILED'));
282
+ }
283
+ }
284
+ async handleCommandError(s, command, e) {
285
+ if (e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED') {
286
+ s.diagnostic ??= e.diagnostic ?? diagnostic('transport', 'transport_lost');
287
+ s.state = 'disconnected';
288
+ s.reason = 'transport_lost';
289
+ await this.cleanup(s, false);
290
+ if (command === 'state')
224
291
  return this.result(s);
225
- if (!s.worker || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state))
226
- throw new DebuggerError('DEBUGGER_DISCONNECTED');
227
- if (['stack', 'locals', 'step', 'resume'].includes(command)) {
228
- if (args.stopId !== s.stop?.stop_id)
229
- throw new DebuggerError('DEBUGGER_STALE_REFERENCE');
230
- if (s.state !== 'paused')
231
- throw new DebuggerError('DEBUGGER_NOT_PAUSED');
232
- }
233
- if (command === 'step' || command === 'resume') {
234
- s.state = 'running';
235
- s.stop = null;
236
- s.pausedAt = undefined;
237
- }
238
- try {
239
- const data = await s.worker.call(command, args, timeout);
240
- return this.result(s, command === 'state' ? { breakpoints: data.breakpoints } : { result: data });
241
- }
242
- catch (e) {
243
- if (e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED') {
244
- s.state = 'disconnected';
245
- s.reason = 'transport_lost';
246
- await this.cleanup(s, false);
247
- if (command === 'state')
248
- return this.result(s);
249
- }
250
- if ((command === 'step' || command === 'resume' || command === 'set' || command === 'remove') && e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED', 'DEBUGGER_WORKER_FAILED'].includes(e.code)) {
251
- s.state = 'failed';
252
- s.reason = e.code;
253
- await this.cleanup(s, true);
254
- }
255
- throw e;
256
- }
257
- });
292
+ }
293
+ if ((command === 'step' || command === 'resume' || command === 'set' || command === 'remove') && e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED', 'DEBUGGER_WORKER_FAILED'].includes(e.code)) {
294
+ s.diagnostic ??= e.diagnostic;
295
+ s.state = 'failed';
296
+ s.reason = e.code;
297
+ await this.cleanup(s, true);
298
+ }
299
+ throw e;
258
300
  }
259
301
  async shutdown() { this.closing = true; await Promise.allSettled([...this.sessions.values()].filter(s => s.cleanup.status !== 'complete').map(s => this.serial(s, () => this.cleanup(s, true)))); return [...this.sessions.values()].filter(s => s.cleanup.status !== 'complete').map(s => this.result(s)); }
260
302
  }
@@ -1,3 +1,6 @@
1
+ import { DebuggerError, classify, diagnostic, atStage } from './diagnostics.js';
2
+ export { DebuggerError } from './diagnostics.js';
3
+ import { parseWorkerMessage } from './protocol.js';
1
4
  import { AsyncLocalStorage } from 'node:async_hooks';
2
5
  import { execFile, spawn } from 'node:child_process';
3
6
  import { promisify } from 'node:util';
@@ -27,71 +30,60 @@ export function nativeError(error, fallback) {
27
30
  return error;
28
31
  if (debuggerSignal()?.aborted)
29
32
  return new DebuggerError('DEBUGGER_CANCELLED');
30
- if (error?.killed)
33
+ if (typeof error === 'object' && error !== null && 'killed' in error && error.killed)
31
34
  return new DebuggerError('DEBUGGER_TIMEOUT');
32
35
  return new DebuggerError(fallback);
33
36
  }
34
- export class DebuggerError extends Error {
35
- code;
36
- constructor(code) {
37
- super(code);
38
- this.code = code;
39
- }
40
- }
41
- export const workerSource = fileURLToPath(new URL('../../worker/DebuggerWorker.java', import.meta.url));
37
+ export const workerSource = fileURLToPath(new URL('../../src/debugger/java/DebuggerWorker.java', import.meta.url));
42
38
  let compiled;
43
39
  export async function prepareWorker() {
40
+ let stage = 'prerequisite';
44
41
  compiled ??= (async () => {
45
- const home = await detectJavaHome({ quiet: true });
42
+ const home = await detectJavaHome({ quiet: true }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', 'unavailable'); });
46
43
  const java = home ? path.join(home, 'bin', 'java') : 'java';
47
44
  const javac = home ? path.join(home, 'bin', 'javac') : 'javac';
48
- const version = await exec(java, ['-version'], { timeout: debuggerRemaining(5000), signal: debuggerSignal() });
45
+ const version = await exec(java, ['-version'], { timeout: debuggerRemaining(5000), signal: debuggerSignal() }).catch(e => { throw classify(e, 'prerequisite', 'DEBUGGER_ATTACH_FAILED', debuggerSignal()?.aborted ? 'cancelled' : typeof e === 'object' && e !== null && 'killed' in e && e.killed ? 'timeout' : 'unavailable'); });
49
46
  const major = Number(/version "(\d+)/.exec(version.stderr)?.[1]);
50
47
  if (!(major >= 17))
51
- throw new DebuggerError('DEBUGGER_UNSUPPORTED');
52
- const source = await readFile(workerSource);
48
+ throw new DebuggerError('DEBUGGER_UNSUPPORTED', diagnostic('prerequisite', 'unsupported'));
49
+ stage = 'compilation';
50
+ const source = await readFile(workerSource).catch(e => { throw classify(e, 'compilation', 'DEBUGGER_ATTACH_FAILED', 'unavailable'); });
53
51
  const key = createHash('sha256').update(source).update(version.stderr).digest('hex').slice(0, 16);
54
- const directory = await mkdtemp(path.join(os.tmpdir(), `mobile-debug-jdi-${key}-`));
55
- await chmod(directory, 0o700);
52
+ const directory = await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => mkdtemp(path.join(os.tmpdir(), `mobile-debug-jdi-${key}-`)));
56
53
  try {
54
+ await atStage('compilation', 'DEBUGGER_ATTACH_FAILED', () => chmod(directory, 0o700));
57
55
  await exec(javac, ['--release', '17', '--add-modules', 'jdk.jdi', '-d', directory, workerSource], { timeout: debuggerRemaining(15000), signal: debuggerSignal(), maxBuffer: 1024 * 1024 });
58
56
  }
59
57
  catch (e) {
60
58
  await rm(directory, { recursive: true, force: true });
61
- throw nativeError(e, 'DEBUGGER_UNSUPPORTED');
59
+ throw classify(nativeError(e, 'DEBUGGER_UNSUPPORTED'), 'compilation', 'DEBUGGER_UNSUPPORTED', 'operation_failed');
62
60
  }
63
61
  return { java, directory };
64
- })().catch(error => { compiled = undefined; throw error; });
62
+ })().catch(error => { compiled = undefined; throw classify(error, stage, 'DEBUGGER_ATTACH_FAILED'); });
65
63
  return compiled;
66
64
  }
65
+ function commandStage(command) { return command === 'attach' ? 'attach' : command === 'detach' ? 'cleanup' : 'command'; }
67
66
  export class JdiWorker {
67
+ onEvent;
68
68
  child;
69
69
  pending = new Map();
70
70
  sequence = 0;
71
71
  dead = false;
72
+ failure;
73
+ replied = false;
72
74
  exited;
73
75
  constructor(java, directory, onEvent) {
76
+ this.onEvent = onEvent;
74
77
  this.child = spawn(java, ['-Xmx128m', '--add-modules', 'jdk.jdi', '-cp', directory, 'DebuggerWorker'], { stdio: ['pipe', 'pipe', 'pipe'] });
75
78
  let buffer = '';
76
- const fail = () => {
77
- if (this.dead)
78
- return;
79
- this.dead = true;
80
- for (const request of this.pending.values()) {
81
- clearTimeout(request.timer);
82
- request.reject(new DebuggerError('DEBUGGER_WORKER_FAILED'));
83
- }
84
- this.pending.clear();
85
- onEvent({ event: 'worker_failed' });
86
- };
87
- this.exited = new Promise(resolve => { this.child.once('exit', () => { fail(); resolve(); }); this.child.once('error', () => { fail(); resolve(); }); });
88
- this.child.stdin.on('error', fail);
79
+ this.exited = new Promise(resolve => { this.child.once('exit', () => { this.fail('process_exited'); resolve(); }); this.child.once('error', () => { this.fail('spawn_failed'); resolve(); }); });
80
+ this.child.stdin.on('error', () => this.fail('write_failed'));
89
81
  this.child.stderr.resume(); // Never copy worker data or VM diagnostics into MCP stdout/logs.
90
82
  this.child.stdout.setEncoding('utf8');
91
83
  this.child.stdout.on('data', (chunk) => {
92
84
  buffer += chunk;
93
85
  if (Buffer.byteLength(buffer) > 2 * 1024 * 1024) {
94
- fail();
86
+ this.fail('message_limit');
95
87
  this.child.kill('SIGKILL');
96
88
  return;
97
89
  }
@@ -102,37 +94,51 @@ export class JdiWorker {
102
94
  try {
103
95
  if (Buffer.byteLength(line) > 1024 * 1024)
104
96
  throw new Error('IPC_LIMIT');
105
- const msg = JSON.parse(line);
106
- if (msg.event)
107
- onEvent(msg);
97
+ const msg = parseWorkerMessage(JSON.parse(line));
98
+ if (msg.kind === 'event')
99
+ onEvent(msg.value);
108
100
  else {
101
+ this.replied = true;
109
102
  const request = this.pending.get(msg.id);
110
103
  if (!request)
111
104
  continue;
112
105
  clearTimeout(request.timer);
113
106
  this.pending.delete(msg.id);
114
107
  if (msg.error)
115
- request.reject(new DebuggerError(msg.error.code ?? 'DEBUGGER_WORKER_FAILED'));
108
+ request.reject(classify(new DebuggerError(msg.error.code ?? 'DEBUGGER_WORKER_FAILED', msg.error.diagnostic), commandStage(request.command), 'DEBUGGER_WORKER_FAILED'));
116
109
  else
117
110
  request.resolve(msg.result);
118
111
  }
119
112
  }
120
113
  catch {
121
- fail();
114
+ this.fail(Buffer.byteLength(line) > 1024 * 1024 ? 'message_limit' : 'invalid_message');
122
115
  this.child.kill('SIGKILL');
123
116
  return;
124
117
  }
125
118
  }
126
119
  });
127
120
  }
121
+ fail(reason) {
122
+ if (this.dead)
123
+ return;
124
+ this.dead = true;
125
+ const stage = reason === 'spawn_failed' || (reason === 'process_exited' && !this.replied) ? 'worker_start' : 'ipc';
126
+ this.failure = new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic(stage, reason));
127
+ for (const request of this.pending.values()) {
128
+ clearTimeout(request.timer);
129
+ request.reject(this.failure);
130
+ }
131
+ this.pending.clear();
132
+ this.onEvent({ event: 'worker_failed', diagnostic: this.failure.diagnostic });
133
+ }
128
134
  call(command, args = {}, timeout = 10000) {
129
135
  if (this.dead)
130
- return Promise.reject(new DebuggerError('DEBUGGER_WORKER_FAILED'));
136
+ return Promise.reject(this.failure ?? new DebuggerError('DEBUGGER_WORKER_FAILED', diagnostic('ipc', 'unknown')));
131
137
  try {
132
138
  timeout = Math.min(timeout, debuggerRemaining(timeout));
133
139
  }
134
140
  catch (e) {
135
- return Promise.reject(e);
141
+ return Promise.reject(classify(e, commandStage(command), 'DEBUGGER_WORKER_FAILED'));
136
142
  }
137
143
  const id = ++this.sequence;
138
144
  const message = JSON.stringify({ ...args, command, id }) + '\n';
@@ -140,12 +146,17 @@ export class JdiWorker {
140
146
  return Promise.reject(new DebuggerError('INVALID_ARGUMENT'));
141
147
  return new Promise((resolve, reject) => {
142
148
  const signal = debuggerSignal();
143
- const abort = () => { this.pending.delete(id); clearTimeout(timer); reject(new DebuggerError('DEBUGGER_CANCELLED')); };
149
+ const abort = () => { this.pending.delete(id); clearTimeout(timer); done(); reject(new DebuggerError('DEBUGGER_CANCELLED', diagnostic(commandStage(command), 'cancelled'))); };
144
150
  const done = () => signal?.removeEventListener('abort', abort);
145
- const timer = setTimeout(() => { this.pending.delete(id); done(); reject(new DebuggerError('DEBUGGER_TIMEOUT')); }, timeout);
146
- this.pending.set(id, { resolve: value => { done(); resolve(value); }, reject: error => { done(); reject(error); }, timer });
151
+ const timer = setTimeout(() => { this.pending.delete(id); done(); reject(new DebuggerError('DEBUGGER_TIMEOUT', diagnostic(commandStage(command), 'timeout'))); }, timeout);
152
+ this.pending.set(id, { command, resolve: value => { done(); resolve(value); }, reject: error => { done(); reject(error); }, timer });
147
153
  signal?.addEventListener('abort', abort, { once: true });
148
- this.child.stdin.write(message);
154
+ try {
155
+ this.child.stdin.write(message);
156
+ }
157
+ catch {
158
+ this.fail('write_failed');
159
+ }
149
160
  });
150
161
  }
151
162
  async close() {
@@ -0,0 +1,41 @@
1
+ import { systemClock } from '../../runtime/clock.js';
2
+ import { createTraceStep, buildActionExecutionResult } from '../../runtime/action-results.js';
3
+ import { completeAdjustment } from './adjustment-result.js';
4
+ export async function createAdjustment(input, deps) {
5
+ const platform = input.platform || 'android';
6
+ const tolerance = typeof input.tolerance === 'number' && Number.isFinite(input.tolerance) ? Math.max(0, input.tolerance) : null;
7
+ const limit = input.maxAttempts == null ? 5 : Math.max(1, Math.floor(Number(input.maxAttempts) || 1));
8
+ const stored = input.element_id ? deps.getHandle(input.element_id) ?? null : null;
9
+ const before = await deps.fingerprint(platform, input.deviceId);
10
+ return { clock: deps.clock ?? systemClock, input: { ...input, property: input.property === undefined ? 'value' : input.property }, platform, deviceId: input.deviceId, stored,
11
+ selector: input.selector ?? (input.element_id ? { elementId: input.element_id } : null), tolerance, limit, before,
12
+ fallback: null, trace: [], sequence: 0, observed: null, mode: 'gesture', target: null,
13
+ attempts: 0, cached: null, coordinatePending: false };
14
+ }
15
+ export function recordAdjustment(s, stage, result, metadata) {
16
+ s.trace.push(createTraceStep({ stage, timestamp: s.clock.now(), result, attemptIndex: s.sequence++, metadata }));
17
+ }
18
+ export function failAdjustment(s, code, reason, retryable = false, target = s.target, device = s.device, observed = s.observed, attempts = s.attempts, mode = s.mode, after = null) {
19
+ if (!s.trace.some(step => step.stage === 'resolve'))
20
+ recordAdjustment(s, 'resolve', 'failure', { reason, failure_code: code });
21
+ if (!s.trace.some(step => step.stage === 'recover'))
22
+ recordAdjustment(s, 'recover', retryable ? 'retry' : 'failure', {
23
+ reason, failure_code: code, retry_allowed: retryable, recovery_attempts: attempts, retry_depth: attempts
24
+ });
25
+ const tolerance = s.tolerance ?? 0;
26
+ const base = buildActionExecutionResult({ actionType: 'adjust_control', sourceModule: 'interact', device,
27
+ selector: s.selector, resolved: target, success: false, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
28
+ failure: { failureCode: code, retryable }, traceSteps: s.trace,
29
+ details: { target_value: s.input.targetValue, tolerance, property: s.input.property, attempts,
30
+ adjustment_mode: mode, actual_state: observed, converged: false, within_tolerance: false, reason } });
31
+ return { ...base, target_state: { property: s.input.property, target_value: s.input.targetValue, tolerance },
32
+ actual_state: observed, within_tolerance: false, converged: false, attempts, adjustment_mode: mode };
33
+ }
34
+ export async function succeedAdjustment(s, p, deps, reason = 'control converged to target value', mode = s.mode, device = p.actionDevice ?? s.device) {
35
+ const after = await deps.fingerprint(s.platform, s.deviceId);
36
+ return completeAdjustment({ actionType: 'adjust_control', sourceModule: 'interact', device,
37
+ selector: s.selector, resolved: s.target, success: true, uiFingerprintBefore: s.before, uiFingerprintAfter: after,
38
+ traceSteps: s.trace, details: { target_value: p.normalizedTargetValue, tolerance: p.effectiveTolerance,
39
+ property: s.input.property, attempts: s.attempts, adjustment_mode: mode, actual_state: p.observed,
40
+ converged: true, within_tolerance: true, reason } });
41
+ }