@moretea-labs/forge 1.8.1 → 1.8.2

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 (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -1,6 +1,10 @@
1
1
  (() => {
2
2
  const core = globalThis.ForgeComputerChatgptCore;
3
3
  if (!core) throw new Error('FORGE_COMPUTER_CHATGPT_CORE_MISSING');
4
+ // Declarative document_idle loading and exact-tab receiver recovery can race.
5
+ // Install one listener set per document or one Send could be handled twice.
6
+ if (globalThis.__forgeComputerChatgptContentInstalledV1 === true) return;
7
+ globalThis.__forgeComputerChatgptContentInstalledV1 = true;
4
8
  const ASSISTANT = '[data-message-author-role="assistant"], [data-chatgpt-search-unit-key$=":assistant"], [data-content-search-unit-key$=":assistant"]';
5
9
  const USER = '[data-message-author-role="user"], [data-chatgpt-search-unit-key$=":user"], [data-content-search-unit-key$=":user"]';
6
10
  const COMPOSER = '#prompt-textarea, [data-testid="prompt-textarea"], [data-testid="composer-text-input"], textarea[name="prompt"], textarea[placeholder*="Message"], textarea[placeholder*="消息"], form [contenteditable="true"], div[role="textbox"][contenteditable="true"]';
@@ -52,7 +56,9 @@
52
56
  const messageText = (node) => {
53
57
  if (!node) return '';
54
58
  const semanticContent = node.querySelector?.('[data-chatgpt-selection-message-id]');
55
- return core.normalizeText(semanticContent?.innerText ?? semanticContent?.textContent ?? node.innerText ?? node.textContent);
59
+ // Message line boundaries carry the final Supervisor receipt. Normalizing
60
+ // them like UI labels destroys the protocol before the owner can parse it.
61
+ return String(semanticContent?.innerText ?? semanticContent?.textContent ?? node.innerText ?? node.textContent ?? '').trim();
56
62
  };
57
63
  const roleNodes = (selector) => {
58
64
  const seen = new Set();
@@ -111,7 +117,8 @@
111
117
  const status = Array.from(document.querySelectorAll('[role="alert"], [role="status"], [aria-live="assertive"], [aria-live="polite"]'))
112
118
  .filter(visible);
113
119
  const capacity = Array.from(conversationRoot().querySelectorAll('p, span, div'))
114
- .filter((node) => visible(node) && !node.closest(TURN) && !node.closest('[data-message-author-role]'))
120
+ // Exclude message subtrees before layout-dependent visibility reads.
121
+ .filter((node) => !node.closest(TURN) && !node.closest('[data-message-author-role]') && visible(node))
115
122
  .filter((node) => {
116
123
  const text = core.normalizeText(node.innerText ?? node.textContent);
117
124
  return text.length <= 250 && /(?:你已达到此对话的长度上限|此对话已达到长度上限|you(?:'ve| have) reached the (?:maximum length for this conversation|conversation length limit)|this conversation has reached its maximum length)/i.test(text);
@@ -120,12 +127,46 @@
120
127
  .map((node) => core.normalizeText(node.innerText ?? node.textContent))
121
128
  .filter(Boolean).slice(-8).join('\n').slice(-PROVIDER_FAILURE_CHARS);
122
129
  };
123
- const composer = () => document.querySelector(COMPOSER);
124
- const composerText = () => core.normalizeText(composer()?.value ?? composer()?.innerText ?? composer()?.textContent);
130
+ // Project pages can retain hidden SSR editors beside the live hydrated one.
131
+ // Resolve one visible editor, preferring the active main, or fail closed:
132
+ // a hidden editor's draft is not proof that the task-owned composer is busy.
133
+ const composer = () => {
134
+ const visibleEditors = Array.from(document.querySelectorAll(COMPOSER))
135
+ .filter((node) => visible(node) && !node.closest?.('[aria-hidden="true"], [hidden], [inert]'));
136
+ const root = conversationRoot();
137
+ const inMain = root === document ? [] : visibleEditors.filter((node) => root.contains?.(node));
138
+ const candidates = inMain.length ? inMain : visibleEditors;
139
+ return candidates.length === 1 ? candidates[0] : undefined;
140
+ };
141
+ // Editor text is a provider payload, not a UI label: preserve its whitespace.
142
+ const exactComposerText = (value) => String(value ?? '').replace(/\r\n?/g, '\n');
143
+ const composerText = (editor = composer()) => {
144
+ const blocks = editor?.isContentEditable ? Array.from(editor.children ?? []) : [];
145
+ // ChatGPT's ProseMirror writes one P block per entered newline but
146
+ // HTMLElement.innerText synthesizes TWO newlines between those blocks.
147
+ // Reconstruct the actual editor payload from its direct paragraphs rather
148
+ // than comparing synthetic layout whitespace to an immutable effect.
149
+ const observed = blocks.length > 0 && blocks.every((block) => block.tagName === 'P')
150
+ ? blocks.map((block) => {
151
+ // innerText may insert a *visual* newline around auto-linked URLs or
152
+ // inline styled descendants even inside one ProseMirror paragraph.
153
+ // One direct <p> is one actual editor line: read its text nodes.
154
+ const value = exactComposerText(block.textContent ?? block.innerText);
155
+ return !value.trim() ? '' : value;
156
+ }).join('\n')
157
+ : editor?.value ?? editor?.innerText ?? editor?.textContent;
158
+ const text = exactComposerText(observed);
159
+ // Only whitespace-only *empty* editors collapse; all nonblank text,
160
+ // including leading/trailing spaces and intentional blank lines, remains
161
+ // byte-for-byte subject only to CRLF normalization.
162
+ return text.trim() ? text : '';
163
+ };
125
164
  const pageSnapshot = (options = {}) => {
126
165
  const userNodes = roleNodes(USER);
127
166
  const assistantNodes = roleNodes(ASSISTANT);
128
- const bodyText = String(document.body?.innerText ?? '');
167
+ // Reading innerText on the entire page can force layout on every poll.
168
+ // Only collect it when a caller explicitly requests the full page.
169
+ const editor = composer();
129
170
  return {
130
171
  url: location.href,
131
172
  title: document.title,
@@ -135,10 +176,15 @@
135
176
  userMessages: userNodes.map(messageText).filter(Boolean),
136
177
  assistantMessages: assistantNodes.map(messageText).filter(Boolean),
137
178
  } : {}),
138
- ...(composer() ? { composerText: composerText() } : {}),
179
+ ...(editor ? { composerText: composerText(editor) } : {}),
180
+ composerSelectorVersion: 'visible-single-v2',
181
+ composerReplacementContract: 'exact-backed-up-project-draft-v1',
182
+ // Read-only hydration attestation for a NEW Project bootstrap tab.
183
+ // URL/DOM shell visibility alone cannot prove that the React model
184
+ // picker has mounted or that the provider editor is send-ready.
139
185
  providerActivityText: providerActivityText(),
140
186
  providerFailureText: providerFailureText(),
141
- ...(options.includePageText === true ? { pageText: bodyText.slice(-500000) } : {}),
187
+ ...(options.includePageText === true ? { pageText: String(document.body?.innerText ?? '').slice(-500000) } : {}),
142
188
  latestTurnRole: latestTurnRole(),
143
189
  isGenerating: isGenerating(),
144
190
  };
@@ -165,98 +211,27 @@
165
211
  selection?.removeAllRanges();
166
212
  }
167
213
  };
168
- // Reasoning is a provider UI preference, not a Supervisor effect or workflow
169
- // decision. Observe the concrete composer control and fail before inserting
170
- // any prompt if the page does not acknowledge the requested level.
171
- const reasoningLevel = (value) => {
172
- const label = core.normalizeText(value).toLowerCase().replace(/[\s_-]+/g, '');
173
- if (/extrahigh|xhigh|超高|极高/.test(label)) return 'xhigh';
174
- if (/medium|中等/.test(label) || label === '中') return 'medium';
175
- if (/high|高/.test(label)) return 'high';
176
- return undefined;
177
- };
178
- const reasoningControl = () => {
179
- const selectors = 'button[aria-haspopup="menu"], button[aria-haspopup="listbox"], [role="button"][aria-haspopup="menu"], button[data-testid*="model"], button[data-testid*="reasoning"]';
180
- const candidates = Array.from(conversationRoot().querySelectorAll(selectors)).filter(visible);
181
- return candidates.find((node) => {
182
- const label = core.normalizeText((node.getAttribute?.('aria-label') ?? '') + ' ' + (node.innerText ?? node.textContent ?? ''));
183
- const normalized = label.toLowerCase().replace(/[\s_-]+/g, '');
184
- return /^gpt\d/.test(normalized) || normalized.includes('reasoning') || normalized.includes('thinking')
185
- || normalized.includes('推理') || normalized.includes('思考') || reasoningLevel(label) !== undefined;
186
- });
187
- };
188
- const reasoningControlLabel = (node) => core.normalizeText((node?.getAttribute?.('aria-label') ?? '') + ' ' + (node?.innerText ?? node?.textContent ?? ''));
189
- const selectedReasoningOption = (level) => Array.from(document.querySelectorAll('[role="menuitemradio"], [role="option"], [role="radio"]'))
190
- .filter(visible)
191
- .find((node) => reasoningLevel(reasoningControlLabel(node)) === level
192
- && (node.getAttribute('aria-checked') === 'true' || node.getAttribute('aria-selected') === 'true'));
193
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
194
- const ensureReasoning = async (level) => {
195
- if (level !== 'medium' && level !== 'high' && level !== 'xhigh') {
196
- return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_INVALID' };
197
- }
198
- const control = reasoningControl();
199
- if (!control) return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_CONTROL_UNAVAILABLE' };
200
- if (reasoningLevel(reasoningControlLabel(control)) === level) return { verified: true, level };
201
- control.click();
202
- await sleep(80);
203
- const menuOptions = Array.from(document.querySelectorAll('[role="menuitemradio"], [role="option"], [role="radio"]'))
204
- .filter(visible)
205
- .map((node) => ({ node, level: reasoningLevel(reasoningControlLabel(node)), disabled: node.hasAttribute('disabled') || node.getAttribute('aria-disabled') === 'true' }));
206
- // xhigh means highest *available*, not a claim that every plan exposes
207
- // Extra High. Prefer it, then High, then Medium; record the actual level.
208
- const menuLevel = level === 'xhigh'
209
- ? ['xhigh', 'high', 'medium'].find((candidate) => menuOptions.some((item) => item.level === candidate && !item.disabled))
210
- : level;
211
- const option = menuOptions.find((item) => item.level === menuLevel);
212
- if (option) {
213
- if (option.disabled) return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_OPTION_DISABLED' };
214
- option.node.click();
215
- await sleep(80);
216
- if (reasoningLevel(reasoningControlLabel(reasoningControl())) === menuLevel || selectedReasoningOption(menuLevel)) {
217
- return { verified: true, level: menuLevel };
218
- }
219
- return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_NOT_VERIFIED' };
220
- }
221
- // Older ChatGPT layouts expose a five-position slider in the model menu.
222
- // React must acknowledge each navigation by changing aria-valuenow; writing
223
- // our own attribute would be a false verification.
224
- const sliders = Array.from(document.querySelectorAll('[role="slider"][aria-valuenow]')).filter(visible);
225
- if (sliders.length !== 1) return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_OPTION_UNAVAILABLE' };
226
- const slider = sliders[0];
227
- const max = Number(slider.getAttribute('aria-valuemax'));
228
- const min = Number(slider.getAttribute('aria-valuemin') ?? '0');
229
- const target = level === 'xhigh' ? max : level === 'high' ? 3 : 2;
230
- const actualLevel = target === 4 ? 'xhigh' : target === 3 ? 'high' : target === 2 ? 'medium' : undefined;
231
- let current = Number(slider.getAttribute('aria-valuenow'));
232
- if (![min, max, current, target].every(Number.isInteger) || min < 0 || max > 4 || max < 2
233
- || current < min || current > max || target < min || target > max || !actualLevel) {
234
- return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_STATE_UNAVAILABLE' };
235
- }
236
- for (let step = 0; step < Math.abs(target - current); step += 1) {
237
- slider.focus?.();
238
- slider.dispatchEvent(new KeyboardEvent('keydown', { key: target > current ? 'ArrowRight' : 'ArrowLeft', bubbles: true }));
239
- await sleep(40);
240
- const next = Number(slider.getAttribute('aria-valuenow'));
241
- if (next !== current + (target > current ? 1 : -1)) {
242
- return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_NOT_VERIFIED' };
243
- }
244
- current = next;
245
- }
246
- if (Number(slider.getAttribute('aria-valuenow')) !== target) {
247
- return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_NOT_VERIFIED' };
248
- }
249
- const valueText = slider.getAttribute('aria-valuetext');
250
- if (valueText && reasoningLevel(valueText) !== actualLevel) {
251
- return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_NOT_VERIFIED' };
252
- }
253
- return { verified: true, level: actualLevel };
254
- };
255
- const dispatchPrompt = async (prompt, mode = 'send', reasoning, expectedConversationId, prepareOnly = false) => {
256
- const exactConversationIsCurrent = () => !expectedConversationId
257
- || core.parseConversation(location.href)?.conversationId === expectedConversationId;
214
+ const dispatchPrompt = async (prompt, mode = 'send', expectedConversationId, prepareOnly = false, expectedProjectId) => {
215
+ const exactConversationIsCurrent = () => expectedConversationId
216
+ ? core.parseConversation(location.href)?.conversationId === expectedConversationId
217
+ : expectedProjectId ? core.projectId(location.href) === expectedProjectId && !core.parseConversation(location.href)
218
+ : true;
258
219
  if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
259
220
  if (typeof prompt !== 'string' || !prompt.trim()) return { dispatched: false, reason: 'prompt_required' };
221
+ // This bounded prepare-only envelope is produced by the canonical
222
+ // Supervisor only after an exact 0600 on-device backup and operator marker.
223
+ // It is never part of a sent effect and never authorizes another provider.
224
+ const replacement = /^<<<FORGE_DRAFT_REPLACE_V1:([a-f0-9]{24}):([a-f0-9]{32})>>>\n/.exec(prompt);
225
+ if (replacement && (!prepareOnly || !expectedProjectId || mode !== 'send')) {
226
+ return { dispatched: false, reason: 'composer_replacement_mode_invalid' };
227
+ }
228
+ // Prepare-only exact retirement; never submits or retries the stopped task.
229
+ const retired = /^<<<FORGE_RETIRED_DRAFT_V1:((?:fx|crpe)_[a-zA-Z0-9_-]{8,120}):([a-f0-9]{24})>>>\n/.exec(prompt);
230
+ if (retired && (!prepareOnly || !expectedProjectId || mode !== 'send' || replacement)) {
231
+ return { dispatched: false, reason: 'composer_replacement_mode_invalid' };
232
+ }
233
+ const actualPrompt = replacement ? prompt.slice(replacement[0].length)
234
+ : retired ? prompt.slice(retired[0].length) : prompt;
260
235
  if (mode === 'recover') {
261
236
  const stop = document.querySelector(STOP);
262
237
  if (prepareOnly && stop instanceof HTMLElement) return { prepared: false, reason: 'provider_recovery_stop_pending' };
@@ -272,66 +247,114 @@
272
247
  } else if (isGenerating()) return { dispatched: false, reason: 'provider_busy' };
273
248
  const node = composer();
274
249
  if (!node) return { dispatched: false, reason: 'composer_missing' };
275
- const supervisor = /^@forge\s+<<<FORGE_WORKFLOW_EFFECT_V1:fx_[a-zA-Z0-9_-]+>>>/.test(prompt.trim());
276
- const payload = supervisor ? prompt.trim().replace(/^@forge\s+/, '') : prompt;
277
- const expected = core.normalizeText(payload);
278
- const triggerDraft = `${payload}\n@`;
279
- const triggerText = core.normalizeText(triggerDraft);
280
- const boundText = core.normalizeText(`${payload} forge`);
281
- const boundChip = () => {
282
- const chips = Array.from(node.querySelectorAll('[plugin-mention-name], [app-mention-name]'));
283
- return chips.length === 1
284
- && chips[0]?.getAttribute('app-mention-name') === 'forge'
285
- && chips[0]?.getAttribute('app-mention-path') === 'app://asdk_app_6ac09022b26081918f20ff868c773824'
286
- && composerText() === boundText;
287
- };
250
+ // Project instructions select tools. No @forge UI mention is needed.
251
+ const supervisor = /^(?:@forge\s+)?<<<FORGE_WORKFLOW_EFFECT_V1:(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}>>>/.test(actualPrompt);
252
+ if (supervisor && !expectedConversationId && !expectedProjectId) return { dispatched: false, reason: 'target_identity_unavailable' };
253
+ // Strip only the exact legacy prefix on already-durable effects.
254
+ const payload = supervisor ? actualPrompt.replace(/^@forge\s+(?=<<<FORGE_WORKFLOW_EFFECT_V1:(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}>>>)/, '') : actualPrompt;
255
+ const expected = exactComposerText(payload);
288
256
  const existing = composerText();
289
- if (existing && existing !== expected && !(supervisor && (existing === triggerText || boundChip()))) return { dispatched: false, reason: 'composer_not_empty' };
290
- let reasoningVerified;
291
- if (reasoning !== undefined) {
292
- const checked = await ensureReasoning(reasoning);
293
- if (!checked.verified) return { dispatched: false, reason: checked.reason };
294
- reasoningVerified = checked.level;
257
+ // A restored draft belongs to this effect only when it matches precisely.
258
+ // Never overwrite another task's draft or a human-authored message.
259
+ const ownedDrafts = supervisor
260
+ ? [expected, expected + '\n', exactComposerText(payload + '\n@'), exactComposerText(payload + '\nforge'), exactComposerText('forge ' + payload)]
261
+ : [expected];
262
+ if (node.querySelector('[plugin-mention-name], [app-mention-name]')) return { dispatched: false, reason: 'composer_has_app_mention' };
263
+ const unowned = Boolean(existing && !ownedDrafts.includes(existing));
264
+ if (unowned && ((!replacement && !retired) || !supervisor)) return { dispatched: false, reason: 'composer_not_empty' };
265
+ if (retired && (!unowned || !existing.startsWith(`<<<FORGE_WORKFLOW_EFFECT_V1:${retired[1]}>>>`))) {
266
+ return { dispatched: false, reason: 'composer_not_empty' };
267
+ }
268
+ if (unowned) {
269
+ // Hash the *currently visible* single editor, not the page or another
270
+ // tab. If it changed since the backup, do not touch it.
271
+ if (!globalThis.crypto?.subtle || !globalThis.TextEncoder) return { dispatched: false, reason: 'composer_replacement_digest_unavailable' };
272
+ const bytes = new TextEncoder().encode('preflight-composer-v1:\0' + existing);
273
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
274
+ const prefix = Array.from(digest).map((byte) => byte.toString(16).padStart(2, '0')).join('').slice(0, 24);
275
+ if (prefix !== (replacement ? replacement[1] : retired?.[2])) return { dispatched: false, reason: 'composer_not_empty' };
295
276
  }
296
277
  if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
297
- if (supervisor && !boundChip()) writeComposer(node, triggerDraft);
298
- else if (existing !== expected && !boundChip()) writeComposer(node, payload);
299
- await new Promise((resolve) => setTimeout(resolve, 75));
278
+ // Hashing and model-picker hydration yield to page/user edits. Revalidate
279
+ // the same single editor before overwriting any bytes observed above.
280
+ if (composer() !== node || composerText(node) !== existing) return { dispatched: false, reason: 'composer_not_empty' };
281
+ if (existing !== expected && existing !== expected + '\n') writeComposer(node, payload);
282
+ // React may acknowledge the exact draft before it enables Send. Observe
283
+ // this same target for at most two seconds, below the broker deadline;
284
+ // this is read-only readiness, never another write or Send attempt.
285
+ const ready = await new Promise((resolve) => {
286
+ let timer;
287
+ let settled = false;
288
+ const finish = (value) => {
289
+ if (settled) return;
290
+ settled = true;
291
+ observer.disconnect();
292
+ document.removeEventListener('input', check, true);
293
+ clearTimeout(timer);
294
+ resolve(value);
295
+ };
296
+ const check = (expired = false) => {
297
+ if (!exactConversationIsCurrent()) { finish({ reason: 'target_identity_changed' }); return; }
298
+ // ProseMirror may expose one editor-generated final newline. Do not
299
+ // widen matching beyond that break or tolerate another visible draft.
300
+ const text = composerText();
301
+ if (text !== expected && text !== expected + '\n') { finish({ reason: 'composer_write_unconfirmed' }); return; }
302
+ if (isGenerating()) { finish({ reason: 'provider_busy' }); return; }
303
+ const buttons = Array.from(document.querySelectorAll(SEND)).filter(visible);
304
+ const send = buttons.length === 1 ? buttons[0] : undefined;
305
+ if (send && !send.disabled && send.getAttribute('aria-disabled') !== 'true') { finish({ send }); return; }
306
+ if (expired === true) finish({ reason: send ? 'send_button_disabled' : 'send_button_missing' });
307
+ };
308
+ const observer = new MutationObserver(() => check());
309
+ observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true, attributes: true });
310
+ document.addEventListener('input', check, true);
311
+ timer = setTimeout(() => check(true), 2_000);
312
+ check();
313
+ });
314
+ if (!ready.send) return { dispatched: false, reason: ready.reason };
315
+ // Await resumes in a new microtask: retain the exact pre-click fences even
316
+ // if an input/route update was queued alongside the readiness observation.
300
317
  if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
301
- if (supervisor) {
302
- if (!boundChip()) {
303
- if (composerText() !== triggerText) return { dispatched: false, reason: 'composer_write_unconfirmed' };
304
- // The final standalone @ paragraph was inserted together with the
305
- // payload; typing ` @` later drops the space in live ProseMirror.
306
- node.focus();
307
- const selection = window.getSelection();
308
- if (!selection) return { dispatched: false, reason: 'COMPUTER_CHATGPT_PLUGIN_SELECTION_UNAVAILABLE' };
309
- const range = document.createRange(); range.selectNodeContents(node); range.collapse(false);
310
- selection.removeAllRanges(); selection.addRange(range);
311
- if (!document.execCommand('insertText', false, 'forge')) return { dispatched: false, reason: 'COMPUTER_CHATGPT_PLUGIN_SELECTION_UNAVAILABLE' };
312
- await sleep(150);
313
- const entries = Array.from(document.querySelectorAll('[data-mention-section-items] > button'));
314
- const selected = entries.filter((entry) => {
315
- const label = core.normalizeText(entry.innerText ?? entry.textContent);
316
- return label === 'forge' || label.startsWith('forge Current Forge 1.8.1');
317
- });
318
- if (selected.length !== 1) {
319
- writeComposer(node, payload);
320
- return { dispatched: false, reason: 'COMPUTER_CHATGPT_FORGE_PLUGIN_UNAVAILABLE' };
321
- }
322
- if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
323
- selected[0].click();
324
- await sleep(150);
325
- }
326
- if (!boundChip()) return { dispatched: false, reason: 'COMPUTER_CHATGPT_FORGE_PLUGIN_CHIP_UNVERIFIED' };
327
- } else if (composerText() !== expected) return { dispatched: false, reason: 'composer_write_unconfirmed' };
328
- const send = document.querySelector(SEND);
329
- if (!send) return { dispatched: false, reason: 'send_button_missing' };
318
+ const text = composerText();
319
+ if (text !== expected && text !== expected + '\n') return { dispatched: false, reason: 'composer_write_unconfirmed' };
320
+ if (isGenerating()) return { dispatched: false, reason: 'provider_busy' };
321
+ const send = ready.send;
322
+ if (!send.isConnected || !visible(send)) return { dispatched: false, reason: 'send_button_missing' };
330
323
  if (send.disabled || send.getAttribute('aria-disabled') === 'true') return { dispatched: false, reason: 'send_button_disabled' };
331
- if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
332
324
  if (prepareOnly) return { prepared: true };
333
- send.click();
334
- return { dispatched: true, ...(reasoningVerified ? { reasoningVerified } : {}) };
325
+ // Confirm the committed turn inside the content script, rather than
326
+ // requesting delayed snapshots from the extension background worker.
327
+ const beforeSend = roleNodes(USER).at(-1);
328
+ // Once click starts, an exception cannot prove that the provider did not
329
+ // receive the message. Keep post-click observation failures outcome-unknown.
330
+ try {
331
+ send.click();
332
+ const confirmed = await new Promise((resolve) => {
333
+ // Unobserved submission is unknown, never proof that Send did not run.
334
+ let timer;
335
+ let settled = false;
336
+ const finish = (value) => {
337
+ if (settled) return;
338
+ settled = true;
339
+ observer.disconnect();
340
+ clearTimeout(timer);
341
+ resolve(value);
342
+ };
343
+ const check = () => {
344
+ if (expectedConversationId && !exactConversationIsCurrent()) return;
345
+ const latest = roleNodes(USER).at(-1);
346
+ if (latest && latest !== beforeSend
347
+ && exactComposerText(messageText(latest)) === expected) finish(true);
348
+ };
349
+ const observer = new MutationObserver(check);
350
+ observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
351
+ timer = setTimeout(() => finish(false), 2_000);
352
+ check();
353
+ });
354
+ return { dispatched: true, confirmed };
355
+ } catch {
356
+ return { dispatched: true, confirmed: false };
357
+ }
335
358
  };
336
359
  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
337
360
  if (!message || message.type === 'forge-computer-chatgpt-scan') { notify(); sendResponse?.({ ok: true }); return false; }
@@ -340,21 +363,22 @@
340
363
  return false;
341
364
  }
342
365
  if (message.type === 'forge-computer-chatgpt-snapshot') { sendResponse(pageSnapshot(message.options ?? {})); return false; }
343
- // Read-only exact-tab capability attestation. A stale content script must
344
- // never receive an xhigh-required send that it would silently ignore.
366
+ // Read-only exact-tab attestation for bootstrap/draft target guards.
345
367
  if (message.type === 'forge-computer-chatgpt-capabilities') {
346
- sendResponse({ reasoningPreflight: 'verified_before_send_v1' });
368
+ sendResponse({ bootstrapTargetGuard: 'project-checked-before-send-v1',
369
+ composerReplacementContract: 'exact-backed-up-project-draft-v1' });
347
370
  return false;
348
371
  }
349
372
  if (message.type === 'forge-computer-chatgpt-prepare') {
350
- dispatchPrompt(String(message.prompt ?? ''), String(message.mode ?? 'send'), message.reasoning, message.expectedConversationId, true)
351
- .then((result) => sendResponse(result?.prepared === true ? { prepared: true }
373
+ dispatchPrompt(String(message.prompt ?? ''), String(message.mode ?? 'send'), message.expectedConversationId, true, message.expectedProjectId)
374
+ .then((result) => sendResponse(result?.prepared === true
375
+ ? { prepared: true }
352
376
  : { prepared: false, reason: String(result?.reason ?? 'COMPUTER_CHATGPT_PREPARE_REJECTED') }),
353
377
  (error) => sendResponse({ prepared: false, reason: String(error?.message ?? error) }));
354
378
  return true;
355
379
  }
356
380
  if (message.type === 'forge-computer-chatgpt-dispatch') {
357
- dispatchPrompt(String(message.prompt ?? ''), String(message.mode ?? 'send'), message.reasoning, message.expectedConversationId)
381
+ dispatchPrompt(String(message.prompt ?? ''), String(message.mode ?? 'send'), message.expectedConversationId, false, message.expectedProjectId)
358
382
  .then(sendResponse, (error) => sendResponse({ dispatched: false, reason: String(error?.message ?? error) }));
359
383
  return true;
360
384
  }
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Forge Computer ChatGPT Provider",
4
- "version": "0.3.1",
4
+ "version": "0.3.3",
5
5
  "description": "Computer-owned stable ChatGPT target provider for Forge Runtime.",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxFzo1eixjsWsZbN1pBuyzdKinJSAAuzTRavD0xFQFwrTIEQ7hxxueEBEhifpGq9nplNnmxmZvIL7PJycEAYT8mrbyXBLCPR1jQBuL4YR775phnlNVpF3dHX5OWDHLWRnHRgOO1FibQ54fM2rKylr66+x+J6/4C7a9dpiSMuxf3fOStXA6wJb0d7A4E22Q1+GGfWgs0NCyVI5k4aczK+J5Ao61ZXKBr8Qw/FCmwhCcDgQdIpURgoMHkyvQH3ryYWocucjRhMVsU8H65adIIKFHkEhPJCiVY64L6bu6kNR2fpf0yJ1GvI5ota6Hf4NAEi7Yt7PL7i3ISyv3pPQWW1nIwIDAQAB",
7
7
  "permissions": [
8
8
  "nativeMessaging",
9
+ "storage",
9
10
  "tabs",
11
+ "scripting",
10
12
  "alarms"
11
13
  ],
12
14
  "host_permissions": [
@@ -290,22 +290,34 @@ export async function callDesktopOperatorComputerBrowserAutomation(
290
290
  export function createDesktopOperatorComputerProvider(
291
291
  options: DesktopOperatorComputerProviderOptions = {},
292
292
  ): DesktopOperatorComputerProvider {
293
- let active: { key: string; binding: DesktopOperatorComputerBinding } | undefined;
294
- const binding = (): DesktopOperatorComputerBinding => {
293
+ let active: { key: string; binding: DesktopOperatorComputerBinding; observations?: DesktopOperatorComputerBinding } | undefined;
294
+ const binding = (observation = false): DesktopOperatorComputerBinding => {
295
295
  const endpoint = resolveDesktopOperatorComputerEndpoint(options);
296
296
  const key = endpointBindingKey(endpoint);
297
- if (active?.key === key) return active.binding;
298
- active?.binding.close();
299
- active = { key, binding: createBinding(endpoint) };
297
+ if (active?.key !== key) {
298
+ active?.binding.close();
299
+ active?.observations?.close();
300
+ active = { key, binding: createBinding(endpoint) };
301
+ }
302
+ // Desktop Operator executes RPCs serially within each socket connection.
303
+ // A slow Browser RPC must not head-of-line block native snapshots/capture.
304
+ // Keep all input, console and legacy Browser requests on their original
305
+ // ordered stream; target/session locks still serialize observation epochs.
306
+ if (observation) return active.observations ??= createBinding(endpoint);
300
307
  return active.binding;
301
308
  };
302
309
  return {
303
310
  providerId: DESKTOP_OPERATOR_PROVIDER_PLUGIN_ID,
304
311
  capabilities: desktopOperatorComputerProviderCapabilities(options),
305
- execute: async (request, timeoutMs) => await binding().execute(request, timeoutMs),
312
+ execute: async (request, timeoutMs) => await binding(
313
+ request.capability === COMPUTER_OBSERVE_CAPABILITY
314
+ || request.capability === COMPUTER_ELEMENT_OBSERVE_CAPABILITY
315
+ || request.capability === COMPUTER_CAPTURE_CAPABILITY,
316
+ ).execute(request, timeoutMs),
306
317
  executeBrowserCompatibility: async (request, timeoutMs) => await binding().executeBrowserCompatibility(request, timeoutMs),
307
318
  dispose: () => {
308
319
  active?.binding.close();
320
+ active?.observations?.close();
309
321
  active = undefined;
310
322
  },
311
323
  };
@@ -218,6 +218,7 @@ interface ComputerSurfaceIndexReadyRecord {
218
218
  schemaVersion: 1;
219
219
  status: 'ready';
220
220
  indexedAt: string;
221
+ terminalBindingIndexReady?: true;
221
222
  }
222
223
 
223
224
  const COMPUTER_SURFACE_INDEX_READY_KEY = 'surface-index-v1-ready';
@@ -231,6 +232,12 @@ function surfaceBindingIndexKey(binding: ComputerSurfaceProviderBinding | undefi
231
232
  return `binding-${digest40(`${binding.providerId}:${binding.browserProduct ?? ''}:${binding.windowId}:${binding.tabId}`)}`;
232
233
  }
233
234
 
235
+ // Migration-only negative evidence; never claims the live tab binding.
236
+ function terminalBindingIndexKey(binding: ComputerSurfaceProviderBinding | undefined): string | undefined {
237
+ const key = surfaceBindingIndexKey(binding);
238
+ return key ? `terminal-${key}` : undefined;
239
+ }
240
+
234
241
  function surfaceStableIdentityIndexKey(identity: ComputerSurfaceStableIdentity): string | undefined {
235
242
  const normalized = normalizeSurfaceStableIdentity(identity);
236
243
  if (!normalized.resource) return undefined;
@@ -282,7 +289,10 @@ function writeSurfaceIndexes(
282
289
  ): void {
283
290
  // Terminal imports retain stable/alias identity, never a physical tab claim.
284
291
  const indexed = status === 'tombstoned' ? { ...target, providerBinding: undefined } : target;
285
- for (const key of surfaceIndexKeysForTarget(indexed)) {
292
+ const keys = surfaceIndexKeysForTarget(indexed);
293
+ const terminal = status === 'tombstoned' ? terminalBindingIndexKey(target.providerBinding) : undefined;
294
+ if (terminal) keys.push(terminal);
295
+ for (const key of keys) {
286
296
  const current = transaction.read<ComputerSurfaceIndexRecord>(COMPUTER_TARGET_INDEX_NAMESPACE, COMPUTER_TARGET_SCOPE, key);
287
297
  if (current?.value.targetId === target.targetId) continue;
288
298
  if (current) {
@@ -311,7 +321,7 @@ function ensureSurfaceIndexes(transaction: ComputerTargetPersistenceTransaction)
311
321
  COMPUTER_TARGET_SCOPE,
312
322
  COMPUTER_SURFACE_INDEX_READY_KEY,
313
323
  );
314
- if (ready?.value.status === 'ready') return;
324
+ if (ready?.value.status === 'ready' && ready.value.terminalBindingIndexReady === true) return;
315
325
  const surfaces = transaction.listAll<ComputerInteractionTargetEntry>({
316
326
  namespace: COMPUTER_TARGET_NAMESPACE,
317
327
  scope: COMPUTER_TARGET_SCOPE,
@@ -324,7 +334,7 @@ function ensureSurfaceIndexes(transaction: ComputerTargetPersistenceTransaction)
324
334
  scope: COMPUTER_TARGET_SCOPE,
325
335
  key: COMPUTER_SURFACE_INDEX_READY_KEY,
326
336
  schemaVersion: 1,
327
- value: { schemaVersion: 1, status: 'ready', indexedAt: now() } satisfies ComputerSurfaceIndexReadyRecord,
337
+ value: { schemaVersion: 1, status: 'ready', indexedAt: now(), terminalBindingIndexReady: true } satisfies ComputerSurfaceIndexReadyRecord,
328
338
  action: ready ? 'computer_surface_index_ready_repair' : 'computer_surface_index_ready',
329
339
  expectedRevision: ready?.revision ?? null,
330
340
  });
@@ -347,9 +357,12 @@ function readIndexedSurfaceRecord(
347
357
  const index = transaction.read<ComputerSurfaceIndexRecord>(COMPUTER_TARGET_INDEX_NAMESPACE, COMPUTER_TARGET_SCOPE, indexKey);
348
358
  if (!index) return undefined;
349
359
  const target = transaction.read<ComputerInteractionTargetEntry>(COMPUTER_TARGET_NAMESPACE, COMPUTER_TARGET_SCOPE, index.value.targetId);
350
- if (target?.value.target.kind === 'surface'
351
- && (target.value.status === 'active' || !indexKey.startsWith('binding-'))
352
- && surfaceIndexKeysForTarget(normalizedSurfaceTarget(target.value.target)).includes(indexKey)) return target;
360
+ const surface = target?.value.target.kind === 'surface' ? normalizedSurfaceTarget(target.value.target) : undefined;
361
+ const valid = indexKey.startsWith('terminal-')
362
+ ? target?.value.status === 'tombstoned' && terminalBindingIndexKey(surface?.providerBinding) === indexKey
363
+ : (target?.value.status === 'active' || !indexKey.startsWith('binding-'))
364
+ && Boolean(surface && surfaceIndexKeysForTarget(surface).includes(indexKey));
365
+ if (target && valid) return target;
353
366
  transaction.delete({
354
367
  namespace: COMPUTER_TARGET_INDEX_NAMESPACE,
355
368
  scope: COMPUTER_TARGET_SCOPE,
@@ -363,8 +376,14 @@ function readIndexedSurfaceRecord(
363
376
  function deleteSurfaceIndexes(
364
377
  transaction: ComputerTargetPersistenceTransaction,
365
378
  target: ComputerSurfaceTarget,
379
+ status: 'active' | 'tombstoned' = 'active',
366
380
  ): void {
367
- for (const key of surfaceIndexKeysForTarget(target)) {
381
+ const keys = surfaceIndexKeysForTarget(target);
382
+ if (status === 'tombstoned') {
383
+ const terminal = terminalBindingIndexKey(target.providerBinding);
384
+ if (terminal) keys.push(terminal);
385
+ }
386
+ for (const key of keys) {
368
387
  const index = transaction.read<ComputerSurfaceIndexRecord>(COMPUTER_TARGET_INDEX_NAMESPACE, COMPUTER_TARGET_SCOPE, key);
369
388
  if (!index || index.value.targetId !== target.targetId) continue;
370
389
  transaction.delete({
@@ -607,6 +626,15 @@ export function createComputerInteractionTargetAuthority(
607
626
  const inputIndexKeys = surfaceInputIndexKeys({ stableIdentity, visibility, compatibilityAliases, repositoryIds, providerBinding });
608
627
  return persistence.transaction(controllerHome, (transaction) => {
609
628
  ensureSurfaceIndexes(transaction);
629
+ // An old repository's import is observation, not permission to revive a
630
+ // terminal physical tab. Current authoritative writes remain unaffected.
631
+ if (rawInput.reactivate === false) {
632
+ const terminalKey = terminalBindingIndexKey(providerBinding);
633
+ const terminal = terminalKey ? readIndexedSurfaceRecord(transaction, terminalKey) : undefined;
634
+ if (terminal?.value.status === 'tombstoned' && terminal.value.target.kind === 'surface') {
635
+ return { target: structuredClone(normalizedSurfaceTarget(terminal.value.target)), status: 'tombstoned', created: false };
636
+ }
637
+ }
610
638
  const indexed = new Map<string, ComputerTargetPersistenceRecord<ComputerInteractionTargetEntry>>();
611
639
  for (const indexKey of inputIndexKeys) {
612
640
  const candidate = readIndexedSurfaceRecord(transaction, indexKey);
@@ -674,7 +702,7 @@ export function createComputerInteractionTargetAuthority(
674
702
  ...(existing.value.status === 'tombstoned' ? { providerBinding } : providerBinding ? { providerBinding } : {}),
675
703
  updatedAt: at,
676
704
  };
677
- deleteSurfaceIndexes(transaction, currentTarget);
705
+ deleteSurfaceIndexes(transaction, currentTarget, existing.value.status);
678
706
  transaction.write({
679
707
  namespace: COMPUTER_TARGET_NAMESPACE,
680
708
  scope: COMPUTER_TARGET_SCOPE,
@@ -869,6 +897,7 @@ export function createComputerInteractionTargetAuthority(
869
897
  action: 'computer_surface_target_tombstone',
870
898
  expectedRevision: current.revision,
871
899
  });
900
+ writeSurfaceIndexes(transaction, normalizedSurfaceTarget(current.value.target), 'tombstoned');
872
901
  return true;
873
902
  });
874
903
  }
@@ -962,6 +991,7 @@ export function createComputerInteractionTargetAuthority(
962
991
  action: 'computer_surface_target_tombstone',
963
992
  expectedRevision: current.revision,
964
993
  });
994
+ writeSurfaceIndexes(transaction, normalizedSurfaceTarget(current.value.target), 'tombstoned');
965
995
  });
966
996
  return structuredClone(target);
967
997
  },
@@ -1008,7 +1038,7 @@ export function createComputerInteractionTargetAuthority(
1008
1038
  const currentAtMs = Date.parse(current.value.tombstonedAt ?? current.updatedAt);
1009
1039
  const stillExpired = !Number.isFinite(currentAtMs) || nowMs - currentAtMs >= ttlMs;
1010
1040
  if (!stillExpired && !capacityEligible) return false;
1011
- if (current.value.target.kind === 'surface') deleteSurfaceIndexes(transaction, normalizedSurfaceTarget(current.value.target));
1041
+ if (current.value.target.kind === 'surface') deleteSurfaceIndexes(transaction, normalizedSurfaceTarget(current.value.target), 'tombstoned');
1012
1042
  return transaction.delete({
1013
1043
  namespace: COMPUTER_TARGET_NAMESPACE,
1014
1044
  scope: COMPUTER_TARGET_SCOPE,