@moretea-labs/forge 1.6.0 → 1.7.1

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 (469) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +39 -0
  3. package/CLAUDE.md +4 -0
  4. package/README.md +2 -2
  5. package/README.zh-CN.md +1 -1
  6. package/SKILL.md +2 -2
  7. package/THIRD_PARTY_NOTICES.md +1 -1
  8. package/agents/openai.yaml +1 -1
  9. package/assets/hooks/hook-input.sh +11 -8
  10. package/assets/hooks/lib/session-state.sh +10 -7
  11. package/assets/hooks/lib/workflow-state.sh +13 -177
  12. package/assets/hooks/post-tool-observer.sh +8 -2
  13. package/assets/hooks/prompt-guard.sh +5 -81
  14. package/assets/hooks/session-start-context.sh +56 -1
  15. package/assets/partials/05-workflow.partial.md +1 -1
  16. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  17. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  18. package/assets/plugin-registry.v1.json +3 -3
  19. package/assets/reference-configs/document-generation.md +1 -1
  20. package/assets/reference-configs/harness-overview.md +1 -1
  21. package/assets/reference-configs/sprint-contracts.md +3 -3
  22. package/assets/skill-version.json +2 -2
  23. package/assets/templates/contract.template.md +2 -7
  24. package/assets/templates/helpers/capture-plan.sh +2 -2
  25. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  26. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  27. package/assets/templates/helpers/contract-worktree.sh +1 -21
  28. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  29. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  32. package/assets/templates/helpers/new-plan.sh +1 -1
  33. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  34. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/helpers/workflow-contract.ts +5 -1
  38. package/assets/templates/plan.template.md +1 -1
  39. package/assets/templates/review.template.md +9 -34
  40. package/assets/workflow-contract.v1.json +10 -19
  41. package/docs/README.md +10 -10
  42. package/docs/architecture/CURRENT.md +178 -0
  43. package/docs/architecture/EVOLUTION.md +42 -0
  44. package/docs/architecture/current/README.md +4 -156
  45. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  46. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  47. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  48. package/docs/architecture/history/README.md +7 -0
  49. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  50. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  51. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  52. package/docs/architecture/history.md +3 -34
  53. package/docs/architecture/index.md +15 -98
  54. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  55. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  56. package/docs/architecture/versions/1.6.md +38 -0
  57. package/docs/architecture/versions/1.7.md +41 -0
  58. package/docs/forge-chatgpt-browser-engine.md +2 -2
  59. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  60. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  61. package/docs/forge-github-issue-launcher.md +1 -1
  62. package/docs/forge-plugin-management.md +1 -1
  63. package/docs/forge-runtime-self-healing-loop.md +1 -1
  64. package/docs/operations/controller-browser-plugin.md +26 -6
  65. package/docs/operations/controller-performance-and-502.md +143 -0
  66. package/docs/operations/controller-reliability-runbook.md +1 -1
  67. package/docs/operations/features.md +2 -2
  68. package/docs/operations/github-repository.md +1 -1
  69. package/docs/operations/homebrew.md +2 -2
  70. package/docs/operations/mcp-tool-exposure.md +5 -3
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Integrations.md +2 -2
  74. package/docs/wiki/Work-Lifecycle.md +1 -1
  75. package/package.json +33 -11
  76. package/scripts/activate-source-baseline.ts +17 -13
  77. package/scripts/architecture-event.ts +2 -820
  78. package/scripts/architecture-queue.sh +2 -645
  79. package/scripts/archive-architecture-request.sh +2 -242
  80. package/scripts/archive-workflow.sh +1 -291
  81. package/scripts/benchmark-background-check-overlap.ts +228 -0
  82. package/scripts/benchmark-mcp-host.sh +117 -0
  83. package/scripts/benchmark-mcp-transport.ts +170 -0
  84. package/scripts/benchmark-quality-harness.ts +146 -0
  85. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  86. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  87. package/scripts/benchmark-typescript-navigation.ts +186 -0
  88. package/scripts/benchmark-work-efficiency.ts +334 -0
  89. package/scripts/capability-config.ts +2 -392
  90. package/scripts/capability-resolver.ts +2 -503
  91. package/scripts/capture-plan.sh +1 -332
  92. package/scripts/check-agent-tooling.sh +2 -1558
  93. package/scripts/check-architecture-sync.sh +2 -371
  94. package/scripts/check-brain-manifest.sh +2 -253
  95. package/scripts/check-context-files.sh +2 -47
  96. package/scripts/check-deploy-sql-order.sh +2 -115
  97. package/scripts/check-mcp-compatibility.ts +14 -0
  98. package/scripts/check-runtime-architecture.mjs +349 -79
  99. package/scripts/check-skill-version.ts +3 -191
  100. package/scripts/check-source-duplication.mjs +61 -0
  101. package/scripts/check-stable-baseline.ts +92 -0
  102. package/scripts/check-task-sync.sh +2 -63
  103. package/scripts/check-task-workflow.sh +2 -1000
  104. package/scripts/codex-handoff-resume.sh +2 -267
  105. package/scripts/context-contract-sync.sh +2 -514
  106. package/scripts/contract-run.ts +2 -463
  107. package/scripts/contract-worktree.sh +1 -722
  108. package/scripts/ensure-task-workflow.sh +1 -1225
  109. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  110. package/scripts/inspect-project-state.ts +4 -279
  111. package/scripts/install-standalone-recovery.ts +24 -0
  112. package/scripts/lib/project-init-lib.sh +22 -46
  113. package/scripts/lib/run-forge-helper.sh +19 -0
  114. package/scripts/lib/run-forge-helper.ts +26 -0
  115. package/scripts/migrate-project-template.sh +1 -1189
  116. package/scripts/migrate-workflow-docs.ts +4 -412
  117. package/scripts/new-plan.sh +1 -180
  118. package/scripts/new-spec.sh +1 -47
  119. package/scripts/new-sprint.sh +1 -46
  120. package/scripts/plan-to-todo.sh +1 -796
  121. package/scripts/prepare-codex-handoff.sh +2 -201
  122. package/scripts/prepare-handoff.sh +1 -78
  123. package/scripts/refresh-current-status.sh +2 -467
  124. package/scripts/run-governed-gate.ts +5 -1
  125. package/scripts/select-agent-context-blocks.sh +2 -64
  126. package/scripts/ship-worktrees.sh +1 -558
  127. package/scripts/smoke-ios-physical-device.ts +4 -2
  128. package/scripts/smoke-runtime-recovery.ts +1 -35
  129. package/scripts/smoke-schedule-engine.ts +1 -20
  130. package/scripts/sprint-backlog.sh +1 -834
  131. package/scripts/stage-runtime-release.ts +2 -1
  132. package/scripts/summarize-failures.sh +2 -123
  133. package/scripts/switch-plan.sh +1 -150
  134. package/scripts/sync-brain-docs.sh +2 -379
  135. package/scripts/sync-generated-authority.mjs +175 -0
  136. package/scripts/verify-contract.sh +2 -703
  137. package/scripts/verify-forge-runtime.sh +0 -1
  138. package/scripts/verify-sprint.sh +1 -677
  139. package/scripts/workflow-contract.ts +4 -205
  140. package/scripts/workstream-sync.sh +2 -261
  141. package/src/cli/agent-jobs/executor-health.ts +1 -1
  142. package/src/cli/commands/init-hook.ts +3 -3
  143. package/src/cli/commands/mcp.ts +9 -5
  144. package/src/cli/commands/plugin.ts +12 -3
  145. package/src/cli/commands/recovery.ts +25 -3
  146. package/src/cli/commands/runtime.ts +55 -3
  147. package/src/cli/commands/security.ts +12 -1
  148. package/src/cli/commands/setup-profile.ts +31 -21
  149. package/src/cli/controller/check-runner.ts +113 -5
  150. package/src/cli/controller/composite-result.ts +1 -1
  151. package/src/cli/controller/context/focus.ts +51 -0
  152. package/src/cli/controller/context/instruction-context.ts +114 -0
  153. package/src/cli/controller/context/known-paths.ts +76 -0
  154. package/src/cli/controller/context/query-planning.ts +110 -0
  155. package/src/cli/controller/context/rule-resolution.ts +532 -0
  156. package/src/cli/controller/context/source-materializer.ts +278 -0
  157. package/src/cli/controller/context/structural.ts +16 -0
  158. package/src/cli/controller/context/types.ts +286 -0
  159. package/src/cli/controller/context-pack.ts +828 -487
  160. package/src/cli/controller/launch-agents.ts +77 -2
  161. package/src/cli/controller/operational-plan.ts +2 -2
  162. package/src/cli/controller/work-mode.ts +6 -4
  163. package/src/cli/editing/edit-session.ts +60 -7
  164. package/src/cli/github/contracts.ts +16 -0
  165. package/src/cli/github/github.ts +135 -16
  166. package/src/cli/github/session.ts +2 -1
  167. package/src/cli/installer/managed-entries.ts +15 -1
  168. package/src/cli/local-bridge/console-automations.ts +54 -78
  169. package/src/cli/local-bridge/console-view-models.ts +1 -35
  170. package/src/cli/local-bridge/facade-api.ts +37 -264
  171. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  172. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  173. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  174. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  175. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  176. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  177. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  178. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  179. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  180. package/src/cli/local-bridge/job-store.ts +1 -490
  181. package/src/cli/local-bridge/server.ts +2 -515
  182. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  183. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  184. package/src/cli/mcp/auth.ts +5 -5
  185. package/src/cli/mcp/instructions.ts +3 -2
  186. package/src/cli/mcp/legacy-tool-service.ts +10 -222
  187. package/src/cli/mcp/multi-repository.ts +16 -5
  188. package/src/cli/mcp/oauth.ts +175 -26
  189. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  190. package/src/cli/mcp/repository-tools.ts +282 -146
  191. package/src/cli/mcp/server.ts +586 -116
  192. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  193. package/src/cli/mcp/tool-contract.ts +14 -0
  194. package/src/cli/mcp/toolset-selection.ts +1 -1
  195. package/src/cli/mcp/toolset.ts +44 -7
  196. package/src/cli/mcp/transports/http.ts +124 -14
  197. package/src/cli/mcp/transports/session-registry.ts +43 -5
  198. package/src/cli/mcp/types.ts +3 -3
  199. package/src/cli/repositories/command-classifier.ts +103 -21
  200. package/src/cli/repositories/command-executor.ts +78 -526
  201. package/src/cli/repositories/command-process.ts +108 -0
  202. package/src/cli/repositories/command-scope.ts +99 -41
  203. package/src/cli/repositories/controller-home.ts +7 -8
  204. package/src/cli/repositories/git-command-executor.ts +2 -0
  205. package/src/cli/repositories/locks.ts +53 -16
  206. package/src/cli/repositories/repository-snapshot.ts +124 -0
  207. package/src/cli/repositories/selected-path-actions.ts +17 -9
  208. package/src/cli/repositories/structured-git.ts +118 -6
  209. package/src/cli/repository/inspector.ts +563 -65
  210. package/src/cli/repository/session-cache.ts +58 -17
  211. package/src/cli/v81-entry.ts +1 -4
  212. package/src/effects/fs-transaction.ts +85 -17
  213. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  214. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  215. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  216. package/src/runtime/context/lsp-client.ts +283 -0
  217. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  218. package/src/runtime/context/semantic-navigation.ts +223 -0
  219. package/src/runtime/context/swift-navigation.ts +565 -0
  220. package/src/runtime/context/typescript-navigation.ts +226 -0
  221. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  222. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +48 -16
  223. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  224. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  225. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  226. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  227. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  228. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  229. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  230. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  231. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  232. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  233. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  234. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  235. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  236. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  237. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  238. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  239. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  240. package/src/runtime/control-plane/execution/work-task-receipt.ts +22 -18
  241. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +156 -11
  242. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  243. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  244. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  245. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +90 -9
  246. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  247. package/src/runtime/control-plane/facade/controller-round-relay.ts +1118 -0
  248. package/src/runtime/control-plane/facade/controller-session-store.ts +302 -12
  249. package/src/runtime/control-plane/facade/goal-workloop-access.ts +17 -1
  250. package/src/runtime/control-plane/facade/goal-workloop.ts +928 -190
  251. package/src/runtime/control-plane/facade/index.ts +3 -0
  252. package/src/runtime/control-plane/facade/plan-contract-store.ts +275 -42
  253. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  254. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  255. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  256. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  257. package/src/runtime/control-plane/facade/types.ts +90 -10
  258. package/src/runtime/control-plane/facade/work-contract-store.ts +157 -261
  259. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  260. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  261. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  262. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  263. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  264. package/src/runtime/control-plane/global-scheduler/maintenance.ts +152 -0
  265. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  266. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  267. package/src/runtime/control-plane/global-scheduler/scheduler.ts +278 -543
  268. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  269. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  270. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  271. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  272. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  273. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  274. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  275. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  276. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  277. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  278. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +63 -0
  279. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +365 -117
  280. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +89 -0
  281. package/src/runtime/control-plane/launcher/thin-launcher.ts +244 -11
  282. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  283. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  284. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  285. package/src/runtime/control-plane/release-retention.ts +62 -2
  286. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  287. package/src/runtime/control-plane/routing/route-policy.ts +24 -21
  288. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  289. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  290. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  291. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  292. package/src/runtime/diagnostics/performance.ts +24 -1
  293. package/src/runtime/evidence/event-ledger.ts +1 -1
  294. package/src/runtime/evidence/process-check-execution.ts +18 -0
  295. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  296. package/src/runtime/execution/jobs/store.ts +58 -4
  297. package/src/runtime/execution/jobs/types.ts +1 -1
  298. package/src/runtime/execution/managed-workspace.ts +203 -1
  299. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  300. package/src/runtime/execution/process-runtime/check-result.ts +88 -0
  301. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  302. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  303. package/src/runtime/execution/process-runtime/command-facade.ts +367 -40
  304. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  305. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1201 -0
  306. package/src/runtime/execution/process-runtime/persisted-check.ts +51 -5
  307. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  308. package/src/runtime/execution/process-runtime/runtime.ts +53 -3
  309. package/src/runtime/execution/process-runtime/store.ts +1 -0
  310. package/src/runtime/execution/process-runtime/types.ts +4 -12
  311. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  312. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  313. package/src/runtime/execution/work-activity.ts +21 -0
  314. package/src/runtime/execution/workers/executor.ts +18 -61
  315. package/src/runtime/gateway/mcp/execution-tools.ts +35 -1852
  316. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  317. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  318. package/src/runtime/gateway/mcp/router.ts +186 -84
  319. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +565 -0
  320. package/src/runtime/gateway/mcp/runtime-tools.ts +2378 -1947
  321. package/src/runtime/plugins/app-store-connect-adapter.ts +216 -47
  322. package/src/runtime/plugins/browser-adapter.ts +2662 -829
  323. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  324. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  325. package/src/runtime/plugins/browser-handoff.ts +110 -5
  326. package/src/runtime/plugins/browser-macos-bridge.ts +701 -63
  327. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  328. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  329. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  330. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  331. package/src/runtime/plugins/browser-runtime-contract.ts +154 -0
  332. package/src/runtime/plugins/browser-runtime.ts +193 -0
  333. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  334. package/src/runtime/plugins/browser-session-store.ts +121 -0
  335. package/src/runtime/plugins/browser-session-types.ts +98 -0
  336. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  337. package/src/runtime/plugins/desktop-operator-registration.ts +10 -4
  338. package/src/runtime/plugins/external-adapter.ts +358 -3
  339. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  340. package/src/runtime/plugins/first-party-registry.ts +2 -0
  341. package/src/runtime/plugins/github-adapter.ts +125 -0
  342. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  343. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  344. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  345. package/src/runtime/plugins/ios-adapter.ts +106 -16
  346. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  347. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  348. package/src/runtime/plugins/lightweight-action.ts +153 -0
  349. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  350. package/src/runtime/plugins/macos-capability-broker.ts +47 -9
  351. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  352. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  353. package/src/runtime/plugins/resend-adapter.ts +1 -1
  354. package/src/runtime/plugins/store.ts +330 -33
  355. package/src/runtime/plugins/types.ts +38 -3
  356. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  357. package/src/runtime/projections/git-status-sampler.ts +16 -16
  358. package/src/runtime/projections/materialized-view.ts +28 -10
  359. package/src/runtime/recovery/maintenance-executor.ts +426 -25
  360. package/src/runtime/root/mcp-transport.ts +180 -10
  361. package/src/runtime/root/package-connector-service.ts +230 -10
  362. package/src/runtime/root/package-runtime-release.ts +143 -17
  363. package/src/runtime/root/package-runtime-service.ts +461 -22
  364. package/src/runtime/root/release-manifest.ts +144 -18
  365. package/src/runtime/root/release-materialize.ts +125 -32
  366. package/src/runtime/root/release-store.ts +48 -11
  367. package/src/runtime/root/runtime.ts +23 -5
  368. package/src/runtime/root/service.ts +100 -29
  369. package/src/runtime/root/types.ts +12 -0
  370. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  371. package/src/runtime/shared/trusted-executable.ts +62 -0
  372. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  373. package/src/runtime/standalone-recovery/core.ts +1079 -240
  374. package/src/runtime/standalone-recovery/entry.ts +100 -24
  375. package/src/runtime/standalone-recovery/installer.ts +20 -4
  376. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  377. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  378. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  379. package/src/runtime/workflow/schedules/engine.ts +364 -202
  380. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  381. package/src/runtime/workflow/schedules/settlement.ts +64 -17
  382. package/src/runtime/workflow/schedules/store.ts +209 -19
  383. package/src/runtime/workflow/schedules/types.ts +2 -4
  384. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  385. package/src/testing/test-governance.ts +8 -40
  386. package/assets/reference-configs/heartbeat-triage.md +0 -57
  387. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  388. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  389. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  390. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  391. package/docs/architecture/current/architecture-invariants.md +0 -331
  392. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  393. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  394. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  395. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  396. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  397. package/docs/architecture/current/entity-model.md +0 -612
  398. package/docs/architecture/current/external-plugin-governance.md +0 -396
  399. package/docs/architecture/current/failure-recovery.md +0 -447
  400. package/docs/architecture/current/governance.md +0 -213
  401. package/docs/architecture/current/human-interaction-plane.md +0 -87
  402. package/docs/architecture/current/implementation-status.md +0 -128
  403. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  404. package/docs/architecture/current/legacy-governance.md +0 -58
  405. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  406. package/docs/architecture/current/migration-roadmap.md +0 -105
  407. package/docs/architecture/current/multi-repository-execution.md +0 -371
  408. package/docs/architecture/current/operations-runbook.md +0 -45
  409. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  410. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  411. package/docs/architecture/current/repository-access-modes.md +0 -102
  412. package/docs/architecture/current/route-policy.md +0 -15
  413. package/docs/architecture/current/route-session-concurrency.md +0 -85
  414. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  415. package/docs/architecture/current/runtime-directory-map.md +0 -49
  416. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  417. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  418. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  419. package/docs/architecture/current/system-overview.md +0 -169
  420. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  421. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  422. package/docs/architecture/current/thin-harness-v1.md +0 -321
  423. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  424. package/docs/forge-autonomous-goal-loop.md +0 -9
  425. package/docs/forge-provider-configuration.md +0 -7
  426. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  427. package/docs/operations/gmail-assistant-routines.md +0 -186
  428. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  429. package/docs/operations/provider-configuration.md +0 -31
  430. package/scripts/harness-trace-grade.sh +0 -150
  431. package/scripts/heartbeat-triage.sh +0 -361
  432. package/scripts/maintenance-triage.sh +0 -167
  433. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  434. package/src/cli/repositories/goal-registry.ts +0 -239
  435. package/src/runtime/assistant/action-proposals.ts +0 -279
  436. package/src/runtime/assistant/intent.ts +0 -395
  437. package/src/runtime/assistant/model-provider.ts +0 -460
  438. package/src/runtime/assistant/openapi.ts +0 -223
  439. package/src/runtime/assistant/policy.ts +0 -89
  440. package/src/runtime/assistant/readiness.ts +0 -258
  441. package/src/runtime/assistant/routine-runtime.ts +0 -477
  442. package/src/runtime/assistant/schedule-binding.ts +0 -171
  443. package/src/runtime/assistant/standing-grants.ts +0 -375
  444. package/src/runtime/assistant/store.ts +0 -243
  445. package/src/runtime/assistant/types.ts +0 -135
  446. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  447. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  448. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  449. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  450. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  451. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  452. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  453. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  454. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  455. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  456. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  457. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  458. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  459. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  460. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  461. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  462. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  463. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  464. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  465. package/src/runtime/workflow/findings/store.ts +0 -204
  466. package/src/runtime/workflow/findings/types.ts +0 -58
  467. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  468. package/src/runtime/workflow/portfolio/store.ts +0 -185
  469. 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
+ }