mobile-debug-mcp 0.34.0 → 0.36.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 (500) 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/.cursor-plugin/marketplace.json +1 -1
  11. package/.github/copilot-instructions.md +14 -36
  12. package/.github/workflows/ci.yml +4 -4
  13. package/AGENTS.md +12 -3
  14. package/CONTRIBUTING.md +79 -0
  15. package/README.md +218 -129
  16. package/agent-plugin/.antigravity-plugin/README.md +48 -0
  17. package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
  18. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  19. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  20. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  21. package/agent-plugin/.mcp.json +1 -1
  22. package/agent-plugin/README.md +50 -11
  23. package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
  24. package/agent-plugin/cursor/LICENSE +201 -0
  25. package/agent-plugin/cursor/README.md +9 -0
  26. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
  27. package/agent-plugin/mcp.json +1 -1
  28. package/agent-plugin/mcp_config.json +13 -0
  29. package/agent-plugin/plugin.json +1 -1
  30. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +62 -17
  31. package/dist/contracts/execution-controls.js +24 -0
  32. package/dist/contracts/selector.js +8 -0
  33. package/dist/contracts/target.js +15 -0
  34. package/dist/contracts/ui.js +1 -0
  35. package/dist/debugger/android.js +2 -1
  36. package/dist/debugger/diagnostics.js +54 -0
  37. package/dist/debugger/guard.js +4 -4
  38. package/dist/debugger/ios-worker.js +62 -0
  39. package/dist/debugger/ios.js +57 -0
  40. package/dist/debugger/protocol.js +33 -0
  41. package/dist/debugger/sessions.js +133 -74
  42. package/dist/debugger/worker.js +59 -46
  43. package/dist/interact/adjustment/adjustment-context.js +41 -0
  44. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  45. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  46. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  47. package/dist/interact/adjustment/adjustment-result.js +19 -0
  48. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  49. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  50. package/dist/interact/adjustment/adjustment.js +52 -0
  51. package/dist/interact/adjustment/control-geometry.js +87 -0
  52. package/dist/interact/adjustment/control-values.js +136 -0
  53. package/dist/interact/adjustment/slider-labels.js +84 -0
  54. package/dist/interact/assertions/screen-assertion.js +37 -0
  55. package/dist/interact/assertions/state-assertion-context.js +35 -0
  56. package/dist/interact/assertions/state-assertion.js +76 -0
  57. package/dist/interact/assertions/state-comparison.js +141 -0
  58. package/dist/interact/assertions/visibility-assertion.js +34 -0
  59. package/dist/interact/conditional-action.js +12 -7
  60. package/dist/interact/index.js +77 -3092
  61. package/dist/interact/input-service.js +1 -0
  62. package/dist/interact/interaction-trace.js +21 -0
  63. package/dist/interact/model.js +1 -0
  64. package/dist/interact/observation.js +1 -0
  65. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  66. package/dist/interact/targeting/candidate-result.js +23 -0
  67. package/dist/interact/targeting/candidate-scoring.js +50 -0
  68. package/dist/interact/targeting/element-helpers.js +216 -0
  69. package/dist/interact/targeting/find-element.js +65 -0
  70. package/dist/interact/targeting/nearby-control.js +56 -0
  71. package/dist/interact/targeting/resolved-elements.js +21 -0
  72. package/dist/interact/targeting/tap-target.js +17 -0
  73. package/dist/interact/targeting/target-scope.js +104 -0
  74. package/dist/interact/waits/legacy-log-wait.js +38 -0
  75. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  76. package/dist/interact/waits/legacy-wait-state.js +34 -0
  77. package/dist/interact/waits/legacy-wait.js +48 -0
  78. package/dist/interact/waits/screen-wait.js +49 -0
  79. package/dist/interact/waits/ui-change-helpers.js +157 -0
  80. package/dist/interact/waits/ui-change-state.js +18 -0
  81. package/dist/interact/waits/ui-change-wait.js +77 -0
  82. package/dist/interact/waits/ui-wait-condition.js +56 -0
  83. package/dist/interact/waits/ui-wait-state.js +38 -0
  84. package/dist/interact/waits/ui-wait.js +106 -0
  85. package/dist/manage/android.js +55 -51
  86. package/dist/manage/index.js +38 -75
  87. package/dist/manage/ios/build-types.js +1 -0
  88. package/dist/manage/ios/build.js +210 -0
  89. package/dist/manage/ios/project-discovery.js +29 -0
  90. package/dist/manage/ios/scheme.js +18 -0
  91. package/dist/manage/ios.js +44 -256
  92. package/dist/observe/android/context.js +1 -0
  93. package/dist/observe/android/current-screen.js +55 -0
  94. package/dist/observe/android/fingerprint.js +13 -0
  95. package/dist/observe/android/log-stream.js +132 -0
  96. package/dist/observe/android/logs.js +107 -0
  97. package/dist/observe/android/screenshot.js +70 -0
  98. package/dist/observe/android/ui-tree.js +107 -0
  99. package/dist/observe/android.js +16 -520
  100. package/dist/observe/evidence.js +114 -75
  101. package/dist/observe/index.js +16 -263
  102. package/dist/observe/ios/context.js +16 -0
  103. package/dist/observe/ios/fingerprint.js +12 -0
  104. package/dist/observe/ios/log-stream.js +105 -0
  105. package/dist/observe/ios/logs.js +168 -0
  106. package/dist/observe/ios/screenshot.js +22 -0
  107. package/dist/observe/ios/ui-elements.js +241 -0
  108. package/dist/observe/ios/ui-tree.js +137 -0
  109. package/dist/observe/ios-trace/artifact.js +63 -0
  110. package/dist/observe/ios-trace/capabilities.js +86 -0
  111. package/dist/observe/ios-trace/native.js +93 -0
  112. package/dist/observe/ios-trace/notification.js +35 -0
  113. package/dist/observe/ios-trace/process.js +98 -0
  114. package/dist/observe/ios-trace/sessions.js +244 -0
  115. package/dist/observe/ios.js +17 -715
  116. package/dist/observe/logs/model.js +1 -0
  117. package/dist/{network → observe/network}/index.js +2 -2
  118. package/dist/observe/readiness-service.js +5 -3
  119. package/dist/observe/readiness.js +33 -14
  120. package/dist/observe/screenshot-encoding.js +41 -0
  121. package/dist/observe/snapshot-collection.js +79 -0
  122. package/dist/observe/snapshot-logs.js +30 -0
  123. package/dist/observe/snapshot-metadata.js +43 -31
  124. package/dist/observe/snapshot-semantic.js +115 -0
  125. package/dist/observe/system-trace-error.js +7 -0
  126. package/dist/observe/system-trace-router.js +81 -0
  127. package/dist/observe/system-trace.js +26 -21
  128. package/dist/runtime/action-results.js +270 -0
  129. package/dist/runtime/clock.js +4 -0
  130. package/dist/server/common.js +1 -256
  131. package/dist/server/definitions/assert.js +162 -0
  132. package/dist/server/definitions/diagnostic.js +118 -0
  133. package/dist/server/definitions/input.js +274 -0
  134. package/dist/server/definitions/manage.js +194 -0
  135. package/dist/server/definitions/observe.js +149 -0
  136. package/dist/server/definitions/target.js +138 -0
  137. package/dist/server/definitions/wait.js +105 -0
  138. package/dist/server/handlers/action-execution.js +163 -0
  139. package/dist/server/handlers/correlation.js +23 -0
  140. package/dist/server/handlers/debugger-handlers.js +67 -0
  141. package/dist/server/handlers/debugger-response.js +9 -0
  142. package/dist/server/handlers/diagnostic.js +77 -0
  143. package/dist/server/handlers/handler-types.js +1 -0
  144. package/dist/server/handlers/input.js +154 -0
  145. package/dist/server/handlers/interact.js +113 -0
  146. package/dist/server/handlers/journey-assertions.js +35 -0
  147. package/dist/server/handlers/journey.js +147 -0
  148. package/dist/server/handlers/manage.js +119 -0
  149. package/dist/server/handlers/observe.js +95 -0
  150. package/dist/server/handlers/payload.js +4 -0
  151. package/dist/server/handlers/tool-handlers.js +170 -0
  152. package/dist/server/schemas/condition-schema.js +2 -0
  153. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
  154. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  155. package/dist/server/schemas/system-trace-schema.js +22 -0
  156. package/dist/server/tool-definitions.js +58 -1135
  157. package/dist/server-core.js +2 -2
  158. package/dist/server.js +4 -4
  159. package/dist/system/android-status.js +101 -0
  160. package/dist/system/android.js +37 -27
  161. package/dist/system/index.js +13 -439
  162. package/dist/system/ios-status.js +132 -0
  163. package/dist/system/ios.js +11 -8
  164. package/dist/system/model.js +1 -0
  165. package/dist/system/probes.js +105 -0
  166. package/dist/system/status-summary.js +43 -0
  167. package/dist/system/unsupported-status.js +52 -0
  168. package/dist/utils/android/commands.js +89 -0
  169. package/dist/utils/android/gradle.js +139 -0
  170. package/dist/utils/android/logs.js +103 -0
  171. package/dist/utils/android/ui-tree.js +233 -0
  172. package/dist/utils/android/utils.js +6 -564
  173. package/dist/utils/cli/idb/idb-helper.js +4 -6
  174. package/dist/utils/cli/idb/install-idb.js +13 -9
  175. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  176. package/dist/utils/ios/utils.js +48 -46
  177. package/dist/utils/java.js +65 -49
  178. package/dist/utils/resolve-device.js +4 -2
  179. package/dist/utils/ui/index.js +26 -26
  180. package/docs/CHANGELOG.md +67 -1
  181. package/docs/README.md +38 -0
  182. package/docs/agent-plugin-installation-verification.md +39 -0
  183. package/docs/benchmarks/README.md +18 -0
  184. package/docs/benchmarks/interaction-latency/README.md +14 -0
  185. package/docs/benchmarks/interaction-latency/final.md +4 -1
  186. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  187. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  188. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  189. package/docs/code-structure.md +120 -0
  190. package/docs/specs/001-state-verification/spec.md +16 -14
  191. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  192. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  193. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  194. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  195. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  196. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  197. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  198. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  199. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  200. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  201. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  202. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  203. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  204. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  205. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  206. package/docs/specs/016-environment-tools/spec.md +20 -20
  207. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  208. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  209. package/docs/specs/020-conditional-actions/intent.md +5 -3
  210. package/docs/specs/020-conditional-actions/plan.md +8 -9
  211. package/docs/specs/020-conditional-actions/spec.md +4 -2
  212. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  213. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  214. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  215. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  216. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  217. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  218. package/docs/specs/023-android-system-traces/intent.md +3 -2
  219. package/docs/specs/023-android-system-traces/plan.md +5 -7
  220. package/docs/specs/023-android-system-traces/spec.md +3 -1
  221. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  222. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  223. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  224. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  225. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  226. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  227. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  228. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  229. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  230. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  231. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  232. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  233. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  234. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  235. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  236. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  237. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  238. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  239. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  240. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  241. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  242. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  243. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  244. package/docs/specs/031-ios-system-traces/intent.md +38 -0
  245. package/docs/specs/031-ios-system-traces/investigation.md +92 -0
  246. package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
  247. package/docs/specs/031-ios-system-traces/plan.md +138 -0
  248. package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
  249. package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
  250. package/docs/specs/031-ios-system-traces/spec.md +135 -0
  251. package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
  252. package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
  253. package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
  254. package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
  255. package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
  256. package/docs/tools/TOOLS.md +31 -18
  257. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  258. package/docs/tools/debugger.md +120 -4
  259. package/docs/tools/interact.md +42 -20
  260. package/docs/tools/manage.md +25 -44
  261. package/docs/tools/observe.md +55 -23
  262. package/docs/tools/system-traces.md +89 -14
  263. package/docs/tools/system.md +12 -5
  264. package/package.json +12 -8
  265. package/scripts/agent-plugin.mjs +55 -5
  266. package/scripts/architecture-graph.mjs +69 -0
  267. package/scripts/check-architecture.mjs +47 -0
  268. package/scripts/code-quality-baseline.json +1 -0
  269. package/scripts/code-quality.mjs +161 -0
  270. package/src/contracts/execution-controls.ts +26 -0
  271. package/src/contracts/selector.ts +17 -0
  272. package/src/contracts/target.ts +19 -0
  273. package/src/contracts/ui.ts +94 -0
  274. package/src/debugger/android.ts +3 -2
  275. package/src/debugger/diagnostics.ts +43 -0
  276. package/src/debugger/guard.ts +10 -5
  277. package/src/debugger/ios-worker.ts +49 -0
  278. package/src/debugger/ios.ts +45 -0
  279. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  280. package/src/debugger/lldb/breakpoints.py +51 -0
  281. package/src/debugger/lldb/inspection.py +109 -0
  282. package/src/debugger/lldb/worker.py +203 -0
  283. package/src/debugger/protocol.ts +37 -0
  284. package/src/debugger/sessions.ts +326 -155
  285. package/src/debugger/worker.ts +57 -41
  286. package/src/interact/adjustment/adjustment-context.ts +83 -0
  287. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  288. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  289. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  290. package/src/interact/adjustment/adjustment-result.ts +34 -0
  291. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  292. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  293. package/src/interact/adjustment/adjustment.ts +49 -0
  294. package/src/interact/adjustment/control-geometry.ts +120 -0
  295. package/src/interact/adjustment/control-values.ts +141 -0
  296. package/src/interact/adjustment/slider-labels.ts +84 -0
  297. package/src/interact/assertions/screen-assertion.ts +39 -0
  298. package/src/interact/assertions/state-assertion-context.ts +48 -0
  299. package/src/interact/assertions/state-assertion.ts +64 -0
  300. package/src/interact/assertions/state-comparison.ts +168 -0
  301. package/src/interact/assertions/visibility-assertion.ts +41 -0
  302. package/src/interact/conditional-action.ts +18 -12
  303. package/src/interact/index.ts +89 -3494
  304. package/src/interact/input-service.ts +11 -0
  305. package/src/interact/interaction-trace.ts +36 -0
  306. package/src/interact/model.ts +109 -0
  307. package/src/interact/observation.ts +7 -0
  308. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  309. package/src/interact/targeting/candidate-result.ts +24 -0
  310. package/src/interact/targeting/candidate-scoring.ts +42 -0
  311. package/src/interact/targeting/element-helpers.ts +243 -0
  312. package/src/interact/targeting/find-element.ts +47 -0
  313. package/src/interact/targeting/nearby-control.ts +46 -0
  314. package/src/interact/targeting/resolved-elements.ts +20 -0
  315. package/src/interact/targeting/tap-target.ts +22 -0
  316. package/src/interact/targeting/target-scope.ts +121 -0
  317. package/src/interact/waits/legacy-log-wait.ts +30 -0
  318. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  319. package/src/interact/waits/legacy-wait-state.ts +48 -0
  320. package/src/interact/waits/legacy-wait.ts +34 -0
  321. package/src/interact/waits/screen-wait.ts +37 -0
  322. package/src/interact/waits/ui-change-helpers.ts +184 -0
  323. package/src/interact/waits/ui-change-state.ts +26 -0
  324. package/src/interact/waits/ui-change-wait.ts +59 -0
  325. package/src/interact/waits/ui-wait-condition.ts +52 -0
  326. package/src/interact/waits/ui-wait-state.ts +55 -0
  327. package/src/interact/waits/ui-wait.ts +84 -0
  328. package/src/manage/android.ts +56 -50
  329. package/src/manage/index.ts +63 -93
  330. package/src/manage/ios/build-types.ts +14 -0
  331. package/src/manage/ios/build.ts +230 -0
  332. package/src/manage/ios/project-discovery.ts +26 -0
  333. package/src/manage/ios/scheme.ts +16 -0
  334. package/src/manage/ios.ts +67 -298
  335. package/src/observe/android/context.ts +1 -0
  336. package/src/observe/android/current-screen.ts +63 -0
  337. package/src/observe/android/fingerprint.ts +13 -0
  338. package/src/observe/android/log-stream.ts +127 -0
  339. package/src/observe/android/logs.ts +110 -0
  340. package/src/observe/android/screenshot.ts +79 -0
  341. package/src/observe/android/ui-tree.ts +113 -0
  342. package/src/observe/android.ts +17 -531
  343. package/src/observe/evidence.ts +122 -70
  344. package/src/observe/index.ts +24 -286
  345. package/src/observe/ios/context.ts +22 -0
  346. package/src/observe/ios/fingerprint.ts +12 -0
  347. package/src/observe/ios/log-stream.ts +103 -0
  348. package/src/observe/ios/logs.ts +179 -0
  349. package/src/observe/ios/screenshot.ts +26 -0
  350. package/src/observe/ios/ui-elements.ts +266 -0
  351. package/src/observe/ios/ui-tree.ts +143 -0
  352. package/src/observe/ios-trace/artifact.ts +55 -0
  353. package/src/observe/ios-trace/capabilities.ts +74 -0
  354. package/src/observe/ios-trace/native.ts +72 -0
  355. package/src/observe/ios-trace/notification.ts +30 -0
  356. package/src/observe/ios-trace/process.ts +82 -0
  357. package/src/observe/ios-trace/sessions.ts +204 -0
  358. package/src/observe/ios.ts +18 -749
  359. package/src/observe/logs/model.ts +11 -0
  360. package/src/{network → observe/network}/index.ts +2 -2
  361. package/src/observe/readiness-service.ts +6 -4
  362. package/src/observe/readiness.ts +42 -21
  363. package/src/observe/screenshot-encoding.ts +32 -0
  364. package/src/observe/snapshot-collection.ts +69 -0
  365. package/src/observe/snapshot-logs.ts +26 -0
  366. package/src/observe/snapshot-metadata.ts +51 -31
  367. package/src/observe/snapshot-semantic.ts +153 -0
  368. package/src/observe/system-trace-error.ts +3 -0
  369. package/src/observe/system-trace-router.ts +61 -0
  370. package/src/observe/system-trace.ts +28 -19
  371. package/src/runtime/action-results.ts +376 -0
  372. package/src/runtime/clock.ts +9 -0
  373. package/src/server/common.ts +9 -359
  374. package/src/server/definitions/assert.ts +164 -0
  375. package/src/server/definitions/diagnostic.ts +120 -0
  376. package/src/server/definitions/input.ts +276 -0
  377. package/src/server/definitions/manage.ts +196 -0
  378. package/src/server/definitions/observe.ts +151 -0
  379. package/src/server/definitions/target.ts +140 -0
  380. package/src/server/definitions/wait.ts +107 -0
  381. package/src/server/handlers/action-execution.ts +164 -0
  382. package/src/server/handlers/correlation.ts +20 -0
  383. package/src/server/handlers/debugger-handlers.ts +58 -0
  384. package/src/server/handlers/debugger-response.ts +7 -0
  385. package/src/server/handlers/diagnostic.ts +86 -0
  386. package/src/server/handlers/handler-types.ts +7 -0
  387. package/src/server/handlers/input.ts +173 -0
  388. package/src/server/handlers/interact.ts +135 -0
  389. package/src/server/handlers/journey-assertions.ts +39 -0
  390. package/src/server/handlers/journey.ts +132 -0
  391. package/src/server/handlers/manage.ts +141 -0
  392. package/src/server/handlers/observe.ts +116 -0
  393. package/src/server/handlers/payload.ts +36 -0
  394. package/src/server/handlers/tool-handlers.ts +170 -0
  395. package/src/server/schemas/condition-schema.ts +2 -0
  396. package/src/server/schemas/debugger-schema.ts +46 -0
  397. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  398. package/src/server/schemas/system-trace-schema.ts +23 -0
  399. package/src/server/tool-definitions.ts +67 -1143
  400. package/src/server-core.ts +2 -2
  401. package/src/server.ts +4 -4
  402. package/src/system/android-status.ts +124 -0
  403. package/src/system/android.ts +33 -21
  404. package/src/system/index.ts +21 -525
  405. package/src/system/ios-status.ts +147 -0
  406. package/src/system/ios.ts +14 -10
  407. package/src/system/model.ts +41 -0
  408. package/src/system/probes.ts +114 -0
  409. package/src/system/status-summary.ts +47 -0
  410. package/src/system/unsupported-status.ts +55 -0
  411. package/src/types.ts +16 -106
  412. package/src/utils/android/commands.ts +81 -0
  413. package/src/utils/android/gradle.ts +132 -0
  414. package/src/utils/android/logs.ts +103 -0
  415. package/src/utils/android/ui-tree.ts +239 -0
  416. package/src/utils/android/utils.ts +16 -556
  417. package/src/utils/cli/idb/check-idb.ts +4 -4
  418. package/src/utils/cli/idb/idb-helper.ts +6 -8
  419. package/src/utils/cli/idb/install-idb.ts +15 -9
  420. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  421. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  422. package/src/utils/ios/utils.ts +60 -61
  423. package/src/utils/java.ts +62 -43
  424. package/src/utils/resolve-device.ts +9 -6
  425. package/src/utils/ui/index.ts +28 -28
  426. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  427. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  428. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  429. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  430. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  431. package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
  432. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  433. package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
  434. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  435. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  436. package/test/fixtures/contracts/tool-definitions.json +3696 -0
  437. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  438. package/test/fixtures/coroutine-app/README.md +48 -0
  439. package/test/fixtures/coroutine-app/build.ts +35 -0
  440. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  441. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  442. package/test/fixtures/debugger-app/README.md +52 -0
  443. package/test/fixtures/debugger-app/build.ts +25 -15
  444. package/test/fixtures/ios-debugger-app/Main.swift +95 -0
  445. package/test/fixtures/ios-debugger-app/README.md +27 -0
  446. package/test/fixtures/ios-debugger-app/build.ts +21 -0
  447. package/test/fixtures/latency-journey-app/README.md +2 -0
  448. package/test/fixtures/readiness-app/README.md +2 -0
  449. package/test/helpers/ios-trace-shutdown.ts +29 -0
  450. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  451. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  452. package/test/unit/debugger/diagnostics.test.ts +80 -0
  453. package/test/unit/debugger/guards.test.ts +4 -0
  454. package/test/unit/debugger/handle-guards.test.ts +88 -0
  455. package/test/unit/debugger/ios-preparation.test.ts +57 -0
  456. package/test/unit/debugger/ios.test.ts +104 -0
  457. package/test/unit/debugger/lldb-worker.py +166 -0
  458. package/test/unit/debugger/lldb-worker.test.ts +10 -0
  459. package/test/unit/debugger/preparation.test.ts +32 -0
  460. package/test/unit/debugger/protocol.test.ts +15 -0
  461. package/test/unit/debugger/sessions.test.ts +1 -1
  462. package/test/unit/interact/adjustment_service.test.ts +42 -0
  463. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  464. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  465. package/test/unit/interact/element_helpers.test.ts +19 -0
  466. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  467. package/test/unit/interact/state_comparison.test.ts +20 -0
  468. package/test/unit/interact/target_scope.test.ts +35 -0
  469. package/test/unit/network/get_network_activity.test.ts +1 -1
  470. package/test/unit/observe/ios-trace-native.test.ts +90 -0
  471. package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
  472. package/test/unit/observe/readiness.test.ts +1 -1
  473. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  474. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  475. package/test/unit/observe/system-trace-router.test.ts +49 -0
  476. package/test/unit/quality/agent-plugin-export.test.ts +38 -0
  477. package/test/unit/quality/architecture.test.ts +60 -0
  478. package/test/unit/quality/code-quality.test.ts +58 -0
  479. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  480. package/test/unit/server/conditional_actions.test.ts +1 -1
  481. package/test/unit/server/conditional_schema.test.ts +2 -2
  482. package/test/unit/server/debugger-response.test.ts +63 -0
  483. package/test/unit/server/system-trace.test.ts +12 -3
  484. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  485. package/dist/server/debugger-handlers.js +0 -38
  486. package/dist/server/system-trace-schema.js +0 -17
  487. package/dist/server/tool-handlers.js +0 -1000
  488. package/docs/ROADMAP.md +0 -646
  489. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  490. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  491. package/docs/specs/baseline-spec-v0.md +0 -312
  492. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  493. package/src/server/debugger-handlers.ts +0 -29
  494. package/src/server/debugger-schema.ts +0 -37
  495. package/src/server/system-trace-schema.ts +0 -17
  496. package/src/server/tool-handlers.ts +0 -1008
  497. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  498. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  499. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  500. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -1,10 +1,13 @@
1
+ import { atStage, classify, diagnostic } from './diagnostics.js';
1
2
  import { registerDebuggerPauseProvider } from './guard.js';
3
+ import { IOSDebugNative } from './ios.js';
4
+ import { createIOSWorker, prepareIOSWorker, iosCapabilities, iosLimitations } from './ios-worker.js';
2
5
  import { randomUUID } from 'node:crypto';
3
6
  import { AndroidDebugNative } from './android.js';
4
7
  import { createWorker, prepareWorker, DebuggerError, withDebuggerBudget, debuggerRemaining } from './worker.js';
5
8
  export const debuggerCapabilities = { attach: true, breakpoints: true, stacks: true, locals: true, step: true, resume: true,
6
9
  evaluation: false, mutation: false, coroutines: false, native: false };
7
- const limitations = ['JAVA_KOTLIN_BYTECODE_ONLY', 'DEBUG_METADATA_REQUIRED', 'SHALLOW_LOCALS', 'ALL_THREADS_SUSPEND', 'DEVICE_WIDE_UI_GUARD'];
10
+ export const androidLimitations = ['JAVA_KOTLIN_BYTECODE_ONLY', 'DEBUG_METADATA_REQUIRED', 'SHALLOW_LOCALS', 'ALL_THREADS_SUSPEND', 'DEVICE_WIDE_UI_GUARD'];
8
11
  export function debuggerExpiry(created, activity, pausedAt, now) {
9
12
  return pausedAt !== undefined && now - pausedAt >= 60000 ? 'pause_limit' : now - activity >= 300000 ? 'idle_limit' : now - created >= 1800000 ? 'session_limit' : null;
10
13
  }
@@ -12,13 +15,17 @@ export class DebuggerSessions {
12
15
  native;
13
16
  workerFactory;
14
17
  prerequisite;
18
+ options;
15
19
  sessions = new Map();
16
20
  closing = false;
17
- constructor(native = new AndroidDebugNative(), workerFactory = createWorker, prerequisite = prepareWorker) {
21
+ constructor(native = new AndroidDebugNative(), workerFactory = createWorker, prerequisite = prepareWorker, options = {}) {
18
22
  this.native = native;
19
23
  this.workerFactory = workerFactory;
20
24
  this.prerequisite = prerequisite;
25
+ this.options = options;
21
26
  }
27
+ get platform() { return this.options.platform ?? 'android'; }
28
+ get workerResource() { return this.platform === 'ios' ? 'lldb_worker' : 'jdi_worker'; }
22
29
  prune() {
23
30
  const terminal = [...this.sessions.values()].filter(s => s.terminalAt !== undefined && s.cleanup.status === 'complete');
24
31
  for (const s of terminal)
@@ -28,21 +35,30 @@ export class DebuggerSessions {
28
35
  for (const s of remaining.slice(0, Math.max(0, remaining.length - 100)))
29
36
  this.sessions.delete(s.id);
30
37
  }
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; }
38
+ get(id) {
39
+ this.prune();
40
+ const s = this.sessions.get(id);
41
+ if (!s)
42
+ throw new DebuggerError('DEBUGGER_NOT_FOUND');
43
+ s.activity = Date.now();
44
+ return s;
45
+ }
33
46
  serial(s, fn) { const out = s.queue.then(fn); s.queue = out.catch(() => { }); return out; }
34
47
  result(s, payload = {}) {
35
48
  const success = payload.success ?? (!['failed', 'disconnected'].includes(s.state) && s.cleanup.status !== 'failed');
36
49
  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,
38
- device: { platform: 'android', id: s.device }, target: s.target, state: s.state, captured_at_ms: Date.now(),
39
- capabilities: debuggerCapabilities, limitations, cleanup: s.cleanup, reason: s.reason, stop: s.stop, ...payload });
50
+ 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,
51
+ device: { platform: this.platform, id: s.device }, target: s.target, state: s.state, captured_at_ms: Date.now(),
52
+ capabilities: this.options.capabilities ?? debuggerCapabilities, limitations: this.options.limitations ?? androidLimitations, cleanup: s.cleanup, reason: s.reason, stop: s.stop, ...payload });
40
53
  }
54
+ owns(id) { return this.sessions.has(id); }
41
55
  summary(id) { return this.result(this.get(id)); }
42
56
  hasActiveSessions() { return [...this.sessions.values()].some(s => s.cleanup.status !== 'complete'); }
43
57
  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'); }
58
+ assertRunning(device) {
59
+ if (this.paused(device))
60
+ throw new DebuggerError('DEBUGGER_PAUSED');
61
+ }
46
62
  event(s, event) {
47
63
  if (s.cleanup.status === 'complete')
48
64
  return;
@@ -52,6 +68,8 @@ export class DebuggerSessions {
52
68
  s.pausedAt ??= Date.now();
53
69
  }
54
70
  if (event.event === 'terminated' || event.event === 'worker_failed') {
71
+ if (!s.cleaning)
72
+ s.diagnostic ??= event.diagnostic ?? diagnostic(event.event === 'worker_failed' ? 'ipc' : 'transport', event.reason === 'transport_lost' ? 'transport_lost' : 'unknown');
55
73
  if (!s.cleaning && s.state !== 'failed')
56
74
  s.reason = event.reason ?? 'worker_failed';
57
75
  if (!s.cleaning) {
@@ -66,33 +84,38 @@ export class DebuggerSessions {
66
84
  s.events.shift();
67
85
  }
68
86
  async capabilities(device, app, pid) {
69
- const prerequisites = { jdk: false, device: false, process: false };
87
+ const prerequisites = { [this.platform === 'ios' ? 'lldb' : 'jdk']: false, device: false, process: false };
70
88
  let candidates = [];
71
89
  const reasons = [];
90
+ const diagnostics = [];
72
91
  try {
73
- await this.prerequisite();
74
- prerequisites.jdk = true;
92
+ await atStage('prerequisite', 'DEBUGGER_UNSUPPORTED', () => this.prerequisite());
93
+ prerequisites[this.platform === 'ios' ? 'lldb' : 'jdk'] = true;
75
94
  }
76
95
  catch (e) {
77
96
  if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code))
78
97
  throw e;
79
98
  reasons.push('DEBUGGER_UNSUPPORTED');
99
+ diagnostics.push(classify(e, 'prerequisite', 'DEBUGGER_UNSUPPORTED').diagnostic ?? diagnostic('prerequisite', 'unknown'));
80
100
  }
81
101
  try {
82
- candidates = await this.native.candidates(device, app);
102
+ candidates = await atStage('target_resolution', 'DEVICE_UNAVAILABLE', () => this.native.candidates(device, app));
83
103
  prerequisites.device = true;
84
104
  prerequisites.process = candidates.some(t => pid === undefined || pid === t.pid);
85
- if (!prerequisites.process)
105
+ if (!prerequisites.process) {
86
106
  reasons.push('PROCESS_NOT_FOUND');
107
+ diagnostics.push(diagnostic('target_resolution', 'unavailable'));
108
+ }
87
109
  }
88
110
  catch (e) {
89
111
  if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED'].includes(e.code))
90
112
  throw e;
91
113
  reasons.push(e instanceof DebuggerError ? e.code : 'DEVICE_UNAVAILABLE');
114
+ diagnostics.push(classify(e, 'target_resolution', 'DEVICE_UNAVAILABLE').diagnostic ?? diagnostic('target_resolution', 'unknown'));
92
115
  }
93
- return { success: true, debug_session_id: null, device: { platform: 'android', id: device }, target: null, state: null,
116
+ return { success: true, debug_session_id: null, device: { platform: this.platform, id: device }, target: null, state: null,
94
117
  captured_at_ms: Date.now(), supported: Object.values(prerequisites).every(Boolean), prerequisites, candidates,
95
- capabilities: debuggerCapabilities, reasons, limitations, cleanup: { status: 'complete', remaining_resources: [] } };
118
+ ...(diagnostics.length ? { diagnostics } : {}), capabilities: this.options.capabilities ?? debuggerCapabilities, reasons, limitations: this.options.limitations ?? androidLimitations, cleanup: { status: 'complete', remaining_resources: [] } };
96
119
  }
97
120
  async attach(device, app, pid, timeout = 10000) {
98
121
  this.prune();
@@ -103,27 +126,47 @@ export class DebuggerSessions {
103
126
  queue: Promise.resolve(), events: [], sequence: 0, cleanup: { status: 'pending', remaining_resources: [] }, created: Date.now(), activity: Date.now() };
104
127
  this.sessions.set(s.id, s);
105
128
  return this.serial(s, async () => {
129
+ let stage = 'prerequisite';
106
130
  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; };
131
+ const remaining = () => {
132
+ try {
133
+ debuggerRemaining(timeout);
134
+ const ms = deadline - Date.now();
135
+ if (ms <= 0)
136
+ throw new DebuggerError('DEBUGGER_TIMEOUT');
137
+ return ms;
138
+ }
139
+ catch (e) {
140
+ throw classify(e, stage, 'DEBUGGER_ATTACH_FAILED');
141
+ }
142
+ };
109
143
  try {
110
- await this.prerequisite();
144
+ await atStage('prerequisite', 'DEBUGGER_ATTACH_FAILED', () => this.prerequisite());
111
145
  remaining();
112
- const candidates = (await this.native.candidates(device, app)).filter(t => pid === undefined || t.pid === pid);
146
+ stage = 'target_resolution';
147
+ const candidates = (await atStage('target_resolution', 'DEBUGGER_ATTACH_FAILED', () => this.native.candidates(device, app))).filter(t => pid === undefined || t.pid === pid);
113
148
  remaining();
114
149
  if (!candidates.length)
115
- throw new DebuggerError('PROCESS_NOT_FOUND');
150
+ throw new DebuggerError('PROCESS_NOT_FOUND', diagnostic('target_resolution', 'unavailable'));
116
151
  if (candidates.length > 1)
117
152
  throw new DebuggerError('PROCESS_AMBIGUOUS');
118
153
  s.target = candidates[0];
119
- s.port = await this.native.forward(device, s.target.pid);
120
- remaining();
121
- s.cleanup.remaining_resources.push(`forward:tcp:${s.port}`);
122
- s.worker = await this.workerFactory(event => this.event(s, event));
154
+ if (this.platform === 'android') {
155
+ stage = 'forwarding';
156
+ s.port = await atStage('forwarding', 'DEBUGGER_ATTACH_FAILED', () => this.native.forward(device, s.target.pid));
157
+ remaining();
158
+ s.cleanup.remaining_resources.push(`forward:tcp:${s.port}`);
159
+ }
160
+ stage = 'worker_start';
161
+ s.worker = await atStage('worker_start', 'DEBUGGER_ATTACH_FAILED', () => this.workerFactory(event => this.event(s, event)));
123
162
  remaining();
124
- 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);
163
+ s.cleanup.remaining_resources.push(this.workerResource);
164
+ if (this.platform === 'ios')
165
+ await atStage('target_resolution', 'DEBUGGER_ATTACH_FAILED', () => this.native.identity(device, s.target));
166
+ stage = 'attach';
167
+ await atStage('attach', 'DEBUGGER_ATTACH_FAILED', () => s.worker.call('attach', { port: s.port, ...(this.platform === 'ios' ? { pid: s.target.pid, executablePath: s.target.executable_path } : {}), timeoutMs: remaining() }, remaining()));
168
+ stage = 'target_resolution';
169
+ await atStage('target_resolution', 'DEBUGGER_ATTACH_FAILED', () => this.native.identity(device, s.target));
127
170
  remaining();
128
171
  if (s.state === 'attaching')
129
172
  s.state = 'running';
@@ -140,10 +183,11 @@ export class DebuggerSessions {
140
183
  s.timer.unref();
141
184
  }
142
185
  catch (e) {
186
+ s.diagnostic ??= classify(e, stage, 'DEBUGGER_ATTACH_FAILED').diagnostic;
143
187
  s.state = 'failed';
144
188
  s.reason = e instanceof DebuggerError ? e.code : 'DEBUGGER_ATTACH_FAILED';
145
189
  await this.cleanup(s, true);
146
- return this.result(s, { success: false, error: { code: s.reason, message: s.reason, retryable: false } });
190
+ return this.result(s, { success: false, error: { code: s.reason, message: s.reason, retryable: false, diagnostic: s.diagnostic } });
147
191
  }
148
192
  return this.result(s);
149
193
  });
@@ -171,7 +215,7 @@ export class DebuggerSessions {
171
215
  s.worker = undefined;
172
216
  }
173
217
  catch {
174
- remaining.push('jdi_worker');
218
+ remaining.push(this.workerResource);
175
219
  }
176
220
  }
177
221
  if (s.port !== undefined && s.target) {
@@ -203,60 +247,75 @@ export class DebuggerSessions {
203
247
  if (after > s.sequence)
204
248
  throw new DebuggerError('INVALID_ARGUMENT');
205
249
  if (args.signal?.aborted)
206
- throw new DebuggerError('DEBUGGER_CANCELLED');
250
+ throw new DebuggerError('DEBUGGER_CANCELLED', diagnostic('command', 'cancelled'));
207
251
  if (s.worker && ['running', 'paused'].includes(s.state))
208
- await s.worker.call('heartbeat', {}, Math.min(timeout, 1000));
252
+ await atStage('command', 'DEBUGGER_WORKER_FAILED', () => s.worker.call('heartbeat', {}, Math.min(timeout, 1000)));
209
253
  while (Date.now() < deadline && !s.events.some(e => e.sequence > after) && s.cleanup.status === 'pending') {
210
254
  if (args.signal?.aborted)
211
- throw new DebuggerError('DEBUGGER_CANCELLED');
255
+ throw new DebuggerError('DEBUGGER_CANCELLED', diagnostic('command', 'cancelled'));
212
256
  await new Promise(resolve => setTimeout(resolve, Math.min(50, deadline - Date.now())));
213
257
  }
214
258
  const events = s.events.filter(e => e.sequence > after).slice(0, 100);
215
259
  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
260
  }
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)))
261
+ return this.serial(s, () => this.executeCommand(s, command, args, timeout));
262
+ }
263
+ assertCommandTarget(s, command, args) {
264
+ if (!s.worker || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state))
265
+ throw new DebuggerError('DEBUGGER_DISCONNECTED', s.diagnostic ?? diagnostic('command', 'unavailable'));
266
+ if (command === 'set' && (this.platform === 'ios' ? typeof args.sourceFile !== 'string' || args.className !== undefined : typeof args.className !== 'string' || args.sourceFile !== undefined))
267
+ throw new DebuggerError('INVALID_ARGUMENT');
268
+ if (['stack', 'locals', 'step', 'resume'].includes(command)) {
269
+ if (args.stopId !== s.stop?.stop_id)
270
+ throw new DebuggerError('DEBUGGER_STALE_REFERENCE');
271
+ if (s.state !== 'paused')
272
+ throw new DebuggerError('DEBUGGER_NOT_PAUSED');
273
+ }
274
+ }
275
+ async executeCommand(s, command, args, timeout) {
276
+ if (command === 'detach') {
277
+ if (s.cleanup.status !== 'complete')
278
+ await this.cleanup(s, true);
279
+ return this.result(s, { success: s.cleanup.status === 'complete', execution_state: 'unknown' });
280
+ }
281
+ if (command === 'state' && (s.cleanup.status === 'complete' || ['detaching', 'detached', 'disconnected', 'failed'].includes(s.state)))
282
+ return this.result(s);
283
+ this.assertCommandTarget(s, command, args);
284
+ const previous = { state: s.state, stop: s.stop, pausedAt: s.pausedAt };
285
+ if (command === 'step' || command === 'resume') {
286
+ s.state = 'running';
287
+ s.stop = null;
288
+ s.pausedAt = undefined;
289
+ }
290
+ try {
291
+ const data = await s.worker.call(command, args, timeout);
292
+ return this.result(s, command === 'state' ? { breakpoints: data.breakpoints } : { result: data });
293
+ }
294
+ catch (e) {
295
+ if (['step', 'resume'].includes(command) && s.state === 'running' && s.stop === null && e instanceof DebuggerError && ['INVALID_ARGUMENT', 'DEBUGGER_STALE_REFERENCE'].includes(e.code))
296
+ Object.assign(s, previous);
297
+ return this.handleCommandError(s, command, classify(e, 'command', 'DEBUGGER_WORKER_FAILED'));
298
+ }
299
+ }
300
+ async handleCommandError(s, command, e) {
301
+ if (e instanceof DebuggerError && e.code === 'DEBUGGER_DISCONNECTED') {
302
+ s.diagnostic ??= e.diagnostic ?? diagnostic('transport', 'transport_lost');
303
+ s.state = 'disconnected';
304
+ s.reason = 'transport_lost';
305
+ await this.cleanup(s, false);
306
+ if (command === 'state')
224
307
  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
- });
308
+ }
309
+ if ((command === 'step' || command === 'resume' || command === 'set' || command === 'remove') && e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED', 'DEBUGGER_WORKER_FAILED'].includes(e.code)) {
310
+ s.diagnostic ??= e.diagnostic;
311
+ s.state = 'failed';
312
+ s.reason = e.code;
313
+ await this.cleanup(s, true);
314
+ }
315
+ throw e;
258
316
  }
259
317
  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
318
  }
261
319
  export const debuggerSessions = new DebuggerSessions();
262
- registerDebuggerPauseProvider(device => debuggerSessions.paused(device));
320
+ export const iosDebuggerSessions = new DebuggerSessions(new IOSDebugNative(), createIOSWorker, prepareIOSWorker, { platform: 'ios', capabilities: iosCapabilities, limitations: iosLimitations });
321
+ registerDebuggerPauseProvider((device, platform = 'android') => (platform === 'ios' ? iosDebuggerSessions : debuggerSessions).paused(device));
@@ -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
  }
67
- export class JdiWorker {
65
+ function commandStage(command) { return command === 'attach' ? 'attach' : command === 'detach' ? 'cleanup' : 'command'; }
66
+ export class DebuggerProcess {
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
- constructor(java, directory, onEvent) {
74
- this.child = spawn(java, ['-Xmx128m', '--add-modules', 'jdk.jdi', '-cp', directory, 'DebuggerWorker'], { stdio: ['pipe', 'pipe', 'pipe'] });
75
+ constructor(java, directory, onEvent, launch) {
76
+ this.onEvent = onEvent;
77
+ this.child = spawn(java, launch?.args ?? ['-Xmx128m', '--add-modules', 'jdk.jdi', '-cp', directory, 'DebuggerWorker'], { stdio: ['pipe', 'pipe', 'pipe'], ...(launch ? { env: launch.env } : {}) });
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() {
@@ -161,5 +172,7 @@ export class JdiWorker {
161
172
  }
162
173
  export async function createWorker(onEvent) {
163
174
  const { java, directory } = await prepareWorker();
164
- return new JdiWorker(java, directory, onEvent);
175
+ return new DebuggerProcess(java, directory, onEvent);
165
176
  }
177
+ // Compatibility export for existing JDI transport callers/tests.
178
+ export { DebuggerProcess as JdiWorker };
@@ -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
+ }
@@ -0,0 +1,55 @@
1
+ import { _controlAxis, _buildConservativeControlPoint, _buildControlPoint, _buildAdjustmentProbePoints } from './control-geometry.js';
2
+ import { recordAdjustment, succeedAdjustment, failAdjustment } from './adjustment-context.js';
3
+ import { verifyControl } from './adjustment-semantic.js';
4
+ function points(p) {
5
+ const bounds = p.bounds, range = p.valueRange, value = p.currentValue;
6
+ const axis = _controlAxis(p.element, bounds);
7
+ return { axis, target: _buildConservativeControlPoint(bounds, p.normalizedTargetValue, value, range.min, range.max, axis),
8
+ current: _buildControlPoint(bounds, (value - range.min) / (range.max - range.min), axis) };
9
+ }
10
+ export async function coordinateAdjustment(s, p, deps) {
11
+ if (s.coordinatePending)
12
+ return 'next';
13
+ const geometry = points(p);
14
+ const probes = p.inferredSliderState ? [geometry.target] : _buildAdjustmentProbePoints(p.bounds, p.normalizedTargetValue, p.currentValue, p.valueRange.min, p.valueRange.max, geometry.axis);
15
+ for (let i = 0; i < probes.length && s.attempts < s.limit; i++) {
16
+ const point = probes[i];
17
+ s.mode = 'coordinate';
18
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'coordinate', point });
19
+ const action = await deps.tap({ platform: s.platform, x: point.x, y: point.y, deviceId: s.deviceId });
20
+ s.attempts++;
21
+ p.actionDevice = action.device ?? p.actionDevice;
22
+ if (!action.success) {
23
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts, mode: 'coordinate', point, success: false });
24
+ continue;
25
+ }
26
+ await verifyControl(s, p, deps, 'control did not converge yet');
27
+ if (p.verification.withinTolerance)
28
+ return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value');
29
+ s.coordinatePending = true;
30
+ s.cached = null;
31
+ if (s.attempts < s.limit)
32
+ return 'retry';
33
+ }
34
+ return 'next';
35
+ }
36
+ export async function gestureAdjustment(s, p, deps) {
37
+ if (s.attempts >= s.limit)
38
+ return 'next';
39
+ const geometry = points(p);
40
+ s.coordinatePending = false;
41
+ s.mode = 'gesture';
42
+ recordAdjustment(s, 'execute', 'retry', { attempt: s.attempts + 1, mode: 'gesture', start: geometry.current, end: geometry.target });
43
+ const action = await deps.swipe({ platform: s.platform, x1: geometry.current.x, y1: geometry.current.y,
44
+ x2: geometry.target.x, y2: geometry.target.y, duration: p.inferredSliderState ? 520 : 220, deviceId: s.deviceId });
45
+ s.attempts++;
46
+ if (!action.success) {
47
+ recordAdjustment(s, 'execute', 'failure', { attempt: s.attempts, mode: 'gesture', start: geometry.current, end: geometry.target, success: false });
48
+ return failAdjustment(s, 'UNKNOWN', action.error ?? 'adjustment gesture failed', false, s.target, action.device ?? p.actionDevice, p.observed ?? p.actualState);
49
+ }
50
+ p.actionDevice = action.device ?? p.actionDevice;
51
+ await verifyControl(s, p, deps, 'gesture adjustment did not converge yet');
52
+ if (p.verification.withinTolerance)
53
+ return succeedAdjustment(s, p, deps, p.verification.verification?.reason ?? 'control converged to target value');
54
+ return 'next';
55
+ }