@moretea-labs/forge 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -1,8 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { parseChatgptConversationIdentity } from './chatgpt-conversation';
3
- import { parseSupervisorCompletion, renderEffectMarker, renderSupervisorPrompt, sha256, validateEffectId } from './protocol';
3
+ import { parseSupervisorCompletion, renderEffectMarker, renderSupervisorPrompt, sha256, validateEffectId, validateSupervisorObjective } from './protocol';
4
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';
5
+ import { getWorkContract } from '../packages/kernel/work/api/index';
6
+ import { readRequirement } from '../src/runtime/control-plane/persistence/requirement-store';
7
+ import { resolveWorkflowSupervisorForgeHome } from './paths';
8
+ import type { WorkflowAssistantObservation, WorkflowAssistantObservationResult, WorkflowContractValidation, WorkflowEffectKind, WorkflowSupervisorBrowserPollResult, WorkflowSupervisorBrowserTask, WorkflowSupervisorCompletion, WorkflowSupervisorDiscoveredConversation, WorkflowSupervisorEffect, WorkflowSupervisorLifecycleHooks, WorkflowSupervisorProjectScope, WorkflowSupervisorTask, WorkflowSupervisorTaskInput, WorkflowSupervisorTerminalState, WorkflowSupervisorValidators, WorkflowSupervisorPreflightComposerObservation } from './types';
6
9
 
7
10
  function compactProjectIdentity(value: string): string {
8
11
  return value.normalize('NFKC').toLocaleLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');
@@ -65,13 +68,47 @@ export class WorkflowSupervisorControlPlane {
65
68
  expectedObjectiveSha256: string;
66
69
  requestId: string;
67
70
  authorizedBy?: string;
68
- }): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; refreshedEffect?: WorkflowSupervisorEffect } {
71
+ workId?: string;
72
+ requirementId?: string;
73
+ expectedCompletionContractSha256?: string;
74
+ }): { task: WorkflowSupervisorTask; changed: boolean; deduplicated: boolean; objectiveSha256: string; completionContractSha256: string; semanticBindingChanged: boolean; refreshedEffect?: WorkflowSupervisorEffect } {
69
75
  const task = this.requireTask(input.taskId);
70
76
  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');
77
+ const objective = validateSupervisorObjective(input.objective);
73
78
  if (!/^[0-9a-f]{64}$/i.test(input.expectedObjectiveSha256)) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_EXPECTED_SHA_INVALID');
74
79
  if (!input.requestId.trim()) throw new Error('WORKFLOW_SUPERVISOR_OBJECTIVE_REVISION_REQUEST_REQUIRED');
80
+ const workId = input.workId?.trim() || undefined;
81
+ const requirementId = input.requirementId?.trim() || undefined;
82
+ if (workId && requirementId) throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_TARGET_INVALID');
83
+ if ((workId || requirementId) && !input.expectedCompletionContractSha256) {
84
+ throw new Error('WORKFLOW_SUPERVISOR_COMPLETION_CONTRACT_REVISION_EXPECTED_SHA_REQUIRED');
85
+ }
86
+ if (!workId && !requirementId && input.expectedCompletionContractSha256) {
87
+ throw new Error('WORKFLOW_SUPERVISOR_COMPLETION_CONTRACT_REVISION_TARGET_REQUIRED');
88
+ }
89
+ let semanticBinding: { workId?: string; requirementId?: string; expectedCompletionContractSha256: string } | undefined;
90
+ if (workId || requirementId) {
91
+ if (task.completionContract.kind !== 'model_semantic_completion') {
92
+ throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REVISION_KIND_UNSUPPORTED');
93
+ }
94
+ const controllerHome = typeof task.completionContract.controller_home === 'string' ? task.completionContract.controller_home.trim() : '';
95
+ const repoId = typeof task.completionContract.repo_id === 'string' ? task.completionContract.repo_id.trim() : '';
96
+ if (!controllerHome || !repoId
97
+ || resolveWorkflowSupervisorForgeHome(controllerHome) !== resolveWorkflowSupervisorForgeHome(this.store.forgeHome)) {
98
+ throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_CONTEXT_REQUIRED');
99
+ }
100
+ if (workId && !getWorkContract({ controllerHome, scopeKey: repoId }, workId)) {
101
+ throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_WORK_NOT_FOUND');
102
+ }
103
+ if (requirementId && !readRequirement({ controllerHome }, requirementId)?.value) {
104
+ throw new Error('WORKFLOW_SUPERVISOR_SEMANTIC_BINDING_REQUIREMENT_NOT_FOUND');
105
+ }
106
+ semanticBinding = {
107
+ ...(workId ? { workId } : {}),
108
+ ...(requirementId ? { requirementId } : {}),
109
+ expectedCompletionContractSha256: input.expectedCompletionContractSha256!,
110
+ };
111
+ }
75
112
  const pending = this.store.currentUnappliedEffect(task.taskId);
76
113
  let refreshEffect: { effectId: string; expectedPrompt: string; prompt: string } | undefined;
77
114
  if (pending && !this.store.latestEffectDispatch(pending.effectId)) {
@@ -89,6 +126,7 @@ export class WorkflowSupervisorControlPlane {
89
126
  expectedObjectiveSha256: input.expectedObjectiveSha256.toLowerCase(),
90
127
  requestId: input.requestId.trim(),
91
128
  authorizedBy: input.authorizedBy?.trim() || 'operator',
129
+ ...(semanticBinding ? { semanticBinding } : {}),
92
130
  ...(refreshEffect ? { refreshEffect } : {}),
93
131
  });
94
132
  }
@@ -134,6 +172,14 @@ export class WorkflowSupervisorControlPlane {
134
172
  throw new Error('WORKFLOW_SUPERVISOR_RECOVERY_SOURCE_CHANGED');
135
173
  }
136
174
  if (pending) {
175
+ if (this.store.effectDispatchBudget(pending.effectId).generations === 0) {
176
+ // Browser provider was repaired independently of Forge's source release.
177
+ // The old enrollment text and effect identity remain authoritative;
178
+ // rearm a single preparation without manufacturing a provider send.
179
+ this.store.authorizeOperatorPreflightRetry({ effectId: pending.effectId, requestId: input.requestId,
180
+ reason: input.reason, authorizedBy });
181
+ return { recoveryEffect: pending, action: 'retry_authorized' };
182
+ }
137
183
  // Read-only reclassification first: a dispatch-owner pre-send return is a
138
184
  // stronger fact than any operator decision and makes the effect retryable
139
185
  // through the normal mechanical path.
@@ -236,6 +282,31 @@ export class WorkflowSupervisorControlPlane {
236
282
  const tasks = this.store.listTasks();
237
283
  return activeOnly ? tasks.filter((task) => !this.store.terminalAction(task.taskId)) : tasks;
238
284
  }
285
+ /**
286
+ * Completed/blocked/stopped history is a read-only projection of canonical
287
+ * terminal events. It never archives the ChatGPT conversation or rewrites Work.
288
+ */
289
+ listTerminalTasks(): Array<{ task: WorkflowSupervisorTask; terminal: WorkflowSupervisorTerminalState }> {
290
+ return this.store.listTasks().flatMap((task) => {
291
+ const terminal = this.store.terminalAction(task.taskId);
292
+ return terminal ? [{ task, terminal }] : [];
293
+ });
294
+ }
295
+ /** Reconcile Work-owned task closure after a crash or a direct Work writer.
296
+ * Independent Requirement and standalone Goals are intentionally excluded.
297
+ */
298
+ reconcileTerminalWorkBoundTasks(): string[] {
299
+ if (!this.hooks.terminalWorkStopReasonForTask) return [];
300
+ const stopped: string[] = [];
301
+ for (const task of this.store.listTasks()) {
302
+ if (this.store.terminalAction(task.taskId)) continue;
303
+ const reason = this.hooks.terminalWorkStopReasonForTask(task);
304
+ if (!reason) continue;
305
+ const result = this.store.stopTask(task.taskId, reason);
306
+ if (!result.deduplicated) stopped.push(task.taskId);
307
+ }
308
+ return stopped;
309
+ }
239
310
  stopTask(taskId: string, reason = 'Stopped by operator request.'): { taskId: string; terminal: 'STOPPED'; deduplicated: boolean } {
240
311
  return this.store.stopTask(taskId, reason);
241
312
  }
@@ -251,6 +322,51 @@ export class WorkflowSupervisorControlPlane {
251
322
  return task;
252
323
  }
253
324
  getEffect(id: string): WorkflowSupervisorEffect | undefined { return this.store.getEffect(validateEffectId(id)); }
325
+ preSendComposerObservation(effectId: string): WorkflowSupervisorPreflightComposerObservation | undefined {
326
+ return this.store.preSendComposerObservation(validateEffectId(effectId));
327
+ }
328
+ /** Bind the one canonical Scheduler occurrence lane to an exact Work-owned Supervisor task. */
329
+ bindScheduledWake(input: { taskId: string; repoId: string; workId: string; scheduleId: string }) {
330
+ const task = this.requireTask(input.taskId);
331
+ requireNonTerminalTask(this.store, task.taskId);
332
+ const bound = this.store.bindScheduledWake(input);
333
+ const pending = this.store.currentUnappliedEffect(task.taskId);
334
+ if (pending && !this.store.latestEffectDispatch(pending.effectId) && !this.store.effectApplied(pending.effectId)) {
335
+ const source = pending.sourceCompletionFingerprint ? this.store.getCompletion(pending.sourceCompletionFingerprint) : undefined;
336
+ const checkpoint = source?.proposal.reason === 'compact_receipt' ? undefined : source?.proposal.checkpoint;
337
+ this.store.refreshUndispatchedEffectPrompt({
338
+ taskId: task.taskId,
339
+ effectId: pending.effectId,
340
+ expectedPrompt: pending.prompt,
341
+ prompt: this.renderPrompt(bound.task, pending.effectId, pending.kind, checkpoint),
342
+ });
343
+ }
344
+ return bound;
345
+ }
346
+ /** Consume a completed WAIT receipt exactly once for one exact Scheduler occurrence. */
347
+ wakeScheduledTask(input: { taskId: string; scheduleId: string; occurrenceId: string }): { effect: WorkflowSupervisorEffect; deduplicated: boolean } {
348
+ const task = this.requireTask(input.taskId);
349
+ if (task.continuationPolicy.scheduled_wake_schedule_id !== input.scheduleId) {
350
+ throw new Error('WORKFLOW_SUPERVISOR_SCHEDULE_BINDING_MISMATCH');
351
+ }
352
+ const originKey = `scheduled-wake:${input.scheduleId}:${input.occurrenceId}`;
353
+ const id = stableEffectId(originKey);
354
+ const prior = this.store.getEffectByOriginKey(originKey);
355
+ if (prior) {
356
+ if (prior.taskId !== task.taskId || prior.effectId !== id || prior.kind !== 'continuation' || !prior.sourceCompletionFingerprint) {
357
+ throw new Error('WORKFLOW_SUPERVISOR_SCHEDULED_WAKE_IDENTITY_CONFLICT');
358
+ }
359
+ return { effect: prior, deduplicated: true };
360
+ }
361
+ requireNonTerminalTask(this.store, task.taskId);
362
+ return this.store.reserveScheduledWake({
363
+ taskId: task.taskId,
364
+ scheduleId: input.scheduleId,
365
+ occurrenceId: input.occurrenceId,
366
+ effectId: id,
367
+ prompt: this.renderPrompt(task, id, 'continuation', `scheduled wake ${input.scheduleId} occurrence ${input.occurrenceId}`),
368
+ });
369
+ }
254
370
  /** Read-only stall classification for one task; no lifecycle authority. */
255
371
  taskStall(taskId: string): ReturnType<WorkflowSupervisorStore['taskStall']> { return this.store.taskStall(this.requireTask(taskId).taskId); }
256
372
  /**
@@ -407,39 +523,41 @@ export class WorkflowSupervisorControlPlane {
407
523
  if (!task.conversationId.startsWith('bootstrap:')) throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_PENDING');
408
524
  const terminal = this.store.terminalAction(task.taskId);
409
525
  if (terminal === 'STOPPED') return { authorized: true, task: browserTask(task), terminal };
410
- const pending = this.store.nextBrowserEffect(task.taskId);
411
- if (pending) {
526
+ // While identity is still bootstrap, reconcile the latest applied leaf
527
+ // before dispatching any successor. An unstarted recovery child must not
528
+ // send from a Project/new-composer surface while the source conversation
529
+ // is still unbound. The existing effect ledger owns observation cooldown.
530
+ const applied = this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
531
+ if (applied) {
532
+ if (!this.store.bootstrapAppliedReconcileDue(applied.effectId)) {
533
+ return { authorized: true, task: browserTask(task) };
534
+ }
535
+ const dispatch = this.store.latestEffectDispatch(applied.effectId);
412
536
  return {
413
537
  authorized: true,
414
538
  task: browserTask(task),
415
539
  command: {
416
- mode: pending.mode,
417
- effectId: pending.effect.effectId,
418
- kind: pending.effect.kind,
419
- prompt: pending.effect.prompt,
420
- dispatchGeneration: pending.generation,
540
+ mode: 'reconcile',
541
+ effectId: applied.effectId,
542
+ kind: applied.kind,
543
+ prompt: applied.prompt,
544
+ dispatchGeneration: dispatch?.generation ?? 1,
421
545
  conversationId: task.conversationId,
422
546
  conversationUrl: task.conversationUrl,
423
547
  },
424
548
  };
425
549
  }
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);
550
+ const pending = this.store.nextBrowserEffect(task.taskId);
551
+ if (!pending) return { authorized: true, task: browserTask(task) };
434
552
  return {
435
553
  authorized: true,
436
554
  task: browserTask(task),
437
555
  command: {
438
- mode: 'reconcile',
439
- effectId: applied.effectId,
440
- kind: applied.kind,
441
- prompt: applied.prompt,
442
- dispatchGeneration: dispatch?.generation ?? 1,
556
+ mode: pending.mode,
557
+ effectId: pending.effect.effectId,
558
+ kind: pending.effect.kind,
559
+ prompt: pending.effect.prompt,
560
+ dispatchGeneration: pending.generation,
443
561
  conversationId: task.conversationId,
444
562
  conversationUrl: task.conversationUrl,
445
563
  },
@@ -468,13 +586,17 @@ export class WorkflowSupervisorControlPlane {
468
586
  const pending = this.store.nextBrowserEffect(task.taskId);
469
587
  const dispatch = this.store.latestEffectDispatch(effect.effectId);
470
588
  if (!pending || pending.effect.effectId !== effect.effectId || pending.mode !== 'reconcile'
471
- || !dispatch || pending.generation !== dispatch.generation) {
589
+ || !dispatch || pending.generation !== dispatch.generation
590
+ || dispatch.evidence.surface !== 'computer-bootstrap'
591
+ || input.evidence?.dispatch_mutation !== 'not_attempted'
592
+ || input.evidence?.send_clicked !== false) {
472
593
  throw new Error('WORKFLOW_SUPERVISOR_BOOTSTRAP_NOT_APPLIED_PROOF_INVALID');
473
594
  }
474
595
  this.store.recordEffectNotAppliedProof(effect.effectId, input.observationId, {
475
- surface: 'computer-bootstrap',
596
+ surface: 'computer-bootstrap', send_clicked: false,
476
597
  dispatch_generation: dispatch.generation,
477
598
  pre_send_rejection: true,
599
+ attestation: 'local_computer_dispatch_returned_without_send',
478
600
  });
479
601
  return;
480
602
  }
@@ -482,12 +604,12 @@ export class WorkflowSupervisorControlPlane {
482
604
  this.observeEffect({ effectId: effect.effectId, observationId: input.observationId, outcome: input.outcome, evidence });
483
605
  if (input.outcome === 'applied') this.hooks.effectApplied?.(task, effect, { observationId: input.observationId, evidence });
484
606
  }
485
- /** Local Computer preparation returned before Send intent; persist in the existing effect ledger. */
486
- recordProviderPreparationFailure(input: { taskId: string; effectId: string; reasonCode: string }): void {
607
+ /** Persist a bounded preflight rejection; started generations require an exact no-click proof first. */
608
+ recordProviderPreparationFailure(input: { taskId: string; effectId: string; reasonCode: string; composerObservation?: WorkflowSupervisorPreflightComposerObservation }): void {
487
609
  const task = this.requireTask(input.taskId);
488
610
  const effect = this.store.getEffect(input.effectId);
489
611
  if (effect?.taskId !== task.taskId) throw new Error('WORKFLOW_SUPERVISOR_PRE_SEND_EFFECT_TASK_MISMATCH');
490
- this.store.recordEffectPreparationFailure(input.effectId, input.reasonCode);
612
+ this.store.recordEffectPreparationFailure(input.effectId, input.reasonCode, input.composerObservation);
491
613
  }
492
614
 
493
615
  browserBeginEffect(input: { conversationId: string; conversationUrl: string; effectId: string; dispatchId: string; dispatchGeneration: number; evidence?: Record<string, unknown> }): { started: boolean; mode: 'send' | 'reconcile'; generation: number } {
@@ -543,17 +665,25 @@ export class WorkflowSupervisorControlPlane {
543
665
  if (dispatch?.generation !== input.dispatchGeneration) throw new Error('WORKFLOW_SUPERVISOR_DISPATCH_GENERATION_CHANGED');
544
666
  this.store.recordEffectNotAppliedProof(input.effectId, input.observationId, {
545
667
  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.
668
+ // This call is the dispatch owner's exact mechanical return before any
669
+ // Send click. The store refunds this generation once; the existing preflight
670
+ // gate bounds ordinary repeats without spending actual send budget.
550
671
  pre_send_rejection: true,
551
672
  });
673
+ const recoveryStopPending = effect.kind === 'recovery' && input.reason.trim() === 'provider_recovery_stop_pending';
674
+ if (!recoveryStopPending) {
675
+ this.recordProviderPreparationFailure({ taskId: task.taskId, effectId: effect.effectId, reasonCode: input.reason });
676
+ }
552
677
  }
553
678
  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
679
  const task = this.requireBrowserTask(input.conversationId, input.conversationUrl);
555
680
  if (!this.browserTaskActiveForExternalEffect(task)) return { state: 'inactive' };
556
- const sourceEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId);
681
+ // Exhaustion stops new recovery mutations, not read-only observation of the exhausted applied leaf.
682
+ let sourceEffect = this.store.latestAppliedEffectWithoutCompletion(task.taskId);
683
+ if (!sourceEffect) {
684
+ const exhaustedLeaf = this.store.latestAppliedLeafEffectWithoutCompletion(task.taskId);
685
+ if (exhaustedLeaf && this.store.providerResumeExhausted(exhaustedLeaf.effectId)) sourceEffect = exhaustedLeaf;
686
+ }
557
687
  if (!sourceEffect) return { state: 'none' };
558
688
  const recoveryId = stableEffectId(`provider-recovery:${sourceEffect.effectId}`);
559
689
  const providerFailureCode = input.providerFailureCode?.trim();
@@ -613,8 +743,8 @@ export class WorkflowSupervisorControlPlane {
613
743
  if (committed.successorEffect) reconciled += 1;
614
744
  continue;
615
745
  }
616
- const validator = latest.action === 'DONE' ? this.validators.completionContract : this.validators.userBlockerPolicy;
617
- const validation = await validator(task, latest.proposal);
746
+ if (latest.action === 'WAIT') continue;
747
+ const validation = await this.validateTerminalProposal(task, latest.proposal);
618
748
  const correctionId = validation.valid ? undefined : stableEffectId(`completion:${latest.completionFingerprint}`);
619
749
  this.store.resolveTerminal({
620
750
  completionFingerprint: latest.completionFingerprint,
@@ -646,9 +776,10 @@ export class WorkflowSupervisorControlPlane {
646
776
  // original effect context for idempotent duplicate observation.
647
777
  const priorCompletion = expectedEffect ? undefined : this.store.getCompletionByResponseSha256(task.taskId, responseSha256);
648
778
  const expectedEffectId = expectedEffect?.effectId ?? priorCompletion?.sourceEffectId;
779
+ const receiptEffect = expectedEffect ?? (expectedEffectId ? this.store.getEffect(expectedEffectId) : undefined);
649
780
  const parsed = parseSupervisorCompletion(
650
781
  input.responseText,
651
- expectedEffectId ? { task, effectId: expectedEffectId } : undefined,
782
+ expectedEffectId ? { task, effectId: expectedEffectId, prompt: receiptEffect?.prompt } : undefined,
652
783
  );
653
784
  const sourceEffect = this.store.getEffect(parsed.proposal.sourceEffectId);
654
785
  if (!sourceEffect || sourceEffect.taskId !== task.taskId || !this.store.effectApplied(sourceEffect.effectId)) throw new Error('WORKFLOW_SUPERVISOR_CAUSAL_EFFECT_NOT_APPLIED');
@@ -675,81 +806,19 @@ export class WorkflowSupervisorControlPlane {
675
806
  const committed = this.reserveContinuation(task, completion);
676
807
  return { action: 'CONTINUE', completionFingerprint, terminal: false, successorEffect: committed.successorEffect!, deduplicated: committed.deduplicated };
677
808
  }
809
+ if (parsed.proposal.action === 'WAIT') {
810
+ const committed = this.store.commitCompletion(completion);
811
+ return { action: 'WAIT', completionFingerprint, terminal: false, deduplicated: committed.deduplicated };
812
+ }
678
813
  const committed = this.store.commitCompletion(completion);
679
814
 
680
- const validator = parsed.proposal.action === 'DONE' ? this.validators.completionContract : this.validators.userBlockerPolicy;
681
- const validation = await validator(task, parsed.proposal);
815
+ const validation = await this.validateTerminalProposal(task, parsed.proposal);
682
816
  const correctionId = validation.valid ? undefined : stableEffectId(`completion:${completionFingerprint}`);
683
817
  const resolved = this.store.resolveTerminal({ completionFingerprint, taskId: task.taskId, action: parsed.proposal.action, accepted: validation.valid, reason: validation.reason,
684
818
  ...(correctionId ? { correction: { effectId: correctionId, prompt: this.renderPrompt(task, correctionId, 'correction', parsed.proposal.reason === 'compact_receipt' ? undefined : parsed.proposal.checkpoint, validation.reason) } } : {}) });
685
819
  return { action: parsed.proposal.action, completionFingerprint, terminal: validation.valid, ...(resolved.successorEffect ? { successorEffect: resolved.successorEffect } : {}), validation, deduplicated: committed.deduplicated || resolved.deduplicated };
686
820
  }
687
821
 
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
822
  /** Model CONTINUE and its causal successor commit in the same authority transaction. */
754
823
  private reserveContinuation(task: WorkflowSupervisorTask, completion: WorkflowSupervisorCompletion) {
755
824
  const nextId = stableEffectId(`completion:${completion.completionFingerprint}`);
@@ -765,6 +834,32 @@ export class WorkflowSupervisorControlPlane {
765
834
  });
766
835
  }
767
836
 
837
+ private async validateTerminalProposal(
838
+ task: WorkflowSupervisorTask,
839
+ proposal: WorkflowSupervisorCompletion['proposal'],
840
+ ): Promise<WorkflowContractValidation> {
841
+ const validator = proposal.action === 'DONE' ? this.validators.completionContract : this.validators.userBlockerPolicy;
842
+ const validation = await validator(task, proposal);
843
+ if (typeof task.continuationPolicy.scheduled_wake_schedule_id !== 'string') return validation;
844
+ if (proposal.action === 'DONE') {
845
+ return {
846
+ ...validation,
847
+ valid: false,
848
+ reason: validation.valid ? 'scheduled_work_must_wait' : validation.reason,
849
+ evidence: [...(validation.evidence ?? []), 'A Work-bound recurring Schedule owns the next run; use WAIT instead of terminal DONE.'],
850
+ };
851
+ }
852
+ if (proposal.action === 'NEEDS_USER') {
853
+ return {
854
+ ...validation,
855
+ valid: false,
856
+ reason: validation.valid ? 'scheduled_work_must_remain_open' : validation.reason,
857
+ evidence: [...(validation.evidence ?? []), 'Persist the exact user blocker and use WAIT; a recurring scheduled Work must remain open.'],
858
+ };
859
+ }
860
+ return validation;
861
+ }
862
+
768
863
  private renderPrompt(
769
864
  task: WorkflowSupervisorTask,
770
865
  effectId: string,
@@ -801,18 +896,18 @@ export class WorkflowSupervisorControlPlane {
801
896
  if (!effect.sourceCompletionFingerprint) return true;
802
897
  const completion = this.store.getCompletion(effect.sourceCompletionFingerprint);
803
898
  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';
899
+ // A compact completion is committed only after observing the provider's
900
+ // finished assistant reply and checking its causal challenge. ChatGPT may
901
+ // subsequently collapse that historical reply or its original user marker;
902
+ // requiring the bytes to remain in the DOM would strand a valid successor.
903
+ // Persisted legacy tool receipts are read-only migration evidence; the MCP
904
+ // receipt writer has been retired. In both cases the committed completion,
905
+ // not a new heuristic, is the one authority for the successor.
906
+ const durableReceipt = completion.proposal.reason === 'compact_receipt'
907
+ || completion.proposal.reason === 'automation_tool_receipt';
813
908
  const sourceEffect = this.store.getEffect(completion.sourceEffectId);
814
909
  if (!sourceEffect || sourceEffect.taskId !== task.taskId) return false;
815
- if (automationToolReceipt) return true;
910
+ if (durableReceipt) return true;
816
911
  if (sha256(snapshot.latestAssistantResponse) !== completion.responseSha256) return false;
817
912
  return normalizeBrowserText(snapshot.latestUserText) === normalizeBrowserText(sourceEffect.prompt)
818
913
  || browserTextHasEffect(snapshot.latestUserText, sourceEffect.effectId);
@@ -1,5 +1,6 @@
1
1
  import { isLegacyMachineRequirementWait, readRequirement } from '../src/runtime/control-plane/persistence/requirement-store';
2
- import { getWorkContract, isTerminalSemanticWorkState } from '../packages/kernel/work/api/index';
2
+ import { getWorkContract, isTerminalSemanticWorkState, readWorkContractLineageSnapshot, type SemanticWorkState } from '../packages/kernel/work/api/index';
3
+ import { SEMANTIC_SCOPE_KEY } from '../src/cli/repositories/controller-home';
3
4
  import type { WorkflowContractValidation, WorkflowSupervisorProposal, WorkflowSupervisorTask, WorkflowSupervisorValidators } from './types';
4
5
 
5
6
  function contractText(task: WorkflowSupervisorTask, key: string): string | undefined {
@@ -11,8 +12,8 @@ function requirementFor(task: WorkflowSupervisorTask, proposal?: WorkflowSupervi
11
12
  const controllerHome = contractText(task, 'controller_home');
12
13
  const repoId = contractText(task, 'repo_id');
13
14
  const workId = contractText(task, 'work_id');
14
- const workRequirementId = controllerHome && repoId && workId
15
- ? getWorkContract({ controllerHome, repoId }, workId)?.requirementId
15
+ const workRequirementId = controllerHome && workId
16
+ ? getWorkContract({ controllerHome, scopeKey: repoId ?? SEMANTIC_SCOPE_KEY }, workId)?.requirementId
16
17
  : undefined;
17
18
  // activeScope is legacy read compatibility only. New receipts never make model
18
19
  // output authoritative for Requirement identity.
@@ -28,37 +29,103 @@ function workFor(task: WorkflowSupervisorTask) {
28
29
  const controllerHome = contractText(task, 'controller_home');
29
30
  const repoId = contractText(task, 'repo_id');
30
31
  const workId = contractText(task, 'work_id');
31
- return controllerHome && repoId && workId ? getWorkContract({ controllerHome, repoId }, workId) : undefined;
32
+ return controllerHome && workId
33
+ ? getWorkContract({ controllerHome, scopeKey: repoId ?? SEMANTIC_SCOPE_KEY }, workId) : undefined;
34
+ }
35
+
36
+
37
+ function boundWorkLineage(task: WorkflowSupervisorTask, requirementId?: string): WorkflowContractValidation | undefined {
38
+ const controllerHome = contractText(task, 'controller_home');
39
+ const repoId = contractText(task, 'repo_id');
40
+ const workId = requirementId ? undefined : contractText(task, 'work_id');
41
+ if (!controllerHome || (!workId && !requirementId)) return undefined;
42
+ // Repository rows are compatibility evidence; portable semantic rows win by identity.
43
+ const scopes = repoId ? [repoId, SEMANTIC_SCOPE_KEY] : [SEMANTIC_SCOPE_KEY];
44
+ const byId = new Map<string, { workId: string; requirementId?: string; parentWorkId?: string;
45
+ semanticParentWorkId?: string; dependsOnWorkIds?: string[]; predecessorWorkId?: string;
46
+ supersedes?: string[]; supersededBy?: string; semanticState?: SemanticWorkState; error?: string }>();
47
+ for (const scopeKey of new Set(scopes)) {
48
+ const snapshot = readWorkContractLineageSnapshot({ controllerHome, scopeKey });
49
+ if (snapshot.contracts.length + snapshot.invalid.length >= 5_000) return unsupported('work_lineage_read_limit');
50
+ for (const work of [...snapshot.contracts, ...snapshot.invalid]) byId.set(work.workId, work);
51
+ }
52
+ const relevant = new Set<string>(workId ? [workId] : [...byId.values()]
53
+ .filter((work) => work.requirementId === requirementId).map((work) => work.workId));
54
+ let expanded = true;
55
+ while (expanded) {
56
+ expanded = false;
57
+ for (const work of byId.values()) {
58
+ if (relevant.has(work.parentWorkId ?? '') || relevant.has(work.semanticParentWorkId ?? '')
59
+ || relevant.has(work.predecessorWorkId ?? '') || (work.supersedes ?? []).some((id) => relevant.has(id))) {
60
+ if (!relevant.has(work.workId)) { relevant.add(work.workId); expanded = true; }
61
+ }
62
+ if (relevant.has(work.workId)) {
63
+ for (const dependency of [...(work.dependsOnWorkIds ?? []), ...(work.supersededBy ? [work.supersededBy] : [])]) {
64
+ if (!relevant.has(dependency)) { relevant.add(dependency); expanded = true; }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ const blockers = [...relevant].filter((id) => {
70
+ const work = byId.get(id);
71
+ return !work || Boolean(work.error) || !work.semanticState || !isTerminalSemanticWorkState(work.semanticState);
72
+ }).sort();
73
+ return blockers.length > 0
74
+ ? { valid: false, reason: 'bound_work_lineage_not_terminal', evidence: blockers.slice(0, 8) }
75
+ : undefined;
76
+ }
77
+
78
+ function validateWorkCompletion(task: WorkflowSupervisorTask): WorkflowContractValidation {
79
+ const work = workFor(task);
80
+ return work && work.semanticState === 'completed'
81
+ ? boundWorkLineage(task) ?? { valid: true, reason: 'work_terminal_committed' }
82
+ : unsupported('work_not_completed');
83
+ }
84
+
85
+ function validateRequirementCompletion(task: WorkflowSupervisorTask, proposal: WorkflowSupervisorProposal): WorkflowContractValidation {
86
+ const requirement = requirementFor(task, proposal);
87
+ if (!requirement) return unsupported('requirement_not_found');
88
+ if (requirement.state !== 'done' || !requirement.semanticAcceptance) {
89
+ return { valid: false, reason: 'requirement_not_semantically_done:' + requirement.state, evidence: requirement.auditRefs.slice(-8) };
90
+ }
91
+ return boundWorkLineage(task, requirement.requirementId)
92
+ ?? { valid: true, reason: 'requirement_semantic_acceptance_committed', evidence: requirement.auditRefs.slice(-8) };
32
93
  }
33
94
 
34
95
  /**
35
- * Workflow Supervisor validates only the already-authoritative Forge Goal state.
36
- * It never re-derives Work/Plan completion and never turns infrastructure failure
37
- * into a user blocker on its own.
96
+ * A standalone task may intentionally have no semantic target. When it names a
97
+ * Work or Requirement, DONE must use the same committed semantic proof as the
98
+ * corresponding typed completion contract. Repository provenance alone is not
99
+ * a semantic binding, and proposal text never creates one.
38
100
  */
101
+ function validateSemanticCompletionBinding(task: WorkflowSupervisorTask, proposal: WorkflowSupervisorProposal): WorkflowContractValidation {
102
+ const workId = contractText(task, 'work_id');
103
+ const requirementId = contractText(task, 'requirement_id');
104
+ if (workId && requirementId) return unsupported('semantic_completion_binding_ambiguous');
105
+ if (workId) return validateWorkCompletion(task);
106
+ if (requirementId) return validateRequirementCompletion(task, proposal);
107
+ return { valid: true, reason: 'model_semantic_completion' };
108
+ }
109
+
110
+ /** Read committed semantic Goal and explicit Work lineage without changing either authority. */
39
111
  export function forgeWorkflowSupervisorValidators(): WorkflowSupervisorValidators {
40
112
  return {
41
113
  completionContract: async (task, proposal) => {
42
114
  if (task.completionContract.kind === 'model_semantic_completion') {
43
- return { valid: true, reason: 'model_semantic_completion' };
44
- }
45
- if (task.completionContract.kind === 'forge_work_done') {
46
- const work = workFor(task);
47
- return work && isTerminalSemanticWorkState(work.semanticState)
48
- ? { valid: true, reason: 'work_terminal_committed' }
49
- : unsupported('work_not_terminal');
115
+ return validateSemanticCompletionBinding(task, proposal);
50
116
  }
117
+ if (task.completionContract.kind === 'forge_work_done') return validateWorkCompletion(task);
51
118
  if (!['forge_requirement_done', 'forge_dynamic_requirement_done'].includes(String(task.completionContract.kind ?? ''))) return unsupported('completion_contract_kind_unsupported');
52
- const requirement = requirementFor(task, proposal);
53
- if (!requirement) return unsupported('requirement_not_found');
54
- if (requirement.state !== 'done' || !requirement.semanticAcceptance) {
55
- return { valid: false, reason: `requirement_not_semantically_done:${requirement.state}`, evidence: requirement.auditRefs.slice(-8) };
56
- }
57
- return { valid: true, reason: 'requirement_semantic_acceptance_committed', evidence: requirement.auditRefs.slice(-8) };
119
+ return validateRequirementCompletion(task, proposal);
58
120
  },
59
121
  userBlockerPolicy: async (task, proposal) => {
60
122
  if (task.userBlockerPolicy.kind === 'model_semantic_user_blocker') {
61
- return { valid: true, reason: 'model_semantic_user_blocker' };
123
+ // Tool visibility is a provider failure, not a decision or permission
124
+ // only the user can supply. Use the immutable completion evidence;
125
+ // do not add another task state or rely on transient browser text.
126
+ return proposal.evidence.includes('NO_TOOL_RECEIPT')
127
+ ? { valid: false, reason: 'tool_unavailable_not_user_blocker' }
128
+ : { valid: true, reason: 'model_semantic_user_blocker' };
62
129
  }
63
130
  if (task.userBlockerPolicy.kind === 'forge_work_waiting_for_user') {
64
131
  const work = workFor(task);