@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
@@ -1,18 +1,21 @@
1
1
  import { execFile } from 'child_process';
2
- import { existsSync, writeFileSync } from 'fs';
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { homedir } from 'os';
4
+ import { basename, extname } from 'path';
4
5
  import {
5
6
  callBrowserAutomationBroker,
6
7
  captureBrowserAutomationRegion,
7
8
  type BrowserAutomationBrokerAction,
9
+ type BrowserAutomationTrustedInput,
8
10
  } from './browser-automation-service';
9
11
  import { AssistantPluginError } from './errors';
12
+ import type { BrowserNativeAttachAttempt, BrowserNativeProduct } from './browser-session-types';
10
13
 
11
14
  const RECORD_SEPARATOR = String.fromCharCode(30);
12
15
  const DEFAULT_NATIVE_TIMEOUT_MS = 5_000;
13
16
  const MAX_NATIVE_OUTPUT_BYTES = 1_048_576;
14
17
 
15
- export type MacOsBrowserProduct = 'chrome' | 'vivaldi';
18
+ export type MacOsBrowserProduct = BrowserNativeProduct;
16
19
 
17
20
  interface MacOsBrowserDefinition {
18
21
  product: MacOsBrowserProduct;
@@ -27,6 +30,20 @@ export interface MacOsBrowserTabRef {
27
30
  tabId: string;
28
31
  }
29
32
 
33
+ export interface MacOsBrowserCreateTabNavigationProvenance {
34
+ provenanceVersion: 1;
35
+ requestedUrl: string;
36
+ assignmentAccepted: true;
37
+ acceptedBy?: string;
38
+ observedUrlAfterAssignment?: string;
39
+ }
40
+
41
+ export interface MacOsBrowserCreateTabEvidence {
42
+ ref: MacOsBrowserTabRef;
43
+ refSource: 'structured' | 'legacy_value';
44
+ navigation?: MacOsBrowserCreateTabNavigationProvenance;
45
+ }
46
+
30
47
  export interface MacOsBrowserMetadata {
31
48
  product: MacOsBrowserProduct;
32
49
  appName: string;
@@ -41,15 +58,22 @@ export interface MacOsBrowserMetadata {
41
58
  loading?: boolean;
42
59
  }
43
60
 
44
- export interface MacOsBrowserAttachAttempt {
61
+ export interface MacOsBrowserTabInventoryEntry {
62
+ windowId: string;
63
+ tabId: string;
64
+ url: string;
65
+ title: string;
66
+ active: boolean;
67
+ }
68
+
69
+ export interface MacOsBrowserTabInventory {
45
70
  product: MacOsBrowserProduct;
46
- appName: string;
47
- bundleId: string;
48
- status: 'selected' | 'available' | 'not_installed' | 'not_running' | 'unavailable';
49
- frontmost?: boolean;
50
- error?: string;
71
+ tabs: MacOsBrowserTabInventoryEntry[];
72
+ truncated: boolean;
51
73
  }
52
74
 
75
+ export type MacOsBrowserAttachAttempt = BrowserNativeAttachAttempt;
76
+
53
77
  export interface MacOsBrowserAttachment {
54
78
  metadata: MacOsBrowserMetadata;
55
79
  attempts: MacOsBrowserAttachAttempt[];
@@ -74,7 +98,9 @@ interface MacOsBrowserRuntimeHooks {
74
98
  // Test-only escape hatches. Production intentionally leaves these undefined so
75
99
  // Apple Events and screen capture are attributed to the stable helper process.
76
100
  runAppleScript?: (script: string, args: string[], timeoutMs: number) => Promise<string>;
101
+ tabInventory?: (product: MacOsBrowserProduct, timeoutMs: number) => Promise<MacOsBrowserTabInventory>;
77
102
  captureRegion?: (region: { x: number; y: number; width: number; height: number }, path: string, timeoutMs: number) => Promise<Buffer>;
103
+ trustedInput?: (request: Extract<BrowserAutomationBrokerAction, { action: 'trusted_input' }>, timeoutMs: number) => Promise<void>;
78
104
  }
79
105
 
80
106
  function execFileText(file: string, args: string[], timeoutMs: number): Promise<string> {
@@ -114,6 +140,87 @@ const defaultRuntimeHooks: MacOsBrowserRuntimeHooks = {
114
140
  },
115
141
  };
116
142
 
143
+ export function parseMacOsBrowserCreateTabBrokerResult(result: Record<string, unknown>): MacOsBrowserCreateTabEvidence {
144
+ const structuredRef = result.ref;
145
+ let ref: MacOsBrowserTabRef;
146
+ let refSource: MacOsBrowserCreateTabEvidence['refSource'];
147
+ if (structuredRef !== undefined) {
148
+ if (!structuredRef || typeof structuredRef !== 'object' || Array.isArray(structuredRef)) {
149
+ throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned an invalid create-tab ref.', { retryable: true });
150
+ }
151
+ const record = structuredRef as Record<string, unknown>;
152
+ if (typeof record.windowId !== 'string' || !record.windowId.trim() || typeof record.tabId !== 'string' || !record.tabId.trim()) {
153
+ throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned an incomplete create-tab ref.', { retryable: true });
154
+ }
155
+ ref = { windowId: record.windowId, tabId: record.tabId };
156
+ refSource = 'structured';
157
+ if (typeof result.value === 'string') {
158
+ const legacyParts = result.value.split(RECORD_SEPARATOR);
159
+ if (legacyParts.length >= 2) {
160
+ const legacyRef = {
161
+ windowId: parseBrowserId(legacyParts[0] ?? '', 'window id'),
162
+ tabId: parseBrowserId(legacyParts[1] ?? '', 'tab id'),
163
+ };
164
+ if (legacyRef.windowId !== ref.windowId || legacyRef.tabId !== ref.tabId) {
165
+ throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned conflicting structured and legacy create-tab refs.', { retryable: true });
166
+ }
167
+ }
168
+ }
169
+ } else {
170
+ if (typeof result.value !== 'string') {
171
+ throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned no create-tab ref.', { retryable: true });
172
+ }
173
+ const parts = result.value.split(RECORD_SEPARATOR);
174
+ if (parts.length < 2) throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned incomplete legacy create-tab metadata.', { retryable: true });
175
+ ref = { windowId: parseBrowserId(parts[0] ?? '', 'window id'), tabId: parseBrowserId(parts[1] ?? '', 'tab id') };
176
+ refSource = 'legacy_value';
177
+ }
178
+
179
+ const navigationValue = result.navigation;
180
+ if (navigationValue === undefined) return { ref, refSource };
181
+ if (!navigationValue || typeof navigationValue !== 'object' || Array.isArray(navigationValue)) {
182
+ throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned invalid create-tab navigation provenance.', { retryable: true });
183
+ }
184
+ const navigation = navigationValue as Record<string, unknown>;
185
+ if (navigation.provenanceVersion !== 1
186
+ || typeof navigation.requestedUrl !== 'string'
187
+ || navigation.assignmentAccepted !== true) {
188
+ throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned incomplete create-tab navigation provenance.', { retryable: true });
189
+ }
190
+ if (navigation.acceptedBy !== undefined && typeof navigation.acceptedBy !== 'string') {
191
+ throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned invalid create-tab acceptedBy provenance.', { retryable: true });
192
+ }
193
+ if (navigation.observedUrlAfterAssignment !== undefined && typeof navigation.observedUrlAfterAssignment !== 'string') {
194
+ throw new AssistantPluginError('PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR', 'Stable Forge macOS capability broker returned invalid create-tab observed URL provenance.', { retryable: true });
195
+ }
196
+ const acceptedBy = typeof navigation.acceptedBy === 'string' ? navigation.acceptedBy : undefined;
197
+ const observedUrlAfterAssignment = typeof navigation.observedUrlAfterAssignment === 'string' ? navigation.observedUrlAfterAssignment : undefined;
198
+ return {
199
+ ref,
200
+ refSource,
201
+ navigation: {
202
+ provenanceVersion: 1,
203
+ requestedUrl: navigation.requestedUrl,
204
+ assignmentAccepted: true,
205
+ ...(acceptedBy ? { acceptedBy } : {}),
206
+ ...(observedUrlAfterAssignment ? { observedUrlAfterAssignment } : {}),
207
+ },
208
+ };
209
+ }
210
+
211
+ async function runCreateTabAutomation(
212
+ request: Extract<BrowserAutomationBrokerAction, { action: 'create_tab' }>,
213
+ testScript: string,
214
+ testArgs: string[],
215
+ timeoutMs: number,
216
+ ): Promise<MacOsBrowserCreateTabEvidence> {
217
+ if (runtimeHooks.runAppleScript) {
218
+ const value = await runtimeHooks.runAppleScript(testScript, testArgs, timeoutMs);
219
+ return parseMacOsBrowserCreateTabBrokerResult({ value });
220
+ }
221
+ return parseMacOsBrowserCreateTabBrokerResult(await callBrowserAutomationBroker(request, timeoutMs));
222
+ }
223
+
117
224
  async function runBrowserAutomationText(
118
225
  request: BrowserAutomationBrokerAction,
119
226
  testScript: string,
@@ -143,8 +250,72 @@ async function captureBrowserAutomation(
143
250
  return bytes;
144
251
  }
145
252
 
253
+ async function sendBrowserTrustedInput(
254
+ request: Extract<BrowserAutomationBrokerAction, { action: 'trusted_input' }>,
255
+ timeoutMs: number,
256
+ ): Promise<void> {
257
+ if (runtimeHooks.trustedInput) {
258
+ await runtimeHooks.trustedInput(request, timeoutMs);
259
+ return;
260
+ }
261
+ let result: Record<string, unknown>;
262
+ try {
263
+ result = await callBrowserAutomationBroker(request, timeoutMs);
264
+ } catch (error) {
265
+ const message = error instanceof Error ? error.message : String(error ?? '');
266
+ if ((error instanceof AssistantPluginError && error.code === 'BROWSER_AUTOMATION_ACTION_UNSUPPORTED')
267
+ || /\bBROWSER_AUTOMATION_ACTION_UNSUPPORTED\b/.test(message)) {
268
+ throw new AssistantPluginError(
269
+ 'PLUGIN_BROWSER_TRUSTED_INPUT_UNAVAILABLE',
270
+ 'Installed Forge Desktop Operator does not support native browser trusted input. Keep the exact tab explicitly foregrounded and use a separately grounded Desktop Operator interaction, or upgrade the stable Desktop Operator provider.',
271
+ { retryable: false, details: { browserProduct: request.product, requiredProviderAction: 'trusted_input' } },
272
+ );
273
+ }
274
+ throw error;
275
+ }
276
+ if (result.performed !== true) {
277
+ throw new AssistantPluginError(
278
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
279
+ 'Stable Forge macOS capability broker did not confirm trusted browser input.',
280
+ { retryable: true },
281
+ );
282
+ }
283
+ }
284
+
146
285
  let runtimeHooks: MacOsBrowserRuntimeHooks = { ...defaultRuntimeHooks };
147
286
  let lastAttachObservation: MacOsBrowserAttachObservation | undefined;
287
+ const MAX_WARM_NATIVE_PAGE_HANDLES = 128;
288
+ const warmNativePageHandles = new Map<string, MacOsAppleEventsPage>();
289
+
290
+ function nativePageHandleKey(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef): string {
291
+ // tabId is the stable browser entity. The windowId is only a mutable placement hint and
292
+ // is re-resolved from live inventory immediately before exact-target Apple Events operations.
293
+ return `${product}:${ref.tabId}`;
294
+ }
295
+
296
+ function rememberMacOsBrowserPageHandle(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef, page: MacOsAppleEventsPage): void {
297
+ const key = nativePageHandleKey(product, ref);
298
+ warmNativePageHandles.delete(key);
299
+ warmNativePageHandles.set(key, page);
300
+ while (warmNativePageHandles.size > MAX_WARM_NATIVE_PAGE_HANDLES) {
301
+ const oldest = warmNativePageHandles.keys().next().value;
302
+ if (typeof oldest !== 'string') break;
303
+ warmNativePageHandles.delete(oldest);
304
+ }
305
+ }
306
+
307
+ export function invalidateMacOsBrowserPageHandle(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef): void {
308
+ warmNativePageHandles.delete(nativePageHandleKey(product, ref));
309
+ }
310
+
311
+ export function invalidateMacOsBrowserPageHandles(): void {
312
+ warmNativePageHandles.clear();
313
+ }
314
+
315
+ export function macOsBrowserPageHandleStale(error: unknown): boolean {
316
+ const message = error instanceof Error ? error.message : String(error ?? '');
317
+ return /FORGE_BROWSER_TAB_NOT_FOUND|BROWSER_AUTOMATION_TAB_NOT_FOUND|target tab.*not found|can[’']t get tab .*invalid index.*-1719/i.test(message);
318
+ }
148
319
 
149
320
  export function setMacOsBrowserRuntimeHooksForTest(hooks: Partial<MacOsBrowserRuntimeHooks>): void {
150
321
  runtimeHooks = { ...defaultRuntimeHooks, ...hooks };
@@ -153,6 +324,7 @@ export function setMacOsBrowserRuntimeHooksForTest(hooks: Partial<MacOsBrowserRu
153
324
  export function resetMacOsBrowserRuntimeHooksForTest(): void {
154
325
  runtimeHooks = { ...defaultRuntimeHooks };
155
326
  lastAttachObservation = undefined;
327
+ warmNativePageHandles.clear();
156
328
  }
157
329
 
158
330
  export function getMacOsBrowserAttachObservation(): MacOsBrowserAttachObservation | undefined {
@@ -197,12 +369,120 @@ set targetWindow to front window
197
369
  set targetTab to active tab of targetWindow
198
370
  set windowBounds to bounds of targetWindow
199
371
  set separator to ASCII character 30
200
- return (frontmost as text) & separator & (URL of targetTab as text) & separator & (title of targetTab as text) & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text)
372
+ return (frontmost as text) & separator & (URL of targetTab as text) & separator & (title of targetTab as text) & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text) & separator & ((id of targetWindow) as text) & separator & ((id of targetTab) as text) & separator & "true" & separator & (loading of targetTab as text)
201
373
  `);
202
374
  }
203
375
 
376
+ function listTabsScript(browser: MacOsBrowserDefinition): string {
377
+ return `on replaceText(sourceText, needle, replacement)
378
+ set previousDelimiters to AppleScript's text item delimiters
379
+ set AppleScript's text item delimiters to needle
380
+ set sourceItems to every text item of sourceText
381
+ set AppleScript's text item delimiters to replacement
382
+ set resultText to sourceItems as text
383
+ set AppleScript's text item delimiters to previousDelimiters
384
+ return resultText
385
+ end replaceText
386
+
387
+ on cleanField(sourceText, recordSeparator, fieldSeparator)
388
+ set cleaned to my replaceText(sourceText as text, recordSeparator, " ")
389
+ return my replaceText(cleaned, fieldSeparator, " ")
390
+ end cleanField
391
+
392
+ ${browserTellScript(browser, `
393
+ set recordSeparator to ASCII character 30
394
+ set fieldSeparator to ASCII character 31
395
+ set maxTabs to 256
396
+ set returnedCount to 0
397
+ set truncatedInventory to false
398
+ set outputText to "false"
399
+ repeat with candidateWindow in windows
400
+ set activeTabId to ""
401
+ try
402
+ set activeTabId to ((id of active tab of candidateWindow) as text)
403
+ end try
404
+ repeat with candidateTab in tabs of candidateWindow
405
+ if returnedCount is greater than or equal to maxTabs then
406
+ set truncatedInventory to true
407
+ exit repeat
408
+ end if
409
+ set candidateWindowId to ((id of candidateWindow) as text)
410
+ set candidateTabId to ((id of candidateTab) as text)
411
+ set candidateURL to my cleanField((URL of candidateTab as text), recordSeparator, fieldSeparator)
412
+ set candidateTitle to my cleanField((title of candidateTab as text), recordSeparator, fieldSeparator)
413
+ set candidateActive to (candidateTabId is activeTabId)
414
+ set outputText to outputText & recordSeparator & candidateWindowId & fieldSeparator & candidateTabId & fieldSeparator & (candidateActive as text) & fieldSeparator & candidateURL & fieldSeparator & candidateTitle
415
+ set returnedCount to returnedCount + 1
416
+ end repeat
417
+ if truncatedInventory then exit repeat
418
+ end repeat
419
+ if truncatedInventory then
420
+ set outputText to "true" & text 6 thru -1 of outputText
421
+ end if
422
+ return outputText
423
+ `)}
424
+ `;
425
+ }
426
+
427
+ function parseTabInventory(product: MacOsBrowserProduct, raw: string): MacOsBrowserTabInventory {
428
+ const records = raw.split(RECORD_SEPARATOR);
429
+ const marker = (records.shift() ?? '').trim().toLowerCase();
430
+ if (marker !== 'true' && marker !== 'false') {
431
+ throw new AssistantPluginError(
432
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
433
+ 'Stable Forge macOS capability broker returned an invalid browser tab inventory marker.',
434
+ { retryable: true },
435
+ );
436
+ }
437
+ const tabs: MacOsBrowserTabInventoryEntry[] = [];
438
+ const fieldSeparator = String.fromCharCode(31);
439
+ for (const record of records) {
440
+ if (!record) continue;
441
+ const fields = record.split(fieldSeparator);
442
+ if (fields.length < 5) {
443
+ throw new AssistantPluginError(
444
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
445
+ 'Stable Forge macOS capability broker returned incomplete browser tab inventory metadata.',
446
+ { retryable: true },
447
+ );
448
+ }
449
+ const windowId = (fields[0] ?? '').trim();
450
+ const tabId = (fields[1] ?? '').trim();
451
+ const activeText = (fields[2] ?? '').trim().toLowerCase();
452
+ if (!windowId || !tabId || (activeText !== 'true' && activeText !== 'false')) {
453
+ throw new AssistantPluginError(
454
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
455
+ 'Stable Forge macOS capability broker returned invalid browser tab inventory metadata.',
456
+ { retryable: true },
457
+ );
458
+ }
459
+ tabs.push({ windowId, tabId, active: activeText === 'true', url: fields[3] ?? '', title: fields.slice(4).join(fieldSeparator) });
460
+ if (tabs.length > 256) {
461
+ throw new AssistantPluginError(
462
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
463
+ 'Stable Forge macOS capability broker exceeded the bounded browser tab inventory size.',
464
+ { retryable: true },
465
+ );
466
+ }
467
+ }
468
+ return { product, tabs, truncated: marker === 'true' };
469
+ }
470
+
204
471
  function targetTabPreamble(ref: MacOsBrowserTabRef): string {
205
- return `set targetWindow to first window whose id is ${quotedAppleScript(ref.windowId)}\nset targetTab to first tab of targetWindow whose id is ${quotedAppleScript(ref.tabId)}`;
472
+ return `set targetTabId to ${quotedAppleScript(ref.tabId)}
473
+ set targetWindow to missing value
474
+ set targetTab to missing value
475
+ repeat with candidateWindow in windows
476
+ repeat with candidateTab in tabs of candidateWindow
477
+ if ((id of candidateTab) as text) is targetTabId then
478
+ set targetWindow to candidateWindow
479
+ set targetTab to candidateTab
480
+ exit repeat
481
+ end if
482
+ end repeat
483
+ if targetTab is not missing value then exit repeat
484
+ end repeat
485
+ if targetTab is missing value then error "FORGE_BROWSER_TAB_NOT_FOUND:" & targetTabId`;
206
486
  }
207
487
 
208
488
  function targetMetadataScript(browser: MacOsBrowserDefinition, ref: MacOsBrowserTabRef): string {
@@ -211,7 +491,7 @@ ${targetTabPreamble(ref)}
211
491
  set windowBounds to bounds of targetWindow
212
492
  set separator to ASCII character 30
213
493
  set targetIsActive to ((id of active tab of targetWindow) is (id of targetTab))
214
- return (frontmost as text) & separator & (URL of targetTab as text) & separator & "" & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text) & separator & "" & separator & "" & separator & (targetIsActive as text) & separator & (loading of targetTab as text)
494
+ return (frontmost as text) & separator & (URL of targetTab as text) & separator & "" & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text) & separator & ((id of targetWindow) as text) & separator & ((id of targetTab) as text) & separator & (targetIsActive as text) & separator & (loading of targetTab as text)
215
495
  `);
216
496
  }
217
497
 
@@ -416,7 +696,8 @@ function serializeEvaluation(expression: string | ((...args: unknown[]) => unkno
416
696
  return `(() => {
417
697
  try {
418
698
  const value = (${source});
419
- return JSON.stringify({ ok: true, value: value === undefined ? { __forgeUndefined: true } : value });
699
+ const page = { url: String(document.location.href || ''), title: String(document.title || '') };
700
+ return JSON.stringify({ ok: true, value: value === undefined ? { __forgeUndefined: true } : value, page });
420
701
  } catch (error) {
421
702
  return JSON.stringify({ ok: false, error: String(error && (error.stack || error.message) || error) });
422
703
  }
@@ -464,27 +745,53 @@ function keyEventSource(selector: string | undefined, keySpec: string): string {
464
745
  })()`;
465
746
  }
466
747
 
748
+ export function nativeDomLoadStateSatisfied(
749
+ state: string,
750
+ readyState: string,
751
+ currentUrl: string,
752
+ requireHttpUrl: boolean,
753
+ ): boolean {
754
+ if (requireHttpUrl && !/^https?:\/\//i.test(currentUrl ?? '')) return false;
755
+ return state === 'load' || state === 'networkidle'
756
+ ? readyState === 'complete'
757
+ : readyState === 'interactive' || readyState === 'complete';
758
+ }
759
+
467
760
  export class MacOsAppleEventsPage {
468
761
  private metadata: MacOsBrowserMetadata;
469
762
  private readonly browser: MacOsBrowserDefinition;
470
- private readonly timeoutMs: number;
471
- private readonly targetRef?: MacOsBrowserTabRef;
763
+ private timeoutMs: number;
764
+ private targetRef?: MacOsBrowserTabRef;
765
+ private readonly createTabEvidence?: MacOsBrowserCreateTabEvidence;
472
766
 
473
- constructor(attachment: MacOsBrowserAttachment, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS, targetRef?: MacOsBrowserTabRef) {
767
+ constructor(attachment: MacOsBrowserAttachment, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS, targetRef?: MacOsBrowserTabRef, createTabEvidence?: MacOsBrowserCreateTabEvidence) {
474
768
  this.metadata = { ...attachment.metadata, ...(targetRef ? { windowId: targetRef.windowId, tabId: targetRef.tabId } : {}) };
475
769
  this.browser = browserDefinition(attachment.metadata.product);
476
770
  this.timeoutMs = timeoutMs;
477
771
  this.targetRef = targetRef;
772
+ this.createTabEvidence = createTabEvidence ? {
773
+ ...createTabEvidence,
774
+ ref: { ...createTabEvidence.ref },
775
+ ...(createTabEvidence.navigation ? { navigation: { ...createTabEvidence.navigation } } : {}),
776
+ } : undefined;
478
777
  }
479
778
 
480
779
  private async runAutomation(
481
780
  request: BrowserAutomationBrokerAction,
482
- script: string,
781
+ script: string | ((resolvedRef?: MacOsBrowserTabRef) => string),
483
782
  args: string[] = [],
484
783
  timeoutMs = this.timeoutMs,
485
784
  ): Promise<string> {
486
785
  try {
487
- return await runBrowserAutomationText(request, script, args, timeoutMs);
786
+ let effectiveRequest = request;
787
+ let resolvedRef: MacOsBrowserTabRef | undefined;
788
+ if ('ref' in request && request.ref) {
789
+ resolvedRef = await resolveCurrentMacOsBrowserTabRef(this.browser.product, request.ref, timeoutMs);
790
+ this.targetRef = resolvedRef;
791
+ effectiveRequest = { ...request, ref: resolvedRef } as BrowserAutomationBrokerAction;
792
+ }
793
+ const effectiveScript = typeof script === 'function' ? script(resolvedRef ?? this.targetRef) : script;
794
+ return await runBrowserAutomationText(effectiveRequest, effectiveScript, args, timeoutMs);
488
795
  } catch (error) {
489
796
  const message = error instanceof Error ? error.message : String(error);
490
797
  throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OPERATION_FAILED', `${this.browser.appName} Apple Events operation failed: ${message}`, {
@@ -495,11 +802,13 @@ export class MacOsAppleEventsPage {
495
802
  }
496
803
 
497
804
  private async refreshMetadata(): Promise<MacOsBrowserMetadata> {
498
- const script = this.targetRef ? targetMetadataScript(this.browser, this.targetRef) : metadataScript(this.browser);
499
805
  this.metadata = parseMetadata(this.browser.product, await this.runAutomation(
500
806
  { action: 'metadata', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}) },
501
- script,
807
+ (resolvedRef) => resolvedRef ? targetMetadataScript(this.browser, resolvedRef) : metadataScript(this.browser),
502
808
  ));
809
+ if (this.targetRef && this.metadata.windowId && this.metadata.tabId) {
810
+ this.targetRef = { windowId: this.metadata.windowId, tabId: this.metadata.tabId };
811
+ }
503
812
  return this.metadata;
504
813
  }
505
814
 
@@ -507,7 +816,7 @@ export class MacOsAppleEventsPage {
507
816
  try {
508
817
  return await this.runAutomation(
509
818
  { action: 'execute_javascript', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}), source },
510
- executeJavaScriptScript(this.browser, this.targetRef),
819
+ (resolvedRef) => executeJavaScriptScript(this.browser, resolvedRef),
511
820
  [source],
512
821
  timeoutMs,
513
822
  );
@@ -527,18 +836,36 @@ export class MacOsAppleEventsPage {
527
836
  return this.targetRef ? { ...this.targetRef } : undefined;
528
837
  }
529
838
 
839
+ creationEvidence(): MacOsBrowserCreateTabEvidence | undefined {
840
+ return this.createTabEvidence ? {
841
+ ...this.createTabEvidence,
842
+ ref: { ...this.createTabEvidence.ref },
843
+ ...(this.createTabEvidence.navigation ? { navigation: { ...this.createTabEvidence.navigation } } : {}),
844
+ } : undefined;
845
+ }
846
+
847
+ updateTimeout(timeoutMs: number): void {
848
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) this.timeoutMs = Math.trunc(timeoutMs);
849
+ }
850
+
851
+ attachmentSnapshot(): MacOsBrowserAttachment {
852
+ return {
853
+ metadata: { ...this.metadata, bounds: { ...this.metadata.bounds } },
854
+ attempts: [{
855
+ product: this.browser.product,
856
+ appName: this.browser.appName,
857
+ bundleId: this.browser.bundleId,
858
+ status: 'selected',
859
+ frontmost: this.metadata.frontmost,
860
+ }],
861
+ };
862
+ }
863
+
530
864
  async goto(url: string, options: Record<string, unknown> = {}): Promise<unknown> {
531
865
  const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
532
- if (this.targetRef) {
533
- throw new AssistantPluginError(
534
- 'PLUGIN_BROWSER_BACKGROUND_NAVIGATION_REQUIRES_REPLACEMENT',
535
- 'Chrome/Vivaldi background tabs cannot be navigated reliably in place through Apple Events; create a replacement plugin-owned tab instead.',
536
- { retryable: true, details: { browserProduct: this.browser.product, windowId: this.targetRef.windowId, tabId: this.targetRef.tabId } },
537
- );
538
- }
539
866
  await this.runAutomation(
540
- { action: 'navigate', product: this.browser.product, url },
541
- navigateScript(this.browser),
867
+ { action: 'navigate', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}), url },
868
+ (resolvedRef) => navigateScript(this.browser, resolvedRef),
542
869
  [url],
543
870
  timeout,
544
871
  );
@@ -551,7 +878,7 @@ export class MacOsAppleEventsPage {
551
878
  const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
552
879
  await this.runAutomation(
553
880
  { action: 'reload', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}) },
554
- reloadScript(this.browser, this.targetRef),
881
+ (resolvedRef) => reloadScript(this.browser, resolvedRef),
555
882
  [],
556
883
  timeout,
557
884
  );
@@ -609,21 +936,183 @@ export class MacOsAppleEventsPage {
609
936
 
610
937
  async evaluate<T>(expression: string | ((...args: unknown[]) => unknown), arg?: unknown): Promise<T> {
611
938
  const raw = await this.executeJavaScriptRaw(serializeEvaluation(expression, arg));
612
- let parsed: { ok?: boolean; value?: unknown; error?: string };
939
+ let parsed: { ok?: boolean; value?: unknown; error?: string; page?: { url?: unknown; title?: unknown } };
613
940
  try {
614
- parsed = JSON.parse(raw) as { ok?: boolean; value?: unknown; error?: string };
941
+ parsed = JSON.parse(raw) as { ok?: boolean; value?: unknown; error?: string; page?: { url?: unknown; title?: unknown } };
615
942
  } catch {
616
943
  throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_PROTOCOL_ERROR', `${this.browser.appName} returned an invalid JavaScript result.`, {
617
944
  retryable: true,
618
945
  });
619
946
  }
620
947
  if (!parsed.ok) throw new Error(parsed.error || 'Browser JavaScript evaluation failed.');
948
+ if (parsed.page && typeof parsed.page.url === 'string' && parsed.page.url) {
949
+ this.metadata = {
950
+ ...this.metadata,
951
+ url: parsed.page.url,
952
+ title: typeof parsed.page.title === 'string' ? parsed.page.title : this.metadata.title,
953
+ };
954
+ }
621
955
  if (parsed.value && typeof parsed.value === 'object' && (parsed.value as { __forgeUndefined?: unknown }).__forgeUndefined === true) {
622
956
  return undefined as T;
623
957
  }
624
958
  return parsed.value as T;
625
959
  }
626
960
 
961
+ async setInputFiles(selector: string, files: string | string[]): Promise<void> {
962
+ const filePaths = Array.isArray(files) ? files : [files];
963
+ if (filePaths.length === 0 || filePaths.length > 32) {
964
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Native browser file input accepts 1-32 local files.', { retryable: false });
965
+ }
966
+ for (const filePath of filePaths) {
967
+ if (!filePath || !existsSync(filePath)) {
968
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Local file does not exist.', { retryable: false });
969
+ }
970
+ }
971
+
972
+ if (filePaths.length === 1) {
973
+ const filePath = filePaths[0];
974
+ const bytes = readFileSync(filePath);
975
+ const fileName = basename(filePath);
976
+ const extension = extname(fileName).toLowerCase();
977
+ const mimeType = extension === '.pdf' ? 'application/pdf'
978
+ : extension === '.zip' ? 'application/zip'
979
+ : extension === '.png' ? 'image/png'
980
+ : extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
981
+ : 'application/octet-stream';
982
+ const bufferKey = `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`;
983
+ const encoded = bytes.toString('base64');
984
+ const chunkChars = 192 * 1024;
985
+ try {
986
+ await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] = ''; return true; })()`);
987
+ for (let offset = 0; offset < encoded.length; offset += chunkChars) {
988
+ const chunk = encoded.slice(offset, offset + chunkChars);
989
+ await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
990
+ }
991
+ const attached = await this.evaluate<{ name?: string; size?: number }>(`(() => {
992
+ const element = document.querySelector(${JSON.stringify(selector)});
993
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
994
+ const expectedName = ${JSON.stringify(fileName)};
995
+ const expectedSize = ${bytes.length};
996
+ const encoded = String(globalThis[${JSON.stringify(bufferKey)}] || '');
997
+ const binary = atob(encoded);
998
+ const data = new Uint8Array(binary.length);
999
+ for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
1000
+ const file = new File([data], expectedName, { type: ${JSON.stringify(mimeType)} });
1001
+ const transfer = new DataTransfer();
1002
+ transfer.items.add(file);
1003
+ element.files = transfer.files;
1004
+ element.dispatchEvent(new Event('input', { bubbles: true }));
1005
+ element.dispatchEvent(new Event('change', { bubbles: true }));
1006
+ const selected = element.files && element.files[0];
1007
+ return selected ? { name: selected.name, size: selected.size, expectedName, expectedSize } : null;
1008
+ })()`);
1009
+ if (attached?.name !== fileName || attached?.size !== bytes.length) {
1010
+ throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local file on the target input.', {
1011
+ retryable: true,
1012
+ details: { expectedName: fileName, expectedSize: bytes.length, actualName: attached?.name, actualSize: attached?.size },
1013
+ });
1014
+ }
1015
+ } finally {
1016
+ await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
1017
+ }
1018
+ return;
1019
+ }
1020
+
1021
+ const attachments = filePaths.map((filePath) => {
1022
+ const bytes = readFileSync(filePath);
1023
+ const fileName = basename(filePath);
1024
+ const extension = extname(fileName).toLowerCase();
1025
+ const mimeType = extension === '.pdf' ? 'application/pdf'
1026
+ : extension === '.zip' ? 'application/zip'
1027
+ : extension === '.png' ? 'image/png'
1028
+ : extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
1029
+ : 'application/octet-stream';
1030
+ return {
1031
+ bytes,
1032
+ fileName,
1033
+ mimeType,
1034
+ bufferKey: `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`,
1035
+ };
1036
+ });
1037
+
1038
+ if (attachments.length > 1) {
1039
+ await this.evaluate(`(() => {
1040
+ const element = document.querySelector(${JSON.stringify(selector)});
1041
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
1042
+ if (!element.multiple) throw new Error('Target input does not allow multiple files.');
1043
+ const transfer = new DataTransfer();
1044
+ element.files = transfer.files;
1045
+ return true;
1046
+ })()`);
1047
+ }
1048
+
1049
+ const chunkChars = 192 * 1024;
1050
+ try {
1051
+ for (let index = 0; index < attachments.length; index += 1) {
1052
+ const attachment = attachments[index];
1053
+ const encoded = attachment.bytes.toString('base64');
1054
+ await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] = ''; return true; })()`);
1055
+ for (let offset = 0; offset < encoded.length; offset += chunkChars) {
1056
+ const chunk = encoded.slice(offset, offset + chunkChars);
1057
+ await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
1058
+ }
1059
+ const append = attachments.length > 1 && index > 0;
1060
+ await this.evaluate(`(() => {
1061
+ const element = document.querySelector(${JSON.stringify(selector)});
1062
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
1063
+ const expectedName = ${JSON.stringify(attachment.fileName)};
1064
+ const expectedSize = ${attachment.bytes.length};
1065
+ const encoded = String(globalThis[${JSON.stringify(attachment.bufferKey)}] || '');
1066
+ const binary = atob(encoded);
1067
+ const data = new Uint8Array(binary.length);
1068
+ for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
1069
+ const file = new File([data], expectedName, { type: ${JSON.stringify(attachment.mimeType)} });
1070
+ const transfer = new DataTransfer();
1071
+ if (${append ? 'true' : 'false'}) {
1072
+ for (const existing of Array.from(element.files || [])) transfer.items.add(existing);
1073
+ }
1074
+ transfer.items.add(file);
1075
+ element.files = transfer.files;
1076
+ return Array.from(element.files || []).map((selected) => ({ name: selected.name, size: selected.size }));
1077
+ })()`);
1078
+ }
1079
+
1080
+ const selected = await this.evaluate<Array<{ name?: string; size?: number }>>(`(() => {
1081
+ const element = document.querySelector(${JSON.stringify(selector)});
1082
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
1083
+ element.dispatchEvent(new Event('input', { bubbles: true }));
1084
+ element.dispatchEvent(new Event('change', { bubbles: true }));
1085
+ return Array.from(element.files || []).map((file) => ({ name: file.name, size: file.size }));
1086
+ })()`);
1087
+ const expected = attachments.map((attachment) => ({ name: attachment.fileName, size: attachment.bytes.length }));
1088
+ const matches = selected.length === expected.length && expected.every((item, index) => selected[index]?.name === item.name && selected[index]?.size === item.size);
1089
+ if (!matches) {
1090
+ throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local files on the target input.', {
1091
+ retryable: true,
1092
+ details: { expected, actual: selected },
1093
+ });
1094
+ }
1095
+ } finally {
1096
+ for (const attachment of attachments) {
1097
+ await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(attachment.bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ async foregroundState(): Promise<{ frontmost: boolean; active: boolean }> {
1103
+ const metadata = await this.refreshMetadata();
1104
+ return { frontmost: metadata.frontmost, active: metadata.active === true };
1105
+ }
1106
+
1107
+ async trustedInput(input: BrowserAutomationTrustedInput): Promise<void> {
1108
+ if (!this.targetRef) {
1109
+ throw new AssistantPluginError('PLUGIN_BROWSER_TRUSTED_INPUT_UNAVAILABLE', 'Native trusted input requires an exact saved browser tab.', { retryable: false });
1110
+ }
1111
+ this.targetRef = await resolveCurrentMacOsBrowserTabRef(this.browser.product, this.targetRef, this.timeoutMs);
1112
+ await sendBrowserTrustedInput({ action: 'trusted_input', product: this.browser.product, ref: this.targetRef, input }, this.timeoutMs);
1113
+ await this.refreshMetadata();
1114
+ }
1115
+
627
1116
  async screenshot(options: Record<string, unknown>): Promise<Buffer> {
628
1117
  const path = typeof options.path === 'string' ? options.path : undefined;
629
1118
  if (!path) throw new Error('Native browser screenshot requires an output path.');
@@ -638,7 +1127,40 @@ export class MacOsAppleEventsPage {
638
1127
  }
639
1128
 
640
1129
  async click(selector: string): Promise<void> {
641
- await this.evaluate(selectorSource(selector, 'if (typeof element.click !== "function") throw new Error("Element is not clickable."); element.click(); return true;'));
1130
+ await this.evaluate(selectorSource(selector, `
1131
+ if (element instanceof HTMLOptionElement) {
1132
+ const selectElement = element.closest('select');
1133
+ if (!(selectElement instanceof HTMLSelectElement)) throw new Error('Option is not attached to a select.');
1134
+ if (element.disabled || selectElement.disabled) throw new Error('Option is disabled.');
1135
+ if (!selectElement.multiple) {
1136
+ const valueSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
1137
+ if (valueSetter) valueSetter.call(selectElement, element.value);
1138
+ else selectElement.value = element.value;
1139
+ } else {
1140
+ element.selected = true;
1141
+ }
1142
+ selectElement.dispatchEvent(new Event('input', { bubbles: true }));
1143
+ selectElement.dispatchEvent(new Event('change', { bubbles: true }));
1144
+ if (!Array.from(selectElement.selectedOptions).includes(element)) throw new Error('Option selection did not persist.');
1145
+ return true;
1146
+ }
1147
+ if (typeof element.click !== "function") throw new Error("Element is not clickable.");
1148
+ const rect = typeof element.getBoundingClientRect === 'function' ? element.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 };
1149
+ const clientX = Number(rect.left || 0) + Number(rect.width || 0) / 2;
1150
+ const clientY = Number(rect.top || 0) + Number(rect.height || 0) / 2;
1151
+ const mouseInit = { bubbles: true, cancelable: true, composed: true, view: window, button: 0, buttons: 1, clientX, clientY };
1152
+ if (typeof PointerEvent === 'function') {
1153
+ element.dispatchEvent(new PointerEvent('pointerdown', { ...mouseInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
1154
+ }
1155
+ element.dispatchEvent(new MouseEvent('mousedown', mouseInit));
1156
+ if (typeof element.focus === 'function') element.focus();
1157
+ if (typeof PointerEvent === 'function') {
1158
+ element.dispatchEvent(new PointerEvent('pointerup', { ...mouseInit, buttons: 0, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
1159
+ }
1160
+ element.dispatchEvent(new MouseEvent('mouseup', { ...mouseInit, buttons: 0 }));
1161
+ element.click();
1162
+ return true;
1163
+ `));
642
1164
  }
643
1165
 
644
1166
  async dblclick(selector: string): Promise<void> {
@@ -681,10 +1203,20 @@ export class MacOsAppleEventsPage {
681
1203
  return await this.evaluate(selectorSource(selector, `
682
1204
  if (!(element instanceof HTMLSelectElement)) throw new Error('Element is not a select.');
683
1205
  const selected = new Set(${JSON.stringify(normalized)});
684
- for (const option of element.options) option.selected = selected.has(option.value) || selected.has(option.label);
1206
+ let matched = 0;
1207
+ for (const option of element.options) {
1208
+ const shouldSelect = selected.has(option.value) || selected.has(option.label);
1209
+ option.selected = shouldSelect;
1210
+ if (shouldSelect) matched += 1;
1211
+ }
1212
+ if (matched === 0) throw new Error('Requested select option was not found.');
685
1213
  element.dispatchEvent(new Event('input', { bubbles: true }));
686
1214
  element.dispatchEvent(new Event('change', { bubbles: true }));
687
- return Array.from(element.selectedOptions).map((option) => option.value);
1215
+ const actual = Array.from(element.selectedOptions);
1216
+ if (!actual.some((option) => selected.has(option.value) || selected.has(option.label))) {
1217
+ throw new Error('Requested select option did not persist.');
1218
+ }
1219
+ return actual.map((option) => option.value);
688
1220
  `));
689
1221
  }
690
1222
 
@@ -730,15 +1262,19 @@ export class MacOsAppleEventsPage {
730
1262
 
731
1263
  async waitForLoadState(state = 'domcontentloaded', options: Record<string, unknown> = {}): Promise<void> {
732
1264
  const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
1265
+ const requireHttpUrl = options.requireHttpUrl === true;
733
1266
  const deadline = Date.now() + timeout;
734
1267
  while (Date.now() <= deadline) {
735
1268
  if (this.targetRef) {
736
1269
  try {
737
1270
  const metadata = await this.refreshMetadata();
738
- const transitionalUrl = !metadata.url
739
- || metadata.url === 'about:blank'
740
- || metadata.url === 'chrome://newtab/'
741
- || metadata.url === 'vivaldi://newtab/';
1271
+ // Browser actions only admit HTTP(S) targets. Native Chrome/Vivaldi can
1272
+ // expose several internal URLs (new-tab, error/interstitial, extension
1273
+ // bootstrap pages) while navigation is still settling. Treat every
1274
+ // non-HTTP(S) metadata URL as transitional here; the adapter still
1275
+ // applies the strict final HTTP(S) URL validator before persisting a
1276
+ // session, so this does not widen the accepted navigation surface.
1277
+ const transitionalUrl = !/^https?:\/\//i.test(metadata.url ?? '');
742
1278
  if (transitionalUrl) {
743
1279
  await new Promise((resolve) => setTimeout(resolve, 100));
744
1280
  continue;
@@ -749,20 +1285,24 @@ export class MacOsAppleEventsPage {
749
1285
  }
750
1286
  }
751
1287
  try {
752
- const readyState = await this.evaluate<string>('document.readyState');
753
- if (state === 'load' || state === 'networkidle') {
754
- if (readyState === 'complete') return;
755
- } else if (readyState === 'interactive' || readyState === 'complete') return;
1288
+ const domState = await this.evaluate<{ readyState: string; url: string }>(
1289
+ '({ readyState: String(document.readyState || ""), url: String(document.location.href || "") })',
1290
+ );
1291
+ if (nativeDomLoadStateSatisfied(state, domState.readyState, domState.url, requireHttpUrl)) return;
756
1292
  } catch (error) {
757
1293
  if (error instanceof AssistantPluginError && error.code === 'PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED') {
758
1294
  const metadata = await this.refreshMetadata();
759
- if (metadata.url && metadata.url !== 'about:blank' && metadata.url !== 'chrome://newtab/' && metadata.url !== 'vivaldi://newtab/') return;
1295
+ if (/^https?:\/\//i.test(metadata.url ?? '')) return;
760
1296
  }
761
1297
  // Navigation can transiently invalidate the current JavaScript execution context.
762
1298
  }
763
1299
  await new Promise((resolve) => setTimeout(resolve, 100));
764
1300
  }
765
- throw new Error(`Timeout waiting for browser load state ${state}.`);
1301
+ throw new AssistantPluginError(
1302
+ 'PLUGIN_BROWSER_LOAD_STATE_TIMEOUT',
1303
+ `Timed out waiting for native browser load state ${state}.`,
1304
+ { retryable: true, details: { state, timeoutMs: timeout, requireHttpUrl } },
1305
+ );
766
1306
  }
767
1307
 
768
1308
  locator(_selector: string): { screenshot: (options?: Record<string, unknown>) => Promise<Buffer> } {
@@ -776,6 +1316,9 @@ export class MacOsAppleEventsPage {
776
1316
  async bringToFront(): Promise<void> {
777
1317
  if (this.targetRef) {
778
1318
  this.metadata = await activateMacOsBrowserOwnedTab(this.browser.product, this.targetRef, this.timeoutMs);
1319
+ if (this.metadata.windowId && this.metadata.tabId) {
1320
+ this.targetRef = { windowId: this.metadata.windowId, tabId: this.metadata.tabId };
1321
+ }
779
1322
  return;
780
1323
  }
781
1324
  await this.runAutomation(
@@ -791,15 +1334,71 @@ export class MacOsAppleEventsPage {
791
1334
  };
792
1335
  }
793
1336
 
1337
+ async function resolveCurrentMacOsBrowserTabRef(
1338
+ product: MacOsBrowserProduct,
1339
+ ref: MacOsBrowserTabRef,
1340
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1341
+ ): Promise<MacOsBrowserTabRef> {
1342
+ // Existing unit-level AppleScript hooks historically model one exact script at a time.
1343
+ // Preserve that narrow test seam unless a live inventory hook is explicitly supplied;
1344
+ // production always resolves through the stable Desktop Operator broker inventory.
1345
+ const inventory = runtimeHooks.tabInventory
1346
+ ? await runtimeHooks.tabInventory(product, timeoutMs)
1347
+ : runtimeHooks.runAppleScript
1348
+ ? undefined
1349
+ : await listMacOsBrowserTabs(product, timeoutMs);
1350
+ if (!inventory) return ref;
1351
+
1352
+ const matches = inventory.tabs.filter((candidate) => candidate.tabId === ref.tabId);
1353
+ if (inventory.truncated || matches.length !== 1) {
1354
+ throw new AssistantPluginError(
1355
+ 'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN',
1356
+ inventory.truncated
1357
+ ? `Cannot prove the current ${product} tab identity because live inventory was truncated.`
1358
+ : matches.length === 0
1359
+ ? `Saved ${product} tab ${ref.tabId} no longer exists in live inventory.`
1360
+ : `Saved ${product} tab ${ref.tabId} is ambiguous in live inventory.`,
1361
+ {
1362
+ retryable: true,
1363
+ details: {
1364
+ browserProduct: product,
1365
+ tabId: ref.tabId,
1366
+ savedWindowId: ref.windowId,
1367
+ candidateCount: matches.length,
1368
+ inventoryTruncated: inventory.truncated,
1369
+ },
1370
+ },
1371
+ );
1372
+ }
1373
+
1374
+ const match = matches[0]!;
1375
+ return { windowId: match.windowId, tabId: match.tabId };
1376
+ }
1377
+
1378
+ export async function listMacOsBrowserTabs(
1379
+ product: MacOsBrowserProduct,
1380
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1381
+ ): Promise<MacOsBrowserTabInventory> {
1382
+ const browser = browserDefinition(product);
1383
+ const raw = await runBrowserAutomationText(
1384
+ { action: 'list_tabs', product },
1385
+ listTabsScript(browser),
1386
+ [],
1387
+ timeoutMs,
1388
+ );
1389
+ return parseTabInventory(product, raw);
1390
+ }
1391
+
794
1392
  export async function readMacOsBrowserOwnedTabMetadata(
795
1393
  product: MacOsBrowserProduct,
796
1394
  ref: MacOsBrowserTabRef,
797
1395
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
798
1396
  ): Promise<MacOsBrowserMetadata> {
799
1397
  const browser = browserDefinition(product);
1398
+ const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
800
1399
  return parseMetadata(product, await runBrowserAutomationText(
801
- { action: 'metadata', product, ref },
802
- targetMetadataScript(browser, ref),
1400
+ { action: 'metadata', product, ref: resolvedRef },
1401
+ targetMetadataScript(browser, resolvedRef),
803
1402
  [],
804
1403
  timeoutMs,
805
1404
  ));
@@ -811,13 +1410,33 @@ export async function activateMacOsBrowserOwnedTab(
811
1410
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
812
1411
  ): Promise<MacOsBrowserMetadata> {
813
1412
  const browser = browserDefinition(product);
1413
+ const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
814
1414
  await runBrowserAutomationText(
815
- { action: 'activate', product, ref },
816
- activateTargetTabScript(browser, ref),
1415
+ { action: 'activate', product, ref: resolvedRef },
1416
+ activateTargetTabScript(browser, resolvedRef),
817
1417
  [],
818
1418
  timeoutMs,
819
1419
  );
820
- return readMacOsBrowserOwnedTabMetadata(product, ref, timeoutMs);
1420
+ const metadata = await readMacOsBrowserOwnedTabMetadata(product, resolvedRef, timeoutMs);
1421
+ if (metadata.tabId !== resolvedRef.tabId || metadata.active !== true) {
1422
+ throw new AssistantPluginError(
1423
+ 'PLUGIN_BROWSER_NATIVE_ACTIVATION_POSTCONDITION_FAILED',
1424
+ `${browser.appName} did not prove that the exact requested tab became active.`,
1425
+ {
1426
+ retryable: true,
1427
+ details: {
1428
+ browserProduct: product,
1429
+ requestedTabId: resolvedRef.tabId,
1430
+ resolvedWindowId: resolvedRef.windowId,
1431
+ observedWindowId: metadata.windowId,
1432
+ observedTabId: metadata.tabId,
1433
+ observedActive: metadata.active,
1434
+ observedFrontmost: metadata.frontmost,
1435
+ },
1436
+ },
1437
+ );
1438
+ }
1439
+ return metadata;
821
1440
  }
822
1441
 
823
1442
  export function createMacOsBrowserPage(attachment: MacOsBrowserAttachment, timeoutMs?: number): MacOsAppleEventsPage {
@@ -830,19 +1449,19 @@ export async function createMacOsBrowserOwnedPage(
830
1449
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
831
1450
  ): Promise<MacOsAppleEventsPage> {
832
1451
  const browser = browserDefinition(attachment.metadata.product);
833
- const raw = await runBrowserAutomationText(
1452
+ const creationEvidence = await runCreateTabAutomation(
834
1453
  { action: 'create_tab', product: attachment.metadata.product, url },
835
1454
  createOwnedTabScript(browser),
836
1455
  [url],
837
1456
  timeoutMs,
838
1457
  );
839
- const parts = raw.split(RECORD_SEPARATOR);
840
- if (parts.length < 2) throw new Error(`Browser scripting returned incomplete owned-tab metadata for ${browser.appName}.`);
841
- const ref = { windowId: parseBrowserId(parts[0] ?? '', 'window id'), tabId: parseBrowserId(parts[1] ?? '', 'tab id') };
842
- return new MacOsAppleEventsPage({
1458
+ const ref = creationEvidence.ref;
1459
+ const page = new MacOsAppleEventsPage({
843
1460
  ...attachment,
844
1461
  metadata: { ...attachment.metadata, url, title: '', windowId: ref.windowId, tabId: ref.tabId, active: false },
845
- }, timeoutMs, ref);
1462
+ }, timeoutMs, ref, creationEvidence);
1463
+ rememberMacOsBrowserPageHandle(attachment.metadata.product, ref, page);
1464
+ return page;
846
1465
  }
847
1466
 
848
1467
  export async function reattachMacOsBrowserOwnedPage(
@@ -850,12 +1469,26 @@ export async function reattachMacOsBrowserOwnedPage(
850
1469
  ref: MacOsBrowserTabRef,
851
1470
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
852
1471
  ): Promise<{ page: MacOsAppleEventsPage; attachment: MacOsBrowserAttachment }> {
1472
+ const cacheKey = nativePageHandleKey(product, ref);
1473
+ const cached = warmNativePageHandles.get(cacheKey);
1474
+ if (cached) {
1475
+ cached.updateTimeout(timeoutMs);
1476
+ // Refresh insertion order so bounded eviction behaves as a small LRU.
1477
+ warmNativePageHandles.delete(cacheKey);
1478
+ warmNativePageHandles.set(cacheKey, cached);
1479
+ return { page: cached, attachment: cached.attachmentSnapshot() };
1480
+ }
853
1481
  const metadata = await readMacOsBrowserOwnedTabMetadata(product, ref, timeoutMs);
854
1482
  const attachment: MacOsBrowserAttachment = {
855
1483
  metadata,
856
1484
  attempts: [{ product, appName: metadata.appName, bundleId: metadata.bundleId, status: 'selected', frontmost: metadata.frontmost }],
857
1485
  };
858
- return { page: new MacOsAppleEventsPage(attachment, timeoutMs, ref), attachment };
1486
+ const resolvedRef = metadata.windowId && metadata.tabId
1487
+ ? { windowId: metadata.windowId, tabId: metadata.tabId }
1488
+ : ref;
1489
+ const page = new MacOsAppleEventsPage(attachment, timeoutMs, resolvedRef);
1490
+ rememberMacOsBrowserPageHandle(product, resolvedRef, page);
1491
+ return { page, attachment };
859
1492
  }
860
1493
 
861
1494
  export async function closeMacOsBrowserOwnedTab(
@@ -864,10 +1497,15 @@ export async function closeMacOsBrowserOwnedTab(
864
1497
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
865
1498
  ): Promise<void> {
866
1499
  const browser = browserDefinition(product);
867
- await runBrowserAutomationText(
868
- { action: 'close_tab', product, ref },
869
- closeOwnedTabScript(browser, ref),
870
- [],
871
- timeoutMs,
872
- );
1500
+ try {
1501
+ const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
1502
+ await runBrowserAutomationText(
1503
+ { action: 'close_tab', product, ref: resolvedRef },
1504
+ closeOwnedTabScript(browser, resolvedRef),
1505
+ [],
1506
+ timeoutMs,
1507
+ );
1508
+ } finally {
1509
+ invalidateMacOsBrowserPageHandle(product, ref);
1510
+ }
873
1511
  }