@moretea-labs/forge 1.6.0 → 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 (396) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -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/03-orchestration.partial.md +1 -1
  17. package/assets/plugin-registry.v1.json +1 -1
  18. package/assets/reference-configs/document-generation.md +1 -1
  19. package/assets/reference-configs/harness-overview.md +1 -1
  20. package/assets/reference-configs/sprint-contracts.md +3 -3
  21. package/assets/skill-version.json +2 -2
  22. package/assets/templates/contract.template.md +2 -7
  23. package/assets/templates/helpers/capture-plan.sh +2 -2
  24. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  25. package/assets/templates/helpers/contract-worktree.sh +1 -21
  26. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  27. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  28. package/assets/templates/helpers/new-plan.sh +1 -1
  29. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  30. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  31. package/assets/templates/helpers/verify-contract.sh +1 -111
  32. package/assets/templates/helpers/verify-sprint.sh +3 -180
  33. package/assets/templates/plan.template.md +1 -1
  34. package/assets/templates/review.template.md +9 -34
  35. package/assets/workflow-contract.v1.json +10 -19
  36. package/docs/README.md +10 -10
  37. package/docs/architecture/CURRENT.md +170 -0
  38. package/docs/architecture/EVOLUTION.md +42 -0
  39. package/docs/architecture/current/README.md +4 -156
  40. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  41. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  42. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  43. package/docs/architecture/history/README.md +7 -0
  44. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  45. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  46. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  47. package/docs/architecture/history.md +3 -34
  48. package/docs/architecture/index.md +15 -98
  49. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  50. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  51. package/docs/architecture/versions/1.6.md +38 -0
  52. package/docs/architecture/versions/1.7.md +41 -0
  53. package/docs/forge-chatgpt-browser-engine.md +2 -2
  54. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  55. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  56. package/docs/forge-github-issue-launcher.md +1 -1
  57. package/docs/forge-runtime-self-healing-loop.md +1 -1
  58. package/docs/operations/controller-browser-plugin.md +26 -6
  59. package/docs/operations/controller-performance-and-502.md +124 -0
  60. package/docs/operations/controller-reliability-runbook.md +1 -1
  61. package/docs/operations/features.md +2 -2
  62. package/docs/operations/github-repository.md +1 -1
  63. package/docs/operations/homebrew.md +2 -2
  64. package/docs/operations/mcp-tool-exposure.md +5 -3
  65. package/docs/wiki/Architecture.md +6 -6
  66. package/docs/wiki/Implementation.md +2 -2
  67. package/docs/wiki/Integrations.md +2 -2
  68. package/docs/wiki/Work-Lifecycle.md +1 -1
  69. package/package.json +29 -10
  70. package/scripts/activate-source-baseline.ts +17 -13
  71. package/scripts/benchmark-background-check-overlap.ts +228 -0
  72. package/scripts/benchmark-mcp-host.sh +117 -0
  73. package/scripts/benchmark-mcp-transport.ts +170 -0
  74. package/scripts/benchmark-quality-harness.ts +146 -0
  75. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  76. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  77. package/scripts/benchmark-typescript-navigation.ts +186 -0
  78. package/scripts/benchmark-work-efficiency.ts +334 -0
  79. package/scripts/capture-plan.sh +3 -3
  80. package/scripts/check-agent-tooling.sh +8 -35
  81. package/scripts/check-architecture-sync.sh +17 -86
  82. package/scripts/check-mcp-compatibility.ts +14 -0
  83. package/scripts/check-runtime-architecture.mjs +107 -64
  84. package/scripts/check-stable-baseline.ts +92 -0
  85. package/scripts/check-task-workflow.sh +4 -4
  86. package/scripts/contract-worktree.sh +1 -21
  87. package/scripts/ensure-task-workflow.sh +22 -55
  88. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  89. package/scripts/install-standalone-recovery.ts +24 -0
  90. package/scripts/lib/project-init-lib.sh +15 -44
  91. package/scripts/migrate-project-template.sh +23 -1179
  92. package/scripts/new-plan.sh +1 -1
  93. package/scripts/plan-to-todo.sh +11 -36
  94. package/scripts/run-governed-gate.ts +2 -1
  95. package/scripts/ship-worktrees.sh +2 -29
  96. package/scripts/smoke-ios-physical-device.ts +4 -2
  97. package/scripts/smoke-runtime-recovery.ts +1 -35
  98. package/scripts/smoke-schedule-engine.ts +1 -20
  99. package/scripts/verify-contract.sh +1 -111
  100. package/scripts/verify-forge-runtime.sh +0 -1
  101. package/scripts/verify-sprint.sh +3 -180
  102. package/scripts/workflow-contract.ts +1 -5
  103. package/src/cli/commands/init-hook.ts +3 -3
  104. package/src/cli/commands/mcp.ts +9 -5
  105. package/src/cli/commands/plugin.ts +1 -1
  106. package/src/cli/commands/recovery.ts +25 -3
  107. package/src/cli/commands/runtime.ts +55 -3
  108. package/src/cli/commands/security.ts +12 -1
  109. package/src/cli/commands/setup-profile.ts +31 -21
  110. package/src/cli/controller/check-runner.ts +106 -3
  111. package/src/cli/controller/composite-result.ts +1 -1
  112. package/src/cli/controller/context/focus.ts +51 -0
  113. package/src/cli/controller/context/instruction-context.ts +114 -0
  114. package/src/cli/controller/context/known-paths.ts +76 -0
  115. package/src/cli/controller/context/query-planning.ts +110 -0
  116. package/src/cli/controller/context/rule-resolution.ts +532 -0
  117. package/src/cli/controller/context/source-materializer.ts +278 -0
  118. package/src/cli/controller/context/structural.ts +16 -0
  119. package/src/cli/controller/context/types.ts +286 -0
  120. package/src/cli/controller/context-pack.ts +785 -469
  121. package/src/cli/controller/launch-agents.ts +77 -2
  122. package/src/cli/controller/operational-plan.ts +2 -2
  123. package/src/cli/controller/work-mode.ts +6 -4
  124. package/src/cli/editing/edit-session.ts +60 -7
  125. package/src/cli/github/github.ts +133 -0
  126. package/src/cli/installer/managed-entries.ts +15 -1
  127. package/src/cli/local-bridge/console-automations.ts +54 -78
  128. package/src/cli/local-bridge/console-view-models.ts +1 -35
  129. package/src/cli/local-bridge/facade-api.ts +0 -217
  130. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  131. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  132. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  133. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  134. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  135. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  136. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  137. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  138. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  139. package/src/cli/local-bridge/job-store.ts +1 -490
  140. package/src/cli/local-bridge/server.ts +0 -513
  141. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  142. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  143. package/src/cli/mcp/auth.ts +5 -5
  144. package/src/cli/mcp/instructions.ts +3 -2
  145. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  146. package/src/cli/mcp/multi-repository.ts +16 -5
  147. package/src/cli/mcp/oauth.ts +175 -26
  148. package/src/cli/mcp/repository-tools.ts +247 -107
  149. package/src/cli/mcp/server.ts +537 -111
  150. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  151. package/src/cli/mcp/toolset-selection.ts +1 -1
  152. package/src/cli/mcp/toolset.ts +42 -5
  153. package/src/cli/mcp/transports/http.ts +101 -14
  154. package/src/cli/mcp/transports/session-registry.ts +43 -5
  155. package/src/cli/mcp/types.ts +3 -3
  156. package/src/cli/repositories/command-classifier.ts +101 -19
  157. package/src/cli/repositories/command-executor.ts +55 -521
  158. package/src/cli/repositories/command-process.ts +108 -0
  159. package/src/cli/repositories/command-scope.ts +99 -41
  160. package/src/cli/repositories/controller-home.ts +7 -8
  161. package/src/cli/repositories/git-command-executor.ts +2 -0
  162. package/src/cli/repositories/locks.ts +53 -16
  163. package/src/cli/repositories/repository-snapshot.ts +121 -0
  164. package/src/cli/repositories/selected-path-actions.ts +17 -9
  165. package/src/cli/repositories/structured-git.ts +61 -3
  166. package/src/cli/repository/inspector.ts +553 -63
  167. package/src/cli/repository/session-cache.ts +58 -17
  168. package/src/cli/v81-entry.ts +1 -4
  169. package/src/effects/fs-transaction.ts +85 -17
  170. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  171. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  172. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  173. package/src/runtime/context/lsp-client.ts +283 -0
  174. package/src/runtime/context/semantic-navigation.ts +275 -0
  175. package/src/runtime/context/swift-navigation.ts +565 -0
  176. package/src/runtime/context/typescript-navigation.ts +226 -0
  177. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  178. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +47 -8
  179. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  180. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  181. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  182. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  183. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  184. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  185. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  186. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +143 -8
  187. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  188. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +85 -8
  189. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  190. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  191. package/src/runtime/control-plane/facade/controller-session-store.ts +132 -1
  192. package/src/runtime/control-plane/facade/goal-workloop-access.ts +2 -0
  193. package/src/runtime/control-plane/facade/goal-workloop.ts +686 -129
  194. package/src/runtime/control-plane/facade/index.ts +1 -0
  195. package/src/runtime/control-plane/facade/plan-contract-store.ts +185 -20
  196. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  197. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  198. package/src/runtime/control-plane/facade/types.ts +22 -5
  199. package/src/runtime/control-plane/facade/work-contract-store.ts +175 -73
  200. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  201. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  202. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  203. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  204. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  205. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  206. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  207. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  208. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  209. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  210. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  211. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  212. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  213. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  214. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  215. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  216. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  217. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  218. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  219. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +355 -116
  220. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  221. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  222. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  223. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  224. package/src/runtime/control-plane/release-retention.ts +62 -2
  225. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  226. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  227. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  228. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  229. package/src/runtime/diagnostics/performance.ts +24 -1
  230. package/src/runtime/evidence/event-ledger.ts +1 -1
  231. package/src/runtime/execution/jobs/store.ts +58 -4
  232. package/src/runtime/execution/jobs/types.ts +1 -1
  233. package/src/runtime/execution/managed-workspace.ts +203 -1
  234. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  235. package/src/runtime/execution/process-runtime/check-result.ts +53 -0
  236. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  237. package/src/runtime/execution/process-runtime/command-facade.ts +345 -40
  238. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  239. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  240. package/src/runtime/execution/process-runtime/persisted-check.ts +47 -4
  241. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  242. package/src/runtime/execution/process-runtime/runtime.ts +43 -3
  243. package/src/runtime/execution/process-runtime/store.ts +1 -0
  244. package/src/runtime/execution/process-runtime/types.ts +2 -0
  245. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  246. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  247. package/src/runtime/execution/workers/executor.ts +0 -48
  248. package/src/runtime/gateway/mcp/execution-tools.ts +1192 -114
  249. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  250. package/src/runtime/gateway/mcp/router.ts +184 -82
  251. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  252. package/src/runtime/gateway/mcp/runtime-tools.ts +2224 -1589
  253. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  254. package/src/runtime/plugins/browser-adapter.ts +2901 -241
  255. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  256. package/src/runtime/plugins/browser-handoff.ts +92 -2
  257. package/src/runtime/plugins/browser-macos-bridge.ts +582 -49
  258. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  259. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  260. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  261. package/src/runtime/plugins/browser-runtime.ts +183 -0
  262. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  263. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  264. package/src/runtime/plugins/desktop-operator-registration.ts +8 -2
  265. package/src/runtime/plugins/external-adapter.ts +324 -3
  266. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  267. package/src/runtime/plugins/first-party-registry.ts +2 -0
  268. package/src/runtime/plugins/github-adapter.ts +125 -0
  269. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  270. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  271. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  272. package/src/runtime/plugins/ios-adapter.ts +106 -16
  273. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  274. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  275. package/src/runtime/plugins/lightweight-action.ts +153 -0
  276. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  277. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  278. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  279. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  280. package/src/runtime/plugins/resend-adapter.ts +1 -1
  281. package/src/runtime/plugins/store.ts +269 -29
  282. package/src/runtime/plugins/types.ts +35 -2
  283. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  284. package/src/runtime/projections/git-status-sampler.ts +16 -16
  285. package/src/runtime/projections/materialized-view.ts +28 -10
  286. package/src/runtime/recovery/maintenance-executor.ts +393 -28
  287. package/src/runtime/root/mcp-transport.ts +180 -10
  288. package/src/runtime/root/package-connector-service.ts +186 -8
  289. package/src/runtime/root/package-runtime-release.ts +143 -17
  290. package/src/runtime/root/package-runtime-service.ts +430 -22
  291. package/src/runtime/root/release-manifest.ts +7 -2
  292. package/src/runtime/root/release-materialize.ts +19 -0
  293. package/src/runtime/root/release-store.ts +48 -11
  294. package/src/runtime/root/runtime.ts +23 -5
  295. package/src/runtime/root/service.ts +100 -29
  296. package/src/runtime/root/types.ts +2 -0
  297. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  298. package/src/runtime/shared/trusted-executable.ts +62 -0
  299. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  300. package/src/runtime/standalone-recovery/core.ts +700 -92
  301. package/src/runtime/standalone-recovery/entry.ts +99 -23
  302. package/src/runtime/standalone-recovery/installer.ts +19 -3
  303. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  304. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  305. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  306. package/src/runtime/workflow/schedules/engine.ts +287 -151
  307. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  308. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  309. package/src/runtime/workflow/schedules/store.ts +131 -10
  310. package/src/runtime/workflow/schedules/types.ts +2 -4
  311. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  312. package/src/testing/test-governance.ts +8 -40
  313. package/assets/reference-configs/heartbeat-triage.md +0 -57
  314. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  315. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  316. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  317. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  318. package/docs/architecture/current/architecture-invariants.md +0 -331
  319. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  320. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  321. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  322. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  323. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  324. package/docs/architecture/current/entity-model.md +0 -612
  325. package/docs/architecture/current/external-plugin-governance.md +0 -396
  326. package/docs/architecture/current/failure-recovery.md +0 -447
  327. package/docs/architecture/current/governance.md +0 -213
  328. package/docs/architecture/current/human-interaction-plane.md +0 -87
  329. package/docs/architecture/current/implementation-status.md +0 -128
  330. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  331. package/docs/architecture/current/legacy-governance.md +0 -58
  332. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  333. package/docs/architecture/current/migration-roadmap.md +0 -105
  334. package/docs/architecture/current/multi-repository-execution.md +0 -371
  335. package/docs/architecture/current/operations-runbook.md +0 -45
  336. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  337. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  338. package/docs/architecture/current/repository-access-modes.md +0 -102
  339. package/docs/architecture/current/route-policy.md +0 -15
  340. package/docs/architecture/current/route-session-concurrency.md +0 -85
  341. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  342. package/docs/architecture/current/runtime-directory-map.md +0 -49
  343. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  344. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  345. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  346. package/docs/architecture/current/system-overview.md +0 -169
  347. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  348. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  349. package/docs/architecture/current/thin-harness-v1.md +0 -321
  350. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  351. package/docs/forge-autonomous-goal-loop.md +0 -9
  352. package/docs/forge-provider-configuration.md +0 -7
  353. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  354. package/docs/operations/gmail-assistant-routines.md +0 -186
  355. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  356. package/docs/operations/provider-configuration.md +0 -31
  357. package/scripts/harness-trace-grade.sh +0 -150
  358. package/scripts/heartbeat-triage.sh +0 -361
  359. package/scripts/maintenance-triage.sh +0 -167
  360. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  361. package/src/cli/repositories/goal-registry.ts +0 -239
  362. package/src/runtime/assistant/action-proposals.ts +0 -279
  363. package/src/runtime/assistant/intent.ts +0 -395
  364. package/src/runtime/assistant/model-provider.ts +0 -460
  365. package/src/runtime/assistant/openapi.ts +0 -223
  366. package/src/runtime/assistant/policy.ts +0 -89
  367. package/src/runtime/assistant/readiness.ts +0 -258
  368. package/src/runtime/assistant/routine-runtime.ts +0 -477
  369. package/src/runtime/assistant/schedule-binding.ts +0 -171
  370. package/src/runtime/assistant/standing-grants.ts +0 -375
  371. package/src/runtime/assistant/store.ts +0 -243
  372. package/src/runtime/assistant/types.ts +0 -135
  373. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  374. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  375. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  376. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  377. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  378. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  379. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  380. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  381. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  382. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  383. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  384. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  385. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  386. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  387. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  388. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  389. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  390. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  391. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  392. package/src/runtime/workflow/findings/store.ts +0 -204
  393. package/src/runtime/workflow/findings/types.ts +0 -58
  394. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  395. package/src/runtime/workflow/portfolio/store.ts +0 -185
  396. 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,15 +1159,19 @@ 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();
738
- const transitionalUrl = !metadata.url
739
- || metadata.url === 'about:blank'
740
- || metadata.url === 'chrome://newtab/'
741
- || metadata.url === 'vivaldi://newtab/';
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 ?? '');
742
1175
  if (transitionalUrl) {
743
1176
  await new Promise((resolve) => setTimeout(resolve, 100));
744
1177
  continue;
@@ -749,14 +1182,14 @@ export class MacOsAppleEventsPage {
749
1182
  }
750
1183
  }
751
1184
  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;
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;
756
1189
  } catch (error) {
757
1190
  if (error instanceof AssistantPluginError && error.code === 'PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED') {
758
1191
  const metadata = await this.refreshMetadata();
759
- if (metadata.url && metadata.url !== 'about:blank' && metadata.url !== 'chrome://newtab/' && metadata.url !== 'vivaldi://newtab/') return;
1192
+ if (/^https?:\/\//i.test(metadata.url ?? '')) return;
760
1193
  }
761
1194
  // Navigation can transiently invalidate the current JavaScript execution context.
762
1195
  }
@@ -776,6 +1209,9 @@ export class MacOsAppleEventsPage {
776
1209
  async bringToFront(): Promise<void> {
777
1210
  if (this.targetRef) {
778
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
+ }
779
1215
  return;
780
1216
  }
781
1217
  await this.runAutomation(
@@ -791,15 +1227,71 @@ export class MacOsAppleEventsPage {
791
1227
  };
792
1228
  }
793
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
+
794
1285
  export async function readMacOsBrowserOwnedTabMetadata(
795
1286
  product: MacOsBrowserProduct,
796
1287
  ref: MacOsBrowserTabRef,
797
1288
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
798
1289
  ): Promise<MacOsBrowserMetadata> {
799
1290
  const browser = browserDefinition(product);
1291
+ const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
800
1292
  return parseMetadata(product, await runBrowserAutomationText(
801
- { action: 'metadata', product, ref },
802
- targetMetadataScript(browser, ref),
1293
+ { action: 'metadata', product, ref: resolvedRef },
1294
+ targetMetadataScript(browser, resolvedRef),
803
1295
  [],
804
1296
  timeoutMs,
805
1297
  ));
@@ -811,13 +1303,33 @@ export async function activateMacOsBrowserOwnedTab(
811
1303
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
812
1304
  ): Promise<MacOsBrowserMetadata> {
813
1305
  const browser = browserDefinition(product);
1306
+ const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
814
1307
  await runBrowserAutomationText(
815
- { action: 'activate', product, ref },
816
- activateTargetTabScript(browser, ref),
1308
+ { action: 'activate', product, ref: resolvedRef },
1309
+ activateTargetTabScript(browser, resolvedRef),
817
1310
  [],
818
1311
  timeoutMs,
819
1312
  );
820
- 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;
821
1333
  }
822
1334
 
823
1335
  export function createMacOsBrowserPage(attachment: MacOsBrowserAttachment, timeoutMs?: number): MacOsAppleEventsPage {
@@ -839,10 +1351,12 @@ export async function createMacOsBrowserOwnedPage(
839
1351
  const parts = raw.split(RECORD_SEPARATOR);
840
1352
  if (parts.length < 2) throw new Error(`Browser scripting returned incomplete owned-tab metadata for ${browser.appName}.`);
841
1353
  const ref = { windowId: parseBrowserId(parts[0] ?? '', 'window id'), tabId: parseBrowserId(parts[1] ?? '', 'tab id') };
842
- return new MacOsAppleEventsPage({
1354
+ const page = new MacOsAppleEventsPage({
843
1355
  ...attachment,
844
1356
  metadata: { ...attachment.metadata, url, title: '', windowId: ref.windowId, tabId: ref.tabId, active: false },
845
1357
  }, timeoutMs, ref);
1358
+ rememberMacOsBrowserPageHandle(attachment.metadata.product, ref, page);
1359
+ return page;
846
1360
  }
847
1361
 
848
1362
  export async function reattachMacOsBrowserOwnedPage(
@@ -850,12 +1364,26 @@ export async function reattachMacOsBrowserOwnedPage(
850
1364
  ref: MacOsBrowserTabRef,
851
1365
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
852
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
+ }
853
1376
  const metadata = await readMacOsBrowserOwnedTabMetadata(product, ref, timeoutMs);
854
1377
  const attachment: MacOsBrowserAttachment = {
855
1378
  metadata,
856
1379
  attempts: [{ product, appName: metadata.appName, bundleId: metadata.bundleId, status: 'selected', frontmost: metadata.frontmost }],
857
1380
  };
858
- 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 };
859
1387
  }
860
1388
 
861
1389
  export async function closeMacOsBrowserOwnedTab(
@@ -864,10 +1392,15 @@ export async function closeMacOsBrowserOwnedTab(
864
1392
  timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
865
1393
  ): Promise<void> {
866
1394
  const browser = browserDefinition(product);
867
- await runBrowserAutomationText(
868
- { action: 'close_tab', product, ref },
869
- closeOwnedTabScript(browser, ref),
870
- [],
871
- timeoutMs,
872
- );
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
+ }
873
1406
  }