@moretea-labs/forge 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (528) hide show
  1. package/AGENTS.md +70 -7
  2. package/CHANGELOG.md +14 -0
  3. package/CLAUDE.md +17 -2
  4. package/CONTRIBUTING.md +3 -2
  5. package/THIRD_PARTY_NOTICES.md +1 -1
  6. package/adapters/chatgpt/browser-delivery-host.ts +7 -1
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
  8. package/adapters/chatgpt/controller-binding-store.ts +101 -22
  9. package/adapters/chatgpt/controller-round-host.ts +15 -25
  10. package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
  11. package/adapters/chatgpt/provider-delivery.ts +228 -1
  12. package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
  13. package/adapters/computer/chatgpt-extension-target.ts +463 -0
  14. package/adapters/computer/chatgpt-target-router.ts +103 -0
  15. package/adapters/computer/chrome-extension/background.js +385 -0
  16. package/adapters/computer/chrome-extension/content.js +378 -0
  17. package/adapters/computer/chrome-extension/core.js +24 -0
  18. package/adapters/computer/chrome-extension/manifest.json +30 -0
  19. package/adapters/computer/desktop-operator-discovery.ts +11 -31
  20. package/adapters/computer/interaction-target-authority.ts +142 -18
  21. package/adapters/mcp/auth.ts +0 -1
  22. package/adapters/mcp/instructions.ts +5 -4
  23. package/adapters/mcp/multi-repository.ts +3 -19
  24. package/adapters/mcp/oauth.ts +73 -0
  25. package/adapters/mcp/paths.ts +4 -1
  26. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
  27. package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
  28. package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
  29. package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
  30. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
  31. package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
  32. package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
  33. package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
  34. package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
  35. package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
  36. package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
  37. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
  38. package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
  39. package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
  40. package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
  41. package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
  42. package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
  43. package/adapters/mcp/runtime-gateway/router.ts +18 -504
  44. package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
  45. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
  46. package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
  47. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
  48. package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
  49. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
  50. package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
  51. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
  52. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
  53. package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
  54. package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
  55. package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
  56. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
  57. package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
  58. package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
  59. package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
  60. package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
  61. package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
  62. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
  63. package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
  64. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
  65. package/adapters/mcp/server.ts +67 -23
  66. package/adapters/mcp/setup.ts +1 -3
  67. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
  68. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
  69. package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
  70. package/adapters/mcp/toolset-names.ts +5 -0
  71. package/adapters/mcp/toolset.ts +6 -5
  72. package/adapters/mcp/transports/http-observation.ts +365 -0
  73. package/adapters/mcp/transports/http.ts +154 -677
  74. package/adapters/mcp/transports/oauth-http.ts +322 -0
  75. package/adapters/mcp/transports/session-registry.ts +6 -0
  76. package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
  77. package/assets/plugin-registry.v1.json +6 -4
  78. package/assets/reference-configs/agentic-development-flow.md +25 -0
  79. package/assets/reference-configs/global-working-rules.md +19 -0
  80. package/assets/skill-version.json +2 -2
  81. package/docs/architecture/CURRENT.md +111 -41
  82. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
  83. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
  84. package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
  85. package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
  86. package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
  87. package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
  88. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
  89. package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
  90. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
  91. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
  92. package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
  93. package/docs/architecture/domains/verification.md +1 -0
  94. package/docs/architecture/domains/workflow-engine.md +6 -0
  95. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  96. package/docs/forge-plugin-management.md +2 -2
  97. package/docs/operations/controller-performance-and-502.md +17 -0
  98. package/docs/operations/controller-reliability-runbook.md +2 -2
  99. package/docs/operations/features.md +1 -1
  100. package/docs/operations/features.zh-CN.md +1 -1
  101. package/docs/operations/homebrew.md +2 -2
  102. package/docs/operations/mcp-tool-exposure.md +2 -0
  103. package/docs/operations/releasing.md +2 -0
  104. package/docs/operations/standalone-disaster-recovery.md +14 -6
  105. package/docs/wiki/Implementation.md +1 -1
  106. package/package.json +25 -14
  107. package/packages/kernel/cognition/api/index.ts +6 -0
  108. package/packages/kernel/cognition/application/activation-service.ts +451 -0
  109. package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
  110. package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
  111. package/packages/kernel/cognition/application/memory-service.ts +64 -0
  112. package/packages/kernel/cognition/domain/learning.ts +106 -0
  113. package/packages/kernel/cognition/domain/memory.ts +254 -0
  114. package/packages/kernel/controller/api/index.ts +1 -0
  115. package/packages/kernel/controller/application/continuation-service.ts +281 -0
  116. package/packages/kernel/controller/application/controller-round-service.ts +12 -0
  117. package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
  118. package/packages/kernel/controller/domain/controller-round.ts +4 -1
  119. package/packages/kernel/controller/domain/execution-quality.ts +3 -1
  120. package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
  121. package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
  122. package/packages/kernel/controller/ports/controller-host.ts +2 -0
  123. package/packages/kernel/identity/api/index.ts +10 -0
  124. package/packages/kernel/identity/domain/activity-projection.ts +43 -0
  125. package/packages/kernel/identity/domain/capability-broker.ts +60 -0
  126. package/packages/kernel/identity/domain/grant.ts +72 -0
  127. package/packages/kernel/identity/domain/owned-resource.ts +116 -0
  128. package/packages/kernel/identity/domain/user-request.ts +93 -0
  129. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
  130. package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
  131. package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
  132. package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
  133. package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
  134. package/packages/kernel/scheduler/api/index.ts +0 -2
  135. package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
  136. package/packages/kernel/scheduler/application/settlement.ts +0 -15
  137. package/packages/kernel/scheduler/domain/schedule.ts +87 -0
  138. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
  139. package/packages/kernel/work/api/index.ts +3 -2
  140. package/packages/kernel/work/application/work-service.ts +17 -42
  141. package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
  142. package/packages/kernel/work/domain/engineering-design.ts +10 -5
  143. package/packages/kernel/work/domain/engineering-profile.ts +8 -12
  144. package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
  145. package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
  146. package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
  147. package/packages/kernel/work/domain/implementation-review.ts +14 -331
  148. package/packages/kernel/work/domain/objective-graph.ts +119 -0
  149. package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
  150. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  151. package/packages/kernel/work/domain/state-machine.ts +25 -63
  152. package/packages/kernel/work/domain/types.ts +57 -51
  153. package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
  154. package/packages/kernel/work/ports/work-contract-store.ts +5 -2
  155. package/packages/plugin-runtime/browser/session-authority.ts +2 -1
  156. package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
  157. package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
  158. package/packages/plugin-runtime/computer/index.ts +2 -0
  159. package/packages/plugin-runtime/computer/target-authority.ts +26 -1
  160. package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
  161. package/packages/protocols/computer/contract.ts +27 -1
  162. package/scripts/activate-source-baseline.ts +7 -9
  163. package/scripts/canary-controller-semantic-wait.ts +12 -24
  164. package/scripts/check-mcp-compatibility.ts +48 -136
  165. package/scripts/check-runtime-architecture.mjs +622 -217
  166. package/scripts/forge-local-recovery-helper.mjs +93 -15
  167. package/scripts/lib/project-init-lib.sh +3 -0
  168. package/scripts/open-source-audit-allowlist.txt +1 -2
  169. package/scripts/run-bounded-command.mjs +5 -0
  170. package/scripts/run-governed-gate.ts +0 -1
  171. package/scripts/run-mobile-monitor-bridge.ts +0 -1
  172. package/scripts/smoke-control-plane-failure-domain.ts +94 -0
  173. package/scripts/smoke-mcp-http-runtime.ts +5 -0
  174. package/scripts/smoke-runtime-recovery.ts +12 -43
  175. package/scripts/smoke-schedule-engine.ts +23 -15
  176. package/scripts/smoke-unattended-transport-recovery.ts +223 -0
  177. package/scripts/smoke-user-request-continuation-restart.ts +232 -0
  178. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
  179. package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
  180. package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
  181. package/scripts/smoke-workflow-supervisor.ts +103 -0
  182. package/scripts/verify-forge-runtime.sh +2 -1
  183. package/src/cli/agent-jobs/executor-health.ts +11 -34
  184. package/src/cli/agent-jobs/integration.ts +18 -49
  185. package/src/cli/agent-jobs/job-manager.ts +7 -3
  186. package/src/cli/commands/brain-assistant.ts +27 -13
  187. package/src/cli/commands/brain.ts +13 -15
  188. package/src/cli/commands/chatgpt.ts +1 -163
  189. package/src/cli/commands/recovery.ts +135 -121
  190. package/src/cli/commands/runtime.ts +3 -3
  191. package/src/cli/commands/setup-profile.ts +8 -0
  192. package/src/cli/controller/check-runner.ts +13 -2
  193. package/src/cli/controller/completion-backlog.ts +3 -3
  194. package/src/cli/controller/completion-orchestrator.ts +4 -43
  195. package/src/cli/controller/completion-target.ts +3 -72
  196. package/src/cli/controller/context/source-materializer.ts +81 -100
  197. package/src/cli/controller/context-pack.ts +10 -8
  198. package/src/cli/controller/execution-completion.ts +4 -7
  199. package/src/cli/controller/execution-policy.ts +36 -153
  200. package/src/cli/controller/governance.ts +2 -3
  201. package/src/cli/controller/issue-store.ts +4 -99
  202. package/src/cli/controller/launch-agents.ts +59 -18
  203. package/src/cli/controller/operational-plan.ts +24 -51
  204. package/src/cli/controller/runtime-config.ts +5 -1
  205. package/src/cli/controller/stuck-state-migration.ts +2 -3
  206. package/src/cli/controller/systemd-user.ts +39 -1
  207. package/src/cli/editing/edit-session.ts +1 -7
  208. package/src/cli/local-bridge/console-automations.ts +18 -13
  209. package/src/cli/local-bridge/console-view-models.ts +3 -12
  210. package/src/cli/local-bridge/facade-api.ts +64 -191
  211. package/src/cli/local-bridge/server.ts +24 -147
  212. package/src/cli/local-bridge/types.ts +0 -1
  213. package/src/cli/mcp/oauth-client-retirement.ts +74 -0
  214. package/src/cli/repositories/command-executor.ts +29 -23
  215. package/src/cli/repositories/command-scope.ts +35 -16
  216. package/src/cli/repositories/controller-home.ts +48 -0
  217. package/src/cli/repositories/ephemeral-workspace.ts +20 -30
  218. package/src/cli/repositories/git-command-executor.ts +2 -2
  219. package/src/cli/repositories/registry.ts +77 -11
  220. package/src/cli/repositories/structured-git.ts +62 -2
  221. package/src/cli/repository/session-cache.ts +92 -6
  222. package/src/runtime/context/assistant-context.ts +20 -7
  223. package/src/runtime/context/assistant-work-context.ts +89 -27
  224. package/src/runtime/context/automatic-learning.ts +565 -0
  225. package/src/runtime/context/cognitive-attention.ts +173 -0
  226. package/src/runtime/context/cognitive-skill-canary.ts +158 -0
  227. package/src/runtime/context/context-closure.ts +46 -9
  228. package/src/runtime/context/context-pack-process.ts +103 -0
  229. package/src/runtime/context/context-plane.ts +1 -9
  230. package/src/runtime/context/direct-controller-learning.ts +298 -0
  231. package/src/runtime/context/semantic-navigation-contract.ts +9 -0
  232. package/src/runtime/context/semantic-navigation.ts +6 -34
  233. package/src/runtime/context/typescript-navigation-process.ts +145 -0
  234. package/src/runtime/context/typescript-navigation.ts +89 -3
  235. package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
  236. package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
  237. package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
  238. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
  239. package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
  240. package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
  241. package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
  242. package/src/runtime/control-plane/execution/session-store.ts +31 -1
  243. package/src/runtime/control-plane/execution/validation.ts +6 -8
  244. package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
  245. package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
  246. package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
  247. package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
  248. package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
  249. package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
  250. package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
  251. package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
  252. package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
  253. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
  254. package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
  255. package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
  256. package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
  257. package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
  258. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
  259. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
  260. package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
  261. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  262. package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
  263. package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
  264. package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
  265. package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
  266. package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
  267. package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
  268. package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
  269. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
  270. package/src/runtime/control-plane/facade/index.ts +0 -1
  271. package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
  272. package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
  273. package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
  274. package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
  275. package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
  276. package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
  277. package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
  278. package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
  279. package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
  280. package/src/runtime/control-plane/facade/types.ts +49 -108
  281. package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
  282. package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
  283. package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
  284. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
  285. package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
  286. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
  287. package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
  288. package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
  289. package/src/runtime/control-plane/governance/access-policy.ts +19 -2
  290. package/src/runtime/control-plane/governance/authorization.ts +20 -3
  291. package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
  292. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
  293. package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
  294. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
  295. package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
  296. package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
  297. package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
  298. package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
  299. package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
  300. package/src/runtime/control-plane/release-retention.ts +135 -47
  301. package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
  302. package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
  303. package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
  304. package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
  305. package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
  306. package/src/runtime/diagnostics/incident-repair.ts +313 -146
  307. package/src/runtime/diagnostics/mcp-timing.ts +114 -0
  308. package/src/runtime/evidence/artifact-store.ts +158 -40
  309. package/src/runtime/evidence/evidence-store.ts +112 -8
  310. package/src/runtime/evidence/output-handle-index.ts +225 -0
  311. package/src/runtime/evidence/result-store.ts +158 -44
  312. package/src/runtime/execution/jobs/store.ts +3 -4
  313. package/src/runtime/execution/managed-workspace.ts +57 -0
  314. package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
  315. package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
  316. package/src/runtime/execution/process-runtime/check-result.ts +105 -1
  317. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
  318. package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
  319. package/src/runtime/execution/process-runtime/gc.ts +15 -2
  320. package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
  321. package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
  322. package/src/runtime/execution/process-runtime/index.ts +3 -0
  323. package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
  324. package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
  325. package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
  326. package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
  327. package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
  328. package/src/runtime/execution/process-runtime/runtime.ts +35 -2
  329. package/src/runtime/execution/process-runtime/store.ts +30 -2
  330. package/src/runtime/execution/process-runtime/types.ts +6 -1
  331. package/src/runtime/execution/workers/executor.ts +2 -6
  332. package/src/runtime/platform/service-inventory.ts +130 -0
  333. package/src/runtime/platform/service-manager.ts +8 -0
  334. package/src/runtime/plugins/action-application.ts +85 -28
  335. package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
  336. package/src/runtime/plugins/browser-adapter.ts +757 -34
  337. package/src/runtime/plugins/browser-automation-service.ts +23 -3
  338. package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
  339. package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
  340. package/src/runtime/plugins/browser-native-session-service.ts +63 -13
  341. package/src/runtime/plugins/browser-registration.ts +16 -0
  342. package/src/runtime/plugins/browser-runtime.ts +14 -1
  343. package/src/runtime/plugins/browser-session-authority.ts +2 -2
  344. package/src/runtime/plugins/browser-session-store.ts +46 -76
  345. package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
  346. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
  347. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
  348. package/src/runtime/plugins/computer-registration.ts +222 -11
  349. package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
  350. package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
  351. package/src/runtime/plugins/external-adapter.ts +42 -6
  352. package/src/runtime/plugins/external-registration.ts +21 -1
  353. package/src/runtime/plugins/gmail-adapter.ts +23 -2
  354. package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
  355. package/src/runtime/plugins/google-shared.ts +14 -7
  356. package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
  357. package/src/runtime/plugins/ios-adapter.ts +1 -1
  358. package/src/runtime/plugins/lightweight-action.ts +208 -2
  359. package/src/runtime/plugins/local-system-adapter.ts +12 -2
  360. package/src/runtime/plugins/managed-process-adapter.ts +12 -5
  361. package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
  362. package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
  363. package/src/runtime/plugins/store.ts +602 -428
  364. package/src/runtime/plugins/types.ts +20 -0
  365. package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
  366. package/src/runtime/projections/invalidation.ts +7 -3
  367. package/src/runtime/projections/materialized-view.ts +80 -36
  368. package/src/runtime/recovery/actions.ts +3 -3
  369. package/src/runtime/recovery/capability-action-application.ts +46 -45
  370. package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
  371. package/src/runtime/recovery/maintenance-executor.ts +374 -123
  372. package/src/runtime/recovery/store.ts +29 -11
  373. package/src/runtime/release/release-coordinator.ts +296 -0
  374. package/src/runtime/release/release-session.ts +716 -0
  375. package/src/runtime/release/release-state-migration.ts +25 -0
  376. package/src/runtime/resources/leases/store.ts +56 -25
  377. package/src/runtime/root/computer-composition.ts +191 -6
  378. package/src/runtime/root/controller-progression-composition.ts +192 -0
  379. package/src/runtime/root/controller-round-composition.ts +20 -1
  380. package/src/runtime/root/deployment-topology.ts +106 -0
  381. package/src/runtime/root/entry.ts +4 -3
  382. package/src/runtime/root/known-good-recovery.ts +184 -0
  383. package/src/runtime/root/local-bridge.ts +0 -1
  384. package/src/runtime/root/mcp-transport.ts +111 -18
  385. package/src/runtime/root/ownership.ts +261 -9
  386. package/src/runtime/root/package-connector-service.ts +105 -19
  387. package/src/runtime/root/package-runtime-release.ts +117 -18
  388. package/src/runtime/root/package-runtime-service.ts +17 -11
  389. package/src/runtime/root/release-execution-canary.ts +44 -12
  390. package/src/runtime/root/release-loader.ts +90 -8
  391. package/src/runtime/root/release-manifest.ts +80 -6
  392. package/src/runtime/root/release-materialize.ts +372 -28
  393. package/src/runtime/root/release-store.ts +395 -36
  394. package/src/runtime/root/runtime-lane.ts +191 -0
  395. package/src/runtime/root/runtime.ts +159 -27
  396. package/src/runtime/root/scheduled-controller-composition.ts +28 -3
  397. package/src/runtime/root/service-runner.ts +2 -2
  398. package/src/runtime/root/service.ts +36 -33
  399. package/src/runtime/root/status.ts +48 -0
  400. package/src/runtime/root/types.ts +32 -3
  401. package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
  402. package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
  403. package/src/runtime/root/write-fence.ts +50 -2
  404. package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
  405. package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
  406. package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
  407. package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
  408. package/src/runtime/shared/process-environment.ts +20 -9
  409. package/src/runtime/shared/response-budget.ts +1 -3
  410. package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
  411. package/src/runtime/standalone-recovery/core.ts +3702 -342
  412. package/src/runtime/standalone-recovery/entry.ts +469 -39
  413. package/src/runtime/standalone-recovery/installer.ts +262 -148
  414. package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
  415. package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
  416. package/src/runtime/standalone-recovery/performance.ts +54 -16
  417. package/src/runtime/standalone-recovery/release.ts +53 -5
  418. package/src/runtime/standalone-recovery/service-labels.ts +5 -2
  419. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
  420. package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
  421. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
  422. package/src/runtime/workflow/schedules/engine.ts +138 -172
  423. package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
  424. package/src/runtime/workflows/runtime.ts +15 -19
  425. package/src/testing/test-governance.ts +5 -1
  426. package/supervisor/chatgpt-conversation.ts +23 -0
  427. package/supervisor/client.ts +201 -0
  428. package/supervisor/control-plane.ts +868 -0
  429. package/supervisor/entry.ts +15 -0
  430. package/supervisor/forge-validators.ts +80 -0
  431. package/supervisor/index.ts +5 -0
  432. package/supervisor/native-browser-adapter.ts +786 -0
  433. package/supervisor/native-messaging/host.ts +39 -0
  434. package/supervisor/native-messaging/manifest.ts +26 -0
  435. package/supervisor/native-messaging/protocol.ts +27 -0
  436. package/supervisor/paths.ts +18 -0
  437. package/supervisor/protocol.ts +202 -0
  438. package/supervisor/server.ts +404 -0
  439. package/supervisor/store.ts +1451 -0
  440. package/supervisor/tsconfig.json +8 -0
  441. package/supervisor/types.ts +255 -0
  442. package/adapters/chatgpt/controller-host.ts +0 -119
  443. package/adapters/mcp/controller-round-compatibility.ts +0 -184
  444. package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
  445. package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
  446. package/packages/kernel/progression/api/index.ts +0 -2
  447. package/packages/kernel/progression/application/projector.ts +0 -211
  448. package/packages/kernel/progression/domain/types.ts +0 -116
  449. package/packages/kernel/progression/index.ts +0 -1
  450. package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
  451. package/packages/kernel/scheduler/domain/continuation.ts +0 -38
  452. package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
  453. package/packages/kernel/work/domain/admission-policy.ts +0 -34
  454. package/scripts/benchmark-background-check-overlap.ts +0 -228
  455. package/scripts/benchmark-computer-hotpath.ts +0 -255
  456. package/scripts/benchmark-controller-hotpath.ts +0 -212
  457. package/scripts/benchmark-controller-round2.ts +0 -191
  458. package/scripts/benchmark-ios-agent-device.ts +0 -511
  459. package/scripts/benchmark-mcp-host.sh +0 -117
  460. package/scripts/benchmark-mcp-transport.ts +0 -169
  461. package/scripts/benchmark-operational-memory-activation.ts +0 -235
  462. package/scripts/benchmark-operational-shadow.ts +0 -28
  463. package/scripts/benchmark-quality-harness.ts +0 -146
  464. package/scripts/benchmark-route-session-concurrency.ts +0 -915
  465. package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
  466. package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
  467. package/scripts/benchmark-thin-harness.ts +0 -251
  468. package/scripts/benchmark-typescript-navigation.ts +0 -186
  469. package/scripts/benchmark-work-efficiency.ts +0 -334
  470. package/scripts/build-controller-ui.ts +0 -28
  471. package/scripts/check-npm-release.sh +0 -32
  472. package/scripts/check-public-release-surface.sh +0 -19
  473. package/scripts/check-release-published.sh +0 -57
  474. package/scripts/check-release-readiness.sh +0 -37
  475. package/scripts/check-release-version.mjs +0 -74
  476. package/scripts/check-tarball-install-smoke.sh +0 -150
  477. package/scripts/package-source-archive.sh +0 -147
  478. package/scripts/public-release-files.txt +0 -39
  479. package/scripts/public-release.ts +0 -174
  480. package/scripts/publish-release-tarball.sh +0 -27
  481. package/scripts/route-nl-vs-ts-eval.ts +0 -679
  482. package/scripts/stage-runtime-release.ts +0 -44
  483. package/src/cli/controller/progress.ts +0 -656
  484. package/src/cli/controller/work-mode.ts +0 -181
  485. package/src/cli/local-bridge/console-assets.ts +0 -17
  486. package/src/cli/local-bridge/dashboard.ts +0 -17
  487. package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
  488. package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
  489. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
  490. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
  491. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
  492. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
  493. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
  494. package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
  495. package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
  496. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
  497. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
  498. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
  499. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
  500. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
  501. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
  502. package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
  503. package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
  504. package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
  505. package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
  506. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
  507. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
  508. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
  509. package/src/cli/local-bridge/frontend/src/styles.css +0 -28
  510. package/src/cli/local-bridge/frontend/src/types.ts +0 -15
  511. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
  512. package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
  513. package/src/cli/local-bridge/ui-dist/app.css +0 -1
  514. package/src/cli/local-bridge/ui-dist/app.js +0 -11228
  515. package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
  516. package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
  517. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
  518. package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
  519. package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
  520. package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
  521. package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
  522. package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
  523. package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
  524. package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
  525. package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
  526. package/src/runtime/control-plane/routing/index.ts +0 -1
  527. package/src/runtime/control-plane/routing/route-policy.ts +0 -401
  528. package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
@@ -0,0 +1,868 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { parseChatgptConversationIdentity } from './chatgpt-conversation';
3
+ import { parseSupervisorCompletion, renderEffectMarker, renderSupervisorPrompt, sha256, validateEffectId } from './protocol';
4
+ import { WorkflowSupervisorStore } from './store';
5
+ import type { WorkflowAssistantObservation, WorkflowAssistantObservationResult, WorkflowContractValidation, WorkflowEffectKind, WorkflowSupervisorAutomationStatus, WorkflowSupervisorBrowserPollResult, WorkflowSupervisorBrowserTask, WorkflowSupervisorCompletion, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorLifecycleHooks, WorkflowSupervisorProjectScope, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTerminalState, WorkflowSupervisorValidators } from './types';
6
+
7
+ function compactProjectIdentity(value: string): string {
8
+ return value.normalize('NFKC').toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');
9
+ }
10
+
11
+ function projectIdentityTokens(value: string): string[] {
12
+ return value.normalize('NFKC').toLocaleLowerCase().split(/[^\p{L}\p{N}]+/gu).filter(Boolean);
13
+ }
14
+
15
+ function boundedProjectIdentityMatch(expected: string, observed: string): boolean {
16
+ const compactExpected = compactProjectIdentity(expected);
17
+ const compactObserved = compactProjectIdentity(observed);
18
+ if (!compactExpected || !compactObserved) return false;
19
+ if (compactExpected === compactObserved) return true;
20
+ if (compactExpected.length < 4) return false;
21
+ const expectedTokens = projectIdentityTokens(expected);
22
+ const observedTokens = projectIdentityTokens(observed);
23
+ if (expectedTokens.length === 0 || expectedTokens.length >= observedTokens.length) return false;
24
+ return expectedTokens.every((token, index) => observedTokens[index] === token);
25
+ }
26
+
27
+ function projectSlugFromUrl(value: string): string | undefined {
28
+ try {
29
+ const parsed = new URL(value);
30
+ const match = /^\/g\/(g-p-[a-z0-9]+)(?:-([^/]+))?\/project\/?$/i.exec(parsed.pathname);
31
+ return match?.[2]?.toLocaleLowerCase();
32
+ } catch {
33
+ return undefined;
34
+ }
35
+ }
36
+
37
+ function projectMatchesScope(conversation: WorkflowSupervisorDiscoveredConversation, scope: WorkflowSupervisorProjectScope): boolean {
38
+ const names = [scope.title, ...(scope.aliases ?? [])]
39
+ .map((value) => value.trim().toLocaleLowerCase())
40
+ .filter(Boolean);
41
+ const projectTitle = conversation.projectTitle?.trim().toLocaleLowerCase();
42
+ if (projectTitle && names.some((name) => boundedProjectIdentityMatch(name, projectTitle))) return true;
43
+ const projectSlug = conversation.projectUrl ? projectSlugFromUrl(conversation.projectUrl) : undefined;
44
+ if (!projectSlug) return false;
45
+ return names.some((name) => boundedProjectIdentityMatch(name, projectSlug));
46
+ }
47
+
48
+ function effectId(): string { return `fx_${randomUUID().replaceAll('-', '')}`; }
49
+ function stableEffectId(originKey: string): string {
50
+ return validateEffectId(`fx_${sha256(originKey).slice(0, 32)}`);
51
+ }
52
+ const rejectUnconfigured = async (): Promise<WorkflowContractValidation> => ({ valid: false, reason: 'validator_unconfigured' });
53
+
54
+ export class WorkflowSupervisorControlPlane {
55
+ readonly validators: WorkflowSupervisorValidators;
56
+ readonly hooks: WorkflowSupervisorLifecycleHooks;
57
+ constructor(readonly store: WorkflowSupervisorStore, validators: Partial<WorkflowSupervisorValidators> = {}, hooks: WorkflowSupervisorLifecycleHooks = {}) {
58
+ this.validators = { completionContract: validators.completionContract ?? rejectUnconfigured, userBlockerPolicy: validators.userBlockerPolicy ?? rejectUnconfigured };
59
+ this.hooks = hooks;
60
+ }
61
+ registerTask(input: WorkflowSupervisorTaskInput): WorkflowSupervisorTask { return this.store.registerTask(input); }
62
+ reviseTaskObjective(input: {
63
+ taskId: string;
64
+ objective: string;
65
+ expectedObjectiveSha256: string;
66
+ requestId: string;
67
+ authorizedBy?: string;
68
+ }): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; refreshedEffect?: WorkflowSupervisorEffect } {
69
+ const task = this.requireTask(input.taskId);
70
+ requireNonTerminalTask(this.store, task.taskId);
71
+ const objective = input.objective.trim();
72
+ if (!objective || objective.length > 8_000) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_INVALID');
73
+ if (!/^[0-9a-f]{64}$/i.test(input.expectedObjectiveSha256)) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_EXPECTED_SHA_INVALID');
74
+ if (!input.requestId.trim()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_REQUIRED');
75
+ const pending = this.store.currentUnappliedEffect(task.taskId);
76
+ let refreshEffect: { effectId: string; expectedPrompt: string; prompt: string } | undefined;
77
+ if (pending && !this.store.latestEffectDispatch(pending.effectId)) {
78
+ const source = pending.sourceCompletionFingerprint ? this.store.getCompletion(pending.sourceCompletionFingerprint) : undefined;
79
+ const checkpoint = source?.proposal.reason === 'compact_receipt' ? undefined : source?.proposal.checkpoint;
80
+ refreshEffect = {
81
+ effectId: pending.effectId,
82
+ expectedPrompt: pending.prompt,
83
+ prompt: this.renderPrompt({ ...task, objective }, pending.effectId, pending.kind, checkpoint),
84
+ };
85
+ }
86
+ return this.store.reviseTaskObjective({
87
+ taskId: task.taskId,
88
+ objective,
89
+ expectedObjectiveSha256: input.expectedObjectiveSha256.toLowerCase(),
90
+ requestId: input.requestId.trim(),
91
+ authorizedBy: input.authorizedBy?.trim() || 'operator',
92
+ ...(refreshEffect ? { refreshEffect } : {}),
93
+ });
94
+ }
95
+ /**
96
+ * Explicit operator recovery. It is deliberately evidence-classified rather
97
+ * than a generic "try again": each class names the durable mechanical fact
98
+ * that makes exactly one bounded next step safe, and none of them can replay
99
+ * a submission whose outcome is unknown.
100
+ *
101
+ * - proven-un-applied effect at its retry ceiling -> refund the mechanical
102
+ * budget once and re-dispatch that same effect;
103
+ * - unknown-outcome effect -> read-only reclassification only, unless the
104
+ * operator explicitly authorizes retiring it so a *fresh* turn can run;
105
+ * - applied effect whose single provider resume is exhausted -> reserve one
106
+ * new, separately identified recovery turn on the same conversation.
107
+ */
108
+ recoverTask(input: {
109
+ taskId: string;
110
+ sourceEffectId?: string;
111
+ requestId: string;
112
+ reason: string;
113
+ /** Explicit operator authority required to retire an outcome-unknown effect. */
114
+ supersedeUnknown?: boolean;
115
+ authorizedBy?: string;
116
+ }): { recoveryEffect: WorkflowSupervisorEffect; action: 'retry_authorized' | 'recovery_reserved' | 'unknown_superseded' } {
117
+ const task = this.requireTask(input.taskId);
118
+ requireNonTerminalTask(this.store, task.taskId);
119
+ if (!input.requestId.trim() || !input.reason.trim()) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_REASON_REQUIRED');
120
+ const authorizedBy = input.authorizedBy?.trim() || 'operator';
121
+ // Identity-stable replay first: an operator recovery already reserved for
122
+ // this exact source effect is the durable answer, whether or not it has
123
+ // since been dispatched.
124
+ const legacyOrigin = input.sourceEffectId ? `provider-recovery:${input.sourceEffectId}` : undefined;
125
+ const existingLegacy = legacyOrigin ? this.store.getEffectByOriginKey(legacyOrigin) : undefined;
126
+ if (existingLegacy?.taskId === task.taskId) return { recoveryEffect: existingLegacy, action: 'recovery_reserved' };
127
+ // The same explicit supersession decision is one durable fact; replaying it
128
+ // returns its replacement instead of failing on the now-retired source.
129
+ const supersedeOrigin = `operator-recovery:${input.sourceEffectId ?? ''}:${input.requestId}`;
130
+ const existingSupersede = this.store.getEffectByOriginKey(supersedeOrigin);
131
+ if (existingSupersede?.taskId === task.taskId) return { recoveryEffect: existingSupersede, action: 'unknown_superseded' };
132
+ const pending = this.store.currentUnappliedEffect(task.taskId);
133
+ if (input.sourceEffectId && pending && pending.effectId !== input.sourceEffectId) {
134
+ throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
135
+ }
136
+ if (pending) {
137
+ // Read-only reclassification first: a dispatch-owner pre-send return is a
138
+ // stronger fact than any operator decision and makes the effect retryable
139
+ // through the normal mechanical path.
140
+ if (this.store.reconcileNativePreSendFailure(pending.effectId, input.requestId)) {
141
+ return { recoveryEffect: pending, action: 'retry_authorized' };
142
+ }
143
+ try {
144
+ this.store.authorizeOperatorRetry({ effectId: pending.effectId, requestId: input.requestId, reason: input.reason, authorizedBy });
145
+ return { recoveryEffect: pending, action: 'retry_authorized' };
146
+ } catch (error) {
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ if (message !== 'WORKFLOW_SUPERVISOR_RECOVERY_OUTCOME_UNKNOWN') throw error;
149
+ if (input.supersedeUnknown !== true) throw error;
150
+ }
151
+ // The operator explicitly decided to stop waiting on an unknown
152
+ // submission. Record that decision durably and reserve one *new* turn in
153
+ // the same transaction; the unknown effect is never resent and never
154
+ // silently discarded.
155
+ const origin = `operator-recovery:${pending.effectId}:${input.requestId}`;
156
+ const supersededId = stableEffectId(origin);
157
+ const prompt = this.renderPrompt(task, supersededId, 'recovery', undefined,
158
+ `The operator explicitly superseded outcome-unknown effect ${pending.effectId} (${input.requestId}): ${input.reason}. That effect is retired and must never be resent; read durable Forge state and continue this same task with one fresh turn.`);
159
+ const superseded = this.store.supersedeUnknownEffect({
160
+ effectId: pending.effectId, requestId: input.requestId, reason: input.reason, authorizedBy,
161
+ replacement: { effectId: supersededId, prompt },
162
+ });
163
+ return { recoveryEffect: superseded.recoveryEffect, action: 'unknown_superseded' };
164
+ }
165
+ const source = this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
166
+ if (!source || (input.sourceEffectId && source.effectId !== input.sourceEffectId)) throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
167
+ const origin = `provider-recovery:${source.effectId}`;
168
+ const id = stableEffectId(origin);
169
+ if (this.store.providerResumeExhausted(source.effectId)) {
170
+ // The automatic resume ceiling is the exact mechanical fact that permits
171
+ // a further operator turn; without it this path must not exist.
172
+ const prompt = this.renderPrompt(task, id, 'recovery', undefined,
173
+ `The operator explicitly requested recovery (${input.requestId}): ${input.reason}. The single bounded provider resume for applied effect ${source.effectId} is exhausted; the source effect remains applied and must not be replayed. Read durable Forge state and continue this same task.`);
174
+ return { recoveryEffect: this.store.reserveOperatorProviderRecovery({
175
+ taskId: task.taskId, sourceEffectId: source.effectId, requestId: input.requestId, reason: input.reason,
176
+ authorizedBy, originKey: origin, effectId: id, prompt,
177
+ }), action: 'recovery_reserved' };
178
+ }
179
+ const prompt = this.renderPrompt(task, id, 'recovery', undefined,
180
+ `The user explicitly requested recovery (${input.requestId}): ${input.reason}. Preserve the prior applied effect and all completed source work; read durable state and continue this same task. This is one operator-authorized recovery, not a reset of automatic recovery or dispatch budgets.`);
181
+ return { recoveryEffect: this.store.reserveOperatorRecovery({
182
+ taskId: task.taskId, sourceEffectId: source.effectId, requestId: input.requestId, reason: input.reason, effectId: id, prompt,
183
+ }), action: 'recovery_reserved' };
184
+ }
185
+ reserveEnrollment(taskId: string, canonicalEffectId?: string): WorkflowSupervisorEffect {
186
+ const task = this.requireTask(taskId);
187
+ // A task that already reached a terminal supervisor action is not deliverable.
188
+ // Reserving another enrollment effect for it produced an "enrolled" result that
189
+ // could never be delivered, so the ControllerRound waited forever instead of
190
+ // surfacing the operator/provider decision that terminal state represents.
191
+ requireNonTerminalTask(this.store, task.taskId);
192
+ const originKey = `enrollment:${taskId}`;
193
+ // Supervisor owns the outer-turn effect identity. A lower ControllerRound may
194
+ // rotate when the semantic Work carrier changes, but the Requirement + exact
195
+ // conversation task must keep one enrollment effect. Reuse by origin before
196
+ // considering a newer lower-layer canonical effect id.
197
+ const existingForOrigin = this.store.getEffectByOriginKey(originKey);
198
+ let effect: WorkflowSupervisorEffect;
199
+ if (existingForOrigin) {
200
+ if (existingForOrigin.taskId !== taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${originKey}`);
201
+ if (existingForOrigin.kind !== 'enrollment') throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${originKey}`);
202
+ effect = existingForOrigin;
203
+ } else {
204
+ const id = canonicalEffectId ? validateEffectId(canonicalEffectId) : stableEffectId(originKey);
205
+ const existing = this.store.getEffect(id);
206
+ if (existing) {
207
+ if (existing.taskId !== taskId) throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_TASK_CONFLICT:${originKey}`);
208
+ if (existing.kind !== 'enrollment') throw new Error(`WORKFLOW_SUPERVISOR_EFFECT_KIND_CONFLICT:${originKey}`);
209
+ effect = existing;
210
+ } else {
211
+ effect = this.store.reserveEffect({ taskId, effectId: id, kind: 'enrollment', originKey, prompt: this.renderPrompt(task, id, 'enrollment') });
212
+ }
213
+ }
214
+ const inheritedDispatch = this.hooks.inheritedEffectDispatch?.(task, effect);
215
+ if (inheritedDispatch && !this.store.effectApplied(effect.effectId)) {
216
+ this.store.recordEffectDispatchStarted(
217
+ effect.effectId,
218
+ inheritedDispatch.generation,
219
+ inheritedDispatch.dispatchId,
220
+ inheritedDispatch.evidence,
221
+ );
222
+ }
223
+ return effect;
224
+ }
225
+ observeEffect(input: { effectId: string; observationId: string; outcome: 'applied' | 'not_applied' | 'unknown'; evidence?: Record<string, unknown> }): void {
226
+ this.store.recordEffectObservation(validateEffectId(input.effectId), input.observationId, input.outcome, input.evidence);
227
+ }
228
+ getTask(taskId: string): WorkflowSupervisorTask | undefined { return this.store.getTask(taskId); }
229
+ getTaskByConversationId(conversationId: string): { task: WorkflowSupervisorTask; terminal?: WorkflowSupervisorTerminalState } | undefined {
230
+ const task = this.store.getTaskByConversationId(conversationId);
231
+ if (!task) return undefined;
232
+ const terminal = this.store.terminalAction(task.taskId);
233
+ return { task, ...(terminal ? { terminal } : {}) };
234
+ }
235
+ listTasks(activeOnly = false): WorkflowSupervisorTask[] {
236
+ const tasks = this.store.listTasks();
237
+ return activeOnly ? tasks.filter((task) => !this.store.terminalAction(task.taskId)) : tasks;
238
+ }
239
+ stopTask(taskId: string, reason = 'Stopped by operator request.'): { taskId: string; terminal: 'STOPPED'; deduplicated: boolean } {
240
+ return this.store.stopTask(taskId, reason);
241
+ }
242
+ bindBootstrapConversation(input: { taskId: string; conversationId: string; conversationUrl: string }): WorkflowSupervisorTask {
243
+ const identity = parseChatgptConversationIdentity(input.conversationUrl);
244
+ if (identity.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_CONVERSATION_MISMATCH');
245
+ const pendingMigration = this.store.freshConversationMigrationContext(input.taskId);
246
+ const task = this.store.bindBootstrapConversation(input.taskId, identity.conversationId, identity.canonicalUrl);
247
+ this.hooks.bootstrapConversationBound?.(task, pendingMigration ? {
248
+ migratedFromConversationId: pendingMigration.fromConversationId,
249
+ migrationRequestId: pendingMigration.requestId,
250
+ } : undefined);
251
+ return task;
252
+ }
253
+ getEffect(id: string): WorkflowSupervisorEffect | undefined { return this.store.getEffect(validateEffectId(id)); }
254
+ /** Read-only stall classification for one task; no lifecycle authority. */
255
+ taskStall(taskId: string): ReturnType<WorkflowSupervisorStore['taskStall']> { return this.store.taskStall(this.requireTask(taskId).taskId); }
256
+ /**
257
+ * Explicit operator move of one task's exact conversation. Chatting in a new
258
+ * conversation is never an automatic answer to provider backpressure: a 429 is
259
+ * answered by fewer requests, and only a conversation that is durably unusable
260
+ * is replaced. Requires the caller's expected current conversation so a stale
261
+ * decision cannot silently move a task that already advanced.
262
+ */
263
+ migrateConversation(input: { taskId: string; expectedConversationId: string; conversationId?: string; conversationUrl?: string; fresh?: boolean; requestId: string; reason: string; authorizedBy?: string }): { taskId: string; conversationId: string; conversationUrl: string; migrated: boolean; freshConversation?: boolean } {
264
+ const task = this.requireTask(input.taskId);
265
+ requireNonTerminalTask(this.store, task.taskId);
266
+ if (!input.requestId.trim() || !input.reason.trim()) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_REASON_REQUIRED');
267
+ if (input.fresh === true) {
268
+ if (input.conversationId?.trim() || input.conversationUrl?.trim()) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_FRESH_CONVERSATION_CONFLICT');
269
+ const origin = `fresh-conversation:${task.taskId}:${input.requestId}`;
270
+ const effectId = stableEffectId(origin);
271
+ const migrated = this.store.migrateToFreshConversation({
272
+ taskId: task.taskId,
273
+ expectedConversationId: input.expectedConversationId,
274
+ requestId: input.requestId,
275
+ reason: input.reason,
276
+ authorizedBy: input.authorizedBy?.trim() || 'operator',
277
+ // A fresh conversation has no prior turns, so the replacement turn must
278
+ // carry the objective again instead of the bare "继续。" continuation.
279
+ replacement: { effectId, prompt: this.renderPrompt(task, effectId, 'enrollment') },
280
+ });
281
+ return { taskId: migrated.task.taskId, conversationId: migrated.task.conversationId, conversationUrl: migrated.task.conversationUrl, migrated: migrated.migrated, freshConversation: migrated.migrated };
282
+ }
283
+ if (!input.conversationId?.trim() || !input.conversationUrl?.trim()) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_REQUIRED');
284
+ const identity = parseChatgptConversationIdentity(input.conversationUrl);
285
+ if (identity.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_MIGRATION_CONVERSATION_MISMATCH');
286
+ const migrated = this.store.migrateConversation({
287
+ taskId: task.taskId,
288
+ expectedConversationId: input.expectedConversationId,
289
+ conversationId: identity.conversationId,
290
+ conversationUrl: identity.canonicalUrl,
291
+ requestId: input.requestId,
292
+ reason: input.reason,
293
+ authorizedBy: input.authorizedBy?.trim() || 'operator',
294
+ });
295
+ return {
296
+ taskId: migrated.task.taskId, conversationId: migrated.task.conversationId,
297
+ conversationUrl: migrated.task.conversationUrl, migrated: migrated.migrated,
298
+ };
299
+ }
300
+ /** Mechanical provider re-dispatch budget for one effect, used by recovery to surface exhaustion. */
301
+ effectDispatchBudget(effectId: string): ReturnType<WorkflowSupervisorStore['effectDispatchBudget']> {
302
+ return this.store.effectDispatchBudget(validateEffectId(effectId));
303
+ }
304
+ continuationProof(input: { repoId?: string; activeReleaseId: string; notBefore: string }) { return this.store.continuationProof(input); }
305
+ browserDiscoverySnapshot() { return this.store.discoverySnapshot(); }
306
+ bootstrapProjectUrl(taskId: string): string {
307
+ const task = this.requireTask(taskId);
308
+ const scope = this.hooks.projectScopeForTask?.(task);
309
+ const title = scope?.title.trim();
310
+ if (!scope || !title) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_PROJECT_SCOPE_REQUIRED');
311
+ const projects = new Map<string, string>();
312
+ for (const conversation of this.store.discoverySnapshot().conversations) {
313
+ if (!projectMatchesScope(conversation, scope)) continue;
314
+ const value = conversation.projectUrl?.trim();
315
+ if (!value) continue;
316
+ try {
317
+ const parsed = new URL(value);
318
+ if (parsed.protocol !== 'https:' || parsed.hostname !== 'chatgpt.com') continue;
319
+ const match = /^\/g\/(g-p-[a-z0-9]+)(?:-[^/]+)?\/project\/?$/i.exec(parsed.pathname);
320
+ if (!match) continue;
321
+ const projectId = match[1]!.toLocaleLowerCase();
322
+ projects.set(projectId, `https://chatgpt.com/g/${match[1]!}/project`);
323
+ } catch {
324
+ continue;
325
+ }
326
+ }
327
+ if (projects.size === 0) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_PROJECT_NOT_DISCOVERED');
328
+ if (projects.size !== 1) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_PROJECT_AMBIGUOUS');
329
+ return [...projects.values()][0]!;
330
+ }
331
+ recordBrowserDiscovery(source: string, conversations: readonly WorkflowSupervisorDiscoveredConversation[]) {
332
+ // Discovery is durable observation only. Creating a Supervisor task/effect
333
+ // requires the explicit Work/current-conversation enrollment path.
334
+ return this.store.recordDiscovery(source, conversations);
335
+ }
336
+ browserProjectScopes(): WorkflowSupervisorProjectScope[] {
337
+ const scopes = new Map<string, WorkflowSupervisorProjectScope>();
338
+ for (const task of this.store.listTasks()) {
339
+ const scope = this.hooks.projectScopeForTask?.(task);
340
+ if (!scope?.title.trim()) continue;
341
+ const normalized: WorkflowSupervisorProjectScope = {
342
+ title: scope.title.trim().slice(0, 512),
343
+ ...(scope.aliases?.length ? { aliases: [...new Set(scope.aliases.map((value) => value.trim().slice(0, 256)).filter(Boolean))].slice(0, 16) } : {}),
344
+ ...(scope.repoId?.trim() ? { repoId: scope.repoId.trim().slice(0, 256) } : {}),
345
+ ...(scope.controllerHome?.trim() ? { controllerHome: scope.controllerHome.trim().slice(0, 2048) } : {}),
346
+ };
347
+ const key = `${normalized.title.toLocaleLowerCase()}\n${normalized.repoId ?? ''}\n${normalized.controllerHome ?? ''}`;
348
+ scopes.set(key, normalized);
349
+ }
350
+ return [...scopes.values()];
351
+ }
352
+ browserTasks(): WorkflowSupervisorBrowserTask[] {
353
+ const active = this.store.listTasks().flatMap((task) => {
354
+ if (this.store.terminalAction(task.taskId)) return [];
355
+ // A Computer page is an ephemeral delivery surface. An applied effect
356
+ // awaiting its receipt remains eligible for read-only provider-health
357
+ // observation, so a terminal stream error can reserve its one bounded
358
+ // recovery effect. Rendered assistant text never decides the workflow;
359
+ // only a persisted receipt does that.
360
+ const pending = this.store.nextBrowserEffect(task.taskId);
361
+ // Any applied effect awaiting its receipt stays observable, including one
362
+ // whose bounded provider resume is already exhausted. Observation is
363
+ // read-only and mints nothing, so keeping it attached is what lets late
364
+ // provider/completion evidence resolve the turn instead of stranding it.
365
+ const providerTurnAwaitingReceipt = this.store.hasAppliedEffectAwaitingCompletion(task.taskId);
366
+ if (!pending && !providerTurnAwaitingReceipt) return [];
367
+ // Bootstrap has no exact conversation yet, so it cannot satisfy the
368
+ // normal Work-boundary predicate. Its already-persisted enrollment effect
369
+ // is the narrow authority to acquire one through Computer exactly once.
370
+ if (!task.conversationId.startsWith('bootstrap:')) {
371
+ // Optional lower-layer Work/Requirement compatibility must be task-local.
372
+ // A stale legacy task is allowed to fail closed, but it must never poison
373
+ // the global Supervisor delivery queue and block standalone tasks.
374
+ try { if (pending?.mode === 'send' && !this.browserTaskActive(task)) return []; }
375
+ catch { return []; }
376
+ }
377
+ return [{ task, mode: pending?.mode ?? 'reconcile' }];
378
+ });
379
+ // Fresh effects are bounded work with no prior external mutation. Outcome-
380
+ // unknown reconciliation remains durable and exactly-once, but cannot be
381
+ // allowed to monopolize the single native browser lane indefinitely.
382
+ active.sort((left, right) => {
383
+ const leftPriority = left.mode === 'send' ? 0 : 1;
384
+ const rightPriority = right.mode === 'send' ? 0 : 1;
385
+ return leftPriority - rightPriority;
386
+ });
387
+ return active.map(({ task }) => browserTask(task));
388
+ }
389
+ browserPoll(input: { conversationId: string; conversationUrl: string }): WorkflowSupervisorBrowserPollResult {
390
+ const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
391
+ const projection = browserTask(task);
392
+ const terminal = this.store.terminalAction(task.taskId);
393
+ // STOPPED is an operator terminal, not a semantic completion. Return it
394
+ // before the normal external-effect activity gate so an already-running
395
+ // browser poll observes the stop and cannot turn it into another command.
396
+ if (terminal === 'STOPPED') return { authorized: true, task: projection, terminal };
397
+ if (terminal) return { authorized: true, task: projection, terminal };
398
+ const pending = this.store.nextBrowserEffect(task.taskId);
399
+ // Read-only reconciliation belongs to the already-started effect even when
400
+ // its Work/conversation binding was retired. It never authorizes a resend.
401
+ if (pending?.mode === 'send' && !this.browserTaskActive(task)) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_TASK_INACTIVE');
402
+ if (!pending) return { authorized: true, task: projection };
403
+ return { authorized: true, task: projection, command: { mode: pending.mode, effectId: pending.effect.effectId, kind: pending.effect.kind, prompt: pending.effect.prompt, dispatchGeneration: pending.generation, conversationId: task.conversationId, conversationUrl: task.conversationUrl } };
404
+ }
405
+ bootstrapPoll(taskId: string): WorkflowSupervisorBrowserPollResult {
406
+ const task = this.requireTask(taskId);
407
+ if (!task.conversationId.startsWith('bootstrap:')) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_PENDING');
408
+ const terminal = this.store.terminalAction(task.taskId);
409
+ if (terminal === 'STOPPED') return { authorized: true, task: browserTask(task), terminal };
410
+ const pending = this.store.nextBrowserEffect(task.taskId);
411
+ if (pending) {
412
+ return {
413
+ authorized: true,
414
+ task: browserTask(task),
415
+ command: {
416
+ mode: pending.mode,
417
+ effectId: pending.effect.effectId,
418
+ kind: pending.effect.kind,
419
+ prompt: pending.effect.prompt,
420
+ dispatchGeneration: pending.generation,
421
+ conversationId: task.conversationId,
422
+ conversationUrl: task.conversationUrl,
423
+ },
424
+ };
425
+ }
426
+ // Provider application and bootstrap identity binding are separate facts.
427
+ // A confirmed send may already be applied while ChatGPT has only just
428
+ // canonicalized the new /c/<conversation> route. Keep one read-only
429
+ // reconciliation command for that applied effect so the exact conversation
430
+ // can be bound without manufacturing another provider generation.
431
+ const applied = this.store.latestAppliedEffectWithoutCompletion(task.taskId);
432
+ if (!applied) return { authorized: true, task: browserTask(task) };
433
+ const dispatch = this.store.latestEffectDispatch(applied.effectId);
434
+ return {
435
+ authorized: true,
436
+ task: browserTask(task),
437
+ command: {
438
+ mode: 'reconcile',
439
+ effectId: applied.effectId,
440
+ kind: applied.kind,
441
+ prompt: applied.prompt,
442
+ dispatchGeneration: dispatch?.generation ?? 1,
443
+ conversationId: task.conversationId,
444
+ conversationUrl: task.conversationUrl,
445
+ },
446
+ };
447
+ }
448
+ bootstrapBeginEffect(input: { taskId: string; effectId: string; dispatchId: string; dispatchGeneration: number }): boolean {
449
+ const task = this.requireTask(input.taskId);
450
+ requireNonTerminalTask(this.store, task.taskId);
451
+ if (!task.conversationId.startsWith('bootstrap:')) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_PENDING');
452
+ const pending = this.store.nextBrowserEffect(task.taskId);
453
+ if (!pending || pending.effect.effectId !== validateEffectId(input.effectId) || pending.mode !== 'send') return false;
454
+ return this.store.recordEffectDispatchStarted(input.effectId, input.dispatchGeneration, input.dispatchId, {
455
+ surface: 'computer-bootstrap',
456
+ ...(this.hooks.effectDispatchEvidence?.({ task, effectId: input.effectId }) ?? {}),
457
+ });
458
+ }
459
+ bootstrapObserveEffect(input: { taskId: string; effectId: string; observationId: string; outcome: 'applied' | 'not_applied' | 'unknown'; evidence?: Record<string, unknown> }): void {
460
+ const task = this.requireTask(input.taskId);
461
+ const effect = this.store.getEffect(validateEffectId(input.effectId));
462
+ if (!effect || effect.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_EFFECT_TASK_MISMATCH');
463
+ if (input.outcome === 'not_applied') {
464
+ // This path is private to the provider dispatch adapter after its exact
465
+ // dispatch function returned without clicking Send. It is therefore a
466
+ // mechanical negative proof, unlike a missing post-send conversation
467
+ // marker, which remains outcome-unknown and must reconcile.
468
+ const pending = this.store.nextBrowserEffect(task.taskId);
469
+ const dispatch = this.store.latestEffectDispatch(effect.effectId);
470
+ if (!pending || pending.effect.effectId !== effect.effectId || pending.mode !== 'reconcile'
471
+ || !dispatch || pending.generation !== dispatch.generation) {
472
+ throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_APPLIED_PROOF_INVALID');
473
+ }
474
+ this.store.recordEffectNotAppliedProof(effect.effectId, input.observationId, {
475
+ surface: 'computer-bootstrap',
476
+ dispatch_generation: dispatch.generation,
477
+ pre_send_rejection: true,
478
+ });
479
+ return;
480
+ }
481
+ const evidence = { surface: 'computer-bootstrap', ...(input.evidence ?? {}) };
482
+ this.observeEffect({ effectId: effect.effectId, observationId: input.observationId, outcome: input.outcome, evidence });
483
+ if (input.outcome === 'applied') this.hooks.effectApplied?.(task, effect, { observationId: input.observationId, evidence });
484
+ }
485
+ /** Local Computer preparation returned before Send intent; persist in the existing effect ledger. */
486
+ recordProviderPreparationFailure(input: { taskId: string; effectId: string; reasonCode: string }): void {
487
+ const task = this.requireTask(input.taskId);
488
+ const effect = this.store.getEffect(input.effectId);
489
+ if (effect?.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_EFFECT_TASK_MISMATCH');
490
+ this.store.recordEffectPreparationFailure(input.effectId, input.reasonCode);
491
+ }
492
+
493
+ browserBeginEffect(input: { conversationId: string; conversationUrl: string; effectId: string; dispatchId: string; dispatchGeneration: number; evidence?: Record<string, unknown> }): { started: boolean; mode: 'send' | 'reconcile'; generation: number } {
494
+ const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
495
+ requireNonTerminalTask(this.store, task.taskId);
496
+ const pending = this.store.nextBrowserEffect(task.taskId);
497
+ const effectId = validateEffectId(input.effectId);
498
+ if (!pending || pending.effect.effectId !== effectId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_EFFECT_NOT_CURRENT');
499
+ if (pending.mode !== 'send' || pending.generation !== input.dispatchGeneration) return { started: false, mode: 'reconcile', generation: pending.generation };
500
+ // Re-check immediately before committing dispatch; a queued browser command
501
+ // must not outlive an explicit conversation rebind.
502
+ if (!this.browserTaskActive(task)) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_TASK_INACTIVE');
503
+ const snapshot = browserSnapshot(input.evidence);
504
+ if (!snapshot || browserTextHasEffect(snapshot.latestUserText, effectId) || !this.browserSnapshotMatchesSource(task, pending.effect, snapshot)) {
505
+ return { started: false, mode: 'reconcile', generation: pending.generation };
506
+ }
507
+ const dispatchEvidence = {
508
+ surface: typeof input.evidence?.surface === 'string' ? input.evidence.surface.slice(0, 128) : 'chrome-extension',
509
+ baseline_user_sha256: browserTextSha256(snapshot.latestUserText),
510
+ baseline_assistant_sha256: sha256(snapshot.latestAssistantResponse),
511
+ baseline_has_source_completion: Boolean(pending.effect.sourceCompletionFingerprint),
512
+ ...(this.hooks.effectDispatchEvidence?.({ task, effectId }) ?? {}),
513
+ };
514
+ const started = this.store.recordEffectDispatchStarted(effectId, input.dispatchGeneration, input.dispatchId, dispatchEvidence);
515
+ return { started, mode: started ? 'send' : 'reconcile', generation: input.dispatchGeneration };
516
+ }
517
+ browserObserveEffect(input: { conversationId: string; conversationUrl: string; effectId: string; observationId: string; outcome: 'applied' | 'not_applied' | 'unknown'; evidence?: Record<string, unknown> }): { recorded: true } {
518
+ const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
519
+ const effect = this.store.getEffect(validateEffectId(input.effectId));
520
+ if (!effect || effect.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_EFFECT_TASK_MISMATCH');
521
+ if (input.outcome !== 'not_applied') {
522
+ const evidence = sanitizeBrowserEvidence(input.evidence);
523
+ this.observeEffect({ effectId: effect.effectId, observationId: input.observationId, outcome: input.outcome, evidence });
524
+ if (input.outcome === 'applied') this.hooks.effectApplied?.(task, effect, { observationId: input.observationId, evidence });
525
+ return { recorded: true };
526
+ }
527
+ // A rendered page can still be stale, including an empty composer and the
528
+ // exact pre-send history. Absence in that page is never proof that the
529
+ // provider rejected a submission. Only the dispatch owner can attest a
530
+ // mechanical pre-send failure (bootstrapObserveEffect); browser observers
531
+ // cannot authorize re-dispatch of an outcome-unknown mutation.
532
+ this.store.recordEffectObservation(effect.effectId, input.observationId, 'unknown', {
533
+ ...sanitizeBrowserEvidence(input.evidence), reconciliation: true, reason: 'not_applied_proof_incomplete',
534
+ });
535
+ return { recorded: true };
536
+ }
537
+ /** Dispatch-owner attestation. Only the adapter that reserved this generation may call it. */
538
+ browserObserveDispatchFailure(input: { conversationId: string; conversationUrl: string; effectId: string; observationId: string; dispatchGeneration: number; reason: string; surface?: string }): void {
539
+ const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
540
+ const effect = this.store.getEffect(input.effectId);
541
+ if (effect?.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_EFFECT_TASK_MISMATCH');
542
+ const dispatch = this.store.latestEffectDispatch(input.effectId);
543
+ if (dispatch?.generation !== input.dispatchGeneration) throw new Error('WORKFLOW_SUPERVISOR_DISPATCH_GENERATION_CHANGED');
544
+ this.store.recordEffectNotAppliedProof(input.effectId, input.observationId, {
545
+ surface: input.surface?.trim().slice(0, 128) || 'macos-native', reason: input.reason, send_clicked: false, dispatch_generation: input.dispatchGeneration,
546
+ // This call is the dispatch owner's own mechanical return, produced before
547
+ // any Send click. Marking it as a pre-send rejection is what lets the one
548
+ // bounded budget refund apply, exactly as the bootstrap path does; without
549
+ // it a purely local obstacle permanently exhausted the effect.
550
+ pre_send_rejection: true,
551
+ });
552
+ }
553
+ browserObserveProviderTurn(input: { conversationId: string; conversationUrl: string; generating: boolean; latestAssistantResponse: string; providerActivityText?: string; providerFailureCode?: string; receiptMissing?: boolean; observedAtMs: number; graceMs: number }): { state: 'inactive' | 'none' | 'generating' | 'idle_pending' | 'recovery_reserved' | 'exhausted'; recoveryEffect?: WorkflowSupervisorEffect } {
554
+ const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
555
+ if (!this.browserTaskActiveForExternalEffect(task)) return { state: 'inactive' };
556
+ const sourceEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId);
557
+ if (!sourceEffect) return { state: 'none' };
558
+ const recoveryId = stableEffectId(`provider-recovery:${sourceEffect.effectId}`);
559
+ const providerFailureCode = input.providerFailureCode?.trim();
560
+ const recoveryReason = providerFailureCode
561
+ ? `Applied Supervisor effect ${sourceEffect.effectId} ended with provider failure ${providerFailureCode} before a committed Supervisor completion. Resume from durable Forge state; the source effect remains applied and must not be replayed.`
562
+ : `Applied Supervisor effect ${sourceEffect.effectId} stopped making observable provider progress without a committed Supervisor completion. Resume from durable Forge state; the source effect remains applied and must not be replayed.`;
563
+ return this.store.observeProviderTurn({
564
+ taskId: task.taskId,
565
+ effectId: sourceEffect.effectId,
566
+ generating: input.generating,
567
+ receiptMissing: input.receiptMissing === true,
568
+ assistantDigest: sha256(`${input.latestAssistantResponse}\n${input.providerActivityText ?? ''}`),
569
+ providerFailureCode,
570
+ observedAtMs: input.observedAtMs,
571
+ graceMs: input.graceMs,
572
+ recovery: { effectId: recoveryId, prompt: this.renderPrompt(task, recoveryId, 'recovery', undefined, recoveryReason) },
573
+ });
574
+ }
575
+ async browserObserveAssistant(input: { conversationId: string; conversationUrl: string; responseText: string }): Promise<WorkflowAssistantObservationResult> {
576
+ const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
577
+ return await this.observeAssistantTurn({ taskId: task.taskId, conversationId: task.conversationId, responseText: input.responseText });
578
+ }
579
+
580
+ /**
581
+ * Repair the crash boundary where a CONTINUE completion was durably committed
582
+ * before successor reservation finished in a previous Runtime. This never
583
+ * replays the provider response; it only reads the committed causal receipt and
584
+ * reserves the exactly-once successor effect.
585
+ */
586
+ async reconcileCommittedContinuations(limit = 16): Promise<{ scanned: number; reconciled: number }> {
587
+ const completions = this.store.listContinueCompletionsAwaitingSuccessor(limit);
588
+ let reconciled = 0;
589
+ for (const completion of completions) {
590
+ const task = this.store.getTask(completion.taskId);
591
+ if (!task || this.store.terminalAction(task.taskId)) continue;
592
+ const committed = this.reserveContinuation(task, completion);
593
+ if (committed.successorEffect) reconciled += 1;
594
+ }
595
+
596
+ // A non-terminal Supervisor task must always have one derivable causal
597
+ // obligation: an unapplied effect, an applied effect awaiting completion,
598
+ // or a committed completion whose successor/terminal resolution can be
599
+ // reconstructed from durable facts. Registration is intentionally a
600
+ // separate transaction from effect reservation, so a Runtime crash between
601
+ // them must converge here instead of leaving an "active" inert task.
602
+ const inertTasks = this.store.listTasksWithoutCausalObligation(limit);
603
+ for (const task of inertTasks) {
604
+ if (this.store.terminalAction(task.taskId)) continue;
605
+ const latest = this.store.getLatestCompletion(task.taskId);
606
+ if (!latest) {
607
+ this.reserveEnrollment(task.taskId);
608
+ reconciled += 1;
609
+ continue;
610
+ }
611
+ if (latest.action === 'CONTINUE') {
612
+ const committed = this.reserveContinuation(task, latest);
613
+ if (committed.successorEffect) reconciled += 1;
614
+ continue;
615
+ }
616
+ const validator = latest.action === 'DONE' ? this.validators.completionContract : this.validators.userBlockerPolicy;
617
+ const validation = await validator(task, latest.proposal);
618
+ const correctionId = validation.valid ? undefined : stableEffectId(`completion:${latest.completionFingerprint}`);
619
+ this.store.resolveTerminal({
620
+ completionFingerprint: latest.completionFingerprint,
621
+ taskId: task.taskId,
622
+ action: latest.action,
623
+ accepted: validation.valid,
624
+ reason: validation.reason,
625
+ ...(correctionId ? {
626
+ correction: {
627
+ effectId: correctionId,
628
+ prompt: this.renderPrompt(task, correctionId, 'correction', latest.proposal.checkpoint, validation.reason),
629
+ },
630
+ } : {}),
631
+ });
632
+ reconciled += 1;
633
+ }
634
+ return { scanned: completions.length + inertTasks.length, reconciled };
635
+ }
636
+
637
+ async observeAssistantTurn(input: WorkflowAssistantObservation): Promise<WorkflowAssistantObservationResult> {
638
+ const task = this.requireTask(input.taskId);
639
+ if (task.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_CONVERSATION_MISMATCH');
640
+ const responseSha256 = sha256(input.responseText);
641
+ const expectedEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId)
642
+ ?? this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
643
+ // A live applied effect always wins: an old compact receipt must never be
644
+ // allowed to satisfy a newer causal obligation. Only when no effect is
645
+ // awaiting completion may an exact persisted response hash recover the
646
+ // original effect context for idempotent duplicate observation.
647
+ const priorCompletion = expectedEffect ? undefined : this.store.getCompletionByResponseSha256(task.taskId, responseSha256);
648
+ const expectedEffectId = expectedEffect?.effectId ?? priorCompletion?.sourceEffectId;
649
+ const parsed = parseSupervisorCompletion(
650
+ input.responseText,
651
+ expectedEffectId ? { task, effectId: expectedEffectId } : undefined,
652
+ );
653
+ const sourceEffect = this.store.getEffect(parsed.proposal.sourceEffectId);
654
+ if (!sourceEffect || sourceEffect.taskId !== task.taskId || !this.store.effectApplied(sourceEffect.effectId)) throw new Error('WORKFLOW_SUPERVISOR_CAUSAL_EFFECT_NOT_APPLIED');
655
+ const explicitIdentityProtocol = sourceEffect.prompt.includes('conversation_id=') && sourceEffect.prompt.includes('task_id=') && sourceEffect.prompt.includes('supervisor_state');
656
+ if (parsed.proposal.conversationId && parsed.proposal.conversationId !== task.conversationId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_CONVERSATION_MISMATCH');
657
+ if (parsed.proposal.taskId && parsed.proposal.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_TASK_MISMATCH');
658
+ if (explicitIdentityProtocol && !parsed.proposal.conversationId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_CONVERSATION_ID_REQUIRED');
659
+ if (explicitIdentityProtocol && !parsed.proposal.taskId) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_TASK_ID_REQUIRED');
660
+ if (explicitIdentityProtocol && !parsed.proposal.supervisorState) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_STATE_REQUIRED');
661
+ if (explicitIdentityProtocol && !parsed.proposal.activeScope) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_ACTIVE_SCOPE_REQUIRED');
662
+ const expectedScope = typeof task.completionContract.requirement_id === 'string' && task.completionContract.requirement_id.trim()
663
+ ? `requirement:${task.completionContract.requirement_id.trim()}`
664
+ : typeof task.continuationPolicy.active_scope === 'string' && task.continuationPolicy.active_scope.trim()
665
+ ? task.continuationPolicy.active_scope.trim()
666
+ : undefined;
667
+ if (expectedScope && parsed.proposal.activeScope && parsed.proposal.activeScope !== expectedScope) throw new Error('WORKFLOW_SUPERVISOR_RESPONSE_ACTIVE_SCOPE_MISMATCH');
668
+ const controlBlockSha256 = sha256(parsed.controlBlock);
669
+ const completionFingerprint = sha256(jsonIdentity(task.taskId, task.conversationId, parsed.proposal.sourceEffectId, responseSha256, controlBlockSha256));
670
+ const completion: WorkflowSupervisorCompletion = { completionFingerprint, taskId: task.taskId, sourceEffectId: parsed.proposal.sourceEffectId, action: parsed.proposal.action, responseSha256, controlBlockSha256, proposal: parsed.proposal, committedAt: new Date().toISOString() };
671
+ const terminal = this.store.terminalAction(task.taskId);
672
+ if (terminal) throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
673
+
674
+ if (parsed.proposal.action === 'CONTINUE') {
675
+ const committed = this.reserveContinuation(task, completion);
676
+ return { action: 'CONTINUE', completionFingerprint, terminal: false, successorEffect: committed.successorEffect!, deduplicated: committed.deduplicated };
677
+ }
678
+ const committed = this.store.commitCompletion(completion);
679
+
680
+ const validator = parsed.proposal.action === 'DONE' ? this.validators.completionContract : this.validators.userBlockerPolicy;
681
+ const validation = await validator(task, parsed.proposal);
682
+ const correctionId = validation.valid ? undefined : stableEffectId(`completion:${completionFingerprint}`);
683
+ const resolved = this.store.resolveTerminal({ completionFingerprint, taskId: task.taskId, action: parsed.proposal.action, accepted: validation.valid, reason: validation.reason,
684
+ ...(correctionId ? { correction: { effectId: correctionId, prompt: this.renderPrompt(task, correctionId, 'correction', parsed.proposal.reason === 'compact_receipt' ? undefined : parsed.proposal.checkpoint, validation.reason) } } : {}) });
685
+ return { action: parsed.proposal.action, completionFingerprint, terminal: validation.valid, ...(resolved.successorEffect ? { successorEffect: resolved.successorEffect } : {}), validation, deduplicated: committed.deduplicated || resolved.deduplicated };
686
+ }
687
+
688
+ /**
689
+ * The autonomous transport records its final state through the Forge tool
690
+ * call, not through text rendered in the ChatGPT page. The exact task and
691
+ * applied effect are derived locally; callers cannot select a different
692
+ * conversation or effect by supplying model-authored identifiers.
693
+ */
694
+ async observeAutomationReceipt(input: {
695
+ taskId: string;
696
+ conversationId: string;
697
+ status: WorkflowSupervisorAutomationStatus;
698
+ receiptId: string;
699
+ }): Promise<WorkflowAssistantObservationResult | { recorded: true }> {
700
+ if (input.status === 'working') return { recorded: true };
701
+ const task = this.requireTask(input.taskId);
702
+ if (task.conversationId !== input.conversationId) throw new Error('WORKFLOW_SUPERVISOR_CONVERSATION_MISMATCH');
703
+ const terminal = this.store.terminalAction(task.taskId);
704
+ if (terminal) throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
705
+ const expectedEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId)
706
+ ?? this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
707
+ // A provider can reuse one tool name/status in successive outer turns.
708
+ // Include the locally selected effect in the durable receipt identity so a
709
+ // later `continue` cannot replay the first effect's completion.
710
+ const replayCompletion = expectedEffect ? undefined : this.store.getLatestCompletion(task.taskId);
711
+ if (!expectedEffect && (!replayCompletion
712
+ || replayCompletion.proposal.checkpoint !== `automation:${input.receiptId}:${replayCompletion.sourceEffectId}`)) {
713
+ throw new Error('WORKFLOW_SUPERVISOR_CAUSAL_EFFECT_NOT_APPLIED');
714
+ }
715
+ const receiptForEffect = `${input.receiptId}:${expectedEffect?.effectId ?? replayCompletion!.sourceEffectId}`;
716
+ const responseText = `automation:${input.status}:${receiptForEffect}`;
717
+ const responseSha256 = sha256(responseText);
718
+ const priorCompletion = expectedEffect ? undefined : this.store.getCompletionByResponseSha256(task.taskId, responseSha256);
719
+ const sourceEffect = expectedEffect ?? (priorCompletion ? this.store.getEffect(priorCompletion.sourceEffectId) : undefined);
720
+ if (!sourceEffect || sourceEffect.taskId !== task.taskId || !this.store.effectApplied(sourceEffect.effectId)) {
721
+ throw new Error('WORKFLOW_SUPERVISOR_CAUSAL_EFFECT_NOT_APPLIED');
722
+ }
723
+ const action = input.status === 'continue' ? 'CONTINUE' : input.status === 'done' ? 'DONE' : 'NEEDS_USER';
724
+ const proposal = {
725
+ action: action as 'CONTINUE' | 'DONE' | 'NEEDS_USER',
726
+ sourceEffectId: sourceEffect.effectId,
727
+ checkpoint: `automation:${receiptForEffect}`,
728
+ reason: 'automation_tool_receipt',
729
+ evidence: [],
730
+ conversationId: task.conversationId,
731
+ taskId: task.taskId,
732
+ supervisorState: action === 'CONTINUE' ? 'running' as const : action === 'DONE' ? 'done' as const : 'needs_user' as const,
733
+ };
734
+ const controlBlockSha256 = sha256(responseText);
735
+ const completionFingerprint = sha256(jsonIdentity(task.taskId, task.conversationId, sourceEffect.effectId, responseSha256, controlBlockSha256));
736
+ const completion: WorkflowSupervisorCompletion = { completionFingerprint, taskId: task.taskId, sourceEffectId: sourceEffect.effectId, action: proposal.action, responseSha256, controlBlockSha256, proposal, committedAt: new Date().toISOString() };
737
+ if (proposal.action === 'CONTINUE') {
738
+ const committed = this.reserveContinuation(task, completion);
739
+ return { action: 'CONTINUE', completionFingerprint, terminal: false, successorEffect: committed.successorEffect!, deduplicated: committed.deduplicated };
740
+ }
741
+ const committed = this.store.commitCompletion(completion);
742
+ const validation = proposal.action === 'DONE'
743
+ ? await this.validators.completionContract(task, proposal)
744
+ : await this.validators.userBlockerPolicy(task, proposal);
745
+ const correctionId = validation.valid ? undefined : stableEffectId(`completion:${completionFingerprint}`);
746
+ const resolved = this.store.resolveTerminal({
747
+ completionFingerprint, taskId: task.taskId, action: proposal.action, accepted: validation.valid, reason: validation.reason,
748
+ ...(correctionId ? { correction: { effectId: correctionId, prompt: this.renderPrompt(task, correctionId, 'correction', proposal.checkpoint, validation.reason) } } : {}),
749
+ });
750
+ return { action: proposal.action, completionFingerprint, terminal: validation.valid, ...(resolved.successorEffect ? { successorEffect: resolved.successorEffect } : {}), validation, deduplicated: committed.deduplicated || resolved.deduplicated };
751
+ }
752
+
753
+ /** Model CONTINUE and its causal successor commit in the same authority transaction. */
754
+ private reserveContinuation(task: WorkflowSupervisorTask, completion: WorkflowSupervisorCompletion) {
755
+ const nextId = stableEffectId(`completion:${completion.completionFingerprint}`);
756
+ const checkpoint = completion.proposal.reason === 'compact_receipt' ? undefined : completion.proposal.checkpoint;
757
+ return this.store.commitCompletion(completion, {
758
+ effectId: nextId,
759
+ kind: 'continuation',
760
+ prompt: this.renderPrompt(task, nextId, 'continuation', checkpoint),
761
+ // Rebuild from the same durable goal in a new logical conversation after
762
+ // a complete 15-receipt cycle; never migrate an uncompleted mutation.
763
+ ...(task.continuationPolicy.kind === 'standalone_supervisor'
764
+ ? { freshEnrollmentPrompt: this.renderPrompt(task, nextId, 'enrollment') } : {}),
765
+ });
766
+ }
767
+
768
+ private renderPrompt(
769
+ task: WorkflowSupervisorTask,
770
+ effectId: string,
771
+ kind: WorkflowEffectKind,
772
+ checkpoint?: string,
773
+ correctionReason?: string,
774
+ lowerLayerContext?: string,
775
+ ): string {
776
+ const canonicalObjective = this.hooks.canonicalObjectiveForTask?.(task)?.trim();
777
+ const promptTask = canonicalObjective ? { ...task, objective: canonicalObjective } : task;
778
+ const cognition = this.hooks.advisoryCognitionForTask?.(promptTask);
779
+ return renderSupervisorPrompt(promptTask, effectId, kind, checkpoint, correctionReason, lowerLayerContext, cognition);
780
+ }
781
+
782
+ private browserTaskActive(task: WorkflowSupervisorTask): boolean { return this.hooks.browserTaskActive?.(task) ?? true; }
783
+ private browserTaskActiveForExternalEffect(task: WorkflowSupervisorTask): boolean {
784
+ return this.store.hasAppliedEffectAwaitingCompletion(task.taskId) || this.browserTaskActive(task);
785
+ }
786
+
787
+ /**
788
+ * Progress is a mechanical change in the canonical Work this turn is meant to
789
+ * advance, never the provider's willingness to answer again. Both endpoints of
790
+ * the comparison are durable facts: the Work projection captured in this
791
+ * effect's dispatch evidence and the current projection. A missing signal on
792
+ * either side is unknown, so it proves neither progress nor its absence.
793
+ */
794
+ private automationTurnChangedNothing(task: WorkflowSupervisorTask, effectId: string): boolean {
795
+ const dispatched = boundedBrowserText(this.store.latestEffectDispatch(effectId)?.evidence.work_progress_fingerprint, 256);
796
+ const current = boundedBrowserText(this.hooks.workProgressFingerprint?.(task), 256);
797
+ return Boolean(dispatched && current && dispatched === current);
798
+ }
799
+
800
+ private browserSnapshotMatchesSource(task: WorkflowSupervisorTask, effect: WorkflowSupervisorEffect, snapshot: { latestUserText: string; latestAssistantResponse: string }): boolean {
801
+ if (!effect.sourceCompletionFingerprint) return true;
802
+ const completion = this.store.getCompletion(effect.sourceCompletionFingerprint);
803
+ if (!completion || completion.taskId !== task.taskId) return false;
804
+ // Text-parsed completions are causally anchored to the exact assistant page
805
+ // response that produced the Supervisor receipt. Automation tool receipts are
806
+ // different: their responseSha256 hashes the synthetic durable receipt carrier,
807
+ // not text rendered in ChatGPT. The page can also collapse the prior automated
808
+ // prompt to ordinary visible prose, so neither the response hash nor its effect
809
+ // marker is a valid prerequisite for delivering the already-reserved successor.
810
+ // The durable receipt has already bound task, conversation, and source effect;
811
+ // retain that local causal fence and skip only the unavailable page-text proof.
812
+ const automationToolReceipt = completion.proposal.reason === 'automation_tool_receipt';
813
+ const sourceEffect = this.store.getEffect(completion.sourceEffectId);
814
+ if (!sourceEffect || sourceEffect.taskId !== task.taskId) return false;
815
+ if (automationToolReceipt) return true;
816
+ if (sha256(snapshot.latestAssistantResponse) !== completion.responseSha256) return false;
817
+ return normalizeBrowserText(snapshot.latestUserText) === normalizeBrowserText(sourceEffect.prompt)
818
+ || browserTextHasEffect(snapshot.latestUserText, sourceEffect.effectId);
819
+ }
820
+
821
+ private requireTask(taskId: string): WorkflowSupervisorTask { const task = this.store.getTask(taskId); if (!task) throw new Error('WORKFLOW_SUPERVISOR_TASK_UNKNOWN'); return task; }
822
+ private requireBrowserTask(conversationId: string, conversationUrl: string): WorkflowSupervisorTask {
823
+ const observed = parseChatgptConversationIdentity(conversationUrl);
824
+ if (observed.conversationId !== conversationId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_CONVERSATION_MISMATCH');
825
+ const task = this.store.getTaskByConversationId(conversationId);
826
+ if (!task) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_CONVERSATION_NOT_ENROLLED');
827
+ const registered = parseChatgptConversationIdentity(task.conversationUrl);
828
+ // Project routing is presentation metadata. A ChatGPT conversation can move
829
+ // between `/c/<id>` and a Project-prefixed route without becoming a different
830
+ // provider conversation, so the durable id is the only identity fence here.
831
+ if (task.conversationId !== conversationId || registered.conversationId !== conversationId || observed.conversationId !== conversationId) throw new Error('WORKFLOW_SUPERVISOR_BROWSER_CONVERSATION_MISMATCH');
832
+ return task;
833
+ }
834
+ }
835
+
836
+ function boundedBrowserText(value: unknown, max: number): string | undefined { return typeof value === 'string' && Buffer.byteLength(value, 'utf8') <= max ? value : undefined; }
837
+ /**
838
+ * A task that already reached DONE/NEEDS_USER can never deliver another effect.
839
+ * Reserving into it reported `enrolled` and left the caller waiting for a turn
840
+ * that the terminal action forbids.
841
+ */
842
+ function requireNonTerminalTask(store: WorkflowSupervisorStore, taskId: string): void {
843
+ const terminal = store.terminalAction(taskId);
844
+ if (terminal) throw new Error(`WORKFLOW_SUPERVISOR_TASK_TERMINAL:${terminal}`);
845
+ }
846
+ function browserSnapshot(evidence: Record<string, unknown> | undefined): { latestUserText: string; latestAssistantResponse: string } | undefined {
847
+ const latestUserText = boundedBrowserText(evidence?.latest_user_text, 128 * 1024);
848
+ const latestAssistantResponse = boundedBrowserText(evidence?.latest_assistant_response, 512 * 1024);
849
+ if (latestUserText === undefined || latestAssistantResponse === undefined) return undefined;
850
+ return { latestUserText, latestAssistantResponse };
851
+ }
852
+ function normalizeBrowserText(value: string): string { return value.replace(/\s+/g, ' ').trim(); }
853
+ function browserTextSha256(value: string): string { return sha256(normalizeBrowserText(value)); }
854
+ function browserTextHasEffect(value: string, effectId: string): boolean { return value.includes(renderEffectMarker(effectId)); }
855
+ const PERSISTED_BROWSER_EVIDENCE_KEYS = new Set(['exact_user_message', 'observation_fingerprint', 'reconciliation', 'reason', 'surface', 'target_marker_present']);
856
+ function sanitizeBrowserEvidence(evidence: Record<string, unknown> | undefined): Record<string, unknown> {
857
+ if (!evidence) return {};
858
+ const sanitized: Record<string, unknown> = {};
859
+ for (const [key, value] of Object.entries(evidence)) {
860
+ if (!PERSISTED_BROWSER_EVIDENCE_KEYS.has(key)) continue;
861
+ if (typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) sanitized[key] = value;
862
+ else if (typeof value === 'string' && value.length <= 512) sanitized[key] = value;
863
+ }
864
+ return sanitized;
865
+ }
866
+
867
+ function browserTask(task: WorkflowSupervisorTask): WorkflowSupervisorBrowserTask { return { taskId: task.taskId, conversationId: task.conversationId, conversationUrl: task.conversationUrl }; }
868
+ function jsonIdentity(...values: string[]): string { return JSON.stringify(values); }