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,5 +1,5 @@
1
1
  import assert from 'node:assert/strict'
2
- import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { copyFileSync, cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import path from 'node:path'
5
5
 
@@ -43,22 +43,53 @@ const cursorManifest = {
43
43
  },
44
44
  },
45
45
  }
46
+ const antigravityManifest = {
47
+ $schema: 'https://antigravity.google/schemas/v1/plugin.json',
48
+ ...sharedMetadata,
49
+ displayName: extensions['com.openai'].interface.displayName,
50
+ logo: 'assets/mobile-debug-tools.svg',
51
+ suggestedPrompts: extensions['com.openai'].interface.defaultPrompt,
52
+ }
53
+ const antigravityMcp = {
54
+ mcpServers: Object.fromEntries(Object.entries(codexMcp.mcpServers).map(([name, server]) => {
55
+ const { type: _transport, ...settings } = server
56
+ return [name, settings]
57
+ })),
58
+ }
59
+ const mcpConfigs = [
60
+ ['.mcp.json', codexMcp],
61
+ ['mcp_config.json', antigravityMcp],
62
+ ]
46
63
  const manifests = [
47
64
  ['.codex-plugin/plugin.json', codexManifest],
48
65
  ['.claude-plugin/plugin.json', claudeManifest],
49
66
  ['.cursor-plugin/plugin.json', cursorManifest],
67
+ ['.antigravity-plugin/plugin.json', antigravityManifest],
50
68
  ]
51
69
 
70
+ const cursorFiles = [
71
+ ['.cursor-plugin/plugin.json', `${JSON.stringify(cursorManifest, null, 2)}\n`],
72
+ ['skills/mobile-debug-mcp/SKILL.md', readFileSync(packagedSkill)],
73
+ ['LICENSE', readFileSync(sourceLicense)],
74
+ ['README.md', '# Mobile Debug Tools for Cursor\n\nGenerated by `npm run sync:agent-plugin`; edit the canonical plugin source.\n\nCopy the contents of this directory into `~/.cursor/plugins/local/mobile-debug-tools`,\nthen run Developer: Reload Window and check Customize for the skill and MCP server.\nDo not copy the portable parent plugin: its `${PLUGIN_DATA}` variable is not expanded by Cursor.\n\nSetup and support: https://github.com/clivejefferies/mobile-debug-tools\n'],
75
+ ]
52
76
  if (process.argv[2] === '--sync') {
53
77
  copyFileSync(sourceLicense, packagedLicense)
54
- writeFileSync(path.join(pluginRoot, '.mcp.json'), `${JSON.stringify(codexMcp, null, 2)}\n`)
78
+ for (const [relativePath, contents] of cursorFiles) {
79
+ const target = path.join(pluginRoot, 'cursor', relativePath)
80
+ mkdirSync(path.dirname(target), { recursive: true })
81
+ writeFileSync(target, contents)
82
+ }
83
+ for (const [relativePath, config] of mcpConfigs) {
84
+ writeFileSync(path.join(pluginRoot, relativePath), `${JSON.stringify(config, null, 2)}\n`)
85
+ }
55
86
  for (const [relativePath, manifest] of manifests) {
56
87
  const manifestPath = path.join(pluginRoot, relativePath)
57
88
  mkdirSync(path.dirname(manifestPath), { recursive: true })
58
89
  writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
59
90
  }
60
91
  }
61
- else if (process.argv[2] !== '--check') throw new Error('Use --sync or --check')
92
+ else if (!['--check', '--export-antigravity'].includes(process.argv[2])) throw new Error('Use --sync, --check or --export-antigravity <new-directory>')
62
93
 
63
94
  assert.equal(plugin.$schema, 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json')
64
95
  assert.equal(plugin.name, 'mobile-debug-tools')
@@ -73,7 +104,10 @@ assert.equal(server.command, 'npx')
73
104
  assert.deepEqual(server.args, ['--yes', `${packageInfo.name}@${packageInfo.version}`, 'server'])
74
105
  assert.equal(server.cwd, '${PLUGIN_DATA}', 'npx must run outside the same-named source checkout')
75
106
  assert.deepEqual(readFileSync(packagedLicense), readFileSync(sourceLicense), 'packaged license must match its source')
76
- assert.deepEqual(JSON.parse(readFileSync(path.join(pluginRoot, '.mcp.json'), 'utf8')), codexMcp, 'Codex MCP configuration must match the portable plugin')
107
+ for (const [relativePath, config] of mcpConfigs) {
108
+ assert.deepEqual(JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')), config, `${relativePath} must match the portable MCP configuration`)
109
+ }
110
+ assert.ok(readFileSync(path.join(pluginRoot, antigravityManifest.logo)).length > 0, 'Antigravity logo must be bundled')
77
111
  for (const [relativePath, expectedManifest] of manifests) {
78
112
  assert.deepEqual(
79
113
  JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')),
@@ -81,6 +115,9 @@ for (const [relativePath, expectedManifest] of manifests) {
81
115
  `${relativePath} must match the portable plugin`,
82
116
  )
83
117
  }
118
+ for (const [relativePath, contents] of cursorFiles) {
119
+ assert.deepEqual(readFileSync(path.join(pluginRoot, 'cursor', relativePath)), Buffer.from(contents), `Cursor native ${relativePath} must match its source`)
120
+ }
84
121
  for (const marketplacePath of [
85
122
  '.agents/plugins/marketplace.json',
86
123
  '.claude-plugin/marketplace.json',
@@ -90,9 +127,22 @@ for (const marketplacePath of [
90
127
  assert.equal(marketplace.name, plugin.name, `${marketplacePath} name must match the plugin`)
91
128
  assert.equal(marketplace.plugins.length, 1, `${marketplacePath} must expose one plugin`)
92
129
  assert.equal(marketplace.plugins[0].name, plugin.name, `${marketplacePath} plugin name must match`)
93
- assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, './agent-plugin', `${marketplacePath} source must point to the plugin`)
130
+ assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, marketplacePath.startsWith('.cursor-plugin') ? './agent-plugin/cursor' : './agent-plugin', `${marketplacePath} source must point to the plugin`)
94
131
  }
95
132
  const skillName = readFileSync(packagedSkill, 'utf8').match(/^---\r?\nname: ([a-z0-9-]+)\r?\n/)?.[1]
96
133
  assert.equal(skillName, path.basename(path.dirname(packagedSkill)), 'packaged skill directory must match its frontmatter name')
97
134
 
98
135
  console.log('Agent plugin package is in sync')
136
+
137
+ if (process.argv[2] === '--export-antigravity') {
138
+ assert.ok(process.argv[3], 'Supply a new output directory for the Antigravity export')
139
+ const target = path.resolve(process.argv[3])
140
+ // Refuse existing directories rather than overwrite source or stale package files.
141
+ mkdirSync(target)
142
+ writeFileSync(path.join(target, 'plugin.json'), `${JSON.stringify(antigravityManifest, null, 2)}\n`)
143
+ writeFileSync(path.join(target, 'mcp_config.json'), `${JSON.stringify(antigravityMcp, null, 2)}\n`)
144
+ copyFileSync(path.join(pluginRoot, '.antigravity-plugin/README.md'), path.join(target, 'README.md'))
145
+ copyFileSync(packagedLicense, path.join(target, 'LICENSE'))
146
+ for (const directory of ['skills', 'assets']) cpSync(path.join(pluginRoot, directory), path.join(target, directory), { recursive: true, filter: file => path.basename(file) !== '.DS_Store' && !path.basename(file).startsWith('._') })
147
+ console.log(`Antigravity package exported to ${target}`)
148
+ }
@@ -0,0 +1,69 @@
1
+ import ts from 'typescript'
2
+ import path from 'node:path'
3
+ import { isBuiltin } from 'node:module'
4
+ import { extractGraph } from 'archunit'
5
+
6
+ // ArchUnitTS 2.5.4 extracts static imports. Add re-exports, import types and
7
+ // literal dynamic imports using the same project's TypeScript resolution.
8
+ export async function architectureGraph(configFile = 'tsconfig.json') {
9
+ const configPath = path.resolve(configFile)
10
+ const root = path.dirname(configPath)
11
+ const config = ts.readConfigFile(configPath, ts.sys.readFile)
12
+ if (config.error) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'))
13
+ const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root)
14
+ if (parsed.errors.length) throw new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, {
15
+ getCanonicalFileName: file => file, getCurrentDirectory: () => root, getNewLine: () => '\n'
16
+ }))
17
+ const graph = await extractGraph(configPath, { clearCache: true })
18
+ const local = file => path.relative(root, file).split(path.sep).join('/')
19
+ const edges = new Map(graph.map(edge => [JSON.stringify([edge.source, edge.target]), edge]))
20
+ const recorded = new Set()
21
+ for (const file of parsed.fileNames) {
22
+ const code = ts.sys.readFile(file)
23
+ if (code === undefined) throw new Error(`Cannot read architecture source: ${file}`)
24
+ const ast = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true)
25
+ const source = local(file)
26
+ function add(specifier, typeOnly = false) {
27
+ const module = specifier.text
28
+ const resolved = ts.resolveModuleName(module, file, parsed.options, ts.sys).resolvedModule
29
+ if (!resolved && !isBuiltin(module)) throw new Error(`Unresolved dependency: ${source} -> ${module}`)
30
+ const external = !resolved || !!resolved.isExternalLibraryImport
31
+ const target = resolved ? local(resolved.resolvedFileName) : module.replace(/^(?!node:)/, 'node:')
32
+ const key = JSON.stringify([source, target])
33
+ const kind = typeOnly ? 'type' : 'value'
34
+ const previous = edges.get(key)
35
+ // Replace the initial static classification, then union repeated dependencies.
36
+ if (previous && recorded.has(key)) {
37
+ previous.importKinds = [...new Set([...previous.importKinds, kind])]
38
+ } else edges.set(key, { source, target, external, importKinds: [kind] })
39
+ recorded.add(key)
40
+ }
41
+ function visit(node) {
42
+ if (ts.isImportDeclaration(node)) {
43
+ const clause = node.importClause
44
+ const bindings = clause?.namedBindings
45
+ const typeOnly = clause?.isTypeOnly || (!clause?.name && bindings && ts.isNamedImports(bindings) &&
46
+ bindings.elements.length > 0 && bindings.elements.every(element => element.isTypeOnly))
47
+ add(node.moduleSpecifier, !!typeOnly)
48
+ } else if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
49
+ const bindings = node.exportClause
50
+ const typeOnly = node.isTypeOnly || (bindings && ts.isNamedExports(bindings) &&
51
+ bindings.elements.length > 0 && bindings.elements.every(element => element.isTypeOnly))
52
+ add(node.moduleSpecifier, !!typeOnly)
53
+ } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
54
+ if (!node.moduleReference.expression) throw new Error(`Missing import target in ${source}`)
55
+ add(node.moduleReference.expression, node.isTypeOnly)
56
+ } else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
57
+ add(node.argument.literal, true)
58
+ } else if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
59
+ (ts.isIdentifier(node.expression) && node.expression.text === 'require'))) {
60
+ const argument = node.arguments[0]
61
+ if (!argument || !ts.isStringLiteralLike(argument)) throw new Error(`Nonliteral dependency in ${source}; architecture cannot resolve it`)
62
+ add(argument)
63
+ }
64
+ ts.forEachChild(node, visit)
65
+ }
66
+ visit(ast)
67
+ }
68
+ return [...edges.values()]
69
+ }
@@ -0,0 +1,47 @@
1
+ import path from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { gatherDependOnFileViolations, gatherCycleViolations, projectEdges, perEdge, perInternalEdge } from 'archunit'
4
+ import { architectureGraph } from './architecture-graph.mjs'
5
+
6
+ export const architectureRules = [
7
+ {
8
+ name: 'Shared contracts stay independent of application implementations',
9
+ source: /^src\/contracts\//,
10
+ forbidden: /^src\/(?!contracts\/)/
11
+ },
12
+ {
13
+ name: 'Capabilities and shared modules cannot import the MCP server layer',
14
+ source: /^src\/(?!server(?:\/|-core\.ts$|\.ts$))/,
15
+ forbidden: /^src\/server(?:\/|-core\.ts$|\.ts$)/
16
+ },
17
+ {
18
+ name: 'Pure interaction calculations cannot import native I/O or service facades',
19
+ source: /^src\/interact\/(?:adjustment\/(?:adjustment-plan|control-values|control-geometry|slider-labels)|targeting\/(?:candidate-scoring|candidate-ancestor|candidate-result|nearby-control|element-helpers|resolved-elements|tap-target|target-scope)|waits\/(?:ui-change-helpers|ui-wait-condition)|assertions\/state-comparison)\.ts$/,
20
+ forbidden: /^(?:node:(?:child_process|fs|net|http|https)(?:\/|$)|src\/(?:utils\/(?:android|ios)\/|(?:interact|observe|manage)\/(?:index|android|ios)\.ts$))/
21
+ }
22
+ ]
23
+ const filter = regExp => ({ regExp, options: { target: 'path', matching: 'partial' } })
24
+
25
+ export function checkArchitecture(graph) {
26
+ const projected = projectEdges(graph, perEdge())
27
+ const results = architectureRules.map(rule => ({
28
+ name: rule.name,
29
+ violations: gatherDependOnFileViolations(projected, [filter(rule.source)], [filter(rule.forbidden)], true)
30
+ }))
31
+ const runtime = graph.filter(edge => edge.importKinds.includes('value') || !edge.importKinds.includes('type'))
32
+ results.push({ name: 'Production runtime dependencies must be cycle-free',
33
+ violations: gatherCycleViolations(projectEdges(runtime, perInternalEdge()), []) })
34
+ return results
35
+ }
36
+
37
+ async function main() {
38
+ const graph = await architectureGraph(process.argv[2])
39
+ const results = checkArchitecture(graph)
40
+ const failures = results.filter(result => result.violations.length)
41
+ for (const result of failures) console.error(`${result.name}\n${JSON.stringify(result.violations, null, 2)}`)
42
+ if (failures.length) process.exitCode = 1
43
+ else console.log(`Architecture: PASS (${results.length} rules; ${graph.filter(edge => edge.source !== edge.target).length} dependency edges, including type, dynamic and re-export dependencies)`)
44
+ }
45
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
46
+ main().catch(error => { console.error(error); process.exitCode = 1 })
47
+ }
@@ -0,0 +1 @@
1
+ []
@@ -0,0 +1,161 @@
1
+ import { ESLint } from 'eslint'
2
+ import { createHash } from 'node:crypto'
3
+ import ts from 'typescript'
4
+ import { parse } from '@typescript-eslint/parser'
5
+ import { readFile, readdir } from 'node:fs/promises'
6
+ import path from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ export const limits = Object.freeze({ 'max-lines': 500, 'class-lines': 300,
10
+ 'max-lines-per-function': 80, complexity: 20, 'max-depth': 4, 'explicit-any': 0 })
11
+ const functionKinds = new Set(['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'])
12
+ const measuredRules = new Set(['max-lines', 'max-lines-per-function', 'complexity', 'max-depth'])
13
+
14
+ function children(node) {
15
+ return Object.entries(node).filter(([key]) => !['parent', 'tokens', 'comments', 'loc', 'range'].includes(key))
16
+ .flatMap(([, value]) => Array.isArray(value) ? value : [value])
17
+ .filter(value => value && typeof value === 'object' && typeof value.type === 'string')
18
+ }
19
+ function label(node, parent) {
20
+ if (node.id?.name) return node.id.name
21
+ if (parent?.type === 'MethodDefinition' || parent?.type === 'Property' || parent?.type === 'PropertyDefinition') {
22
+ const name = parent.key.name ?? parent.key.value ?? '[computed]'
23
+ return ['get', 'set'].includes(parent.kind) ? `${parent.kind}:${name}` : name
24
+ }
25
+ if (parent?.type === 'VariableDeclarator') return parent.id.name ?? '[destructured]'
26
+ if (parent?.type === 'CallExpression' || parent?.type === 'NewExpression') {
27
+ const callee = parent.callee.name ?? parent.callee.property?.name ?? '[call]'
28
+ return `${callee}:argument${parent.arguments.indexOf(node)}`
29
+ }
30
+ return `[${node.type}]`
31
+ }
32
+ function syntax(code) {
33
+ const ast = parse(code, { loc: true, range: true, comment: true, sourceType: 'module', ecmaVersion: 'latest' })
34
+ const symbols = [], anys = [], imports = []
35
+ function visit(node, parent, scope = '<module>') {
36
+ if (['TSInterfaceDeclaration', 'TSTypeAliasDeclaration'].includes(node.type)) scope = `${scope}/${node.id.name}`
37
+ if (functionKinds.has(node.type) || node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
38
+ const anonymous = !node.id?.name && !['MethodDefinition', 'Property', 'PropertyDefinition', 'VariableDeclarator'].includes(parent?.type)
39
+ const suffix = anonymous ? ':' + createHash('sha256').update(code.slice(...node.range).replace(/\s/g, '')).digest('hex').slice(0, 12) : ''
40
+ scope = `${scope}/${label(node, parent)}${suffix}`
41
+ symbols.push({ node, identity: scope })
42
+ }
43
+ if (node.type === 'TSAnyKeyword') anys.push({ node, identity: scope })
44
+ if (['ImportDeclaration', 'ExportNamedDeclaration', 'ExportAllDeclaration', 'ImportExpression', 'TSImportType'].includes(node.type) && node.source) {
45
+ imports.push({ node, value: node.source.value })
46
+ }
47
+ if (node.type === 'CallExpression' && node.callee.name === 'require' && node.arguments[0]?.type === 'Literal') {
48
+ imports.push({ node, value: node.arguments[0].value })
49
+ }
50
+ for (const child of children(node)) visit(child, node, scope)
51
+ }
52
+ visit(ast, null)
53
+ return { ast, symbols, anys, imports }
54
+ }
55
+ function lineCount(code, comments, start = 0, end = code.length) {
56
+ const chars = code.split('')
57
+ for (const c of comments) for (let i = c.range[0]; i < c.range[1]; i++) if (chars[i] !== '\n') chars[i] = ' '
58
+ return chars.slice(start, end).join('').split('\n').filter(line => line.trim()).length
59
+ }
60
+ const eslint = new ESLint({ overrideConfig: [{ files: ['**/*.ts'], linterOptions: { noInlineConfig: true }, rules: {
61
+ 'max-lines': ['error', { max: limits['max-lines'], skipBlankLines: true, skipComments: true }],
62
+ 'max-lines-per-function': ['error', { max: limits['max-lines-per-function'], skipBlankLines: true, skipComments: true }],
63
+ complexity: ['error', { max: limits.complexity, variant: 'classic' }], 'max-depth': ['error', limits['max-depth']]
64
+ } }] })
65
+
66
+ export async function measure(file, code) {
67
+ if (file.endsWith('.java')) {
68
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, ts.LanguageVariant.Standard, code)
69
+ const comments = []
70
+ for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
71
+ if ([ts.SyntaxKind.SingleLineCommentTrivia, ts.SyntaxKind.MultiLineCommentTrivia].includes(token)) {
72
+ comments.push({ range: [scanner.getTokenPos(), scanner.getTextPos()] })
73
+ }
74
+ }
75
+ const value = lineCount(code, comments)
76
+ return value > limits['max-lines'] ? [{ file, rule: 'max-lines', symbol: '<module>', value, line: 1 }] : []
77
+ }
78
+ const { ast, symbols, anys, imports } = syntax(code)
79
+ const findings = []
80
+ const add = (rule, symbol, value, line) => findings.push({ file, rule, symbol, value, line })
81
+ const identities = new Set()
82
+ for (const { node, identity } of symbols) {
83
+ if (identities.has(identity) && !identity.includes(':argument')) add('duplicate-symbol', identity, 1, node.loc.start.line)
84
+ identities.add(identity)
85
+ if (node.type.startsWith('Class')) {
86
+ const value = lineCount(code, ast.comments, ...node.range)
87
+ if (value > limits['class-lines']) add('class-lines', identity, value, node.loc.start.line)
88
+ }
89
+ }
90
+ const anyCounts = new Map()
91
+ for (const { node, identity } of anys) {
92
+ const current = anyCounts.get(identity) ?? { value: 0, line: node.loc.start.line }
93
+ current.value++; anyCounts.set(identity, current)
94
+ }
95
+ for (const [identity, { value, line }] of anyCounts) add('explicit-any', identity, value, line)
96
+ const [result] = await eslint.lintText(code, { filePath: file })
97
+ for (const m of result.messages.filter(m => measuredRules.has(m.ruleId))) {
98
+ const value = Number(/(?:has too many lines \(|complexity of |nested too deeply \()([\d]+)/.exec(m.message)?.[1])
99
+ if (!Number.isFinite(value)) throw new Error(`Cannot read ${m.ruleId} metric: ${m.message}`)
100
+ const candidates = symbols.filter(({ node }) => m.line >= node.loc.start.line && m.line <= node.loc.end.line)
101
+ .sort((a, b) => a.node.range[1] - a.node.range[0] - (b.node.range[1] - b.node.range[0]))
102
+ const symbol = m.ruleId === 'max-lines' ? '<module>' : candidates[0]?.identity ?? '<module>'
103
+ add(m.ruleId, symbol, value, m.line)
104
+ }
105
+ for (const comment of ast.comments) {
106
+ if (/(?:eslint-(?:disable|enable)|^\s*eslint\s)/.test(comment.value) &&
107
+ (!/eslint-(?:disable|enable)(?:-next-line|-line)?\s+(?!-)[\w@]/.test(comment.value) ||
108
+ /max-lines|complexity|max-depth|no-explicit-any|code-quality/.test(comment.value))) {
109
+ add('inline-suppression', '<module>', 1, comment.loc.start.line)
110
+ }
111
+ }
112
+ if (file.startsWith('src/interact/') || file.startsWith('src/runtime/')) {
113
+ for (const { node, value } of imports) if (typeof value === 'string' && /(?:^|\/)server(?:\/|-core)/.test(value)) {
114
+ add('import-boundary', '<module>', 1, node.loc.start.line)
115
+ }
116
+ }
117
+ // Multiple nested blocks can report depth in the same function; retain its maximum.
118
+ return [...findings.reduce((out, f) => {
119
+ const key = `${f.rule}:${f.symbol}`
120
+ if (!out.has(key) || out.get(key).value < f.value) out.set(key, f)
121
+ return out
122
+ }, new Map()).values()]
123
+ }
124
+ export function compare(findings, baseline) {
125
+ const errors = [], entries = new Map()
126
+ const key = f => JSON.stringify([f.file, f.rule, f.symbol])
127
+ for (const b of baseline) {
128
+ if (!b.file || !b.rule || !b.symbol || !b.reason || !Number.isInteger(b.value) || b.value < 1) {
129
+ errors.push('Invalid baseline entry'); continue
130
+ }
131
+ if (entries.has(key(b))) errors.push(`Duplicate baseline entry: ${key(b)}`)
132
+ entries.set(key(b), b)
133
+ }
134
+ for (const f of findings) {
135
+ const b = entries.get(key(f))
136
+ if (['duplicate-symbol', 'inline-suppression', 'import-boundary'].includes(f.rule) || !b || f.value > b.value) {
137
+ errors.push(`${f.file}:${f.line}: ${f.rule} ${f.symbol} = ${f.value}; allowed ${b?.value ?? limits[f.rule] ?? 0}`)
138
+ } else if (f.value < b.value) errors.push(`Lower baseline ceiling for ${key(f)}: ${b.value} -> ${f.value}`)
139
+ entries.delete(key(f))
140
+ }
141
+ for (const b of entries.values()) errors.push(`Remove obsolete baseline entry: ${key(b)}`)
142
+ return errors
143
+ }
144
+ export async function collect(root = 'src') {
145
+ const out = []
146
+ for (const entry of await readdir(root, { withFileTypes: true })) {
147
+ const file = path.posix.join(root, entry.name)
148
+ if (entry.isDirectory()) out.push(...await collect(file))
149
+ else if (/\.(ts|java)$/.test(file)) out.push(...await measure(file, await readFile(file, 'utf8')))
150
+ }
151
+ return out
152
+ }
153
+ async function main() {
154
+ const baseline = JSON.parse(await readFile(new URL('./code-quality-baseline.json', import.meta.url), 'utf8'))
155
+ const errors = compare(await collect(), baseline)
156
+ if (errors.length) { console.error(errors.join('\n')); process.exitCode = 1 }
157
+ else console.log('Code quality: PASS (size, complexity, nesting, types, boundaries and legacy ceilings)')
158
+ }
159
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
160
+ main().catch(error => { console.error(error); process.exitCode = 1 })
161
+ }
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod'
2
+
3
+ export const verificationModes = ['none', 'light', 'full'] as const
4
+ export const responseModes = ['compact', 'debug'] as const
5
+ export const verificationModeSchema = z.enum(verificationModes)
6
+ export const responseModeSchema = z.enum(responseModes)
7
+ export type VerificationMode = z.infer<typeof verificationModeSchema>
8
+ export type ResponseMode = z.infer<typeof responseModeSchema>
9
+
10
+ export const verificationModeJson = { type: 'string', enum: [...verificationModes] }
11
+ export const responseModeJson = { type: 'string', enum: [...responseModes] }
12
+
13
+ export function interactionControls(args: Record<string, unknown>) {
14
+ const verificationMode = ((typeof args.verificationMode === 'string' ? args.verificationMode : undefined) as VerificationMode | undefined) ?? 'full'
15
+ const responseMode = ((typeof args.responseMode === 'string' ? args.responseMode : undefined) as ResponseMode | undefined) ?? 'debug'
16
+ if (!verificationModes.includes(verificationMode) || !responseModes.includes(responseMode)) throw new Error('INVALID_ARGUMENT')
17
+ for (const key of ['verificationMode', 'responseMode']) if (args[key] !== undefined && typeof args[key] !== 'string') throw new Error('INVALID_ARGUMENT')
18
+ for (const key of ['actionTimeoutMs', 'verificationTimeoutMs']) if (args[key] !== undefined && (typeof args[key] !== 'number' || !Number.isInteger(args[key]))) throw new Error('INVALID_ARGUMENT')
19
+ const actionTimeoutMs = (typeof args.actionTimeoutMs === 'number' && Number.isFinite(args.actionTimeoutMs) ? args.actionTimeoutMs : undefined)
20
+ const verificationTimeoutMs = (typeof args.verificationTimeoutMs === 'number' && Number.isFinite(args.verificationTimeoutMs) ? args.verificationTimeoutMs : undefined)
21
+ return { verificationMode, responseMode,
22
+ actionTimeoutMs: actionTimeoutMs === undefined ? undefined : Math.max(100, Math.min(30000, actionTimeoutMs)),
23
+ verificationTimeoutMs: verificationTimeoutMs === undefined ? 3000 : Math.max(100, Math.min(60000, verificationTimeoutMs)) }
24
+
25
+ }
26
+
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod'
2
+
3
+ export type UiSelector = {
4
+ text?: string
5
+ resource_id?: string
6
+ accessibility_id?: string
7
+ contains?: boolean
8
+ }
9
+
10
+ export const selectorSchema = z.object({
11
+ text: z.string().min(1).optional(), resource_id: z.string().min(1).optional(),
12
+ accessibility_id: z.string().min(1).optional(), contains: z.boolean().optional()
13
+ }).strict().refine(value => !!(value.text || value.resource_id || value.accessibility_id), 'Selector requires a target')
14
+
15
+ export const selectorJson = { type: 'object', additionalProperties: false, properties: {
16
+ text: { type: 'string', minLength: 1 }, resource_id: { type: 'string', minLength: 1 }, accessibility_id: { type: 'string', minLength: 1 }, contains: { type: 'boolean' }
17
+ }, anyOf: ['text', 'resource_id', 'accessibility_id'].map(key => ({ required: [key] })) }
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod'
2
+ import { selectorSchema as selector, selectorJson } from './selector.js'
3
+
4
+ export interface DeviceTarget { platform?: 'android' | 'ios'; deviceId?: string }
5
+ export type ScrollSelector = { text?: string; resourceId?: string; contentDesc?: string; className?: string }
6
+ const pollIntervalMs = z.number().int().min(50).max(1000).optional()
7
+ export const waitForSchema = z.object({
8
+ condition: z.enum(['exists', 'visible', 'clickable']).optional(),
9
+ timeoutMs: z.number().int().min(100).max(10000).optional(), pollIntervalMs,
10
+ match: z.object({ index: z.number().int().nonnegative().optional() }).strict().optional()
11
+ }).strict()
12
+ export const targetFields = { selector: selector.optional(), elementId: z.string().min(1).optional() }
13
+ export const exactlyOneTarget = (value: { selector?: unknown, elementId?: string }) => Number(!!value.selector) + Number(!!value.elementId) === 1
14
+ export const tapElementControlsSchema = z.object({
15
+ elementId: z.string().min(1).optional(), selector: selector.optional(), waitFor: waitForSchema.optional()
16
+ }).strict().refine(value => Number(!!value.elementId) + Number(!!value.selector) === 1 && (!value.elementId || !value.waitFor))
17
+
18
+ export const targetJson = { selector: selectorJson, elementId: { type: 'string', minLength: 1 } }
19
+ export const oneTargetJson = [{ required: ['selector'], not: { required: ['elementId'] } }, { required: ['elementId'], not: { required: ['selector'] } }]
@@ -0,0 +1,94 @@
1
+ export interface UIElementState {
2
+ checked?: boolean | null;
3
+ selected?: boolean | string | { id: string; label?: string } | null;
4
+ focused?: boolean | null;
5
+ expanded?: boolean | null;
6
+ enabled?: boolean | null;
7
+ text_value?: string | null;
8
+ value?: number | string | null;
9
+ raw_value?: number | string | null;
10
+ value_range?: {
11
+ min: number;
12
+ max: number;
13
+ step?: number | null;
14
+ } | null;
15
+ step?: number | null;
16
+ }
17
+
18
+ export interface SelectorConfidence {
19
+ score: number;
20
+ reason: string;
21
+ }
22
+
23
+ export interface UIResolutionSelector {
24
+ value: string | null;
25
+ confidence: SelectorConfidence | null;
26
+ }
27
+
28
+ export interface UIElementSemanticMetadata {
29
+ is_clickable: boolean;
30
+ is_container: boolean;
31
+ semantic_role?: 'slider' | 'stepper' | 'dropdown' | 'segmented_control' | 'custom_adjustable' | 'composite_control' | null;
32
+ supported_actions?: string[] | null;
33
+ adjustable?: boolean | null;
34
+ state_shape?: 'continuous' | 'discrete' | 'semantic' | null;
35
+ }
36
+
37
+ export interface LoadingState {
38
+ active: boolean;
39
+ signal: string;
40
+ source: string;
41
+ }
42
+
43
+ export interface SnapshotDelta {
44
+ previous_snapshot_revision: number | null;
45
+ added_elements: number;
46
+ removed_elements: number;
47
+ mutated_elements: number;
48
+ total_elements: number;
49
+ }
50
+
51
+ export interface StateDelta {
52
+ base_snapshot_revision: number | null;
53
+ snapshot_revision: number;
54
+ added: UIElement[];
55
+ removed: Array<{ stable_id?: string; resourceId?: string; contentDescription?: string; text?: string; index: number }>;
56
+ changed: UIElement[];
57
+ truncated: boolean;
58
+ omitted_changes?: number;
59
+ }
60
+
61
+ export interface UIElement {
62
+ text: string | null;
63
+ contentDescription: string | null;
64
+ type: string;
65
+ clickable: boolean;
66
+ enabled: boolean;
67
+ visible: boolean;
68
+ bounds: [number, number, number, number];
69
+ resourceId: string | null;
70
+ parentId?: number;
71
+ children?: number[];
72
+ center?: [number, number];
73
+ depth?: number;
74
+ state?: UIElementState | null;
75
+ stable_id?: string | null;
76
+ role?: string | null;
77
+ test_tag?: string | null;
78
+ selector?: UIResolutionSelector | null;
79
+ semantic?: UIElementSemanticMetadata | null;
80
+ }
81
+
82
+ export interface ReadinessSummary {
83
+ state: 'loading' | 'transitioning' | 'settled' | 'unknown' | 'unresponsive' | 'debugger_paused';
84
+ scope: 'device_ui';
85
+ basis: 'observed' | 'heuristic' | 'insufficient';
86
+ captured_at_ms: number | null;
87
+ sample_count: number;
88
+ stable_for_ms: number;
89
+ snapshot_session_id: string | null;
90
+ snapshot_revision: number | null;
91
+ evidence: Array<{ code: string; source: string; captured_at_ms: number | null; snapshot_revision: number | null }>;
92
+ limitations: string[];
93
+ capabilities: { loading: boolean; transitioning: boolean; settled: boolean; unresponsive: boolean; debugger_paused: boolean };
94
+ }
@@ -3,7 +3,7 @@ import { execFile } from 'node:child_process'
3
3
  import { promisify } from 'node:util'
4
4
  import { DebuggerError, debuggerRemaining, debuggerSignal, nativeError } from './worker.js'
5
5
  const exec = promisify(execFile)
6
- export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number }
6
+ export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number | null; executable_path?: string }
7
7
  export interface DebugNative {
8
8
  candidates(device: string, app: string): Promise<DebugTarget[]>
9
9
  identity(device: string, target: DebugTarget): Promise<DebugTarget>
@@ -34,7 +34,8 @@ export class AndroidDebugNative implements DebugNative {
34
34
  // `adb jdwp` is a subscription, not a one-shot command. Bound the sample.
35
35
  let jdwpOutput: string
36
36
  try { jdwpOutput = (await exec(resolveAdbCmd(), ['-s', device, 'jdwp'], { timeout: debuggerRemaining(500), signal: debuggerSignal(), maxBuffer: 1024 * 1024 })).stdout }
37
- catch (e: any) {
37
+ catch (error: unknown) {
38
+ const e = error as { killed?: boolean; stderr?: string; stdout?: string }
38
39
  if (debuggerSignal()?.aborted) throw new DebuggerError('DEBUGGER_CANCELLED')
39
40
  debuggerRemaining(1)
40
41
  if (!e.killed || e.stderr?.trim()) throw nativeError(e, 'DEVICE_UNAVAILABLE')
@@ -0,0 +1,43 @@
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'] as const
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'] as const
4
+ export type DiagnosticStage = typeof stages[number]
5
+ export type DiagnosticReason = typeof reasons[number]
6
+ export interface DebuggerDiagnostic { stage: DiagnosticStage; reason: DiagnosticReason; recovery: string }
7
+ const recovery: Record<DiagnosticStage, string> = {
8
+ prerequisite: 'Check JDK 17+ availability, including java, javac and jdk.jdi.',
9
+ compilation: 'Check the JDK compiler and worker source/cache availability.',
10
+ worker_start: 'Check Java worker launch availability. Inspect session state and cleanup before attaching again.',
11
+ ipc: 'Inspect session state and cleanup. Detach before attempting a fresh attach.',
12
+ target_resolution: 'Check the explicit device and running debuggable app/process.',
13
+ forwarding: 'Check the explicit device and debugger forwarding availability.',
14
+ attach: 'Check the running debuggable target and whether another debugger is attached. Inspect cleanup before a fresh attach.',
15
+ command: 'Inspect debugger session state and cleanup before another state-changing command.',
16
+ transport: 'Inspect session state and cleanup. Detach before attempting a fresh attach; execution state may be unknown.',
17
+ cleanup: 'Retry detach_debugger and inspect cleanup.remaining_resources. Do not kill the app or ADB server.',
18
+ unknown: 'Inspect debugger session state and cleanup; the failure cause is unknown.'
19
+ }
20
+ export function diagnostic(stage: DiagnosticStage, reason: DiagnosticReason): DebuggerDiagnostic {
21
+ const guidance = reason === 'timeout' || reason === 'cancelled'
22
+ ? 'Execution may be uncertain. Inspect session state and cleanup before another state-changing command.' : recovery[stage]
23
+ return { stage, reason, recovery: guidance }
24
+ }
25
+ export function parseDiagnostic(value: unknown): DebuggerDiagnostic {
26
+ if (!value || typeof value !== 'object') throw new Error('INVALID_WORKER_DIAGNOSTIC')
27
+ const { stage, reason } = value as Record<string, unknown>
28
+ if (!stages.includes(stage as DiagnosticStage) || !reasons.includes(reason as DiagnosticReason)) throw new Error('INVALID_WORKER_DIAGNOSTIC')
29
+ return diagnostic(stage as DiagnosticStage, reason as DiagnosticReason)
30
+ }
31
+ export class DebuggerError extends Error {
32
+ constructor(public code: string, public diagnostic?: DebuggerDiagnostic) { super(code) }
33
+ }
34
+ const validationCodes = new Set(['INVALID_ARGUMENT', 'DEBUGGER_BUSY', 'DEBUGGER_NOT_FOUND', 'DEBUGGER_PAUSED', 'DEBUGGER_NOT_PAUSED', 'DEBUGGER_STALE_REFERENCE'])
35
+ export function classify(error: unknown, stage: DiagnosticStage, fallback: string, reason: DiagnosticReason = 'unknown'): DebuggerError {
36
+ const existing = error instanceof DebuggerError ? error : new DebuggerError(fallback)
37
+ if (existing.diagnostic || validationCodes.has(existing.code)) return existing
38
+ const knownReason = existing.code === 'DEBUGGER_TIMEOUT' ? 'timeout' : existing.code === 'DEBUGGER_CANCELLED' ? 'cancelled' : reason
39
+ return new DebuggerError(existing.code, diagnostic(stage, knownReason))
40
+ }
41
+ export async function atStage<T>(stage: DiagnosticStage, fallback: string, operation: () => Promise<T>): Promise<T> {
42
+ try { return await operation() } catch (error) { throw classify(error, stage, fallback) }
43
+ }
@@ -1,6 +1,11 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks'
2
2
  import { DebuggerError } from './worker.js'
3
- let pauseProvider: (device: string) => any = () => null
3
+ interface PauseEvidence {
4
+ debug_session_id: string
5
+ captured_at_ms: number
6
+ target: unknown
7
+ }
8
+ let pauseProvider: (device: string, platform?: string) => PauseEvidence | null = () => null
4
9
  export function registerDebuggerPauseProvider(provider: typeof pauseProvider) { pauseProvider = provider }
5
10
  const context = new AsyncLocalStorage<{ platform: string; device: string; dispatched: boolean }>()
6
11
  export function withDebuggerGuard<T>(platform: string, device: string, operation: () => Promise<T>) {
@@ -9,13 +14,13 @@ export function withDebuggerGuard<T>(platform: string, device: string, operation
9
14
  // Synchronous checks and native submission share a JS turn with pause updates.
10
15
  export function checkDebuggerGuard(dispatch = false) {
11
16
  const current = context.getStore()
12
- if (!current || current.platform !== 'android') return
13
- if (pauseProvider(current.device)) throw new DebuggerError('DEBUGGER_PAUSED')
17
+ if (!current) return
18
+ if (pauseProvider(current.device, current.platform)) throw new DebuggerError('DEBUGGER_PAUSED')
14
19
  if (dispatch) current.dispatched = true
15
20
  }
16
21
  export function guardDispatched() { return context.getStore()?.dispatched ?? false }
17
- export function pausedReadiness(device: string) {
18
- const paused = pauseProvider(device)
22
+ export function pausedReadiness(device: string, platform = 'android') {
23
+ const paused = pauseProvider(device, platform)
19
24
  if (!paused) return null
20
25
  return { state: 'debugger_paused' as const, scope: 'device_ui' as const, basis: 'observed' as const,
21
26
  captured_at_ms: paused.captured_at_ms, sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,