@moretea-labs/forge 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +46 -0
  3. package/CLAUDE.md +2 -0
  4. package/DESIGN.md +7 -2
  5. package/README.md +4 -4
  6. package/README.zh-CN.md +2 -2
  7. package/SKILL.md +2 -2
  8. package/THIRD_PARTY_NOTICES.md +1 -1
  9. package/agents/openai.yaml +1 -1
  10. package/assets/hooks/hook-input.sh +11 -8
  11. package/assets/hooks/lib/session-state.sh +10 -7
  12. package/assets/hooks/lib/workflow-state.sh +13 -177
  13. package/assets/hooks/post-tool-observer.sh +8 -2
  14. package/assets/hooks/pre-edit-guard.sh +8 -39
  15. package/assets/hooks/prompt-guard.sh +7 -83
  16. package/assets/hooks/session-start-context.sh +56 -1
  17. package/assets/partials/05-workflow.partial.md +1 -1
  18. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  19. package/assets/plugin-registry.v1.json +54 -3
  20. package/assets/reference-configs/document-generation.md +1 -1
  21. package/assets/reference-configs/harness-overview.md +1 -1
  22. package/assets/reference-configs/hook-operations.md +1 -1
  23. package/assets/reference-configs/sprint-contracts.md +3 -3
  24. package/assets/skill-version.json +2 -2
  25. package/assets/templates/contract.template.md +2 -7
  26. package/assets/templates/helpers/capture-plan.sh +2 -2
  27. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  28. package/assets/templates/helpers/contract-worktree.sh +1 -21
  29. package/assets/templates/helpers/ensure-task-workflow.sh +13 -38
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/new-plan.sh +1 -1
  32. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  33. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  34. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/plan.template.md +1 -1
  38. package/assets/templates/review.template.md +9 -34
  39. package/assets/workflow-contract.v1.json +10 -19
  40. package/docs/README.md +10 -10
  41. package/docs/architecture/CURRENT.md +170 -0
  42. package/docs/architecture/EVOLUTION.md +42 -0
  43. package/docs/architecture/current/README.md +4 -156
  44. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  45. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  46. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  47. package/docs/architecture/history/README.md +7 -0
  48. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  49. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  50. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  51. package/docs/architecture/history.md +3 -34
  52. package/docs/architecture/index.md +15 -98
  53. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  54. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  55. package/docs/architecture/versions/1.6.md +38 -0
  56. package/docs/architecture/versions/1.7.md +41 -0
  57. package/docs/forge-chatgpt-browser-engine.md +2 -2
  58. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  59. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  60. package/docs/forge-github-issue-launcher.md +1 -1
  61. package/docs/forge-runtime-self-healing-loop.md +1 -1
  62. package/docs/operations/controller-browser-plugin.md +38 -22
  63. package/docs/operations/controller-performance-and-502.md +124 -0
  64. package/docs/operations/controller-reliability-runbook.md +1 -1
  65. package/docs/operations/features.md +3 -3
  66. package/docs/operations/github-repository.md +1 -1
  67. package/docs/operations/homebrew.md +2 -2
  68. package/docs/operations/mcp-tool-exposure.md +5 -3
  69. package/docs/tutorials/01-install-and-start.md +1 -1
  70. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Installation.md +1 -1
  74. package/docs/wiki/Integrations.md +2 -2
  75. package/docs/wiki/Quick-Start.md +1 -1
  76. package/docs/wiki/Work-Lifecycle.md +1 -1
  77. package/package.json +29 -10
  78. package/scripts/activate-source-baseline.ts +17 -13
  79. package/scripts/benchmark-background-check-overlap.ts +228 -0
  80. package/scripts/benchmark-mcp-host.sh +117 -0
  81. package/scripts/benchmark-mcp-transport.ts +170 -0
  82. package/scripts/benchmark-quality-harness.ts +146 -0
  83. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  84. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  85. package/scripts/benchmark-typescript-navigation.ts +186 -0
  86. package/scripts/benchmark-work-efficiency.ts +334 -0
  87. package/scripts/capture-plan.sh +3 -3
  88. package/scripts/check-agent-tooling.sh +8 -35
  89. package/scripts/check-architecture-sync.sh +17 -86
  90. package/scripts/check-mcp-compatibility.ts +14 -0
  91. package/scripts/check-public-docs.sh +14 -1
  92. package/scripts/check-runtime-architecture.mjs +107 -64
  93. package/scripts/check-stable-baseline.ts +92 -0
  94. package/scripts/check-task-workflow.sh +4 -4
  95. package/scripts/contract-worktree.sh +1 -21
  96. package/scripts/ensure-task-workflow.sh +22 -55
  97. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  98. package/scripts/install-standalone-recovery.ts +24 -0
  99. package/scripts/lib/project-init-lib.sh +15 -44
  100. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  101. package/scripts/migrate-project-template.sh +23 -1179
  102. package/scripts/new-plan.sh +1 -1
  103. package/scripts/plan-to-todo.sh +11 -36
  104. package/scripts/run-governed-gate.ts +2 -1
  105. package/scripts/ship-worktrees.sh +2 -29
  106. package/scripts/smoke-ios-physical-device.ts +4 -2
  107. package/scripts/smoke-runtime-recovery.ts +1 -35
  108. package/scripts/smoke-schedule-engine.ts +1 -20
  109. package/scripts/sprint-backlog.sh +1 -1
  110. package/scripts/verify-contract.sh +1 -111
  111. package/scripts/verify-forge-runtime.sh +0 -1
  112. package/scripts/verify-sprint.sh +3 -180
  113. package/scripts/workflow-contract.ts +1 -5
  114. package/src/cli/commands/chatgpt.ts +31 -2
  115. package/src/cli/commands/init-hook.ts +3 -3
  116. package/src/cli/commands/mcp.ts +9 -5
  117. package/src/cli/commands/plugin.ts +37 -10
  118. package/src/cli/commands/recovery.ts +25 -3
  119. package/src/cli/commands/runtime.ts +55 -3
  120. package/src/cli/commands/security.ts +12 -1
  121. package/src/cli/commands/setup-profile.ts +31 -21
  122. package/src/cli/controller/check-runner.ts +106 -3
  123. package/src/cli/controller/composite-result.ts +1 -1
  124. package/src/cli/controller/context/focus.ts +51 -0
  125. package/src/cli/controller/context/instruction-context.ts +114 -0
  126. package/src/cli/controller/context/known-paths.ts +76 -0
  127. package/src/cli/controller/context/query-planning.ts +110 -0
  128. package/src/cli/controller/context/rule-resolution.ts +532 -0
  129. package/src/cli/controller/context/source-materializer.ts +278 -0
  130. package/src/cli/controller/context/structural.ts +16 -0
  131. package/src/cli/controller/context/types.ts +286 -0
  132. package/src/cli/controller/context-pack.ts +898 -430
  133. package/src/cli/controller/launch-agents.ts +77 -2
  134. package/src/cli/controller/operational-plan.ts +2 -2
  135. package/src/cli/controller/work-mode.ts +6 -4
  136. package/src/cli/editing/edit-session.ts +60 -7
  137. package/src/cli/github/github.ts +133 -0
  138. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  139. package/src/cli/hook/prompt-intents.ts +1 -1
  140. package/src/cli/installer/managed-entries.ts +15 -1
  141. package/src/cli/local-bridge/console-automations.ts +225 -67
  142. package/src/cli/local-bridge/console-view-models.ts +3 -35
  143. package/src/cli/local-bridge/facade-api.ts +2 -217
  144. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  145. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +153 -3
  146. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  147. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +183 -3
  148. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  149. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  150. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -11
  151. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  152. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  153. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +4 -3
  154. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  155. package/src/cli/local-bridge/frontend/src/types.ts +8 -7
  156. package/src/cli/local-bridge/job-store.ts +1 -490
  157. package/src/cli/local-bridge/server.ts +29 -532
  158. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  159. package/src/cli/local-bridge/ui-dist/app.js +5101 -4967
  160. package/src/cli/mcp/auth.ts +5 -5
  161. package/src/cli/mcp/instructions.ts +3 -2
  162. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  163. package/src/cli/mcp/multi-repository.ts +16 -5
  164. package/src/cli/mcp/oauth.ts +175 -26
  165. package/src/cli/mcp/paths.ts +8 -2
  166. package/src/cli/mcp/repository-tools.ts +356 -101
  167. package/src/cli/mcp/server.ts +625 -54
  168. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  169. package/src/cli/mcp/toolset-selection.ts +1 -1
  170. package/src/cli/mcp/toolset.ts +42 -5
  171. package/src/cli/mcp/transports/http.ts +125 -15
  172. package/src/cli/mcp/transports/session-registry.ts +67 -5
  173. package/src/cli/mcp/types.ts +3 -3
  174. package/src/cli/repositories/command-classifier.ts +209 -14
  175. package/src/cli/repositories/command-executor.ts +55 -521
  176. package/src/cli/repositories/command-process.ts +108 -0
  177. package/src/cli/repositories/command-scope.ts +99 -41
  178. package/src/cli/repositories/controller-home.ts +7 -8
  179. package/src/cli/repositories/git-command-executor.ts +2 -0
  180. package/src/cli/repositories/locks.ts +53 -16
  181. package/src/cli/repositories/repository-snapshot.ts +121 -0
  182. package/src/cli/repositories/safe-patch.ts +6 -4
  183. package/src/cli/repositories/selected-path-actions.ts +17 -9
  184. package/src/cli/repositories/structured-git.ts +110 -10
  185. package/src/cli/repository/inspector.ts +565 -62
  186. package/src/cli/repository/session-cache.ts +58 -17
  187. package/src/cli/v81-entry.ts +1 -4
  188. package/src/effects/fs-transaction.ts +85 -17
  189. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  190. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  191. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  192. package/src/runtime/context/lsp-client.ts +283 -0
  193. package/src/runtime/context/semantic-navigation.ts +275 -0
  194. package/src/runtime/context/swift-navigation.ts +565 -0
  195. package/src/runtime/context/typescript-navigation.ts +226 -0
  196. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  197. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +196 -2
  198. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  199. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  200. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  201. package/src/runtime/control-plane/execution/validation.ts +15 -2
  202. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  203. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  204. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  205. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  206. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  207. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +175 -8
  208. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -21
  209. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +87 -9
  210. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  211. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  212. package/src/runtime/control-plane/facade/controller-session-store.ts +144 -14
  213. package/src/runtime/control-plane/facade/goal-workloop-access.ts +9 -2
  214. package/src/runtime/control-plane/facade/goal-workloop.ts +885 -105
  215. package/src/runtime/control-plane/facade/index.ts +1 -0
  216. package/src/runtime/control-plane/facade/plan-contract-store.ts +211 -26
  217. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  218. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  219. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  220. package/src/runtime/control-plane/facade/types.ts +55 -11
  221. package/src/runtime/control-plane/facade/work-contract-store.ts +204 -73
  222. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  223. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  224. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  225. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  226. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  227. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  228. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  229. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  230. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  231. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  232. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  233. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  234. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  235. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  236. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  237. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  238. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  239. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  240. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  241. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +642 -51
  242. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  243. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  244. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  245. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  246. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  247. package/src/runtime/control-plane/release-retention.ts +62 -2
  248. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  249. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  250. package/src/runtime/diagnostics/mcp-timing.ts +19 -0
  251. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  252. package/src/runtime/diagnostics/performance.ts +24 -1
  253. package/src/runtime/evidence/event-ledger.ts +1 -1
  254. package/src/runtime/execution/jobs/store.ts +58 -4
  255. package/src/runtime/execution/jobs/types.ts +1 -1
  256. package/src/runtime/execution/managed-workspace.ts +203 -1
  257. package/src/runtime/execution/process-runtime/check-facade.ts +52 -14
  258. package/src/runtime/execution/process-runtime/check-result.ts +63 -2
  259. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  260. package/src/runtime/execution/process-runtime/command-facade.ts +346 -40
  261. package/src/runtime/execution/process-runtime/gc.ts +98 -12
  262. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  263. package/src/runtime/execution/process-runtime/persisted-check.ts +57 -6
  264. package/src/runtime/execution/process-runtime/resource-claims.ts +97 -8
  265. package/src/runtime/execution/process-runtime/runtime.ts +59 -4
  266. package/src/runtime/execution/process-runtime/store.ts +7 -0
  267. package/src/runtime/execution/process-runtime/types.ts +2 -0
  268. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  269. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  270. package/src/runtime/execution/workers/executor.ts +0 -48
  271. package/src/runtime/gateway/mcp/execution-tools.ts +1219 -126
  272. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  273. package/src/runtime/gateway/mcp/router.ts +185 -83
  274. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  275. package/src/runtime/gateway/mcp/runtime-tools.ts +2555 -1616
  276. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  277. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  278. package/src/runtime/plugins/browser-adapter.ts +2925 -318
  279. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  280. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  281. package/src/runtime/plugins/browser-handoff.ts +92 -5
  282. package/src/runtime/plugins/browser-macos-bridge.ts +586 -45
  283. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  284. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  285. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  286. package/src/runtime/plugins/browser-runtime.ts +183 -0
  287. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  288. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  289. package/src/runtime/plugins/desktop-operator-registration.ts +15 -12
  290. package/src/runtime/plugins/external-adapter.ts +324 -3
  291. package/src/runtime/plugins/external-registration.ts +84 -5
  292. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  293. package/src/runtime/plugins/first-party-registry.ts +4 -2
  294. package/src/runtime/plugins/github-adapter.ts +125 -0
  295. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  296. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  297. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  298. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  299. package/src/runtime/plugins/ios-adapter.ts +106 -16
  300. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  301. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  302. package/src/runtime/plugins/lightweight-action.ts +153 -0
  303. package/src/runtime/plugins/local-system-adapter.ts +125 -8
  304. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  305. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  306. package/src/runtime/plugins/plugin-management-adapter.ts +181 -0
  307. package/src/runtime/plugins/resend-adapter.ts +1 -1
  308. package/src/runtime/plugins/store.ts +366 -24
  309. package/src/runtime/plugins/types.ts +39 -2
  310. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  311. package/src/runtime/projections/git-status-sampler.ts +16 -16
  312. package/src/runtime/projections/materialized-view.ts +28 -10
  313. package/src/runtime/recovery/actions.ts +0 -10
  314. package/src/runtime/recovery/classifier.ts +0 -9
  315. package/src/runtime/recovery/maintenance-executor.ts +415 -18
  316. package/src/runtime/recovery/probe.ts +0 -1
  317. package/src/runtime/recovery/types.ts +0 -1
  318. package/src/runtime/root/mcp-transport.ts +180 -10
  319. package/src/runtime/root/package-connector-service.ts +186 -8
  320. package/src/runtime/root/package-runtime-release.ts +143 -17
  321. package/src/runtime/root/package-runtime-service.ts +430 -22
  322. package/src/runtime/root/release-manifest.ts +7 -2
  323. package/src/runtime/root/release-materialize.ts +162 -19
  324. package/src/runtime/root/release-store.ts +48 -11
  325. package/src/runtime/root/runtime.ts +23 -5
  326. package/src/runtime/root/service.ts +100 -27
  327. package/src/runtime/root/types.ts +2 -0
  328. package/src/runtime/safe-tooling/index.ts +0 -1
  329. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  330. package/src/runtime/safe-tooling/types.ts +0 -30
  331. package/src/runtime/shared/trusted-executable.ts +62 -0
  332. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  333. package/src/runtime/standalone-recovery/core.ts +702 -94
  334. package/src/runtime/standalone-recovery/entry.ts +99 -23
  335. package/src/runtime/standalone-recovery/installer.ts +19 -3
  336. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  337. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  338. package/src/runtime/workflow/schedules/browser-probe.ts +258 -0
  339. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  340. package/src/runtime/workflow/schedules/engine.ts +509 -80
  341. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  342. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  343. package/src/runtime/workflow/schedules/store.ts +131 -10
  344. package/src/runtime/workflow/schedules/types.ts +6 -3
  345. package/src/runtime/workflow/schedules/work-continuation.ts +120 -34
  346. package/src/testing/test-governance.ts +8 -40
  347. package/assets/reference-configs/heartbeat-triage.md +0 -57
  348. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  349. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  350. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  351. package/bin/forge-desktop-helper.mjs +0 -204
  352. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  353. package/docs/architecture/current/architecture-invariants.md +0 -331
  354. package/docs/architecture/current/automation-and-schedule-engine.md +0 -453
  355. package/docs/architecture/current/codegraph-context-provider.md +0 -226
  356. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  357. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  358. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -354
  359. package/docs/architecture/current/entity-model.md +0 -592
  360. package/docs/architecture/current/external-plugin-governance.md +0 -396
  361. package/docs/architecture/current/failure-recovery.md +0 -447
  362. package/docs/architecture/current/governance.md +0 -213
  363. package/docs/architecture/current/human-interaction-plane.md +0 -87
  364. package/docs/architecture/current/implementation-status.md +0 -128
  365. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  366. package/docs/architecture/current/legacy-governance.md +0 -58
  367. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  368. package/docs/architecture/current/migration-roadmap.md +0 -105
  369. package/docs/architecture/current/multi-repository-execution.md +0 -371
  370. package/docs/architecture/current/operations-runbook.md +0 -45
  371. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  372. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  373. package/docs/architecture/current/repository-access-modes.md +0 -102
  374. package/docs/architecture/current/route-policy.md +0 -15
  375. package/docs/architecture/current/route-session-concurrency.md +0 -85
  376. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  377. package/docs/architecture/current/runtime-directory-map.md +0 -49
  378. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  379. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  380. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  381. package/docs/architecture/current/system-overview.md +0 -144
  382. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  383. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  384. package/docs/architecture/current/thin-harness-v1.md +0 -311
  385. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  386. package/docs/forge-autonomous-goal-loop.md +0 -9
  387. package/docs/forge-provider-configuration.md +0 -7
  388. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -83
  389. package/docs/operations/controller-desktop-plugin.md +0 -112
  390. package/docs/operations/gmail-assistant-routines.md +0 -186
  391. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  392. package/docs/operations/provider-configuration.md +0 -31
  393. package/scripts/harness-trace-grade.sh +0 -150
  394. package/scripts/heartbeat-triage.sh +0 -361
  395. package/scripts/maintenance-triage.sh +0 -167
  396. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  397. package/src/cli/repositories/goal-registry.ts +0 -239
  398. package/src/runtime/assistant/action-proposals.ts +0 -279
  399. package/src/runtime/assistant/intent.ts +0 -395
  400. package/src/runtime/assistant/model-provider.ts +0 -460
  401. package/src/runtime/assistant/openapi.ts +0 -223
  402. package/src/runtime/assistant/policy.ts +0 -89
  403. package/src/runtime/assistant/readiness.ts +0 -258
  404. package/src/runtime/assistant/routine-runtime.ts +0 -477
  405. package/src/runtime/assistant/schedule-binding.ts +0 -171
  406. package/src/runtime/assistant/standing-grants.ts +0 -375
  407. package/src/runtime/assistant/store.ts +0 -243
  408. package/src/runtime/assistant/types.ts +0 -135
  409. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  410. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  411. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  412. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  413. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  414. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  415. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  416. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  417. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  418. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  419. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  420. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  421. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  422. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  423. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  424. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  425. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  426. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  427. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  428. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  429. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
  430. package/src/runtime/workflow/findings/store.ts +0 -204
  431. package/src/runtime/workflow/findings/types.ts +0 -58
  432. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  433. package/src/runtime/workflow/portfolio/store.ts +0 -185
  434. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -1,10 +1,12 @@
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';
10
12
 
@@ -41,6 +43,20 @@ export interface MacOsBrowserMetadata {
41
43
  loading?: boolean;
42
44
  }
43
45
 
46
+ export interface MacOsBrowserTabInventoryEntry {
47
+ windowId: string;
48
+ tabId: string;
49
+ url: string;
50
+ title: string;
51
+ active: boolean;
52
+ }
53
+
54
+ export interface MacOsBrowserTabInventory {
55
+ product: MacOsBrowserProduct;
56
+ tabs: MacOsBrowserTabInventoryEntry[];
57
+ truncated: boolean;
58
+ }
59
+
44
60
  export interface MacOsBrowserAttachAttempt {
45
61
  product: MacOsBrowserProduct;
46
62
  appName: string;
@@ -74,7 +90,9 @@ interface MacOsBrowserRuntimeHooks {
74
90
  // Test-only escape hatches. Production intentionally leaves these undefined so
75
91
  // Apple Events and screen capture are attributed to the stable helper process.
76
92
  runAppleScript?: (script: string, args: string[], timeoutMs: number) => Promise<string>;
93
+ tabInventory?: (product: MacOsBrowserProduct, timeoutMs: number) => Promise<MacOsBrowserTabInventory>;
77
94
  captureRegion?: (region: { x: number; y: number; width: number; height: number }, path: string, timeoutMs: number) => Promise<Buffer>;
95
+ trustedInput?: (request: Extract<BrowserAutomationBrokerAction, { action: 'trusted_input' }>, timeoutMs: number) => Promise<void>;
78
96
  }
79
97
 
80
98
  function execFileText(file: string, args: string[], timeoutMs: number): Promise<string> {
@@ -143,8 +161,72 @@ async function captureBrowserAutomation(
143
161
  return bytes;
144
162
  }
145
163
 
164
+ async function sendBrowserTrustedInput(
165
+ request: Extract<BrowserAutomationBrokerAction, { action: 'trusted_input' }>,
166
+ timeoutMs: number,
167
+ ): Promise<void> {
168
+ if (runtimeHooks.trustedInput) {
169
+ await runtimeHooks.trustedInput(request, timeoutMs);
170
+ return;
171
+ }
172
+ let result: Record<string, unknown>;
173
+ try {
174
+ result = await callBrowserAutomationBroker(request, timeoutMs);
175
+ } catch (error) {
176
+ const message = error instanceof Error ? error.message : String(error ?? '');
177
+ if ((error instanceof AssistantPluginError && error.code === 'BROWSER_AUTOMATION_ACTION_UNSUPPORTED')
178
+ || /\bBROWSER_AUTOMATION_ACTION_UNSUPPORTED\b/.test(message)) {
179
+ throw new AssistantPluginError(
180
+ 'PLUGIN_BROWSER_TRUSTED_INPUT_UNAVAILABLE',
181
+ '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.',
182
+ { retryable: false, details: { browserProduct: request.product, requiredProviderAction: 'trusted_input' } },
183
+ );
184
+ }
185
+ throw error;
186
+ }
187
+ if (result.performed !== true) {
188
+ throw new AssistantPluginError(
189
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
190
+ 'Stable Forge macOS capability broker did not confirm trusted browser input.',
191
+ { retryable: true },
192
+ );
193
+ }
194
+ }
195
+
146
196
  let runtimeHooks: MacOsBrowserRuntimeHooks = { ...defaultRuntimeHooks };
147
197
  let lastAttachObservation: MacOsBrowserAttachObservation | undefined;
198
+ const MAX_WARM_NATIVE_PAGE_HANDLES = 128;
199
+ const warmNativePageHandles = new Map<string, MacOsAppleEventsPage>();
200
+
201
+ function nativePageHandleKey(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef): string {
202
+ // tabId is the stable browser entity. The windowId is only a mutable placement hint and
203
+ // is re-resolved from live inventory immediately before exact-target Apple Events operations.
204
+ return `${product}:${ref.tabId}`;
205
+ }
206
+
207
+ function rememberMacOsBrowserPageHandle(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef, page: MacOsAppleEventsPage): void {
208
+ const key = nativePageHandleKey(product, ref);
209
+ warmNativePageHandles.delete(key);
210
+ warmNativePageHandles.set(key, page);
211
+ while (warmNativePageHandles.size > MAX_WARM_NATIVE_PAGE_HANDLES) {
212
+ const oldest = warmNativePageHandles.keys().next().value;
213
+ if (typeof oldest !== 'string') break;
214
+ warmNativePageHandles.delete(oldest);
215
+ }
216
+ }
217
+
218
+ export function invalidateMacOsBrowserPageHandle(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef): void {
219
+ warmNativePageHandles.delete(nativePageHandleKey(product, ref));
220
+ }
221
+
222
+ export function invalidateMacOsBrowserPageHandles(): void {
223
+ warmNativePageHandles.clear();
224
+ }
225
+
226
+ export function macOsBrowserPageHandleStale(error: unknown): boolean {
227
+ const message = error instanceof Error ? error.message : String(error ?? '');
228
+ 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);
229
+ }
148
230
 
149
231
  export function setMacOsBrowserRuntimeHooksForTest(hooks: Partial<MacOsBrowserRuntimeHooks>): void {
150
232
  runtimeHooks = { ...defaultRuntimeHooks, ...hooks };
@@ -153,6 +235,7 @@ export function setMacOsBrowserRuntimeHooksForTest(hooks: Partial<MacOsBrowserRu
153
235
  export function resetMacOsBrowserRuntimeHooksForTest(): void {
154
236
  runtimeHooks = { ...defaultRuntimeHooks };
155
237
  lastAttachObservation = undefined;
238
+ warmNativePageHandles.clear();
156
239
  }
157
240
 
158
241
  export function getMacOsBrowserAttachObservation(): MacOsBrowserAttachObservation | undefined {
@@ -197,12 +280,120 @@ set targetWindow to front window
197
280
  set targetTab to active tab of targetWindow
198
281
  set windowBounds to bounds of targetWindow
199
282
  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)
283
+ 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
284
  `);
202
285
  }
203
286
 
287
+ function listTabsScript(browser: MacOsBrowserDefinition): string {
288
+ return `on replaceText(sourceText, needle, replacement)
289
+ set previousDelimiters to AppleScript's text item delimiters
290
+ set AppleScript's text item delimiters to needle
291
+ set sourceItems to every text item of sourceText
292
+ set AppleScript's text item delimiters to replacement
293
+ set resultText to sourceItems as text
294
+ set AppleScript's text item delimiters to previousDelimiters
295
+ return resultText
296
+ end replaceText
297
+
298
+ on cleanField(sourceText, recordSeparator, fieldSeparator)
299
+ set cleaned to my replaceText(sourceText as text, recordSeparator, " ")
300
+ return my replaceText(cleaned, fieldSeparator, " ")
301
+ end cleanField
302
+
303
+ ${browserTellScript(browser, `
304
+ set recordSeparator to ASCII character 30
305
+ set fieldSeparator to ASCII character 31
306
+ set maxTabs to 256
307
+ set returnedCount to 0
308
+ set truncatedInventory to false
309
+ set outputText to "false"
310
+ repeat with candidateWindow in windows
311
+ set activeTabId to ""
312
+ try
313
+ set activeTabId to ((id of active tab of candidateWindow) as text)
314
+ end try
315
+ repeat with candidateTab in tabs of candidateWindow
316
+ if returnedCount is greater than or equal to maxTabs then
317
+ set truncatedInventory to true
318
+ exit repeat
319
+ end if
320
+ set candidateWindowId to ((id of candidateWindow) as text)
321
+ set candidateTabId to ((id of candidateTab) as text)
322
+ set candidateURL to my cleanField((URL of candidateTab as text), recordSeparator, fieldSeparator)
323
+ set candidateTitle to my cleanField((title of candidateTab as text), recordSeparator, fieldSeparator)
324
+ set candidateActive to (candidateTabId is activeTabId)
325
+ set outputText to outputText & recordSeparator & candidateWindowId & fieldSeparator & candidateTabId & fieldSeparator & (candidateActive as text) & fieldSeparator & candidateURL & fieldSeparator & candidateTitle
326
+ set returnedCount to returnedCount + 1
327
+ end repeat
328
+ if truncatedInventory then exit repeat
329
+ end repeat
330
+ if truncatedInventory then
331
+ set outputText to "true" & text 6 thru -1 of outputText
332
+ end if
333
+ return outputText
334
+ `)}
335
+ `;
336
+ }
337
+
338
+ function parseTabInventory(product: MacOsBrowserProduct, raw: string): MacOsBrowserTabInventory {
339
+ const records = raw.split(RECORD_SEPARATOR);
340
+ const marker = (records.shift() ?? '').trim().toLowerCase();
341
+ if (marker !== 'true' && marker !== 'false') {
342
+ throw new AssistantPluginError(
343
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
344
+ 'Stable Forge macOS capability broker returned an invalid browser tab inventory marker.',
345
+ { retryable: true },
346
+ );
347
+ }
348
+ const tabs: MacOsBrowserTabInventoryEntry[] = [];
349
+ const fieldSeparator = String.fromCharCode(31);
350
+ for (const record of records) {
351
+ if (!record) continue;
352
+ const fields = record.split(fieldSeparator);
353
+ if (fields.length < 5) {
354
+ throw new AssistantPluginError(
355
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
356
+ 'Stable Forge macOS capability broker returned incomplete browser tab inventory metadata.',
357
+ { retryable: true },
358
+ );
359
+ }
360
+ const windowId = (fields[0] ?? '').trim();
361
+ const tabId = (fields[1] ?? '').trim();
362
+ const activeText = (fields[2] ?? '').trim().toLowerCase();
363
+ if (!windowId || !tabId || (activeText !== 'true' && activeText !== 'false')) {
364
+ throw new AssistantPluginError(
365
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
366
+ 'Stable Forge macOS capability broker returned invalid browser tab inventory metadata.',
367
+ { retryable: true },
368
+ );
369
+ }
370
+ tabs.push({ windowId, tabId, active: activeText === 'true', url: fields[3] ?? '', title: fields.slice(4).join(fieldSeparator) });
371
+ if (tabs.length > 256) {
372
+ throw new AssistantPluginError(
373
+ 'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
374
+ 'Stable Forge macOS capability broker exceeded the bounded browser tab inventory size.',
375
+ { retryable: true },
376
+ );
377
+ }
378
+ }
379
+ return { product, tabs, truncated: marker === 'true' };
380
+ }
381
+
204
382
  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)}`;
383
+ return `set targetTabId to ${quotedAppleScript(ref.tabId)}
384
+ set targetWindow to missing value
385
+ set targetTab to missing value
386
+ repeat with candidateWindow in windows
387
+ repeat with candidateTab in tabs of candidateWindow
388
+ if ((id of candidateTab) as text) is targetTabId then
389
+ set targetWindow to candidateWindow
390
+ set targetTab to candidateTab
391
+ exit repeat
392
+ end if
393
+ end repeat
394
+ if targetTab is not missing value then exit repeat
395
+ end repeat
396
+ if targetTab is missing value then error "FORGE_BROWSER_TAB_NOT_FOUND:" & targetTabId`;
206
397
  }
207
398
 
208
399
  function targetMetadataScript(browser: MacOsBrowserDefinition, ref: MacOsBrowserTabRef): string {
@@ -211,7 +402,7 @@ ${targetTabPreamble(ref)}
211
402
  set windowBounds to bounds of targetWindow
212
403
  set separator to ASCII character 30
213
404
  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)
405
+ 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
406
  `);
216
407
  }
217
408
 
@@ -416,7 +607,8 @@ function serializeEvaluation(expression: string | ((...args: unknown[]) => unkno
416
607
  return `(() => {
417
608
  try {
418
609
  const value = (${source});
419
- return JSON.stringify({ ok: true, value: value === undefined ? { __forgeUndefined: true } : value });
610
+ const page = { url: String(document.location.href || ''), title: String(document.title || '') };
611
+ return JSON.stringify({ ok: true, value: value === undefined ? { __forgeUndefined: true } : value, page });
420
612
  } catch (error) {
421
613
  return JSON.stringify({ ok: false, error: String(error && (error.stack || error.message) || error) });
422
614
  }
@@ -464,11 +656,23 @@ function keyEventSource(selector: string | undefined, keySpec: string): string {
464
656
  })()`;
465
657
  }
466
658
 
659
+ export function nativeDomLoadStateSatisfied(
660
+ state: string,
661
+ readyState: string,
662
+ currentUrl: string,
663
+ requireHttpUrl: boolean,
664
+ ): boolean {
665
+ if (requireHttpUrl && !/^https?:\/\//i.test(currentUrl ?? '')) return false;
666
+ return state === 'load' || state === 'networkidle'
667
+ ? readyState === 'complete'
668
+ : readyState === 'interactive' || readyState === 'complete';
669
+ }
670
+
467
671
  export class MacOsAppleEventsPage {
468
672
  private metadata: MacOsBrowserMetadata;
469
673
  private readonly browser: MacOsBrowserDefinition;
470
- private readonly timeoutMs: number;
471
- private readonly targetRef?: MacOsBrowserTabRef;
674
+ private timeoutMs: number;
675
+ private targetRef?: MacOsBrowserTabRef;
472
676
 
473
677
  constructor(attachment: MacOsBrowserAttachment, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS, targetRef?: MacOsBrowserTabRef) {
474
678
  this.metadata = { ...attachment.metadata, ...(targetRef ? { windowId: targetRef.windowId, tabId: targetRef.tabId } : {}) };
@@ -479,12 +683,20 @@ export class MacOsAppleEventsPage {
479
683
 
480
684
  private async runAutomation(
481
685
  request: BrowserAutomationBrokerAction,
482
- script: string,
686
+ script: string | ((resolvedRef?: MacOsBrowserTabRef) => string),
483
687
  args: string[] = [],
484
688
  timeoutMs = this.timeoutMs,
485
689
  ): Promise<string> {
486
690
  try {
487
- return await runBrowserAutomationText(request, script, args, timeoutMs);
691
+ let effectiveRequest = request;
692
+ let resolvedRef: MacOsBrowserTabRef | undefined;
693
+ if ('ref' in request && request.ref) {
694
+ resolvedRef = await resolveCurrentMacOsBrowserTabRef(this.browser.product, request.ref, timeoutMs);
695
+ this.targetRef = resolvedRef;
696
+ effectiveRequest = { ...request, ref: resolvedRef } as BrowserAutomationBrokerAction;
697
+ }
698
+ const effectiveScript = typeof script === 'function' ? script(resolvedRef ?? this.targetRef) : script;
699
+ return await runBrowserAutomationText(effectiveRequest, effectiveScript, args, timeoutMs);
488
700
  } catch (error) {
489
701
  const message = error instanceof Error ? error.message : String(error);
490
702
  throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OPERATION_FAILED', `${this.browser.appName} Apple Events operation failed: ${message}`, {
@@ -495,11 +707,13 @@ export class MacOsAppleEventsPage {
495
707
  }
496
708
 
497
709
  private async refreshMetadata(): Promise<MacOsBrowserMetadata> {
498
- const script = this.targetRef ? targetMetadataScript(this.browser, this.targetRef) : metadataScript(this.browser);
499
710
  this.metadata = parseMetadata(this.browser.product, await this.runAutomation(
500
711
  { action: 'metadata', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}) },
501
- script,
712
+ (resolvedRef) => resolvedRef ? targetMetadataScript(this.browser, resolvedRef) : metadataScript(this.browser),
502
713
  ));
714
+ if (this.targetRef && this.metadata.windowId && this.metadata.tabId) {
715
+ this.targetRef = { windowId: this.metadata.windowId, tabId: this.metadata.tabId };
716
+ }
503
717
  return this.metadata;
504
718
  }
505
719
 
@@ -507,7 +721,7 @@ export class MacOsAppleEventsPage {
507
721
  try {
508
722
  return await this.runAutomation(
509
723
  { action: 'execute_javascript', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}), source },
510
- executeJavaScriptScript(this.browser, this.targetRef),
724
+ (resolvedRef) => executeJavaScriptScript(this.browser, resolvedRef),
511
725
  [source],
512
726
  timeoutMs,
513
727
  );
@@ -527,18 +741,28 @@ export class MacOsAppleEventsPage {
527
741
  return this.targetRef ? { ...this.targetRef } : undefined;
528
742
  }
529
743
 
744
+ updateTimeout(timeoutMs: number): void {
745
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) this.timeoutMs = Math.trunc(timeoutMs);
746
+ }
747
+
748
+ attachmentSnapshot(): MacOsBrowserAttachment {
749
+ return {
750
+ metadata: { ...this.metadata, bounds: { ...this.metadata.bounds } },
751
+ attempts: [{
752
+ product: this.browser.product,
753
+ appName: this.browser.appName,
754
+ bundleId: this.browser.bundleId,
755
+ status: 'selected',
756
+ frontmost: this.metadata.frontmost,
757
+ }],
758
+ };
759
+ }
760
+
530
761
  async goto(url: string, options: Record<string, unknown> = {}): Promise<unknown> {
531
762
  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
763
  await this.runAutomation(
540
- { action: 'navigate', product: this.browser.product, url },
541
- navigateScript(this.browser),
764
+ { action: 'navigate', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}), url },
765
+ (resolvedRef) => navigateScript(this.browser, resolvedRef),
542
766
  [url],
543
767
  timeout,
544
768
  );
@@ -551,7 +775,7 @@ export class MacOsAppleEventsPage {
551
775
  const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
552
776
  await this.runAutomation(
553
777
  { action: 'reload', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}) },
554
- reloadScript(this.browser, this.targetRef),
778
+ (resolvedRef) => reloadScript(this.browser, resolvedRef),
555
779
  [],
556
780
  timeout,
557
781
  );
@@ -609,21 +833,183 @@ export class MacOsAppleEventsPage {
609
833
 
610
834
  async evaluate<T>(expression: string | ((...args: unknown[]) => unknown), arg?: unknown): Promise<T> {
611
835
  const raw = await this.executeJavaScriptRaw(serializeEvaluation(expression, arg));
612
- let parsed: { ok?: boolean; value?: unknown; error?: string };
836
+ let parsed: { ok?: boolean; value?: unknown; error?: string; page?: { url?: unknown; title?: unknown } };
613
837
  try {
614
- parsed = JSON.parse(raw) as { ok?: boolean; value?: unknown; error?: string };
838
+ parsed = JSON.parse(raw) as { ok?: boolean; value?: unknown; error?: string; page?: { url?: unknown; title?: unknown } };
615
839
  } catch {
616
840
  throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_PROTOCOL_ERROR', `${this.browser.appName} returned an invalid JavaScript result.`, {
617
841
  retryable: true,
618
842
  });
619
843
  }
620
844
  if (!parsed.ok) throw new Error(parsed.error || 'Browser JavaScript evaluation failed.');
845
+ if (parsed.page && typeof parsed.page.url === 'string' && parsed.page.url) {
846
+ this.metadata = {
847
+ ...this.metadata,
848
+ url: parsed.page.url,
849
+ title: typeof parsed.page.title === 'string' ? parsed.page.title : this.metadata.title,
850
+ };
851
+ }
621
852
  if (parsed.value && typeof parsed.value === 'object' && (parsed.value as { __forgeUndefined?: unknown }).__forgeUndefined === true) {
622
853
  return undefined as T;
623
854
  }
624
855
  return parsed.value as T;
625
856
  }
626
857
 
858
+ async setInputFiles(selector: string, files: string | string[]): Promise<void> {
859
+ const filePaths = Array.isArray(files) ? files : [files];
860
+ if (filePaths.length === 0 || filePaths.length > 32) {
861
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Native browser file input accepts 1-32 local files.', { retryable: false });
862
+ }
863
+ for (const filePath of filePaths) {
864
+ if (!filePath || !existsSync(filePath)) {
865
+ throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Local file does not exist.', { retryable: false });
866
+ }
867
+ }
868
+
869
+ if (filePaths.length === 1) {
870
+ const filePath = filePaths[0];
871
+ const bytes = readFileSync(filePath);
872
+ const fileName = basename(filePath);
873
+ const extension = extname(fileName).toLowerCase();
874
+ const mimeType = extension === '.pdf' ? 'application/pdf'
875
+ : extension === '.zip' ? 'application/zip'
876
+ : extension === '.png' ? 'image/png'
877
+ : extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
878
+ : 'application/octet-stream';
879
+ const bufferKey = `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`;
880
+ const encoded = bytes.toString('base64');
881
+ const chunkChars = 192 * 1024;
882
+ try {
883
+ await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] = ''; return true; })()`);
884
+ for (let offset = 0; offset < encoded.length; offset += chunkChars) {
885
+ const chunk = encoded.slice(offset, offset + chunkChars);
886
+ await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
887
+ }
888
+ const attached = await this.evaluate<{ name?: string; size?: number }>(`(() => {
889
+ const element = document.querySelector(${JSON.stringify(selector)});
890
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
891
+ const expectedName = ${JSON.stringify(fileName)};
892
+ const expectedSize = ${bytes.length};
893
+ const encoded = String(globalThis[${JSON.stringify(bufferKey)}] || '');
894
+ const binary = atob(encoded);
895
+ const data = new Uint8Array(binary.length);
896
+ for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
897
+ const file = new File([data], expectedName, { type: ${JSON.stringify(mimeType)} });
898
+ const transfer = new DataTransfer();
899
+ transfer.items.add(file);
900
+ element.files = transfer.files;
901
+ element.dispatchEvent(new Event('input', { bubbles: true }));
902
+ element.dispatchEvent(new Event('change', { bubbles: true }));
903
+ const selected = element.files && element.files[0];
904
+ return selected ? { name: selected.name, size: selected.size, expectedName, expectedSize } : null;
905
+ })()`);
906
+ if (attached?.name !== fileName || attached?.size !== bytes.length) {
907
+ throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local file on the target input.', {
908
+ retryable: true,
909
+ details: { expectedName: fileName, expectedSize: bytes.length, actualName: attached?.name, actualSize: attached?.size },
910
+ });
911
+ }
912
+ } finally {
913
+ await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
914
+ }
915
+ return;
916
+ }
917
+
918
+ const attachments = filePaths.map((filePath) => {
919
+ const bytes = readFileSync(filePath);
920
+ const fileName = basename(filePath);
921
+ const extension = extname(fileName).toLowerCase();
922
+ const mimeType = extension === '.pdf' ? 'application/pdf'
923
+ : extension === '.zip' ? 'application/zip'
924
+ : extension === '.png' ? 'image/png'
925
+ : extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
926
+ : 'application/octet-stream';
927
+ return {
928
+ bytes,
929
+ fileName,
930
+ mimeType,
931
+ bufferKey: `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`,
932
+ };
933
+ });
934
+
935
+ if (attachments.length > 1) {
936
+ await this.evaluate(`(() => {
937
+ const element = document.querySelector(${JSON.stringify(selector)});
938
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
939
+ if (!element.multiple) throw new Error('Target input does not allow multiple files.');
940
+ const transfer = new DataTransfer();
941
+ element.files = transfer.files;
942
+ return true;
943
+ })()`);
944
+ }
945
+
946
+ const chunkChars = 192 * 1024;
947
+ try {
948
+ for (let index = 0; index < attachments.length; index += 1) {
949
+ const attachment = attachments[index];
950
+ const encoded = attachment.bytes.toString('base64');
951
+ await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] = ''; return true; })()`);
952
+ for (let offset = 0; offset < encoded.length; offset += chunkChars) {
953
+ const chunk = encoded.slice(offset, offset + chunkChars);
954
+ await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
955
+ }
956
+ const append = attachments.length > 1 && index > 0;
957
+ await this.evaluate(`(() => {
958
+ const element = document.querySelector(${JSON.stringify(selector)});
959
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
960
+ const expectedName = ${JSON.stringify(attachment.fileName)};
961
+ const expectedSize = ${attachment.bytes.length};
962
+ const encoded = String(globalThis[${JSON.stringify(attachment.bufferKey)}] || '');
963
+ const binary = atob(encoded);
964
+ const data = new Uint8Array(binary.length);
965
+ for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
966
+ const file = new File([data], expectedName, { type: ${JSON.stringify(attachment.mimeType)} });
967
+ const transfer = new DataTransfer();
968
+ if (${append ? 'true' : 'false'}) {
969
+ for (const existing of Array.from(element.files || [])) transfer.items.add(existing);
970
+ }
971
+ transfer.items.add(file);
972
+ element.files = transfer.files;
973
+ return Array.from(element.files || []).map((selected) => ({ name: selected.name, size: selected.size }));
974
+ })()`);
975
+ }
976
+
977
+ const selected = await this.evaluate<Array<{ name?: string; size?: number }>>(`(() => {
978
+ const element = document.querySelector(${JSON.stringify(selector)});
979
+ if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
980
+ element.dispatchEvent(new Event('input', { bubbles: true }));
981
+ element.dispatchEvent(new Event('change', { bubbles: true }));
982
+ return Array.from(element.files || []).map((file) => ({ name: file.name, size: file.size }));
983
+ })()`);
984
+ const expected = attachments.map((attachment) => ({ name: attachment.fileName, size: attachment.bytes.length }));
985
+ const matches = selected.length === expected.length && expected.every((item, index) => selected[index]?.name === item.name && selected[index]?.size === item.size);
986
+ if (!matches) {
987
+ throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local files on the target input.', {
988
+ retryable: true,
989
+ details: { expected, actual: selected },
990
+ });
991
+ }
992
+ } finally {
993
+ for (const attachment of attachments) {
994
+ await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(attachment.bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
995
+ }
996
+ }
997
+ }
998
+
999
+ async foregroundState(): Promise<{ frontmost: boolean; active: boolean }> {
1000
+ const metadata = await this.refreshMetadata();
1001
+ return { frontmost: metadata.frontmost, active: metadata.active === true };
1002
+ }
1003
+
1004
+ async trustedInput(input: BrowserAutomationTrustedInput): Promise<void> {
1005
+ if (!this.targetRef) {
1006
+ throw new AssistantPluginError('PLUGIN_BROWSER_TRUSTED_INPUT_UNAVAILABLE', 'Native trusted input requires an exact saved browser tab.', { retryable: false });
1007
+ }
1008
+ this.targetRef = await resolveCurrentMacOsBrowserTabRef(this.browser.product, this.targetRef, this.timeoutMs);
1009
+ await sendBrowserTrustedInput({ action: 'trusted_input', product: this.browser.product, ref: this.targetRef, input }, this.timeoutMs);
1010
+ await this.refreshMetadata();
1011
+ }
1012
+
627
1013
  async screenshot(options: Record<string, unknown>): Promise<Buffer> {
628
1014
  const path = typeof options.path === 'string' ? options.path : undefined;
629
1015
  if (!path) throw new Error('Native browser screenshot requires an output path.');
@@ -638,7 +1024,40 @@ export class MacOsAppleEventsPage {
638
1024
  }
639
1025
 
640
1026
  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;'));
1027
+ await this.evaluate(selectorSource(selector, `
1028
+ if (element instanceof HTMLOptionElement) {
1029
+ const selectElement = element.closest('select');
1030
+ if (!(selectElement instanceof HTMLSelectElement)) throw new Error('Option is not attached to a select.');
1031
+ if (element.disabled || selectElement.disabled) throw new Error('Option is disabled.');
1032
+ if (!selectElement.multiple) {
1033
+ const valueSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
1034
+ if (valueSetter) valueSetter.call(selectElement, element.value);
1035
+ else selectElement.value = element.value;
1036
+ } else {
1037
+ element.selected = true;
1038
+ }
1039
+ selectElement.dispatchEvent(new Event('input', { bubbles: true }));
1040
+ selectElement.dispatchEvent(new Event('change', { bubbles: true }));
1041
+ if (!Array.from(selectElement.selectedOptions).includes(element)) throw new Error('Option selection did not persist.');
1042
+ return true;
1043
+ }
1044
+ if (typeof element.click !== "function") throw new Error("Element is not clickable.");
1045
+ const rect = typeof element.getBoundingClientRect === 'function' ? element.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 };
1046
+ const clientX = Number(rect.left || 0) + Number(rect.width || 0) / 2;
1047
+ const clientY = Number(rect.top || 0) + Number(rect.height || 0) / 2;
1048
+ const mouseInit = { bubbles: true, cancelable: true, composed: true, view: window, button: 0, buttons: 1, clientX, clientY };
1049
+ if (typeof PointerEvent === 'function') {
1050
+ element.dispatchEvent(new PointerEvent('pointerdown', { ...mouseInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
1051
+ }
1052
+ element.dispatchEvent(new MouseEvent('mousedown', mouseInit));
1053
+ if (typeof element.focus === 'function') element.focus();
1054
+ if (typeof PointerEvent === 'function') {
1055
+ element.dispatchEvent(new PointerEvent('pointerup', { ...mouseInit, buttons: 0, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
1056
+ }
1057
+ element.dispatchEvent(new MouseEvent('mouseup', { ...mouseInit, buttons: 0 }));
1058
+ element.click();
1059
+ return true;
1060
+ `));
642
1061
  }
643
1062
 
644
1063
  async dblclick(selector: string): Promise<void> {
@@ -681,10 +1100,20 @@ export class MacOsAppleEventsPage {
681
1100
  return await this.evaluate(selectorSource(selector, `
682
1101
  if (!(element instanceof HTMLSelectElement)) throw new Error('Element is not a select.');
683
1102
  const selected = new Set(${JSON.stringify(normalized)});
684
- for (const option of element.options) option.selected = selected.has(option.value) || selected.has(option.label);
1103
+ let matched = 0;
1104
+ for (const option of element.options) {
1105
+ const shouldSelect = selected.has(option.value) || selected.has(option.label);
1106
+ option.selected = shouldSelect;
1107
+ if (shouldSelect) matched += 1;
1108
+ }
1109
+ if (matched === 0) throw new Error('Requested select option was not found.');
685
1110
  element.dispatchEvent(new Event('input', { bubbles: true }));
686
1111
  element.dispatchEvent(new Event('change', { bubbles: true }));
687
- return Array.from(element.selectedOptions).map((option) => option.value);
1112
+ const actual = Array.from(element.selectedOptions);
1113
+ if (!actual.some((option) => selected.has(option.value) || selected.has(option.label))) {
1114
+ throw new Error('Requested select option did not persist.');
1115
+ }
1116
+ return actual.map((option) => option.value);
688
1117
  `));
689
1118
  }
690
1119
 
@@ -730,25 +1159,37 @@ export class MacOsAppleEventsPage {
730
1159
 
731
1160
  async waitForLoadState(state = 'domcontentloaded', options: Record<string, unknown> = {}): Promise<void> {
732
1161
  const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
1162
+ const requireHttpUrl = options.requireHttpUrl === true;
733
1163
  const deadline = Date.now() + timeout;
734
1164
  while (Date.now() <= deadline) {
735
1165
  if (this.targetRef) {
736
1166
  try {
737
1167
  const metadata = await this.refreshMetadata();
1168
+ // Browser actions only admit HTTP(S) targets. Native Chrome/Vivaldi can
1169
+ // expose several internal URLs (new-tab, error/interstitial, extension
1170
+ // bootstrap pages) while navigation is still settling. Treat every
1171
+ // non-HTTP(S) metadata URL as transitional here; the adapter still
1172
+ // applies the strict final HTTP(S) URL validator before persisting a
1173
+ // session, so this does not widen the accepted navigation surface.
1174
+ const transitionalUrl = !/^https?:\/\//i.test(metadata.url ?? '');
1175
+ if (transitionalUrl) {
1176
+ await new Promise((resolve) => setTimeout(resolve, 100));
1177
+ continue;
1178
+ }
738
1179
  if (metadata.loading === false) return;
739
1180
  } catch {
740
1181
  // Fall through to DOM readiness when native loading metadata is unavailable.
741
1182
  }
742
1183
  }
743
1184
  try {
744
- const readyState = await this.evaluate<string>('document.readyState');
745
- if (state === 'load' || state === 'networkidle') {
746
- if (readyState === 'complete') return;
747
- } else if (readyState === 'interactive' || readyState === 'complete') return;
1185
+ const domState = await this.evaluate<{ readyState: string; url: string }>(
1186
+ '({ readyState: String(document.readyState || ""), url: String(document.location.href || "") })',
1187
+ );
1188
+ if (nativeDomLoadStateSatisfied(state, domState.readyState, domState.url, requireHttpUrl)) return;
748
1189
  } catch (error) {
749
1190
  if (error instanceof AssistantPluginError && error.code === 'PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED') {
750
1191
  const metadata = await this.refreshMetadata();
751
- if (metadata.url && metadata.url !== 'about:blank' && metadata.url !== 'chrome://newtab/' && metadata.url !== 'vivaldi://newtab/') return;
1192
+ if (/^https?:\/\//i.test(metadata.url ?? '')) return;
752
1193
  }
753
1194
  // Navigation can transiently invalidate the current JavaScript execution context.
754
1195
  }
@@ -768,6 +1209,9 @@ export class MacOsAppleEventsPage {
768
1209
  async bringToFront(): Promise<void> {
769
1210
  if (this.targetRef) {
770
1211
  this.metadata = await activateMacOsBrowserOwnedTab(this.browser.product, this.targetRef, this.timeoutMs);
1212
+ if (this.metadata.windowId && this.metadata.tabId) {
1213
+ this.targetRef = { windowId: this.metadata.windowId, tabId: this.metadata.tabId };
1214
+ }
771
1215
  return;
772
1216
  }
773
1217
  await this.runAutomation(
@@ -783,15 +1227,71 @@ export class MacOsAppleEventsPage {
783
1227
  };
784
1228
  }
785
1229
 
1230
+ async function resolveCurrentMacOsBrowserTabRef(
1231
+ product: MacOsBrowserProduct,
1232
+ ref: MacOsBrowserTabRef,
1233
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1234
+ ): Promise<MacOsBrowserTabRef> {
1235
+ // Existing unit-level AppleScript hooks historically model one exact script at a time.
1236
+ // Preserve that narrow test seam unless a live inventory hook is explicitly supplied;
1237
+ // production always resolves through the stable Desktop Operator broker inventory.
1238
+ const inventory = runtimeHooks.tabInventory
1239
+ ? await runtimeHooks.tabInventory(product, timeoutMs)
1240
+ : runtimeHooks.runAppleScript
1241
+ ? undefined
1242
+ : await listMacOsBrowserTabs(product, timeoutMs);
1243
+ if (!inventory) return ref;
1244
+
1245
+ const matches = inventory.tabs.filter((candidate) => candidate.tabId === ref.tabId);
1246
+ if (inventory.truncated || matches.length !== 1) {
1247
+ throw new AssistantPluginError(
1248
+ 'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN',
1249
+ inventory.truncated
1250
+ ? `Cannot prove the current ${product} tab identity because live inventory was truncated.`
1251
+ : matches.length === 0
1252
+ ? `Saved ${product} tab ${ref.tabId} no longer exists in live inventory.`
1253
+ : `Saved ${product} tab ${ref.tabId} is ambiguous in live inventory.`,
1254
+ {
1255
+ retryable: true,
1256
+ details: {
1257
+ browserProduct: product,
1258
+ tabId: ref.tabId,
1259
+ savedWindowId: ref.windowId,
1260
+ candidateCount: matches.length,
1261
+ inventoryTruncated: inventory.truncated,
1262
+ },
1263
+ },
1264
+ );
1265
+ }
1266
+
1267
+ const match = matches[0]!;
1268
+ return { windowId: match.windowId, tabId: match.tabId };
1269
+ }
1270
+
1271
+ export async function listMacOsBrowserTabs(
1272
+ product: MacOsBrowserProduct,
1273
+ timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
1274
+ ): Promise<MacOsBrowserTabInventory> {
1275
+ const browser = browserDefinition(product);
1276
+ const raw = await runBrowserAutomationText(
1277
+ { action: 'list_tabs', product },
1278
+ listTabsScript(browser),
1279
+ [],
1280
+ timeoutMs,
1281
+ );
1282
+ return parseTabInventory(product, raw);
1283
+ }
1284
+
786
1285
  export async function readMacOsBrowserOwnedTabMetadata(
787
1286
  product: MacOsBrowserProduct,
788
1287
  ref: MacOsBrowserTabRef,
789
1288
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
790
1289
  ): Promise<MacOsBrowserMetadata> {
791
1290
  const browser = browserDefinition(product);
1291
+ const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
792
1292
  return parseMetadata(product, await runBrowserAutomationText(
793
- { action: 'metadata', product, ref },
794
- targetMetadataScript(browser, ref),
1293
+ { action: 'metadata', product, ref: resolvedRef },
1294
+ targetMetadataScript(browser, resolvedRef),
795
1295
  [],
796
1296
  timeoutMs,
797
1297
  ));
@@ -803,13 +1303,33 @@ export async function activateMacOsBrowserOwnedTab(
803
1303
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
804
1304
  ): Promise<MacOsBrowserMetadata> {
805
1305
  const browser = browserDefinition(product);
1306
+ const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
806
1307
  await runBrowserAutomationText(
807
- { action: 'activate', product, ref },
808
- activateTargetTabScript(browser, ref),
1308
+ { action: 'activate', product, ref: resolvedRef },
1309
+ activateTargetTabScript(browser, resolvedRef),
809
1310
  [],
810
1311
  timeoutMs,
811
1312
  );
812
- return readMacOsBrowserOwnedTabMetadata(product, ref, timeoutMs);
1313
+ const metadata = await readMacOsBrowserOwnedTabMetadata(product, resolvedRef, timeoutMs);
1314
+ if (metadata.tabId !== resolvedRef.tabId || metadata.active !== true) {
1315
+ throw new AssistantPluginError(
1316
+ 'PLUGIN_BROWSER_NATIVE_ACTIVATION_POSTCONDITION_FAILED',
1317
+ `${browser.appName} did not prove that the exact requested tab became active.`,
1318
+ {
1319
+ retryable: true,
1320
+ details: {
1321
+ browserProduct: product,
1322
+ requestedTabId: resolvedRef.tabId,
1323
+ resolvedWindowId: resolvedRef.windowId,
1324
+ observedWindowId: metadata.windowId,
1325
+ observedTabId: metadata.tabId,
1326
+ observedActive: metadata.active,
1327
+ observedFrontmost: metadata.frontmost,
1328
+ },
1329
+ },
1330
+ );
1331
+ }
1332
+ return metadata;
813
1333
  }
814
1334
 
815
1335
  export function createMacOsBrowserPage(attachment: MacOsBrowserAttachment, timeoutMs?: number): MacOsAppleEventsPage {
@@ -831,10 +1351,12 @@ export async function createMacOsBrowserOwnedPage(
831
1351
  const parts = raw.split(RECORD_SEPARATOR);
832
1352
  if (parts.length < 2) throw new Error(`Browser scripting returned incomplete owned-tab metadata for ${browser.appName}.`);
833
1353
  const ref = { windowId: parseBrowserId(parts[0] ?? '', 'window id'), tabId: parseBrowserId(parts[1] ?? '', 'tab id') };
834
- return new MacOsAppleEventsPage({
1354
+ const page = new MacOsAppleEventsPage({
835
1355
  ...attachment,
836
1356
  metadata: { ...attachment.metadata, url, title: '', windowId: ref.windowId, tabId: ref.tabId, active: false },
837
1357
  }, timeoutMs, ref);
1358
+ rememberMacOsBrowserPageHandle(attachment.metadata.product, ref, page);
1359
+ return page;
838
1360
  }
839
1361
 
840
1362
  export async function reattachMacOsBrowserOwnedPage(
@@ -842,12 +1364,26 @@ export async function reattachMacOsBrowserOwnedPage(
842
1364
  ref: MacOsBrowserTabRef,
843
1365
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
844
1366
  ): Promise<{ page: MacOsAppleEventsPage; attachment: MacOsBrowserAttachment }> {
1367
+ const cacheKey = nativePageHandleKey(product, ref);
1368
+ const cached = warmNativePageHandles.get(cacheKey);
1369
+ if (cached) {
1370
+ cached.updateTimeout(timeoutMs);
1371
+ // Refresh insertion order so bounded eviction behaves as a small LRU.
1372
+ warmNativePageHandles.delete(cacheKey);
1373
+ warmNativePageHandles.set(cacheKey, cached);
1374
+ return { page: cached, attachment: cached.attachmentSnapshot() };
1375
+ }
845
1376
  const metadata = await readMacOsBrowserOwnedTabMetadata(product, ref, timeoutMs);
846
1377
  const attachment: MacOsBrowserAttachment = {
847
1378
  metadata,
848
1379
  attempts: [{ product, appName: metadata.appName, bundleId: metadata.bundleId, status: 'selected', frontmost: metadata.frontmost }],
849
1380
  };
850
- return { page: new MacOsAppleEventsPage(attachment, timeoutMs, ref), attachment };
1381
+ const resolvedRef = metadata.windowId && metadata.tabId
1382
+ ? { windowId: metadata.windowId, tabId: metadata.tabId }
1383
+ : ref;
1384
+ const page = new MacOsAppleEventsPage(attachment, timeoutMs, resolvedRef);
1385
+ rememberMacOsBrowserPageHandle(product, resolvedRef, page);
1386
+ return { page, attachment };
851
1387
  }
852
1388
 
853
1389
  export async function closeMacOsBrowserOwnedTab(
@@ -856,10 +1392,15 @@ export async function closeMacOsBrowserOwnedTab(
856
1392
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
857
1393
  ): Promise<void> {
858
1394
  const browser = browserDefinition(product);
859
- await runBrowserAutomationText(
860
- { action: 'close_tab', product, ref },
861
- closeOwnedTabScript(browser, ref),
862
- [],
863
- timeoutMs,
864
- );
1395
+ try {
1396
+ const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
1397
+ await runBrowserAutomationText(
1398
+ { action: 'close_tab', product, ref: resolvedRef },
1399
+ closeOwnedTabScript(browser, resolvedRef),
1400
+ [],
1401
+ timeoutMs,
1402
+ );
1403
+ } finally {
1404
+ invalidateMacOsBrowserPageHandle(product, ref);
1405
+ }
865
1406
  }