@moretea-labs/forge 1.5.1 → 1.7.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 (434) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +46 -0
  3. package/CLAUDE.md +2 -0
  4. package/DESIGN.md +7 -2
  5. package/README.md +4 -4
  6. package/README.zh-CN.md +2 -2
  7. package/SKILL.md +2 -2
  8. package/THIRD_PARTY_NOTICES.md +1 -1
  9. package/agents/openai.yaml +1 -1
  10. package/assets/hooks/hook-input.sh +11 -8
  11. package/assets/hooks/lib/session-state.sh +10 -7
  12. package/assets/hooks/lib/workflow-state.sh +13 -177
  13. package/assets/hooks/post-tool-observer.sh +8 -2
  14. package/assets/hooks/pre-edit-guard.sh +8 -39
  15. package/assets/hooks/prompt-guard.sh +7 -83
  16. package/assets/hooks/session-start-context.sh +56 -1
  17. package/assets/partials/05-workflow.partial.md +1 -1
  18. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  19. package/assets/plugin-registry.v1.json +54 -3
  20. package/assets/reference-configs/document-generation.md +1 -1
  21. package/assets/reference-configs/harness-overview.md +1 -1
  22. package/assets/reference-configs/hook-operations.md +1 -1
  23. package/assets/reference-configs/sprint-contracts.md +3 -3
  24. package/assets/skill-version.json +2 -2
  25. package/assets/templates/contract.template.md +2 -7
  26. package/assets/templates/helpers/capture-plan.sh +2 -2
  27. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  28. package/assets/templates/helpers/contract-worktree.sh +1 -21
  29. package/assets/templates/helpers/ensure-task-workflow.sh +13 -38
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/new-plan.sh +1 -1
  32. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  33. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  34. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/plan.template.md +1 -1
  38. package/assets/templates/review.template.md +9 -34
  39. package/assets/workflow-contract.v1.json +10 -19
  40. package/docs/README.md +10 -10
  41. package/docs/architecture/CURRENT.md +170 -0
  42. package/docs/architecture/EVOLUTION.md +42 -0
  43. package/docs/architecture/current/README.md +4 -156
  44. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  45. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  46. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  47. package/docs/architecture/history/README.md +7 -0
  48. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  49. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  50. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  51. package/docs/architecture/history.md +3 -34
  52. package/docs/architecture/index.md +15 -98
  53. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  54. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  55. package/docs/architecture/versions/1.6.md +38 -0
  56. package/docs/architecture/versions/1.7.md +41 -0
  57. package/docs/forge-chatgpt-browser-engine.md +2 -2
  58. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  59. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  60. package/docs/forge-github-issue-launcher.md +1 -1
  61. package/docs/forge-runtime-self-healing-loop.md +1 -1
  62. package/docs/operations/controller-browser-plugin.md +38 -22
  63. package/docs/operations/controller-performance-and-502.md +124 -0
  64. package/docs/operations/controller-reliability-runbook.md +1 -1
  65. package/docs/operations/features.md +3 -3
  66. package/docs/operations/github-repository.md +1 -1
  67. package/docs/operations/homebrew.md +2 -2
  68. package/docs/operations/mcp-tool-exposure.md +5 -3
  69. package/docs/tutorials/01-install-and-start.md +1 -1
  70. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Installation.md +1 -1
  74. package/docs/wiki/Integrations.md +2 -2
  75. package/docs/wiki/Quick-Start.md +1 -1
  76. package/docs/wiki/Work-Lifecycle.md +1 -1
  77. package/package.json +29 -10
  78. package/scripts/activate-source-baseline.ts +17 -13
  79. package/scripts/benchmark-background-check-overlap.ts +228 -0
  80. package/scripts/benchmark-mcp-host.sh +117 -0
  81. package/scripts/benchmark-mcp-transport.ts +170 -0
  82. package/scripts/benchmark-quality-harness.ts +146 -0
  83. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  84. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  85. package/scripts/benchmark-typescript-navigation.ts +186 -0
  86. package/scripts/benchmark-work-efficiency.ts +334 -0
  87. package/scripts/capture-plan.sh +3 -3
  88. package/scripts/check-agent-tooling.sh +8 -35
  89. package/scripts/check-architecture-sync.sh +17 -86
  90. package/scripts/check-mcp-compatibility.ts +14 -0
  91. package/scripts/check-public-docs.sh +14 -1
  92. package/scripts/check-runtime-architecture.mjs +107 -64
  93. package/scripts/check-stable-baseline.ts +92 -0
  94. package/scripts/check-task-workflow.sh +4 -4
  95. package/scripts/contract-worktree.sh +1 -21
  96. package/scripts/ensure-task-workflow.sh +22 -55
  97. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  98. package/scripts/install-standalone-recovery.ts +24 -0
  99. package/scripts/lib/project-init-lib.sh +15 -44
  100. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  101. package/scripts/migrate-project-template.sh +23 -1179
  102. package/scripts/new-plan.sh +1 -1
  103. package/scripts/plan-to-todo.sh +11 -36
  104. package/scripts/run-governed-gate.ts +2 -1
  105. package/scripts/ship-worktrees.sh +2 -29
  106. package/scripts/smoke-ios-physical-device.ts +4 -2
  107. package/scripts/smoke-runtime-recovery.ts +1 -35
  108. package/scripts/smoke-schedule-engine.ts +1 -20
  109. package/scripts/sprint-backlog.sh +1 -1
  110. package/scripts/verify-contract.sh +1 -111
  111. package/scripts/verify-forge-runtime.sh +0 -1
  112. package/scripts/verify-sprint.sh +3 -180
  113. package/scripts/workflow-contract.ts +1 -5
  114. package/src/cli/commands/chatgpt.ts +31 -2
  115. package/src/cli/commands/init-hook.ts +3 -3
  116. package/src/cli/commands/mcp.ts +9 -5
  117. package/src/cli/commands/plugin.ts +37 -10
  118. package/src/cli/commands/recovery.ts +25 -3
  119. package/src/cli/commands/runtime.ts +55 -3
  120. package/src/cli/commands/security.ts +12 -1
  121. package/src/cli/commands/setup-profile.ts +31 -21
  122. package/src/cli/controller/check-runner.ts +106 -3
  123. package/src/cli/controller/composite-result.ts +1 -1
  124. package/src/cli/controller/context/focus.ts +51 -0
  125. package/src/cli/controller/context/instruction-context.ts +114 -0
  126. package/src/cli/controller/context/known-paths.ts +76 -0
  127. package/src/cli/controller/context/query-planning.ts +110 -0
  128. package/src/cli/controller/context/rule-resolution.ts +532 -0
  129. package/src/cli/controller/context/source-materializer.ts +278 -0
  130. package/src/cli/controller/context/structural.ts +16 -0
  131. package/src/cli/controller/context/types.ts +286 -0
  132. package/src/cli/controller/context-pack.ts +898 -430
  133. package/src/cli/controller/launch-agents.ts +77 -2
  134. package/src/cli/controller/operational-plan.ts +2 -2
  135. package/src/cli/controller/work-mode.ts +6 -4
  136. package/src/cli/editing/edit-session.ts +60 -7
  137. package/src/cli/github/github.ts +133 -0
  138. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  139. package/src/cli/hook/prompt-intents.ts +1 -1
  140. package/src/cli/installer/managed-entries.ts +15 -1
  141. package/src/cli/local-bridge/console-automations.ts +225 -67
  142. package/src/cli/local-bridge/console-view-models.ts +3 -35
  143. package/src/cli/local-bridge/facade-api.ts +2 -217
  144. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  145. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +153 -3
  146. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  147. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +183 -3
  148. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  149. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  150. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -11
  151. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  152. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  153. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +4 -3
  154. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  155. package/src/cli/local-bridge/frontend/src/types.ts +8 -7
  156. package/src/cli/local-bridge/job-store.ts +1 -490
  157. package/src/cli/local-bridge/server.ts +29 -532
  158. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  159. package/src/cli/local-bridge/ui-dist/app.js +5101 -4967
  160. package/src/cli/mcp/auth.ts +5 -5
  161. package/src/cli/mcp/instructions.ts +3 -2
  162. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  163. package/src/cli/mcp/multi-repository.ts +16 -5
  164. package/src/cli/mcp/oauth.ts +175 -26
  165. package/src/cli/mcp/paths.ts +8 -2
  166. package/src/cli/mcp/repository-tools.ts +356 -101
  167. package/src/cli/mcp/server.ts +625 -54
  168. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  169. package/src/cli/mcp/toolset-selection.ts +1 -1
  170. package/src/cli/mcp/toolset.ts +42 -5
  171. package/src/cli/mcp/transports/http.ts +125 -15
  172. package/src/cli/mcp/transports/session-registry.ts +67 -5
  173. package/src/cli/mcp/types.ts +3 -3
  174. package/src/cli/repositories/command-classifier.ts +209 -14
  175. package/src/cli/repositories/command-executor.ts +55 -521
  176. package/src/cli/repositories/command-process.ts +108 -0
  177. package/src/cli/repositories/command-scope.ts +99 -41
  178. package/src/cli/repositories/controller-home.ts +7 -8
  179. package/src/cli/repositories/git-command-executor.ts +2 -0
  180. package/src/cli/repositories/locks.ts +53 -16
  181. package/src/cli/repositories/repository-snapshot.ts +121 -0
  182. package/src/cli/repositories/safe-patch.ts +6 -4
  183. package/src/cli/repositories/selected-path-actions.ts +17 -9
  184. package/src/cli/repositories/structured-git.ts +110 -10
  185. package/src/cli/repository/inspector.ts +565 -62
  186. package/src/cli/repository/session-cache.ts +58 -17
  187. package/src/cli/v81-entry.ts +1 -4
  188. package/src/effects/fs-transaction.ts +85 -17
  189. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  190. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  191. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  192. package/src/runtime/context/lsp-client.ts +283 -0
  193. package/src/runtime/context/semantic-navigation.ts +275 -0
  194. package/src/runtime/context/swift-navigation.ts +565 -0
  195. package/src/runtime/context/typescript-navigation.ts +226 -0
  196. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  197. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +196 -2
  198. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  199. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  200. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  201. package/src/runtime/control-plane/execution/validation.ts +15 -2
  202. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  203. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  204. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  205. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  206. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  207. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +175 -8
  208. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -21
  209. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +87 -9
  210. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  211. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  212. package/src/runtime/control-plane/facade/controller-session-store.ts +144 -14
  213. package/src/runtime/control-plane/facade/goal-workloop-access.ts +9 -2
  214. package/src/runtime/control-plane/facade/goal-workloop.ts +885 -105
  215. package/src/runtime/control-plane/facade/index.ts +1 -0
  216. package/src/runtime/control-plane/facade/plan-contract-store.ts +211 -26
  217. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  218. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  219. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  220. package/src/runtime/control-plane/facade/types.ts +55 -11
  221. package/src/runtime/control-plane/facade/work-contract-store.ts +204 -73
  222. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  223. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  224. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  225. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  226. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  227. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  228. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  229. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  230. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  231. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  232. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  233. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  234. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  235. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  236. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  237. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  238. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  239. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  240. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  241. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +642 -51
  242. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  243. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  244. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  245. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  246. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  247. package/src/runtime/control-plane/release-retention.ts +62 -2
  248. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  249. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  250. package/src/runtime/diagnostics/mcp-timing.ts +19 -0
  251. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  252. package/src/runtime/diagnostics/performance.ts +24 -1
  253. package/src/runtime/evidence/event-ledger.ts +1 -1
  254. package/src/runtime/execution/jobs/store.ts +58 -4
  255. package/src/runtime/execution/jobs/types.ts +1 -1
  256. package/src/runtime/execution/managed-workspace.ts +203 -1
  257. package/src/runtime/execution/process-runtime/check-facade.ts +52 -14
  258. package/src/runtime/execution/process-runtime/check-result.ts +63 -2
  259. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  260. package/src/runtime/execution/process-runtime/command-facade.ts +346 -40
  261. package/src/runtime/execution/process-runtime/gc.ts +98 -12
  262. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  263. package/src/runtime/execution/process-runtime/persisted-check.ts +57 -6
  264. package/src/runtime/execution/process-runtime/resource-claims.ts +97 -8
  265. package/src/runtime/execution/process-runtime/runtime.ts +59 -4
  266. package/src/runtime/execution/process-runtime/store.ts +7 -0
  267. package/src/runtime/execution/process-runtime/types.ts +2 -0
  268. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  269. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  270. package/src/runtime/execution/workers/executor.ts +0 -48
  271. package/src/runtime/gateway/mcp/execution-tools.ts +1219 -126
  272. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  273. package/src/runtime/gateway/mcp/router.ts +185 -83
  274. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  275. package/src/runtime/gateway/mcp/runtime-tools.ts +2555 -1616
  276. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  277. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  278. package/src/runtime/plugins/browser-adapter.ts +2925 -318
  279. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  280. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  281. package/src/runtime/plugins/browser-handoff.ts +92 -5
  282. package/src/runtime/plugins/browser-macos-bridge.ts +586 -45
  283. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  284. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  285. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  286. package/src/runtime/plugins/browser-runtime.ts +183 -0
  287. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  288. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  289. package/src/runtime/plugins/desktop-operator-registration.ts +15 -12
  290. package/src/runtime/plugins/external-adapter.ts +324 -3
  291. package/src/runtime/plugins/external-registration.ts +84 -5
  292. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  293. package/src/runtime/plugins/first-party-registry.ts +4 -2
  294. package/src/runtime/plugins/github-adapter.ts +125 -0
  295. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  296. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  297. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  298. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  299. package/src/runtime/plugins/ios-adapter.ts +106 -16
  300. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  301. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  302. package/src/runtime/plugins/lightweight-action.ts +153 -0
  303. package/src/runtime/plugins/local-system-adapter.ts +125 -8
  304. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  305. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  306. package/src/runtime/plugins/plugin-management-adapter.ts +181 -0
  307. package/src/runtime/plugins/resend-adapter.ts +1 -1
  308. package/src/runtime/plugins/store.ts +366 -24
  309. package/src/runtime/plugins/types.ts +39 -2
  310. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  311. package/src/runtime/projections/git-status-sampler.ts +16 -16
  312. package/src/runtime/projections/materialized-view.ts +28 -10
  313. package/src/runtime/recovery/actions.ts +0 -10
  314. package/src/runtime/recovery/classifier.ts +0 -9
  315. package/src/runtime/recovery/maintenance-executor.ts +415 -18
  316. package/src/runtime/recovery/probe.ts +0 -1
  317. package/src/runtime/recovery/types.ts +0 -1
  318. package/src/runtime/root/mcp-transport.ts +180 -10
  319. package/src/runtime/root/package-connector-service.ts +186 -8
  320. package/src/runtime/root/package-runtime-release.ts +143 -17
  321. package/src/runtime/root/package-runtime-service.ts +430 -22
  322. package/src/runtime/root/release-manifest.ts +7 -2
  323. package/src/runtime/root/release-materialize.ts +162 -19
  324. package/src/runtime/root/release-store.ts +48 -11
  325. package/src/runtime/root/runtime.ts +23 -5
  326. package/src/runtime/root/service.ts +100 -27
  327. package/src/runtime/root/types.ts +2 -0
  328. package/src/runtime/safe-tooling/index.ts +0 -1
  329. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  330. package/src/runtime/safe-tooling/types.ts +0 -30
  331. package/src/runtime/shared/trusted-executable.ts +62 -0
  332. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  333. package/src/runtime/standalone-recovery/core.ts +702 -94
  334. package/src/runtime/standalone-recovery/entry.ts +99 -23
  335. package/src/runtime/standalone-recovery/installer.ts +19 -3
  336. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  337. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  338. package/src/runtime/workflow/schedules/browser-probe.ts +258 -0
  339. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  340. package/src/runtime/workflow/schedules/engine.ts +509 -80
  341. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  342. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  343. package/src/runtime/workflow/schedules/store.ts +131 -10
  344. package/src/runtime/workflow/schedules/types.ts +6 -3
  345. package/src/runtime/workflow/schedules/work-continuation.ts +120 -34
  346. package/src/testing/test-governance.ts +8 -40
  347. package/assets/reference-configs/heartbeat-triage.md +0 -57
  348. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  349. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  350. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  351. package/bin/forge-desktop-helper.mjs +0 -204
  352. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  353. package/docs/architecture/current/architecture-invariants.md +0 -331
  354. package/docs/architecture/current/automation-and-schedule-engine.md +0 -453
  355. package/docs/architecture/current/codegraph-context-provider.md +0 -226
  356. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  357. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  358. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -354
  359. package/docs/architecture/current/entity-model.md +0 -592
  360. package/docs/architecture/current/external-plugin-governance.md +0 -396
  361. package/docs/architecture/current/failure-recovery.md +0 -447
  362. package/docs/architecture/current/governance.md +0 -213
  363. package/docs/architecture/current/human-interaction-plane.md +0 -87
  364. package/docs/architecture/current/implementation-status.md +0 -128
  365. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  366. package/docs/architecture/current/legacy-governance.md +0 -58
  367. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  368. package/docs/architecture/current/migration-roadmap.md +0 -105
  369. package/docs/architecture/current/multi-repository-execution.md +0 -371
  370. package/docs/architecture/current/operations-runbook.md +0 -45
  371. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  372. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  373. package/docs/architecture/current/repository-access-modes.md +0 -102
  374. package/docs/architecture/current/route-policy.md +0 -15
  375. package/docs/architecture/current/route-session-concurrency.md +0 -85
  376. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  377. package/docs/architecture/current/runtime-directory-map.md +0 -49
  378. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  379. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  380. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  381. package/docs/architecture/current/system-overview.md +0 -144
  382. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  383. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  384. package/docs/architecture/current/thin-harness-v1.md +0 -311
  385. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  386. package/docs/forge-autonomous-goal-loop.md +0 -9
  387. package/docs/forge-provider-configuration.md +0 -7
  388. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -83
  389. package/docs/operations/controller-desktop-plugin.md +0 -112
  390. package/docs/operations/gmail-assistant-routines.md +0 -186
  391. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  392. package/docs/operations/provider-configuration.md +0 -31
  393. package/scripts/harness-trace-grade.sh +0 -150
  394. package/scripts/heartbeat-triage.sh +0 -361
  395. package/scripts/maintenance-triage.sh +0 -167
  396. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  397. package/src/cli/repositories/goal-registry.ts +0 -239
  398. package/src/runtime/assistant/action-proposals.ts +0 -279
  399. package/src/runtime/assistant/intent.ts +0 -395
  400. package/src/runtime/assistant/model-provider.ts +0 -460
  401. package/src/runtime/assistant/openapi.ts +0 -223
  402. package/src/runtime/assistant/policy.ts +0 -89
  403. package/src/runtime/assistant/readiness.ts +0 -258
  404. package/src/runtime/assistant/routine-runtime.ts +0 -477
  405. package/src/runtime/assistant/schedule-binding.ts +0 -171
  406. package/src/runtime/assistant/standing-grants.ts +0 -375
  407. package/src/runtime/assistant/store.ts +0 -243
  408. package/src/runtime/assistant/types.ts +0 -135
  409. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  410. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  411. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  412. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  413. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  414. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  415. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  416. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  417. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  418. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  419. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  420. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  421. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  422. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  423. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  424. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  425. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  426. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  427. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  428. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  429. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
  430. package/src/runtime/workflow/findings/store.ts +0 -204
  431. package/src/runtime/workflow/findings/types.ts +0 -58
  432. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  433. package/src/runtime/workflow/portfolio/store.ts +0 -185
  434. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -0,0 +1,1171 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'child_process';
5
+ import { createInterface } from 'readline';
6
+ import { performance } from 'perf_hooks';
7
+ import { AssistantPluginError } from '../errors';
8
+
9
+ const TOOLCHAIN_VERSION = '10.2.1';
10
+ const WORKER_IDLE_MS = 15 * 60_000;
11
+ const WORKER_STARTUP_MS = 12_000;
12
+ const MUTATION_READY_BUDGET_MS = 2_500;
13
+ const REQUEST_TIMEOUT_MS = 12_000;
14
+ const RSD_ENDPOINT_CACHE_TTL_MS = 30_000;
15
+ const RSD_ENDPOINT_PERSISTED_TTL_MS = 10 * 60_000;
16
+ const MAX_TEXT_LENGTH = 2048;
17
+ const MAX_STDERR = 8 * 1024;
18
+
19
+ export interface RemoteXpcHidInput {
20
+ controllerHome: string;
21
+ deviceIdentifier: string;
22
+ udid: string;
23
+ width?: number;
24
+ height?: number;
25
+ action: 'tap' | 'swipe' | 'type';
26
+ textMode?: 'auto' | 'keys' | 'pasteboard';
27
+ replaceExisting?: boolean;
28
+ x?: number;
29
+ y?: number;
30
+ x2?: number;
31
+ y2?: number;
32
+ durationMs?: number;
33
+ text?: string;
34
+ }
35
+
36
+ export interface RemoteXpcHidResult {
37
+ backend: 'remote-xpc-hid';
38
+ reusedWorker: boolean;
39
+ endpoint: { host: string; port: number };
40
+ result: Record<string, unknown>;
41
+ timings: {
42
+ workerStartupMs: number;
43
+ workerReadyMs: number;
44
+ requestMs: number;
45
+ hidMs?: number;
46
+ };
47
+ }
48
+
49
+ interface RsdEndpoint {
50
+ host: string;
51
+ port: number;
52
+ }
53
+
54
+ interface PendingRequest {
55
+ resolve(value: Record<string, unknown>): void;
56
+ reject(error: Error): void;
57
+ timer: NodeJS.Timeout;
58
+ }
59
+
60
+ class WorkerRequestError extends Error {
61
+ readonly phase?: string;
62
+ readonly mutationDispatched?: boolean;
63
+
64
+ constructor(message: string, details: { phase?: string; mutationDispatched?: boolean } = {}) {
65
+ super(message);
66
+ this.name = 'WorkerRequestError';
67
+ this.phase = details.phase;
68
+ this.mutationDispatched = details.mutationDispatched;
69
+ }
70
+ }
71
+
72
+ interface WarmupFailure {
73
+ generation: number;
74
+ recordedAt: number;
75
+ message: string;
76
+ }
77
+
78
+ interface EndpointCacheEntry {
79
+ observedAt: number;
80
+ endpoints: RsdEndpoint[];
81
+ }
82
+
83
+ interface WorkerRecord {
84
+ key: string;
85
+ endpoint: RsdEndpoint;
86
+ process: ChildProcessWithoutNullStreams;
87
+ pending: Map<string, PendingRequest>;
88
+ ready: Promise<Record<string, unknown>>;
89
+ readyState: 'starting' | 'ready' | 'failed';
90
+ lastUsedAt: number;
91
+ stderrTail: string;
92
+ idleTimer?: NodeJS.Timeout;
93
+ }
94
+
95
+ type TestExecutor = (input: RemoteXpcHidInput) => Promise<RemoteXpcHidResult>;
96
+ let testExecutor: TestExecutor | undefined;
97
+ const workers = new Map<string, WorkerRecord>();
98
+ const warmups = new Map<string, Promise<void>>();
99
+ const workerGenerations = new Map<string, number>();
100
+ const warmupFailures = new Map<string, WarmupFailure>();
101
+ const endpointCache = new Map<string, EndpointCacheEntry>();
102
+
103
+ export function setRemoteXpcHidExecutorForTest(executor: TestExecutor | undefined): void {
104
+ testExecutor = executor;
105
+ }
106
+
107
+ export function resetRemoteXpcHidForTest(): void {
108
+ testExecutor = undefined;
109
+ for (const worker of workers.values()) stopWorker(worker);
110
+ workers.clear();
111
+ warmups.clear();
112
+ workerGenerations.clear();
113
+ warmupFailures.clear();
114
+ endpointCache.clear();
115
+ }
116
+
117
+ export function remoteXpcHidStatus(controllerHome: string, env: NodeJS.ProcessEnv = process.env): Record<string, unknown> {
118
+ const python = resolvePython(controllerHome, env);
119
+ return {
120
+ backend: 'remote-xpc-hid',
121
+ available: Boolean(python),
122
+ python,
123
+ toolchainVersion: TOOLCHAIN_VERSION,
124
+ transport: 'macos-trusted-coredevice-rsd',
125
+ runnerOwned: false,
126
+ };
127
+ }
128
+
129
+ function resolvePython(controllerHome: string, env: NodeJS.ProcessEnv): string | undefined {
130
+ const explicit = env.FORGE_IOS_HID_PYTHON?.trim();
131
+ const candidates = [
132
+ explicit,
133
+ join(controllerHome, 'toolchains', `pymobiledevice3-${TOOLCHAIN_VERSION}-py39`, 'bin', 'python'),
134
+ join(controllerHome, 'toolchains', `pymobiledevice3-${TOOLCHAIN_VERSION}`, 'bin', 'python'),
135
+ ].filter((value): value is string => Boolean(value));
136
+ return candidates.find((candidate) => existsSync(candidate));
137
+ }
138
+
139
+ function workerSource(): string {
140
+ return String.raw`#!/usr/bin/env python3
141
+ import argparse
142
+ import asyncio
143
+ import json
144
+ import sys
145
+ import time
146
+
147
+ from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
148
+ from pymobiledevice3.remote.core_device.pasteboard_service import PasteboardService
149
+ from pymobiledevice3.remote.core_device.hid_service import (
150
+ ASCII_TO_HID,
151
+ DIGITIZER_SURFACE_MAIN_TOUCHSCREEN,
152
+ KEY_BACKSPACE,
153
+ KEY_LEFT_GUI,
154
+ KEY_LEFT_SHIFT,
155
+ TOUCHSCREEN_STATE_CONTACT,
156
+ TOUCHSCREEN_STATE_RELEASE,
157
+ UniversalHIDServiceService,
158
+ touch_session,
159
+ )
160
+
161
+
162
+ def keyboard_mapping(char):
163
+ if char == '\b':
164
+ return (KEY_BACKSPACE, False)
165
+ return ASCII_TO_HID.get(char)
166
+
167
+
168
+ class KeyboardChordError(RuntimeError):
169
+ def __init__(self, primary_error, release_error=None):
170
+ super().__init__(str(primary_error))
171
+ self.primary_error = primary_error
172
+ self.release_error = release_error
173
+
174
+
175
+ class PasteboardOperationError(RuntimeError):
176
+ def __init__(self, primary_error, primary_phase, restore_error=None):
177
+ super().__init__(str(primary_error))
178
+ self.primary_error = primary_error
179
+ self.primary_phase = primary_phase
180
+ self.restore_error = restore_error
181
+
182
+
183
+ async def send_keyboard_chord(hid, service_id, usage_codes, hold_s=0.040, settle_s=0.040):
184
+ primary_error = None
185
+ release_error = None
186
+ try:
187
+ try:
188
+ await hid.send_keyboard(service_id, usage_codes)
189
+ await asyncio.sleep(hold_s)
190
+ except Exception as error:
191
+ primary_error = error
192
+ finally:
193
+ try:
194
+ await hid.send_keyboard(service_id, [])
195
+ except Exception as error:
196
+ release_error = error
197
+ if primary_error is not None or release_error is not None:
198
+ raise KeyboardChordError(primary_error or release_error, release_error) from (primary_error or release_error)
199
+ await asyncio.sleep(settle_s)
200
+
201
+
202
+ def normalized_point(x, y, width, height):
203
+ if width <= 1 or height <= 1:
204
+ raise ValueError('display dimensions must be > 1')
205
+ nx = max(0, min(65535, int(x * 65535 / (width - 1))))
206
+ ny = max(0, min(65535, int(y * 65535 / (height - 1))))
207
+ return nx, ny
208
+
209
+
210
+ def response(request_id, ok, result=None, error=None, details=None):
211
+ payload = {'id': request_id, 'ok': ok}
212
+ if result is not None:
213
+ payload['result'] = result
214
+ if error is not None:
215
+ payload['error'] = error
216
+ if details is not None:
217
+ payload['details'] = details
218
+ print(json.dumps(payload, ensure_ascii=False), flush=True)
219
+
220
+
221
+ async def main():
222
+ parser = argparse.ArgumentParser()
223
+ parser.add_argument('--host', required=True)
224
+ parser.add_argument('--port', required=True, type=int)
225
+ args = parser.parse_args()
226
+ async with RemoteServiceDiscoveryService((args.host, args.port)) as rsd:
227
+ async with touch_session(rsd) as hid:
228
+ connected = await hid.list_connected_services()
229
+ service_rows = connected.get('connectedServices', []) if isinstance(connected, dict) else []
230
+ service_ids = [row.get('_ServiceID') for row in service_rows if isinstance(row, dict)]
231
+ rsd_services = sorted((getattr(rsd, 'peer_info', {}) or {}).get('Services', {}).keys())
232
+ direct_keyboard_service = None
233
+ direct_keyboard_product = None
234
+ for row in service_rows:
235
+ if not isinstance(row, dict):
236
+ continue
237
+ service_id = row.get('_ServiceID')
238
+ if not isinstance(service_id, int) or service_id <= 0:
239
+ continue
240
+ device_hint = str(row.get('DeviceTypeHint') or '').lower()
241
+ product = str(row.get('Product') or '')
242
+ if device_hint == 'keyboard' or 'keyboard' in product.lower():
243
+ direct_keyboard_service = service_id
244
+ direct_keyboard_product = product or None
245
+ break
246
+ print(json.dumps({
247
+ 'ready': True,
248
+ 'serviceIds': service_ids,
249
+ 'keyboardReady': direct_keyboard_service is not None,
250
+ 'keyboardReused': direct_keyboard_service is not None,
251
+ 'keyboardSource': 'connected_coredevice' if direct_keyboard_service is not None else 'none',
252
+ 'keyboardProduct': direct_keyboard_product,
253
+ 'modifierKeyboardSource': 'connected_coredevice' if direct_keyboard_service is not None else 'none',
254
+ 'pasteboardAvailable': 'com.apple.coredevice.pasteboardservice' in rsd_services,
255
+ 'pasteboardTransport': 'independent_rsd',
256
+ }), flush=True)
257
+ while True:
258
+ line = await asyncio.to_thread(sys.stdin.readline)
259
+ if not line:
260
+ return
261
+ request = None
262
+ phase = 'request_decode'
263
+ mutation_dispatched = False
264
+ try:
265
+ request = json.loads(line)
266
+ request_id = str(request.get('id', ''))
267
+ action = request.get('action')
268
+ request_started = time.perf_counter()
269
+ hid_started = time.perf_counter()
270
+ if action == 'tap':
271
+ phase = 'hid_prepare_tap'
272
+ nx, ny = normalized_point(int(request['x']), int(request['y']), int(request['width']), int(request['height']))
273
+ phase = 'hid_tap_contact'
274
+ mutation_dispatched = True
275
+ await hid.send_touchscreen(TOUCHSCREEN_STATE_CONTACT, nx, ny, DIGITIZER_SURFACE_MAIN_TOUCHSCREEN)
276
+ await asyncio.sleep(0.055)
277
+ await hid.send_touchscreen(TOUCHSCREEN_STATE_RELEASE, nx, ny, DIGITIZER_SURFACE_MAIN_TOUCHSCREEN)
278
+ result = {'action': 'tap', 'hid': [nx, ny]}
279
+ elif action == 'swipe':
280
+ x, y = int(request['x']), int(request['y'])
281
+ x2, y2 = int(request['x2']), int(request['y2'])
282
+ width, height = int(request['width']), int(request['height'])
283
+ duration_ms = max(80, min(int(request.get('durationMs', 250)), 3000))
284
+ duration = duration_ms / 1000.0
285
+ steps = max(4, min(60, round(duration * 60)))
286
+ phase = 'hid_swipe_contact'
287
+ mutation_dispatched = True
288
+ for index in range(steps + 1):
289
+ ratio = index / steps
290
+ px = round(x + (x2 - x) * ratio)
291
+ py = round(y + (y2 - y) * ratio)
292
+ nx, ny = normalized_point(px, py, width, height)
293
+ await hid.send_touchscreen(TOUCHSCREEN_STATE_CONTACT, nx, ny, DIGITIZER_SURFACE_MAIN_TOUCHSCREEN)
294
+ await asyncio.sleep(duration / steps)
295
+ nx2, ny2 = normalized_point(x2, y2, width, height)
296
+ await hid.send_touchscreen(TOUCHSCREEN_STATE_RELEASE, nx2, ny2, DIGITIZER_SURFACE_MAIN_TOUCHSCREEN)
297
+ result = {'action': 'swipe', 'durationMs': duration_ms}
298
+ elif action == 'type':
299
+ text = str(request.get('text', ''))
300
+ text_mode = str(request.get('textMode', 'auto'))
301
+ if text_mode not in ('auto', 'keys', 'pasteboard'):
302
+ raise ValueError('unsupported textMode')
303
+ key_supported = all(keyboard_mapping(char) is not None for char in text)
304
+ replace_existing = bool(request.get('replaceExisting', False))
305
+ pasteboard_available = 'com.apple.coredevice.pasteboardservice' in rsd_services
306
+ use_pasteboard = text_mode == 'pasteboard' or (text_mode == 'auto' and (not key_supported or len(text) >= 32))
307
+ if use_pasteboard and not pasteboard_available:
308
+ if text_mode == 'auto' and key_supported:
309
+ use_pasteboard = False
310
+ else:
311
+ raise RuntimeError('CoreDevice pasteboard service is unavailable for Unicode/pasteboard text input')
312
+ if text_mode == 'keys' and not key_supported:
313
+ raise ValueError('unsupported HID character in keys mode')
314
+
315
+ if direct_keyboard_service is None:
316
+ raise RuntimeError('Connected CoreDevice keyboard service is unavailable')
317
+
318
+ async def dispatch_text(keyboard_hid, keyboard_service, keyboard_source, keyboard_create_ms):
319
+ nonlocal phase, mutation_dispatched
320
+ if use_pasteboard:
321
+ phase = 'pasteboard_connect'
322
+ pasteboard_restored = True
323
+ async with RemoteServiceDiscoveryService((args.host, args.port)) as pasteboard_rsd:
324
+ async with PasteboardService(pasteboard_rsd) as pasteboard:
325
+ phase = 'pasteboard_snapshot'
326
+ previous = await pasteboard.get()
327
+ previous_snapshot = previous.get('pasteboard') if isinstance(previous.get('pasteboard'), dict) else previous
328
+ previous_items = previous_snapshot.get('items') if isinstance(previous_snapshot, dict) else None
329
+ previous_source = previous_snapshot.get('sourceMetadata') if isinstance(previous_snapshot, dict) else None
330
+ if not isinstance(previous_items, list):
331
+ raise RuntimeError('CoreDevice pasteboard snapshot is not restorable; refusing temporary clipboard overwrite')
332
+ operation_error = None
333
+ operation_phase = None
334
+ restore_error = None
335
+ try:
336
+ phase = 'pasteboard_set_text'
337
+ mutation_dispatched = True
338
+ await pasteboard.set_text(text)
339
+ if replace_existing:
340
+ select_mapping = keyboard_mapping('a')
341
+ if select_mapping is None:
342
+ raise RuntimeError('HID A key mapping unavailable')
343
+ select_usage, _ = select_mapping
344
+ phase = 'hid_keyboard_select_all'
345
+ await send_keyboard_chord(keyboard_hid, keyboard_service, [select_usage, KEY_LEFT_GUI])
346
+ paste_mapping = keyboard_mapping('v')
347
+ if paste_mapping is None:
348
+ raise RuntimeError('HID V key mapping unavailable')
349
+ paste_usage, _ = paste_mapping
350
+ phase = 'hid_keyboard_paste'
351
+ await send_keyboard_chord(keyboard_hid, keyboard_service, [paste_usage, KEY_LEFT_GUI], settle_s=0.120)
352
+ except Exception as error:
353
+ operation_error = error
354
+ operation_phase = phase
355
+ finally:
356
+ phase = 'pasteboard_restore'
357
+ try:
358
+ await pasteboard.set(previous_items, source_metadata=previous_source if isinstance(previous_source, dict) else None)
359
+ except Exception as error:
360
+ pasteboard_restored = False
361
+ restore_error = error
362
+ if operation_error is not None or restore_error is not None:
363
+ primary_error = operation_error or restore_error
364
+ primary_phase = operation_phase or 'pasteboard_restore'
365
+ raise PasteboardOperationError(primary_error, primary_phase, restore_error) from primary_error
366
+ return {
367
+ 'action': 'type',
368
+ 'length': len(text),
369
+ 'inputMode': 'pasteboard',
370
+ 'replaceExisting': replace_existing,
371
+ 'keyboardSource': keyboard_source,
372
+ 'keyboardCreateMs': round(keyboard_create_ms, 2),
373
+ 'pasteboardTransport': 'independent_rsd',
374
+ 'pasteboardRestored': pasteboard_restored,
375
+ }
376
+
377
+ if replace_existing:
378
+ select_mapping = keyboard_mapping('a')
379
+ if select_mapping is None:
380
+ raise RuntimeError('HID A key mapping unavailable')
381
+ select_usage, _ = select_mapping
382
+ phase = 'hid_keyboard_select_all'
383
+ mutation_dispatched = True
384
+ await send_keyboard_chord(keyboard_hid, keyboard_service, [select_usage, KEY_LEFT_GUI])
385
+ if replace_existing and not text:
386
+ phase = 'hid_keyboard_delete_selection'
387
+ mutation_dispatched = True
388
+ await send_keyboard_chord(keyboard_hid, keyboard_service, [KEY_BACKSPACE])
389
+ else:
390
+ for char in text:
391
+ mapping = keyboard_mapping(char)
392
+ if mapping is None:
393
+ raise ValueError('unsupported HID character in keys mode')
394
+ usage, shifted = mapping
395
+ pressed = [usage]
396
+ if shifted:
397
+ pressed.append(KEY_LEFT_SHIFT)
398
+ phase = 'hid_keyboard_write'
399
+ mutation_dispatched = True
400
+ await send_keyboard_chord(keyboard_hid, keyboard_service, pressed, hold_s=0.018, settle_s=0.018)
401
+ await keyboard_hid.send_keyboard(keyboard_service, [])
402
+ return {
403
+ 'action': 'type',
404
+ 'length': len(text),
405
+ 'inputMode': 'keys',
406
+ 'replaceExisting': replace_existing,
407
+ 'keyboardSource': keyboard_source,
408
+ 'keyboardCreateMs': round(keyboard_create_ms, 2),
409
+ }
410
+
411
+ result = await dispatch_text(hid, direct_keyboard_service, 'connected_coredevice', 0.0)
412
+ else:
413
+ raise ValueError('unsupported action')
414
+ hid_ms = (time.perf_counter() - hid_started) * 1000.0
415
+ result['timings'] = {
416
+ 'hidMs': round(hid_ms, 2),
417
+ 'requestMs': round((time.perf_counter() - request_started) * 1000.0, 2),
418
+ }
419
+ result['mutationDispatched'] = mutation_dispatched
420
+ response(request_id, True, result)
421
+ except Exception as error:
422
+ if isinstance(error, PasteboardOperationError):
423
+ primary_error = error.primary_error
424
+ error_phase = error.primary_phase
425
+ release_error = None
426
+ pasteboard_restore_error = error.restore_error
427
+ else:
428
+ primary_error = error.primary_error if isinstance(error, KeyboardChordError) else error
429
+ error_phase = phase
430
+ release_error = error.release_error if isinstance(error, KeyboardChordError) else None
431
+ pasteboard_restore_error = None
432
+ error_details = {'phase': error_phase, 'mutationDispatched': mutation_dispatched}
433
+ if release_error is not None:
434
+ error_details['releaseFailure'] = f'{type(release_error).__name__}: {release_error}'[:512]
435
+ if pasteboard_restore_error is not None:
436
+ error_details['pasteboardRestoreFailure'] = f'{type(pasteboard_restore_error).__name__}: {pasteboard_restore_error}'[:512]
437
+ response(
438
+ str(request.get('id', '')) if isinstance(request, dict) else '',
439
+ False,
440
+ error=f'{type(primary_error).__name__}: {primary_error}',
441
+ details=error_details,
442
+ )
443
+
444
+
445
+ if __name__ == '__main__':
446
+ asyncio.run(main())
447
+ `;
448
+ }
449
+
450
+ export function remoteXpcHidWorkerSourceForTest(): string {
451
+ return workerSource();
452
+ }
453
+
454
+ function deviceInputRoot(controllerHome: string): string {
455
+ const root = join(controllerHome, 'runtime', 'device-input');
456
+ mkdirSync(root, { recursive: true, mode: 0o700 });
457
+ return root;
458
+ }
459
+
460
+ function materializeWorker(controllerHome: string): string {
461
+ const source = workerSource();
462
+ const hash = createHash('sha256').update(source).digest('hex').slice(0, 16);
463
+ const path = join(deviceInputRoot(controllerHome), `remote-xpc-hid-${hash}.py`);
464
+ if (!existsSync(path)) writeFileSync(path, source, { encoding: 'utf8', mode: 0o700 });
465
+ return path;
466
+ }
467
+
468
+ function persistentEndpointPath(controllerHome: string, udid: string): string {
469
+ const identity = createHash('sha256').update(udid).digest('hex').slice(0, 24);
470
+ return join(deviceInputRoot(controllerHome), `rsd-endpoint-${identity}.json`);
471
+ }
472
+
473
+ function validEndpoint(value: unknown): RsdEndpoint | undefined {
474
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
475
+ const endpoint = value as Record<string, unknown>;
476
+ if (typeof endpoint.host !== 'string' || !/^[0-9a-fA-F:]+$/.test(endpoint.host)) return undefined;
477
+ if (typeof endpoint.port !== 'number' || !Number.isInteger(endpoint.port) || endpoint.port <= 0 || endpoint.port > 65_535) return undefined;
478
+ return { host: endpoint.host, port: endpoint.port };
479
+ }
480
+
481
+ function readPersistentEndpoint(controllerHome: string, udid: string, now = Date.now()): RsdEndpoint | undefined {
482
+ const path = persistentEndpointPath(controllerHome, udid);
483
+ if (!existsSync(path)) return undefined;
484
+ try {
485
+ const record = JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
486
+ const observedAt = typeof record.observedAt === 'number' ? record.observedAt : Number.NaN;
487
+ const ageMs = now - observedAt;
488
+ const endpoint = record.schemaVersion === 1 ? validEndpoint(record.endpoint) : undefined;
489
+ if (!endpoint || !Number.isFinite(ageMs) || ageMs < -5_000 || ageMs > RSD_ENDPOINT_PERSISTED_TTL_MS) {
490
+ try { unlinkSync(path); } catch {}
491
+ return undefined;
492
+ }
493
+ return endpoint;
494
+ } catch {
495
+ try { unlinkSync(path); } catch {}
496
+ return undefined;
497
+ }
498
+ }
499
+
500
+ function rememberPersistentEndpoint(
501
+ controllerHome: string,
502
+ udid: string,
503
+ endpoint: RsdEndpoint,
504
+ observedAt = Date.now(),
505
+ ): void {
506
+ const path = persistentEndpointPath(controllerHome, udid);
507
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
508
+ try {
509
+ writeFileSync(tmp, JSON.stringify({ schemaVersion: 1, observedAt, endpoint }), { encoding: 'utf8', mode: 0o600 });
510
+ renameSync(tmp, path);
511
+ } catch {
512
+ try { unlinkSync(tmp); } catch {}
513
+ }
514
+ }
515
+
516
+ export function remoteXpcHidPersistentEndpointCacheForTest(
517
+ controllerHome: string,
518
+ udid: string,
519
+ endpoint?: RsdEndpoint,
520
+ ageMs = 0,
521
+ ): RsdEndpoint | undefined {
522
+ if (endpoint) rememberPersistentEndpoint(controllerHome, udid, endpoint, Date.now() - Math.max(0, ageMs));
523
+ return readPersistentEndpoint(controllerHome, udid);
524
+ }
525
+
526
+ function persistedEndpointActiveInIfconfig(host: string, output: string): boolean | undefined {
527
+ const normalizedHost = host.toLowerCase();
528
+ const remoteMatch = normalizedHost.match(/^([0-9a-f:]+)::1$/);
529
+ if (!remoteMatch) return undefined;
530
+ const expectedLocal = `${remoteMatch[1]}::2`;
531
+ let inUtun = false;
532
+ let sawUtun = false;
533
+ for (const line of output.split(/\r?\n/)) {
534
+ const interfaceMatch = line.match(/^([A-Za-z0-9._-]+):/);
535
+ if (interfaceMatch) {
536
+ inUtun = interfaceMatch[1].startsWith('utun');
537
+ if (inUtun) sawUtun = true;
538
+ continue;
539
+ }
540
+ if (!inUtun) continue;
541
+ const ipv6 = line.match(/\binet6\s+([0-9a-fA-F:]+)\s+prefixlen\s+(\d+)/);
542
+ if (!ipv6 || Number(ipv6[2]) !== 64) continue;
543
+ if (ipv6[1].toLowerCase() === expectedLocal) return true;
544
+ }
545
+ return sawUtun ? false : undefined;
546
+ }
547
+
548
+ function persistedEndpointActive(host: string): boolean | undefined {
549
+ const result = spawnSync('/sbin/ifconfig', [], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 2_000 });
550
+ if (result.status !== 0 || result.error || typeof result.stdout !== 'string') return undefined;
551
+ return persistedEndpointActiveInIfconfig(host, result.stdout);
552
+ }
553
+
554
+ function invalidatePersistentEndpoint(controllerHome: string, udid: string): void {
555
+ try { unlinkSync(persistentEndpointPath(controllerHome, udid)); } catch {}
556
+ }
557
+
558
+ export function remoteXpcHidPersistedEndpointActiveForTest(host: string, ifconfigOutput: string): boolean | undefined {
559
+ return persistedEndpointActiveInIfconfig(host, ifconfigOutput);
560
+ }
561
+
562
+ export function parseMacOSTrustedRsdEndpoints(output: string, udid: string): RsdEndpoint[] {
563
+ const endpoints: RsdEndpoint[] = [];
564
+ let host: string | undefined;
565
+ let latestHost: string | undefined;
566
+ for (const line of output.split(/\r?\n/)) {
567
+ if (!line.includes(udid)) continue;
568
+ const tunnel = line.match(/remote\s+([0-9a-fA-F:]+)\s*$/);
569
+ if (tunnel) {
570
+ host = tunnel[1];
571
+ latestHost = host;
572
+ continue;
573
+ }
574
+ const portMatch = line.match(/Creating RSD backend client device for server port\s+(\d+)/);
575
+ if (portMatch && host) {
576
+ const port = Number(portMatch[1]);
577
+ if (Number.isInteger(port) && port > 0 && port <= 65_535) endpoints.push({ host, port });
578
+ }
579
+ }
580
+ if (!latestHost) return [];
581
+ const unique = new Map<string, RsdEndpoint>();
582
+ for (const endpoint of endpoints) {
583
+ if (endpoint.host === latestHost) unique.set(`${endpoint.host}:${endpoint.port}`, endpoint);
584
+ }
585
+ return [...unique.values()].slice(-3).reverse();
586
+ }
587
+
588
+ function cachedEndpoints(udid: string, now = Date.now()): RsdEndpoint[] | undefined {
589
+ const cached = endpointCache.get(udid);
590
+ if (!cached) return undefined;
591
+ if (now - cached.observedAt > RSD_ENDPOINT_CACHE_TTL_MS) {
592
+ endpointCache.delete(udid);
593
+ return undefined;
594
+ }
595
+ return cached.endpoints.map((endpoint) => ({ ...endpoint }));
596
+ }
597
+
598
+ function rememberEndpoints(udid: string, endpoints: RsdEndpoint[], now = Date.now()): void {
599
+ endpointCache.set(udid, { observedAt: now, endpoints: endpoints.map((endpoint) => ({ ...endpoint })) });
600
+ }
601
+
602
+ export function remoteXpcHidEndpointCacheForTest(
603
+ udid: string,
604
+ endpoints: RsdEndpoint[],
605
+ ageMs = 0,
606
+ ): RsdEndpoint[] | undefined {
607
+ rememberEndpoints(udid, endpoints, Date.now() - Math.max(0, ageMs));
608
+ return cachedEndpoints(udid);
609
+ }
610
+
611
+ function endpointIdentity(endpoint: RsdEndpoint): string {
612
+ return `${endpoint.host}:${endpoint.port}`;
613
+ }
614
+
615
+ function novelEndpoints(previous: readonly RsdEndpoint[], refreshed: readonly RsdEndpoint[]): RsdEndpoint[] {
616
+ const attempted = new Set(previous.map(endpointIdentity));
617
+ return refreshed.filter((endpoint) => !attempted.has(endpointIdentity(endpoint)));
618
+ }
619
+
620
+ export function remoteXpcHidNovelEndpointsForTest(
621
+ previous: readonly RsdEndpoint[],
622
+ refreshed: readonly RsdEndpoint[],
623
+ ): RsdEndpoint[] {
624
+ return novelEndpoints(previous, refreshed);
625
+ }
626
+
627
+ function trustedTunnelLogArgs(udid: string): string[] {
628
+ if (!/^[A-Za-z0-9-]+$/.test(udid)) {
629
+ throw new AssistantPluginError('IOS_HID_DEVICE_ID_INVALID', 'The physical iPhone UDID cannot be used for trusted-tunnel discovery.', { retryable: false });
630
+ }
631
+ const predicate = `process == "remotepairingd" AND eventMessage CONTAINS "${udid}" AND (eventMessage CONTAINS "Tunnel established" OR eventMessage CONTAINS "Creating RSD backend client device for server port")`;
632
+ return ['show', '--last', '12h', '--style', 'compact', '--predicate', predicate];
633
+ }
634
+
635
+ async function discoverEndpoints(udid: string, forceRefresh = false): Promise<RsdEndpoint[]> {
636
+ if (!forceRefresh) {
637
+ const cached = cachedEndpoints(udid);
638
+ if (cached?.length) return cached;
639
+ }
640
+ const args = trustedTunnelLogArgs(udid);
641
+ return await new Promise<RsdEndpoint[]>((resolve, reject) => {
642
+ const child = spawn('/usr/bin/log', args, { stdio: ['ignore', 'pipe', 'pipe'] });
643
+ let stdout = '';
644
+ let stderr = '';
645
+ let settled = false;
646
+ const timer = setTimeout(() => {
647
+ try { child.kill('SIGTERM'); } catch {}
648
+ finish(new Error('macOS trusted-tunnel discovery timed out after 15000ms.'));
649
+ }, 15_000);
650
+ const finish = (error?: Error) => {
651
+ if (settled) return;
652
+ settled = true;
653
+ clearTimeout(timer);
654
+ if (error) {
655
+ reject(new AssistantPluginError('IOS_HID_RSD_DISCOVERY_FAILED', error.message, { retryable: true }));
656
+ return;
657
+ }
658
+ const endpoints = parseMacOSTrustedRsdEndpoints(stdout, udid);
659
+ if (endpoints.length === 0) {
660
+ reject(new AssistantPluginError('IOS_HID_RSD_UNAVAILABLE', 'No current macOS trusted CoreDevice RSD endpoint was found for the selected iPhone.', {
661
+ retryable: true,
662
+ details: { udid },
663
+ }));
664
+ return;
665
+ }
666
+ rememberEndpoints(udid, endpoints);
667
+ resolve(endpoints);
668
+ };
669
+ child.stdout.on('data', (chunk) => {
670
+ stdout = `${stdout}${String(chunk)}`.slice(-2 * 1024 * 1024);
671
+ });
672
+ child.stderr.on('data', (chunk) => {
673
+ stderr = `${stderr}${String(chunk)}`.slice(-MAX_STDERR);
674
+ });
675
+ child.on('error', (error) => finish(error));
676
+ child.on('close', (code) => {
677
+ if (code === 0) finish();
678
+ else finish(new Error(stderr.trim() || `macOS trusted-tunnel discovery exited with code ${code}.`));
679
+ });
680
+ });
681
+ }
682
+
683
+ function stopWorker(worker: WorkerRecord): void {
684
+ if (worker.idleTimer) clearTimeout(worker.idleTimer);
685
+ for (const pending of worker.pending.values()) {
686
+ clearTimeout(pending.timer);
687
+ pending.reject(new Error('RemoteXPC HID worker stopped.'));
688
+ }
689
+ worker.pending.clear();
690
+ try { worker.process.stdin.end(); } catch {}
691
+ try { worker.process.kill('SIGTERM'); } catch {}
692
+ if (workers.get(worker.key) === worker) workers.delete(worker.key);
693
+ }
694
+
695
+ function scheduleIdleStop(worker: WorkerRecord): void {
696
+ if (worker.idleTimer) clearTimeout(worker.idleTimer);
697
+ worker.idleTimer = setTimeout(() => stopWorker(worker), WORKER_IDLE_MS);
698
+ worker.idleTimer.unref?.();
699
+ }
700
+
701
+ function workerGeneration(deviceIdentifier: string): number {
702
+ return workerGenerations.get(deviceIdentifier) ?? 0;
703
+ }
704
+
705
+ function warmupFailureError(deviceIdentifier: string, failure: WarmupFailure): AssistantPluginError {
706
+ return new AssistantPluginError(
707
+ 'IOS_HID_INPUT_NOT_SENT',
708
+ `RemoteXPC HID warmup failed before any input was sent: ${failure.message}`,
709
+ {
710
+ retryable: true,
711
+ details: {
712
+ deviceIdentifier,
713
+ mutationDispatched: false,
714
+ phase: 'worker_warmup',
715
+ warmupFailureAgeMs: Math.max(0, Date.now() - failure.recordedAt),
716
+ },
717
+ },
718
+ );
719
+ }
720
+
721
+ export function remoteXpcHidWarmupFailureErrorForTest(deviceIdentifier: string, message: string): AssistantPluginError {
722
+ return warmupFailureError(deviceIdentifier, {
723
+ generation: workerGeneration(deviceIdentifier),
724
+ recordedAt: Date.now(),
725
+ message: message.slice(0, 1024),
726
+ });
727
+ }
728
+
729
+ export function stopRemoteXpcHidForDevice(deviceIdentifier: string): Record<string, unknown> {
730
+ const worker = workers.get(deviceIdentifier);
731
+ const cancelledWarmup = warmups.has(deviceIdentifier);
732
+ workerGenerations.set(deviceIdentifier, workerGeneration(deviceIdentifier) + 1);
733
+ warmups.delete(deviceIdentifier);
734
+ warmupFailures.delete(deviceIdentifier);
735
+ if (worker) stopWorker(worker);
736
+ return {
737
+ backend: 'remote-xpc-hid',
738
+ state: 'stopped',
739
+ workerStopped: Boolean(worker),
740
+ warmupCancelled: cancelledWarmup,
741
+ runnerOwned: false,
742
+ };
743
+ }
744
+
745
+ function startWorker(input: Pick<RemoteXpcHidInput, 'controllerHome' | 'deviceIdentifier'>, endpoint: RsdEndpoint): WorkerRecord {
746
+ const python = resolvePython(input.controllerHome, process.env);
747
+ if (!python) {
748
+ throw new AssistantPluginError('IOS_HID_TOOLCHAIN_MISSING', `The Controller-owned pymobiledevice3 ${TOOLCHAIN_VERSION} toolchain is unavailable.`, {
749
+ retryable: false,
750
+ details: { expected: join(input.controllerHome, 'toolchains', `pymobiledevice3-${TOOLCHAIN_VERSION}-py39`, 'bin', 'python') },
751
+ });
752
+ }
753
+ const script = materializeWorker(input.controllerHome);
754
+ const workerPath = dirname(python);
755
+ const child = spawn(python, [
756
+ '-u', script,
757
+ '--host', endpoint.host,
758
+ '--port', String(endpoint.port),
759
+ ], {
760
+ stdio: ['pipe', 'pipe', 'pipe'],
761
+ env: {
762
+ PATH: `${workerPath}:/usr/bin:/bin:/usr/sbin:/sbin`,
763
+ ...(process.env.HOME ? { HOME: process.env.HOME } : {}),
764
+ ...(process.env.TMPDIR ? { TMPDIR: process.env.TMPDIR } : {}),
765
+ PYTHONUNBUFFERED: '1',
766
+ PYTHONDONTWRITEBYTECODE: '1',
767
+ },
768
+ });
769
+ const key = input.deviceIdentifier;
770
+ const pending = new Map<string, PendingRequest>();
771
+ let readyResolve!: (value: Record<string, unknown>) => void;
772
+ let readyReject!: (error: Error) => void;
773
+ const ready = new Promise<Record<string, unknown>>((resolve, reject) => {
774
+ readyResolve = resolve;
775
+ readyReject = reject;
776
+ });
777
+ const worker: WorkerRecord = {
778
+ key, endpoint, process: child, pending, ready, readyState: 'starting', lastUsedAt: Date.now(), stderrTail: '',
779
+ };
780
+ workers.set(key, worker);
781
+
782
+ const startupTimer = setTimeout(() => {
783
+ readyReject(new Error(`RemoteXPC HID worker startup timed out after ${WORKER_STARTUP_MS}ms.`));
784
+ stopWorker(worker);
785
+ }, WORKER_STARTUP_MS);
786
+
787
+ const stdout = createInterface({ input: child.stdout });
788
+ stdout.on('line', (line) => {
789
+ let payload: Record<string, unknown>;
790
+ try { payload = JSON.parse(line) as Record<string, unknown>; } catch { return; }
791
+ if (payload.ready === true) {
792
+ clearTimeout(startupTimer);
793
+ worker.readyState = 'ready';
794
+ readyResolve(payload);
795
+ scheduleIdleStop(worker);
796
+ return;
797
+ }
798
+ const id = typeof payload.id === 'string' ? payload.id : '';
799
+ const request = pending.get(id);
800
+ if (!request) return;
801
+ pending.delete(id);
802
+ clearTimeout(request.timer);
803
+ if (payload.ok === true && payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result)) {
804
+ request.resolve(payload.result as Record<string, unknown>);
805
+ } else {
806
+ const details = payload.details && typeof payload.details === 'object' && !Array.isArray(payload.details)
807
+ ? payload.details as Record<string, unknown>
808
+ : {};
809
+ request.reject(new WorkerRequestError(
810
+ typeof payload.error === 'string' ? payload.error : 'RemoteXPC HID request failed.',
811
+ {
812
+ phase: typeof details.phase === 'string' ? details.phase : undefined,
813
+ mutationDispatched: typeof details.mutationDispatched === 'boolean' ? details.mutationDispatched : undefined,
814
+ },
815
+ ));
816
+ }
817
+ });
818
+ child.stderr.on('data', (chunk) => {
819
+ worker.stderrTail = `${worker.stderrTail}${String(chunk)}`.slice(-MAX_STDERR);
820
+ });
821
+ child.on('exit', (code, signal) => {
822
+ clearTimeout(startupTimer);
823
+ worker.readyState = 'failed';
824
+ const message = `RemoteXPC HID worker exited (${code ?? signal ?? 'unknown'}). ${worker.stderrTail}`.trim();
825
+ readyReject(new Error(message));
826
+ for (const request of pending.values()) {
827
+ clearTimeout(request.timer);
828
+ request.reject(new Error(message));
829
+ }
830
+ pending.clear();
831
+ if (workers.get(key) === worker) workers.delete(key);
832
+ });
833
+ child.on('error', (error) => {
834
+ clearTimeout(startupTimer);
835
+ worker.readyState = 'failed';
836
+ readyReject(error);
837
+ });
838
+ return worker;
839
+ }
840
+
841
+ async function workerRequest(worker: WorkerRecord, input: RemoteXpcHidInput): Promise<{
842
+ result: Record<string, unknown>;
843
+ workerReadyMs: number;
844
+ requestMs: number;
845
+ }> {
846
+ const readyStartedAt = performance.now();
847
+ await worker.ready;
848
+ const workerReadyMs = Math.round((performance.now() - readyStartedAt) * 100) / 100;
849
+ const id = randomUUID();
850
+ const payload: Record<string, unknown> = {
851
+ id,
852
+ action: input.action,
853
+ };
854
+ if (input.width !== undefined) payload.width = input.width;
855
+ if (input.height !== undefined) payload.height = input.height;
856
+ if (input.x !== undefined) payload.x = input.x;
857
+ if (input.y !== undefined) payload.y = input.y;
858
+ if (input.x2 !== undefined) payload.x2 = input.x2;
859
+ if (input.y2 !== undefined) payload.y2 = input.y2;
860
+ if (input.durationMs !== undefined) payload.durationMs = input.durationMs;
861
+ if (input.textMode !== undefined) payload.textMode = input.textMode;
862
+ if (input.replaceExisting !== undefined) payload.replaceExisting = input.replaceExisting;
863
+ if (input.text !== undefined) payload.text = input.text;
864
+ worker.lastUsedAt = Date.now();
865
+ scheduleIdleStop(worker);
866
+ const requestStartedAt = performance.now();
867
+ const result = await new Promise<Record<string, unknown>>((resolve, reject) => {
868
+ const timer = setTimeout(() => {
869
+ worker.pending.delete(id);
870
+ reject(new Error(`RemoteXPC HID request timed out after ${REQUEST_TIMEOUT_MS}ms.`));
871
+ }, REQUEST_TIMEOUT_MS);
872
+ worker.pending.set(id, { resolve, reject, timer });
873
+ worker.process.stdin.write(`${JSON.stringify(payload)}\n`, (error) => {
874
+ if (!error) return;
875
+ const request = worker.pending.get(id);
876
+ if (!request) return;
877
+ worker.pending.delete(id);
878
+ clearTimeout(request.timer);
879
+ reject(error);
880
+ });
881
+ });
882
+ return {
883
+ result,
884
+ workerReadyMs,
885
+ requestMs: Math.round((performance.now() - requestStartedAt) * 100) / 100,
886
+ };
887
+ }
888
+
889
+ function validateInput(input: RemoteXpcHidInput): void {
890
+ if (!input.udid) throw new AssistantPluginError('IOS_HID_UDID_MISSING', 'RemoteXPC HID requires the physical iPhone hardware UDID.', { retryable: false });
891
+ const requiresDisplay = input.action === 'tap' || input.action === 'swipe';
892
+ if (requiresDisplay && (!Number.isFinite(input.width) || !Number.isFinite(input.height) || input.width! <= 1 || input.height! <= 1)) {
893
+ throw new AssistantPluginError('IOS_HID_DISPLAY_INVALID', 'RemoteXPC touch input requires current positive display pixel dimensions.', { retryable: true });
894
+ }
895
+ const points = input.action === 'tap'
896
+ ? [[input.x, input.y, 'tap'] as const]
897
+ : input.action === 'swipe'
898
+ ? [[input.x, input.y, 'from'] as const, [input.x2, input.y2, 'to'] as const]
899
+ : [];
900
+ for (const [x, y, label] of points) {
901
+ if (typeof x !== 'number' || !Number.isFinite(x) || typeof y !== 'number' || !Number.isFinite(y)) {
902
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `${label} coordinates must be finite display pixels.`, { retryable: false });
903
+ }
904
+ if (x < 0 || y < 0 || x > input.width! - 1 || y > input.height! - 1) {
905
+ throw new AssistantPluginError('IOS_HID_COORDINATE_OUT_OF_BOUNDS', `${label} coordinates are outside the current CoreDevice display.`, {
906
+ retryable: false,
907
+ details: { x, y, width: input.width, height: input.height },
908
+ });
909
+ }
910
+ }
911
+ if (input.action === 'type') {
912
+ const text = input.text ?? '';
913
+ if (text.length > MAX_TEXT_LENGTH) throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `RemoteXPC HID text is limited to ${MAX_TEXT_LENGTH} characters.`, { retryable: false });
914
+ const textMode = input.textMode ?? 'auto';
915
+ if (!['auto', 'keys', 'pasteboard'].includes(textMode)) {
916
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', `Unsupported RemoteXPC HID text mode: ${textMode}.`, { retryable: false });
917
+ }
918
+ if (textMode === 'keys' && [...text].some((character) => character.charCodeAt(0) > 0x7f)) {
919
+ throw new AssistantPluginError('IOS_HID_UNICODE_TEXT_UNSUPPORTED', 'RemoteXPC HID keys mode accepts ASCII only; use auto or pasteboard mode for Unicode text.', { retryable: false });
920
+ }
921
+ }
922
+ }
923
+
924
+ async function establishWorker(
925
+ input: Pick<RemoteXpcHidInput, 'controllerHome' | 'deviceIdentifier' | 'udid'>,
926
+ generation = workerGeneration(input.deviceIdentifier),
927
+ ): Promise<void> {
928
+ if (workers.has(input.deviceIdentifier)) return;
929
+ const cached = cachedEndpoints(input.udid);
930
+ let persisted = cached?.length ? undefined : readPersistentEndpoint(input.controllerHome, input.udid);
931
+ if (persisted && persistedEndpointActive(persisted.host) === false) {
932
+ invalidatePersistentEndpoint(input.controllerHome, input.udid);
933
+ persisted = undefined;
934
+ }
935
+ const initialEndpoints = cached?.length
936
+ ? cached
937
+ : persisted
938
+ ? [persisted]
939
+ : await discoverEndpoints(input.udid, true);
940
+ if (workerGeneration(input.deviceIdentifier) !== generation) return;
941
+
942
+ let lastError: unknown;
943
+ let attemptedEndpointCount = 0;
944
+ const tryEndpoints = async (endpoints: readonly RsdEndpoint[]): Promise<'ready' | 'cancelled' | 'failed'> => {
945
+ for (const endpoint of endpoints) {
946
+ if (workerGeneration(input.deviceIdentifier) !== generation) return 'cancelled';
947
+ attemptedEndpointCount += 1;
948
+ const worker = startWorker(input, endpoint);
949
+ try {
950
+ await worker.ready;
951
+ if (workerGeneration(input.deviceIdentifier) !== generation) {
952
+ stopWorker(worker);
953
+ return 'cancelled';
954
+ }
955
+ warmupFailures.delete(input.deviceIdentifier);
956
+ rememberPersistentEndpoint(input.controllerHome, input.udid, endpoint);
957
+ return 'ready';
958
+ } catch (error) {
959
+ lastError = error;
960
+ stopWorker(worker);
961
+ }
962
+ }
963
+ return 'failed';
964
+ };
965
+
966
+ const initialResult = await tryEndpoints(initialEndpoints);
967
+ if (initialResult !== 'failed') return;
968
+
969
+ let refreshedEndpointCount = 0;
970
+ let novelEndpointCount = 0;
971
+ if (cached?.length || persisted) {
972
+ endpointCache.delete(input.udid);
973
+ try {
974
+ const refreshed = await discoverEndpoints(input.udid, true);
975
+ refreshedEndpointCount = refreshed.length;
976
+ const novel = novelEndpoints(initialEndpoints, refreshed);
977
+ novelEndpointCount = novel.length;
978
+ if (novel.length > 0) {
979
+ const refreshedResult = await tryEndpoints(novel);
980
+ if (refreshedResult !== 'failed') return;
981
+ }
982
+ } catch (error) {
983
+ lastError = error;
984
+ }
985
+ } else {
986
+ endpointCache.delete(input.udid);
987
+ }
988
+
989
+ throw new AssistantPluginError('IOS_HID_INPUT_FAILED', lastError instanceof Error ? lastError.message : 'RemoteXPC HID input failed on all discovered trusted tunnel endpoints.', {
990
+ retryable: true,
991
+ details: {
992
+ deviceIdentifier: input.deviceIdentifier,
993
+ endpointCount: attemptedEndpointCount,
994
+ initialEndpointCount: initialEndpoints.length,
995
+ cacheRefreshAttempted: Boolean(cached?.length || persisted),
996
+ persistentEndpointUsed: Boolean(persisted),
997
+ refreshedEndpointCount,
998
+ novelEndpointCount,
999
+ },
1000
+ });
1001
+ }
1002
+
1003
+ export function prewarmRemoteXpcHid(input: Pick<RemoteXpcHidInput, 'controllerHome' | 'deviceIdentifier' | 'udid'>): Record<string, unknown> {
1004
+ if (testExecutor) return { backend: 'remote-xpc-hid', state: 'test', runnerOwned: false };
1005
+ const worker = workers.get(input.deviceIdentifier);
1006
+ if (worker?.readyState === 'ready') return { backend: 'remote-xpc-hid', state: 'ready', runnerOwned: false, reusedWorker: true };
1007
+ if (worker || warmups.has(input.deviceIdentifier)) return { backend: 'remote-xpc-hid', state: 'warming', runnerOwned: false };
1008
+ if (!resolvePython(input.controllerHome, process.env)) {
1009
+ return { backend: 'remote-xpc-hid', state: 'unavailable', runnerOwned: false, reason: 'toolchain_missing' };
1010
+ }
1011
+ const generation = workerGeneration(input.deviceIdentifier);
1012
+ warmupFailures.delete(input.deviceIdentifier);
1013
+ let warmup!: Promise<void>;
1014
+ warmup = establishWorker(input, generation)
1015
+ .catch((error) => {
1016
+ if (workerGeneration(input.deviceIdentifier) === generation) {
1017
+ const message = (error instanceof Error ? error.message : String(error)).slice(0, 1024);
1018
+ warmupFailures.set(input.deviceIdentifier, { generation, recordedAt: Date.now(), message });
1019
+ }
1020
+ })
1021
+ .then(() => undefined)
1022
+ .finally(() => {
1023
+ if (warmups.get(input.deviceIdentifier) === warmup) warmups.delete(input.deviceIdentifier);
1024
+ });
1025
+ warmups.set(input.deviceIdentifier, warmup);
1026
+ return { backend: 'remote-xpc-hid', state: 'warming_started', runnerOwned: false };
1027
+ }
1028
+
1029
+ export async function awaitRemoteXpcHidPrewarm(
1030
+ input: Pick<RemoteXpcHidInput, 'controllerHome' | 'deviceIdentifier' | 'udid'>,
1031
+ budgetMs = 4_000,
1032
+ ): Promise<Record<string, unknown>> {
1033
+ const boundedBudgetMs = Math.max(0, Math.min(Math.round(budgetMs), 10_000));
1034
+ const initial = prewarmRemoteXpcHid(input);
1035
+ if (testExecutor) return { ...initial, waitMs: 0 };
1036
+ if (initial.state === 'ready' || initial.state === 'unavailable') return { ...initial, waitMs: 0 };
1037
+ const startedAt = performance.now();
1038
+ const current = workers.get(input.deviceIdentifier);
1039
+ const warmup = warmups.get(input.deviceIdentifier);
1040
+ const readiness = current
1041
+ ? current.ready.then(() => undefined).catch(() => undefined)
1042
+ : warmup;
1043
+ if (readiness && boundedBudgetMs > 0) {
1044
+ let timer: NodeJS.Timeout | undefined;
1045
+ try {
1046
+ await Promise.race([
1047
+ readiness,
1048
+ new Promise<void>((resolve) => {
1049
+ timer = setTimeout(resolve, boundedBudgetMs);
1050
+ timer.unref?.();
1051
+ }),
1052
+ ]);
1053
+ } finally {
1054
+ if (timer) clearTimeout(timer);
1055
+ }
1056
+ }
1057
+ const waitMs = Math.round((performance.now() - startedAt) * 100) / 100;
1058
+ const worker = workers.get(input.deviceIdentifier);
1059
+ if (worker?.readyState === 'ready') {
1060
+ return { backend: 'remote-xpc-hid', state: 'ready', runnerOwned: false, reusedWorker: initial.state === 'ready', waitMs };
1061
+ }
1062
+ const failure = warmupFailures.get(input.deviceIdentifier);
1063
+ if (failure?.generation === workerGeneration(input.deviceIdentifier) && !warmups.has(input.deviceIdentifier)) {
1064
+ return {
1065
+ backend: 'remote-xpc-hid', state: 'failed', runnerOwned: false, waitMs,
1066
+ errorCode: 'IOS_HID_INPUT_NOT_SENT', phase: 'worker_warmup', reason: failure.message,
1067
+ };
1068
+ }
1069
+ return {
1070
+ backend: 'remote-xpc-hid', state: 'warming', runnerOwned: false, waitMs,
1071
+ readinessBudgetMs: boundedBudgetMs,
1072
+ };
1073
+ }
1074
+
1075
+ function numberTiming(result: Record<string, unknown>, key: string): number | undefined {
1076
+ const timings = result.timings;
1077
+ if (!timings || typeof timings !== 'object' || Array.isArray(timings)) return undefined;
1078
+ const value = (timings as Record<string, unknown>)[key];
1079
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
1080
+ }
1081
+
1082
+ async function waitForMutationWorker(
1083
+ input: Pick<RemoteXpcHidInput, 'controllerHome' | 'deviceIdentifier' | 'udid'>,
1084
+ ): Promise<{ worker: WorkerRecord; waitMs: number; reusedWorker: boolean }> {
1085
+ const startedAt = performance.now();
1086
+ const initiallyReady = workers.get(input.deviceIdentifier)?.readyState === 'ready';
1087
+ if (!workers.has(input.deviceIdentifier) && !warmups.has(input.deviceIdentifier)) {
1088
+ prewarmRemoteXpcHid(input);
1089
+ }
1090
+ const current = workers.get(input.deviceIdentifier);
1091
+ const warmup = warmups.get(input.deviceIdentifier);
1092
+ const readiness = current
1093
+ ? current.ready.then(() => undefined).catch(() => undefined)
1094
+ : warmup;
1095
+ if (readiness) {
1096
+ let timer: NodeJS.Timeout | undefined;
1097
+ try {
1098
+ await Promise.race([
1099
+ readiness,
1100
+ new Promise<void>((resolve) => {
1101
+ timer = setTimeout(resolve, MUTATION_READY_BUDGET_MS);
1102
+ timer.unref?.();
1103
+ }),
1104
+ ]);
1105
+ } finally {
1106
+ if (timer) clearTimeout(timer);
1107
+ }
1108
+ }
1109
+ const waitMs = Math.round((performance.now() - startedAt) * 100) / 100;
1110
+ const worker = workers.get(input.deviceIdentifier);
1111
+ if (!worker || worker.readyState !== 'ready') {
1112
+ const failure = warmupFailures.get(input.deviceIdentifier);
1113
+ if (failure?.generation === workerGeneration(input.deviceIdentifier) && !warmups.has(input.deviceIdentifier)) {
1114
+ throw warmupFailureError(input.deviceIdentifier, failure);
1115
+ }
1116
+ throw new AssistantPluginError(
1117
+ 'IOS_HID_INPUT_NOT_READY',
1118
+ `RemoteXPC HID is still warming after the ${MUTATION_READY_BUDGET_MS}ms mutation readiness budget. No input was sent; retry the same action after prewarm completes.`,
1119
+ {
1120
+ retryable: true,
1121
+ details: {
1122
+ deviceIdentifier: input.deviceIdentifier,
1123
+ mutationDispatched: false,
1124
+ workerReadyWaitMs: waitMs,
1125
+ readinessBudgetMs: MUTATION_READY_BUDGET_MS,
1126
+ prewarmContinues: warmups.has(input.deviceIdentifier) || worker?.readyState === 'starting',
1127
+ },
1128
+ },
1129
+ );
1130
+ }
1131
+ return { worker, waitMs, reusedWorker: initiallyReady };
1132
+ }
1133
+
1134
+ async function executeDefault(input: RemoteXpcHidInput): Promise<RemoteXpcHidResult> {
1135
+ const readiness = await waitForMutationWorker(input);
1136
+ const worker = readiness.worker;
1137
+ try {
1138
+ const request = await workerRequest(worker, input);
1139
+ return {
1140
+ backend: 'remote-xpc-hid', reusedWorker: readiness.reusedWorker, endpoint: worker.endpoint, result: request.result,
1141
+ timings: {
1142
+ workerStartupMs: readiness.reusedWorker ? 0 : readiness.waitMs,
1143
+ workerReadyMs: request.workerReadyMs,
1144
+ requestMs: request.requestMs,
1145
+ hidMs: numberTiming(request.result, 'hidMs'),
1146
+ },
1147
+ };
1148
+ } catch (error) {
1149
+ stopWorker(worker);
1150
+ const workerError = error instanceof WorkerRequestError ? error : undefined;
1151
+ const mutationDispatched = workerError?.mutationDispatched;
1152
+ const message = error instanceof Error ? error.message : 'RemoteXPC HID input failed.';
1153
+ throw new AssistantPluginError(
1154
+ mutationDispatched === false ? 'IOS_HID_INPUT_NOT_SENT' : 'IOS_HID_INPUT_FAILED',
1155
+ mutationDispatched === false ? `RemoteXPC input failed before any HID mutation was sent: ${message}` : message,
1156
+ {
1157
+ retryable: true,
1158
+ details: {
1159
+ deviceIdentifier: input.deviceIdentifier,
1160
+ ...(workerError?.phase ? { phase: workerError.phase } : {}),
1161
+ ...(mutationDispatched === undefined ? {} : { mutationDispatched }),
1162
+ },
1163
+ },
1164
+ );
1165
+ }
1166
+ }
1167
+
1168
+ export async function executeRemoteXpcHidInput(input: RemoteXpcHidInput): Promise<RemoteXpcHidResult> {
1169
+ validateInput(input);
1170
+ return testExecutor ? await testExecutor(input) : await executeDefault(input);
1171
+ }