@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
@@ -1,173 +1,38 @@
1
- import { randomUUID } from "crypto";
2
- import { existsSync } from "fs";
3
- import { join } from "path";
4
- import { collectRuntimePerformanceDiagnostics } from "../../../src/runtime/diagnostics/performance";
5
- import { mintEngineeringAdmissionEvidence } from "./engineering-preconditions";
6
- import type { CallToolResult } from "../../../packages/protocols/mcp/tool-contract";
7
- import type { MultiRepositoryMcpToolContext } from "../multi-repository";
8
- import { result } from "./result-adapter";
9
- import { selected } from "./shared-adapter";
10
- import { controllerReadinessEvidence, invalidFacadeOperation, repositoryRevisionContains } from "./status-inbox-adapter";
11
- import { freshGitIdentity } from "../../../src/cli/repository/inspector";
12
- import { repositoryCheckoutLifecycle, selectRepositoryCheckout } from "../../../src/cli/repositories/registry";
13
- import { repositoryGitStatus } from "../../../src/cli/repositories/structured-git";
14
- import { DEFAULT_WORK_CHECK_LEASE_WAIT_MS, getProcessRecord, isManagedProcessActive, listProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics } from "../../../src/runtime/execution/process-runtime";
15
- import { classifyPersistedCheckTerminalEvidence } from "../../../src/runtime/execution/process-runtime/check-result";
16
- import { listWorkBoundRepositoryProcessEvidence, listWorkBoundRepositoryRemoteEffectProcessEvidence } from "../../../src/runtime/control-plane/execution/work-process-evidence";
17
- import { completeRemoteEffectWorkFromProcessReceipt } from "../../../packages/kernel/work/api/index";
18
- import { executionIdentityForRepository } from "../../../src/runtime/control-plane/execution/execution-identity";
19
- import { executeRegisteredWorkflow, observeAndReconcileRegisteredWorkflow } from "../../../src/runtime/workflows/runtime";
20
- import { readWorkflowRun } from "../../../src/runtime/control-plane/persistence/workflow-run-store";
21
- import { schedulePublicationOutcomeCollection } from "../../../src/runtime/root/assistant-learning-loop";
22
- import { recordControllerExperience, recordControllerOutcome, type ControllerExperienceDraft, type ControllerOutcomeObservationDraft } from "../../../src/runtime/context/assistant-work-context";
23
- import { ensureXiaohongshuWorkflowInstalled, XIAOHONGSHU_WORKFLOW_IDS } from "../../../src/runtime/workflows/first-party/xiaohongshu";
24
- import { readWorkHandle, resolveWorkDeliveryTargetBranch, workDeliveryBaseRevision, type WorkHandleState } from "../../../src/runtime/control-plane/execution/work-handle-store";
25
- import { ensureRepositoryWorkHandle, rebindRepositoryWorkHandleControllerIdentity, reconcileRepositoryWorkHandlePlacement } from "../../../src/runtime/control-plane/execution/work-handle-authority";
26
- import { recoverControllerAuthority } from "../../../src/runtime/control-plane/execution/controller-authority-recovery";
27
- import { reconcileSingleTerminalWorkCleanup, recoverTerminalWorkHandle } from "../../../src/runtime/control-plane/execution/work-terminal-cleanup";
28
- import { commandFingerprint, verificationInputFingerprint, workspaceValidationFingerprint } from "../../../src/runtime/control-plane/execution/verification-evidence";
29
- import { resolveWorkVerificationContext } from "../../../src/runtime/control-plane/execution/work-verification-context";
30
- import { executeWorkVerification, executeWorkVerificationBatch } from "../../../src/runtime/control-plane/execution/work-verification-service";
31
- import { implementationReviewContentFingerprint } from "../../../src/runtime/control-plane/execution/implementation-review-content";
32
- import { implementationReviewCommittedBaseRevision, reconcileDirectCanonicalTargetAdvanceCommand } from "../../../src/runtime/control-plane/execution/work-finalization-service";
33
- import { acceptReviewedDirectEditWorkReconciliation } from "../../../src/runtime/control-plane/execution/direct-edit-work-completion";
34
- import { readForgeRuntimeStatus } from "../../../src/runtime/control-plane/runtime-status-client";
35
- import { ensureControllerDispositionContinuation, repositoryCleanContinuationEventName, triggerWorkContinuationRepositoryEvent } from "../../../src/runtime/workflow/schedules/work-continuation";
36
- import { callRhWorkScheduleAdapter, isRhWorkScheduleOperation } from "./scheduler-adapter";
37
- import { assertAutomatedOperationAllowed } from "../../../src/runtime/control-plane/governance/external-effects";
38
- import { listControllerChecks, readLatestControllerCheckEvidence } from "../../../src/cli/controller/check-runner";
39
- import { finalizeRemoteEffectWorkFromActionReceipt } from "../../../src/runtime/plugins/store";
40
- import { gitSnapshot } from "../../../src/cli/repository/inspector";
41
- import { buildWorkflowWatchdogReport } from "../../../src/runtime/watchdog/workflow-watchdog";
42
- import { applyRuntimeMaintenance, buildRecoveryAuditRecord, buildRuntimeMaintenanceStatus, writeRecoveryAuditRecord, type RecoveryActionDescriptor } from "../../../src/runtime/recovery";
43
- import { callStandaloneRecoveryTool } from "./recovery-client-adapter";
44
- import { allowedFacadeOperations, buildFacadeResult, classifyVerificationOutcome, getHandoffItem, normalizeCheckIds, runGoalWorkloop, runSelfHealingLoop, delegateToCodexCerebellum, buildWorkContinuationSnapshot, acceptPlanStepEvidence, admitPlanContractAsync, approvePlanContractAsync, getPlanContract, listPlanContracts, resolvePlanAdmission, withPrimaryWorkAdmissionLockAsync, repairDanglingPlanStepWorkBinding, repairPlanStepForTechnicalRetry, replanActivePlanBoundWorkScope, repairDraftPlanContractAsync, completePlanStepForWork, summarizePlanContract, summarizeWorkContract, supersedePlanContract, verifyGoalWorkloop } from "../../../src/runtime/control-plane/facade";
45
- import { getWorkContract, listWorkContracts, type WorkContract } from "../../../packages/kernel/work/api/index";
46
- import { readExecutionSession, startExecutionSession, updateExecutionSession } from "../../../src/runtime/control-plane/execution/session-store";
47
- import { changedPaths as workChangedPaths, changedPathsFromUnbornBase as workChangedPathsFromUnbornBase } from "../../../src/runtime/control-plane/execution/work-task-receipt";
48
- import { readRequirement } from "../../../src/runtime/control-plane/persistence/requirement-store";
49
- import { admitRequirement, completeRequirementGoal, continueRequirement } from "../../../src/runtime/control-plane/facade/requirement-authority";
50
- import { ensureManagedWorkspace } from "../../../src/runtime/execution/managed-workspace";
51
- import { materializeRepositoryWorkPlacement } from "../../../src/runtime/control-plane/facade/repository-work-admission";
52
- import { ensureRunningRepositoryWorkCheckout, reauthorizeRetainedCancelledRepositoryWork } from "../../../src/runtime/control-plane/execution/retained-work-resume";
53
- import { currentPermissionSnapshotVersion } from "../../../src/runtime/control-plane/execution/validation";
54
- import { callExecutionTool } from "./execution-tools";
55
- import { launchSuperController } from "../../../src/runtime/control-plane/launcher/thin-launcher";
56
- import { getExternalControllerLaunchReservation } from "../../../src/runtime/control-plane/launcher/launch-reservation-store";
57
- import { providerMcpReservationIdentity } from "../../../src/runtime/control-plane/launcher/provider-mcp-bootstrap";
58
- import { runStandaloneChatgptPrompt, runWorkChatgptContinuation, settleWorkChatgptAutomationTab } from "../../../src/runtime/control-plane/launcher/chatgpt-work-continuation";
59
- import { chatgptControllerRoundBinding, recordChatgptControllerRoundTabSettlement, renderChatgptControllerRoundPrompt, prepareControllerAssistantContext, prepareControllerAssistantContextBundle } from "../../../src/runtime/root/controller-round-composition";
60
- import { assertControllerOwnershipAuthority, bindControllerSessionToCurrentRuntime, controllerRoundBlockerClass, controllerSessionAuthorityDigest, controllerSessionAuthorityMatches, controllerSessionPrincipalId, getControllerSession, getRetainedControllerSession, mintControllerSessionAuthority, releaseControllerSessionWithAuthority, releaseObservedControllerSession, resumeControllerSession, withControllerSessionTerminalizationFence, type ControllerTerminalizationAuthority, acknowledgeControllerRoundClaim, beginControllerRoundRelayAfterRelease, beginInitialControllerRoundDispatch, bindControllerRoundSuccessorWork, reconcileControllerRoundAfterAbandonedRelease, reconcileControllerRoundAfterTerminalWork, finishControllerRoundRelayDispatch, getControllerRoundRelay, rearmControllerRoundAfterProviderRecovery, resolveRequirementControllerRoundRelayForWork, submitControllerRoundDisposition, type ControllerRoundRelayRecord, type ControllerRoundDisposition } from "../../../packages/kernel/controller/api/index";
61
- import { parseControllerDispositionCompatibilityCapability, parseControllerRoundCompatibilityCapability, parsePlanObligationCompatibilityCapability } from "../controller-round-compatibility";
62
- import { parseFrozenSemanticCompatibilityCapability } from "../frozen-client-semantic-compatibility";
1
+ import type { CallToolResult } from '../../../packages/protocols/mcp/tool-contract';
2
+ import type { MultiRepositoryMcpToolContext } from '../multi-repository';
3
+ import { SEMANTIC_SCOPE_KEY } from '../../../src/cli/repositories/controller-home';
4
+ import { listControllerChecks } from '../../../src/cli/controller/check-runner';
5
+ import { repositoryGitStatus } from '../../../src/cli/repositories/structured-git';
6
+ import { getWorkContract, updateWorkContract, workSemanticView } from '../../../packages/kernel/work/api/index';
7
+ import { ensureRepositoryWorkHandle } from '../../../src/runtime/control-plane/execution/work-handle-authority';
8
+ import { ensureManagedWorkspace } from '../../../src/runtime/execution/managed-workspace';
9
+ import { ensureRepositoryProjectOnboarding } from '../../../src/runtime/control-plane/workspace/project-onboarding';
10
+ import { buildFacadeResult } from '../../../src/runtime/control-plane/facade';
11
+ import { isRhWorkAcceptedOperation } from '../../../src/runtime/control-plane/facade/rh-work-operation-contract';
12
+ import { findControlPlaneRecordsByKey, readControlPlaneRecord } from '../../../src/runtime/control-plane/persistence/sqlite-store';
13
+ import { result } from './result-adapter';
14
+ import { selected } from './shared-adapter';
15
+ import { invalidFacadeOperation } from './status-inbox-adapter';
16
+ import { normalizeRhWorkInputWireMigration } from './work-input-wire-migration';
17
+ import { callRhWorkScheduleAdapter, isRhWorkScheduleOperation } from './scheduler-adapter';
18
+ import { callRhWorkRequirementOperation, isRhWorkRequirementOperation } from './work-requirement-operations';
19
+ import { callRhWorkSemanticOperation, semanticWorkId } from './work-semantic-operations';
20
+ import { callRhWorkListProjection } from './work-list-projection';
63
21
  import {
64
- assertFacadeControllerRoundAuthority,
65
- assertSessionlessFacadeControllerAuthority,
66
- authenticatedFacadeControllerIdentity,
67
- bindFacadeControllerOwnership,
68
- currentFacadeTerminalizationAuthority,
69
- currentTerminalCleanupAuthority,
70
- dispatchedChatgptRelayAuthorizesStaleControllerRecovery,
71
- runtimeIdentitySnapshot,
72
- sessionlessFacadeControllerAuthorityMatches,
73
- } from './controller-authority-adapter';
74
- // Bounded internal compatibility export while remaining runtime-gateway callers migrate to the dedicated owner.
22
+ callRhWorkPlanCreateOperation,
23
+ callRhWorkPlanCreateWithoutRepository,
24
+ callRhWorkPlanOperation,
25
+ } from './work-plan-operations';
26
+ import { callRhWorkWorkflowOperation } from './work-workflow-operations';
27
+ import { callRhWorkLearningOperation } from './work-learning-operations';
28
+ import { callRhWorkControllerOperation } from './work-controller-operations';
29
+ import { runFacadeRepair } from './work-repair-adapter';
30
+ import { isAutomationReceiptCompatibilityCall } from './automation-receipt-adapter';
31
+ import { authenticatedFacadeControllerIdentity } from './controller-authority-adapter';
32
+
33
+ export { runFacadeRepair };
75
34
  export { runtimeIdentitySnapshot } from './controller-authority-adapter';
76
35
 
77
- export const RH_WORK_VERIFY_LEASE_WAIT_MS = DEFAULT_WORK_CHECK_LEASE_WAIT_MS;
78
-
79
- const CONTROLLER_PROVIDER_RECOVERY_CAPABILITY_PREFIX = 'controller.provider.recover:';
80
- const CONTROLLER_PROVIDER_RECOVERY_AUDIT_ACTION: RecoveryActionDescriptor = {
81
- id: 'recovery.controller_provider_probe',
82
- title: 'Verify Controller provider recovery',
83
- description: 'Verify the current ChatGPT provider with a non-semantic probe before rearming one exact blocked ControllerRound.',
84
- class: 'unknown',
85
- risk: 'medium',
86
- confirmation: 'authorization',
87
- localOnly: false,
88
- boundedTo: ['controller_round', 'chatgpt_provider'],
89
- };
90
-
91
- export interface ControllerProviderRecoveryInput {
92
- controllerHome: string;
93
- repoId: string;
94
- repoRoot: string;
95
- workId: string;
96
- relayScopeId: string;
97
- authorityId: string;
98
- timeoutMs?: number;
99
- now?: () => string;
100
- probe?: typeof runStandaloneChatgptPrompt;
101
- }
102
-
103
- export async function recoverControllerRoundAfterVerifiedProviderRepair(input: ControllerProviderRecoveryInput) {
104
- const store = { controllerHome: input.controllerHome, repoId: input.repoId };
105
- const workId = input.workId.trim();
106
- const relayScopeId = input.relayScopeId.trim();
107
- const authorityId = input.authorityId.trim();
108
- const blocked = getControllerRoundRelay(store, workId);
109
- if (!blocked) throw new Error(`CONTROLLER_PROVIDER_RECOVERY_RELAY_REQUIRED: ${workId}`);
110
- if (blocked.relayScopeId !== relayScopeId) throw new Error(`CONTROLLER_PROVIDER_RECOVERY_SCOPE_MISMATCH: ${workId}`);
111
- if ((blocked.authorityId?.trim() || '') !== authorityId) throw new Error(`CONTROLLER_PROVIDER_RECOVERY_AUTHORITY_MISMATCH: ${workId}`);
112
- if (controllerRoundBlockerClass(blocked) !== 'consecutive_failures') throw new Error(`CONTROLLER_PROVIDER_RECOVERY_BLOCKER_MISMATCH: ${workId}`);
113
- if (getControllerSession(store, workId)) throw new Error(`CONTROLLER_PROVIDER_RECOVERY_ACTIVE_CLAIM: ${workId}`);
114
-
115
- const probe = input.probe ?? runStandaloneChatgptPrompt;
116
- const nonce = randomUUID();
117
- const result = await probe({
118
- controllerHome: input.controllerHome,
119
- repoId: input.repoId,
120
- repoRoot: input.repoRoot,
121
- scopeId: `controller-provider-recovery:${workId}:${authorityId}`,
122
- prompt: `Forge provider recovery probe ${nonce}. Reply with ACK only. Do not invoke tools or modify external state.`,
123
- tabPolicy: 'new',
124
- timeoutMs: input.timeoutMs ?? 60_000,
125
- });
126
- if (result.status !== 'dispatched' || result.providerDeliveryStatus !== 'dispatch_confirmed') {
127
- const code = result.error?.code ?? `CHATGPT_PROVIDER_${result.providerDeliveryStatus?.toUpperCase() ?? 'PROBE_FAILED'}`;
128
- throw new Error(`CONTROLLER_PROVIDER_RECOVERY_PROBE_FAILED: ${code}: ${result.error?.message ?? result.status}`);
129
- }
130
-
131
- const verifiedAt = input.now?.() ?? new Date().toISOString();
132
- if (Date.parse(verifiedAt) <= Date.parse(blocked.updatedAt)) {
133
- throw new Error(`CONTROLLER_PROVIDER_RECOVERY_EVIDENCE_NOT_FRESH: ${workId}`);
134
- }
135
- const audit = writeRecoveryAuditRecord(
136
- input.controllerHome,
137
- input.repoId,
138
- buildRecoveryAuditRecord({
139
- actor: 'rh_work.controller_provider_recovery',
140
- action: CONTROLLER_PROVIDER_RECOVERY_AUDIT_ACTION,
141
- result: 'succeeded',
142
- reason: `Verified provider recovery for exact ControllerRound ${workId}.`,
143
- evidence: [{
144
- source: 'chatgpt_provider_recovery_probe',
145
- message: 'ChatGPT provider dispatch was confirmed by a non-semantic recovery probe.',
146
- at: verifiedAt,
147
- details: {
148
- workId,
149
- relayScopeId,
150
- controllerAuthorityId: authorityId,
151
- blockedUpdatedAt: blocked.updatedAt,
152
- provider: result.provider,
153
- providerDeliveryStatus: result.providerDeliveryStatus,
154
- browserSessionId: result.browserSessionId,
155
- executionPreferenceVerified: result.executionPreferenceVerified,
156
- },
157
- }],
158
- at: verifiedAt,
159
- }),
160
- );
161
- const relay = rearmControllerRoundAfterProviderRecovery(store, {
162
- workId,
163
- relayScopeId,
164
- authorityId,
165
- expectedUpdatedAt: blocked.updatedAt,
166
- evidenceId: audit.id,
167
- });
168
- return { relay, audit, probe: result };
169
- }
170
-
171
36
  export function contextRecord(value: unknown): Record<string, unknown> {
172
37
  return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
173
38
  }
@@ -177,3010 +42,284 @@ export function contextText(value: unknown, maxChars: number): string | undefine
177
42
  return value.length <= maxChars ? value : `${value.slice(0, Math.max(0, maxChars - 3))}...`;
178
43
  }
179
44
 
180
- export function ensureFacadeWorkHandle(
181
- ctx: MultiRepositoryMcpToolContext,
182
- repository: ReturnType<typeof selected>,
183
- workId: string,
184
- args: Record<string, unknown>,
185
- ): WorkHandleState | undefined {
186
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
187
- return ensureRepositoryWorkHandle({
188
- controllerHome: ctx.controllerHome,
189
- repository,
190
- workId,
191
- identity: { sessionId: identity.sessionId, principalId: identity.principalId },
192
- });
193
- }
194
-
195
- export function materializeFacadeWorkPlacement(
196
- ctx: MultiRepositoryMcpToolContext,
197
- repository: ReturnType<typeof selected>,
198
- workId: string,
199
- args: Record<string, unknown>,
200
- ) {
201
- return materializeRepositoryWorkPlacement(
202
- { controllerHome: ctx.controllerHome, repoId: repository.repoId },
203
- workId,
204
- (contract) => {
205
- const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
206
- requestId: workId,
207
- title: contract.objective,
208
- baseRef: contract.baseRevision,
209
- prepareDependencies: args.needs_dependencies === true,
210
- });
211
- if (workspace.checkoutId === repository.activeCheckoutId) {
212
- throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
213
- }
214
- return workspace;
215
- },
216
- );
217
- }
218
-
219
- export function claimNewFacadeWork(
220
- ctx: MultiRepositoryMcpToolContext,
221
- repository: ReturnType<typeof selected>,
222
- workId: string,
223
- args: Record<string, unknown>,
224
- ) {
225
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
226
- const authority = mintControllerSessionAuthority();
227
- const session = resumeControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
228
- workId,
229
- controllerId: identity.controllerId,
230
- controllerType: identity.controllerType,
231
- sessionId: identity.sessionId,
232
- authorityDigest: authority.authorityDigest,
233
- principalId: identity.principalId,
234
- controllerInstanceId: identity.controllerInstanceId,
235
- leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
236
- });
237
- return { session, controllerAuthorityId: authority.authorityId };
45
+ function boundedStringList(value: unknown, limit: number): string[] {
46
+ return Array.isArray(value)
47
+ ? value.map(String).map((entry) => entry.trim()).filter(Boolean).slice(0, limit)
48
+ : [];
238
49
  }
239
50
 
240
- export function bindFacadeExecutionSession(
241
- ctx: MultiRepositoryMcpToolContext,
242
- repository: ReturnType<typeof selected>,
243
- handle: WorkHandleState,
244
- args: Record<string, unknown>,
245
- ) {
246
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
247
- const session = startExecutionSession(ctx.controllerHome, {
248
- sessionId: identity.sessionId,
249
- principalId: identity.principalId,
250
- controllerInstanceId: identity.controllerInstanceId,
251
- permissionSnapshotVersion: handle.permissionSnapshotVersion,
252
- });
253
- return updateExecutionSession(ctx.controllerHome, {
254
- sessionId: session.sessionId,
255
- principalId: session.principalId,
256
- controllerInstanceId: session.controllerInstanceId,
257
- }, {
258
- activeRepositoryId: repository.repoId,
259
- activeCheckoutId: handle.checkoutId,
260
- activeWorkId: handle.workId,
261
- permissionSnapshotVersion: handle.permissionSnapshotVersion,
262
- lastValidatedAt: new Date().toISOString(),
263
- });
51
+ function repositoryBoundStartRequested(args: Record<string, unknown>): boolean {
52
+ return typeof args.repo_id === 'string' && args.repo_id.trim().length > 0;
264
53
  }
265
54
 
266
- export async function finalizeFacadeWorkHandle(
55
+ async function callRepositoryBoundStart(
267
56
  ctx: MultiRepositoryMcpToolContext,
268
57
  repository: ReturnType<typeof selected>,
269
58
  args: Record<string, unknown>,
270
- operation: 'finalize' | 'stop',
271
- ): Promise<CallToolResult | undefined> {
272
- const workId = String(args.work_id ?? '').trim();
273
- if (!workId) return undefined;
274
- let handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId)
275
- ?? recoverTerminalWorkHandle(ctx.controllerHome, repository.repoId, workId);
276
- if (!handle) return undefined;
277
- const session = bindFacadeExecutionSession(ctx, repository, handle, args);
278
- const explicitTargetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
279
- ? args.target_branch.trim()
280
- : undefined;
281
- const targetBranch = resolveWorkDeliveryTargetBranch(handle, repository.defaultBranch, explicitTargetBranch);
282
- const common = {
283
- session_id: session.sessionId,
284
- repo_id: repository.repoId,
285
- work_id: workId,
286
- target_branch: targetBranch,
287
- delete_branch: args.delete_branch !== false,
288
- cleanup: args.cleanup !== false,
289
- };
290
-
291
- if (operation === 'stop') {
292
- return callExecutionTool(ctx, 'work_finalize', {
293
- ...common,
294
- commit: false,
295
- merge: false,
296
- });
59
+ ): Promise<CallToolResult> {
60
+ const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
61
+ const workId = semanticWorkId(store, args);
62
+ const semanticStart = await callRhWorkSemanticOperation(store, 'start', { ...args, work_id: workId });
63
+ if (!semanticStart) {
64
+ return result(buildFacadeResult({
65
+ status: 'blocked',
66
+ summary: `WORK_REPOSITORY_ADMISSION_SEMANTIC_START_UNAVAILABLE: ${workId}`,
67
+ data: { workId, repoId: repository.repoId },
68
+ }) as unknown as Record<string, unknown>, true);
69
+ }
70
+ if (semanticStart.isError) return semanticStart;
71
+ let contract = getWorkContract(store, workId);
72
+ if (!contract) {
73
+ return result(buildFacadeResult({
74
+ status: 'blocked',
75
+ summary: `WORK_REPOSITORY_ADMISSION_SEMANTIC_CONTEXT_MISSING: ${workId}`,
76
+ data: { workId, repoId: repository.repoId },
77
+ }) as unknown as Record<string, unknown>, true);
297
78
  }
298
79
 
299
- const handleCheckoutId = handle.checkoutId;
300
- const registeredCheckout = repository.checkouts.find((candidate) => candidate.checkoutId === handleCheckoutId);
301
- const registeredLifecycle = registeredCheckout ? repositoryCheckoutLifecycle(registeredCheckout) : undefined;
302
- const checkoutUnavailable = !registeredCheckout || (registeredLifecycle !== 'active' && registeredLifecycle !== 'archived');
303
- if (checkoutUnavailable) {
304
- const exactChangedHead = Boolean(handle.managedWorktree
305
- && handle.expectedHead
306
- && handle.baseCommit
307
- && handle.expectedHead !== handle.baseCommit);
308
- const retainedNoChangeRecovery = args.completion_outcome === 'completed_no_change'
309
- && handle.managedWorktree
310
- && handle.finalization.validation === 'done'
311
- && handle.finalization.worktreeCleanup === 'done'
312
- && Boolean(handle.validatedInputFingerprint)
313
- && (handle.state === 'merged' || handle.state === 'cleaned');
314
- if ((!exactChangedHead && !retainedNoChangeRecovery) || args.cleanup === false) {
315
- throw new Error(`WORK_FINALIZATION_CHECKOUT_UNAVAILABLE: ${repository.repoId}/${handle.checkoutId ?? 'unknown'} is ${registeredLifecycle ?? 'unregistered'}`);
80
+ try {
81
+ const rawConstraints = contextRecord(args.constraints);
82
+ // Placement is caller-owned. Legacy `auto` is a stable mechanical spelling
83
+ // of the durable-Work isolated/frozen-base default; it never inspects dirty
84
+ // paths, risk, task text, or other semantic context to choose topology.
85
+ const requestedWorkspaceMode: 'current' | 'isolated' = rawConstraints.workspace_mode === 'isolated' || rawConstraints.workspace_mode === 'auto'
86
+ ? 'isolated'
87
+ : 'current';
88
+ const sourceStatus = repositoryGitStatus(repository);
89
+ const requestedSourceRevision = typeof args.source_revision === 'string' ? args.source_revision.trim() : '';
90
+ if (requestedSourceRevision && sourceStatus.head && requestedWorkspaceMode !== 'isolated' && requestedSourceRevision !== sourceStatus.head) {
91
+ throw new Error(`WORK_SOURCE_REVISION_MISMATCH: expected ${requestedSourceRevision}, found ${sourceStatus.head}`);
316
92
  }
317
- if (retainedNoChangeRecovery) {
318
- const noChangeEvidence = typeof args.no_change_evidence === 'string' && args.no_change_evidence.trim()
319
- ? args.no_change_evidence.trim()
320
- : `Retained exact validation proves Work ${workId} has no repository delta after managed-worktree cleanup.`;
321
- return callExecutionTool(ctx, 'work_finalize', {
322
- ...common,
323
- commit: false,
324
- merge: false,
325
- completion_outcome: 'completed_no_change',
326
- no_change_evidence: noChangeEvidence,
93
+ const requiresIsolatedWorkspace = requestedWorkspaceMode === 'isolated';
94
+ let checkoutId = repository.activeCheckoutId;
95
+ let worktreeRef: string | undefined;
96
+ let baseRevision = sourceStatus.head ?? undefined;
97
+ if (requiresIsolatedWorkspace) {
98
+ const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
99
+ requestId: workId,
100
+ title: String(args.objective ?? workId),
101
+ ...(requestedSourceRevision ? { baseRef: requestedSourceRevision } : {}),
102
+ prepareDependencies: args.needs_dependencies === true,
327
103
  });
104
+ if (!workspace.managed || !workspace.checkoutId || !workspace.root) throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
105
+ checkoutId = workspace.checkoutId;
106
+ worktreeRef = workspace.root;
107
+ baseRevision = workspace.baseRevision ?? baseRevision;
328
108
  }
329
- // Physical cleanup can succeed before the Work completion receipt is
330
- // persisted. Re-enter the canonical finalizer only after structural registry
331
- // evidence proves this exact checkout is no longer selectable. No human-
332
- // readable error message participates in lifecycle authority.
333
- return callExecutionTool(ctx, 'work_finalize', {
334
- ...common,
335
- commit: false,
336
- merge: false,
337
- completion_outcome: 'completed_changed',
109
+
110
+ const constraints = {
111
+ ...contract.constraints,
112
+ workspaceMode: requestedWorkspaceMode,
113
+ ...(rawConstraints.allow_commit === false ? { allowCommit: false } : rawConstraints.allow_commit === true ? { allowCommit: true } : {}),
114
+ ...(rawConstraints.allow_merge === false ? { allowMerge: false } : rawConstraints.allow_merge === true ? { allowMerge: true } : {}),
115
+ ...(rawConstraints.allow_cleanup === false ? { allowCleanup: false } : rawConstraints.allow_cleanup === true ? { allowCleanup: true } : {}),
116
+ };
117
+ contract = updateWorkContract(store, workId, {
118
+ checkoutId,
119
+ executionPlacement: { schemaVersion: 1, repositoryId: repository.repoId, checkoutId },
120
+ ...(baseRevision ? { baseRevision } : {}),
121
+ ...(worktreeRef ? { worktreeRef } : {}),
122
+ acceptanceCriteria: boundedStringList(args.acceptance_criteria, 20),
123
+ checks: boundedStringList(args.check_ids, 30),
124
+ constraints,
338
125
  });
339
- }
340
- const worktree: ReturnType<typeof selectRepositoryCheckout> = selectRepositoryCheckout(repository, handle.checkoutId, { allowArchived: true });
341
- const status = repositoryGitStatus(worktree);
342
- const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
343
- const committedDelta = Boolean(head && handle.baseCommit && head !== handle.baseCommit);
344
- const requestedCommit = typeof args.commit === 'boolean' ? args.commit : !status.clean;
345
- // A clean checkout whose HEAD already differs from base is already committed.
346
- // Treat commit=true as satisfied by that exact candidate instead of trying to
347
- // manufacture an empty commit and recording GIT_NOTHING_STAGED as a delivery
348
- // failure. The candidate is still adopted/revalidated and merge-fenced below.
349
- const commit = status.clean && committedDelta ? false : requestedCommit;
350
- if (
351
- status.clean
352
- && committedDelta
353
- && commit === false
354
- && head
355
- && handle.expectedHead
356
- && head !== handle.expectedHead
357
- && (handle.state === 'prepared' || handle.state === 'editing')
358
- ) {
359
- // A clean committed successor must enter through the single audited adoption
360
- // authority before finalization, even when allowedPaths/forbiddenPaths are
361
- // empty. Empty allowedPaths is repository-scoped authority; work_prepare still
362
- // fences ancestry, checkout, branch, ownership, cleanliness, and any declared
363
- // path restrictions before advancing expectedHead.
364
- const adopted = await callExecutionTool(ctx, 'work_prepare', {
365
- session_id: session.sessionId,
366
- repo_id: repository.repoId,
367
- checkout_id: handle.checkoutId,
368
- work_id: workId,
369
- expected_previous_head: handle.expectedHead,
370
- adopt_candidate_head: head,
126
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
127
+ const handle = ensureRepositoryWorkHandle({
128
+ controllerHome: ctx.controllerHome,
129
+ repository,
130
+ workId,
131
+ checkoutId,
132
+ identity: { sessionId: identity.sessionId, principalId: identity.principalId },
371
133
  });
372
- if (!adopted || adopted.isError === true) return adopted;
373
- handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId) ?? handle;
374
- }
375
- const requestedOutcome = args.completion_outcome === 'completed_no_change' || args.completion_outcome === 'completed_changed'
376
- ? args.completion_outcome
377
- : undefined;
378
- const completionOutcome = requestedOutcome ?? (!commit && !committedDelta && status.clean ? 'completed_no_change' : 'completed_changed');
379
- // A proven no-change completion has nothing to merge. Defaulting merge=true
380
- // forced the generic Git delivery path to do unnecessary work and conflicted
381
- // with work_finalize's explicit no-change contract.
382
- const merge = typeof args.merge === 'boolean' ? args.merge : completionOutcome !== 'completed_no_change';
383
- const explicitNoChangeEvidence = typeof args.no_change_evidence === 'string' ? args.no_change_evidence.trim() : '';
384
- const noChangeEvidence = completionOutcome === 'completed_no_change'
385
- ? explicitNoChangeEvidence || `Validated Work ${workId} has no repository delta from base ${handle.baseCommit ?? 'unknown'} at clean HEAD ${head ?? 'unknown'}.`
386
- : undefined;
387
- const finalizeArgs = {
388
- ...common,
389
- commit,
390
- merge,
391
- no_ff: args.no_ff === true,
392
- remote_write: args.remote_write === true,
393
- completion_outcome: completionOutcome,
394
- ...(noChangeEvidence ? { no_change_evidence: noChangeEvidence } : {}),
395
- };
396
-
397
- const validateExactWorkspace = async (): Promise<CallToolResult | undefined> => {
398
- const contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
399
- const validation = await callExecutionTool(ctx, 'work_validate', {
400
- session_id: session.sessionId,
401
- repo_id: repository.repoId,
402
- work_id: workId,
403
- check_ids: contract?.checks ?? [],
134
+ if (!handle) throw new Error(`WORK_REPOSITORY_ADMISSION_HANDLE_REQUIRED: ${workId}`);
135
+ const projectOnboarding = ensureRepositoryProjectOnboarding({
136
+ controllerHome: ctx.controllerHome,
137
+ repository,
138
+ sourceRevision: baseRevision,
404
139
  });
405
- if (!validation || validation.isError === true) return validation;
406
- const validationPayload = contextRecord(validation.structuredContent);
407
- return contextRecord(validationPayload.validation).passed === true ? undefined : validation;
408
- };
409
-
410
- let physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
411
- let payload = contextRecord(physical?.structuredContent);
412
- if (physical?.isError === true && contextRecord(payload.error).code === 'WORK_VALIDATION_REQUIRED') {
413
- const validationFailure = await validateExactWorkspace();
414
- if (validationFailure) return validationFailure;
415
- physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
416
- payload = contextRecord(physical?.structuredContent);
417
- }
418
- if (
419
- physical
420
- && physical.isError !== true
421
- && typeof payload.continuation === 'string'
422
- && payload.continuation.startsWith('WORK_COMMITTED_REVALIDATION_REQUIRED')
423
- ) {
424
- const validationFailure = await validateExactWorkspace();
425
- if (validationFailure) return validationFailure;
426
- physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
140
+ return result(buildFacadeResult({
141
+ summary: `Work ${workId} admitted to repository execution on checkout ${checkoutId}.`,
142
+ data: {
143
+ work: workSemanticView(contract),
144
+ deduplicated: Boolean(contextRecord(semanticStart.structuredContent).data && contextRecord(contextRecord(semanticStart.structuredContent).data).deduplicated),
145
+ projectOnboarding,
146
+ executionHandle: { workId: handle.workId, checkoutId: handle.checkoutId, managedWorktree: handle.managedWorktree, state: handle.state },
147
+ },
148
+ }) as unknown as Record<string, unknown>);
149
+ } catch (error) {
150
+ return result(buildFacadeResult({
151
+ status: 'blocked',
152
+ summary: `WORK_REPOSITORY_ADMISSION_FAILED: ${error instanceof Error ? error.message : String(error)}`,
153
+ data: { workId, repoId: repository.repoId, canonicalWorkRetained: true },
154
+ }) as unknown as Record<string, unknown>, true);
427
155
  }
428
- return physical;
429
156
  }
430
157
 
431
- export function planObligationDispositionsFromArgs(value: unknown) {
432
- if (!Array.isArray(value)) return undefined;
433
- return value
434
- .filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry))
435
- .map((entry) => ({
436
- predecessorPlanId: String(entry.predecessor_plan_id ?? ''),
437
- obligationId: String(entry.obligation_id ?? ''),
438
- disposition: String(entry.disposition ?? '') as 'keep' | 'change' | 'defer' | 'drop',
439
- successorRefs: Array.isArray(entry.successor_refs) ? entry.successor_refs.map(String) : [],
440
- rationale: typeof entry.rationale === 'string' ? entry.rationale : undefined,
441
- }));
158
+ function semanticStableSpec(operation: string) {
159
+ if (operation === 'get' || operation === 'revise' || operation === 'complete') {
160
+ return { namespace: 'work_contract', idField: 'work_id', kind: 'work' as const };
161
+ }
162
+ if (operation === 'plan_get' || operation === 'plan_revise') {
163
+ return { namespace: 'plan_contract', idField: 'plan_id', kind: 'plan' as const };
164
+ }
165
+ return undefined;
442
166
  }
443
167
 
444
- export async function runFacadeRepair(
168
+ async function routeStableSemanticOperation(
445
169
  ctx: MultiRepositoryMcpToolContext,
446
- repository: ReturnType<typeof selected>,
170
+ operation: string,
447
171
  args: Record<string, unknown>,
448
- ): Promise<CallToolResult> {
449
- const store = {
450
- controllerHome: ctx.controllerHome,
451
- repoId: repository.repoId,
452
- revisionContains: (ancestorRevision: string, descendantRevision: string) =>
453
- repositoryRevisionContains(repository.canonicalRoot, ancestorRevision, descendantRevision),
454
- };
455
- let maintenanceSnapshot: ReturnType<typeof buildRuntimeMaintenanceStatus> | undefined;
456
- let maintenanceStatus: {
457
- readyForExecution?: boolean;
458
- recommendedActions?: string[];
459
- candidates?: Array<{ kind?: string; reason?: string; suggestedAction?: string; safe?: boolean }>;
460
- warnings?: string[];
461
- } | undefined;
462
- try {
463
- const status = buildRuntimeMaintenanceStatus(repository, ctx.controllerHome, {
464
- minAgeMinutes: typeof args.min_age_minutes === 'number' ? args.min_age_minutes : undefined,
465
- maxCandidates: typeof args.max_candidates === 'number' ? args.max_candidates : 20,
466
- });
467
- maintenanceSnapshot = status;
468
- maintenanceStatus = {
469
- readyForExecution: status.readyForExecution,
470
- recommendedActions: status.recommendedActions,
471
- candidates: status.candidates.map((candidate) => ({
472
- kind: candidate.kind,
473
- reason: candidate.reason,
474
- suggestedAction: candidate.suggestedAction,
475
- safe: candidate.safe,
476
- })),
477
- warnings: status.warnings,
478
- };
479
- } catch {
480
- maintenanceStatus = {
481
- readyForExecution: false,
482
- recommendedActions: [],
483
- candidates: [],
484
- warnings: ['runtime_maintenance_status inspection failed; treating as infrastructure issue, not acceptance failure'],
485
- };
172
+ ): Promise<CallToolResult | undefined> {
173
+ const spec = semanticStableSpec(operation);
174
+ if (!spec) return undefined;
175
+
176
+ const id = String(args[spec.idField] ?? '').trim();
177
+ if (!id) {
178
+ return result(buildFacadeResult({
179
+ status: 'not_found',
180
+ summary: `${spec.kind === 'work' ? 'Work' : 'Plan'} stable id is required.`,
181
+ data: {},
182
+ }) as unknown as Record<string, unknown>, true);
486
183
  }
487
184
 
488
- const repairOperation = args.repair_operation === 'repair' || args.repair_operation === 'verify' || args.repair_operation === 'handoff'
489
- ? args.repair_operation
490
- : 'diagnose';
491
- const dryRun = args.dry_run === undefined ? true : args.dry_run === true;
492
- const elevatedRepair = args.destructive === true || args.remote_write === true || args.remote_effect === true;
493
- const planId = typeof args.plan_id === 'string' ? args.plan_id.trim() : '';
494
- const planStepId = typeof args.plan_step_id === 'string' ? args.plan_step_id.trim() : '';
495
- const exactWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
496
-
497
- if (planId && !planStepId) {
498
- const plan = getPlanContract(store, planId);
499
- if (!plan) {
500
- const facade = buildFacadeResult({ status: 'not_found', summary: `PlanContract ${planId} not found.`, data: { operation: repairOperation, dryRun, planId, repaired: false } });
501
- return result(facade as unknown as Record<string, unknown>, true);
502
- }
503
- const pendingRevision = plan.status === 'replanning' ? plan.pendingRevision : undefined;
504
- if (plan.status !== 'draft' && !pendingRevision) {
505
- const facade = buildFacadeResult({ status: 'blocked', summary: `PLAN_DRAFT_REPAIR_STATUS_INVALID: ${plan.planId}:${plan.status}`, data: { operation: repairOperation, dryRun, planId, repaired: false } });
506
- return result(facade as unknown as Record<string, unknown>, true);
507
- }
508
- const repairBase = pendingRevision ?? plan;
509
- if (repairOperation !== 'repair' || dryRun) {
510
- const facade = buildFacadeResult({
511
- summary: pendingRevision
512
- ? `PlanContract ${plan.planId} has staged revision r${pendingRevision.revision}. Exact in-place revision repair is available; stable Plan identity and committed revision remain authoritative until approval.`
513
- : `PlanContract ${plan.planId} is a draft. Exact in-place repair is available; the Plan identity and Requirement authority are preserved and only a fully valid draft may be persisted.`,
514
- data: { operation: repairOperation, dryRun, plan: summarizePlanContract(plan), repaired: false, repairRequired: true },
515
- suggestedNextActions: [{ label: 'Repair this exact draft Plan', tool: 'rh_work', operation: 'repair', payload: { plan_id: plan.planId, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
516
- });
517
- return result(facade as unknown as Record<string, unknown>);
185
+ const explicitRepoId = typeof args.repo_id === 'string' && args.repo_id.trim()
186
+ ? args.repo_id.trim()
187
+ : undefined;
188
+ let targetScope: string;
189
+ if (explicitRepoId) {
190
+ const exact = readControlPlaneRecord<unknown>(ctx.controllerHome, spec.namespace, explicitRepoId, id);
191
+ if (!exact) {
192
+ return result(buildFacadeResult({
193
+ status: 'not_found',
194
+ summary: `${spec.kind === 'work' ? 'Work' : 'Plan'} ${id} not found in repository ${explicitRepoId}.`,
195
+ data: spec.kind === 'work' ? { workId: id, repoId: explicitRepoId } : { planId: id, repoId: explicitRepoId },
196
+ }) as unknown as Record<string, unknown>, true);
518
197
  }
519
- const rawSteps = Array.isArray(args.plan_steps)
520
- ? args.plan_steps
521
- : repairBase.steps.map((step) => ({
522
- id: step.id, objective: step.objective, dependencies: step.dependencies, authoritative_files: step.authoritativeFiles,
523
- allowed_paths: step.allowedPaths, forbidden_paths: step.forbiddenPaths, check_ids: step.checks, acceptance_criteria: step.acceptanceCriteria,
524
- }));
525
- const steps = rawSteps
526
- .filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step))
527
- .map((step) => ({
528
- id: String(step.id ?? ''),
529
- objective: String(step.objective ?? ''),
530
- dependencies: Array.isArray(step.dependencies) ? step.dependencies.map(String) : [],
531
- authoritativeFiles: Array.isArray(step.authoritative_files) ? step.authoritative_files.map(String) : [],
532
- allowedPaths: Array.isArray(step.allowed_paths) ? step.allowed_paths.map(String) : [],
533
- forbiddenPaths: Array.isArray(step.forbidden_paths) ? step.forbidden_paths.map(String) : [],
534
- checks: Array.isArray(step.check_ids) ? step.check_ids.map(String) : [],
535
- acceptanceCriteria: Array.isArray(step.acceptance_criteria) ? step.acceptance_criteria.map(String) : [],
536
- }));
537
- const availableChecks = listControllerChecks(repository.canonicalRoot);
538
- const normalizedPlanChecks = normalizeCheckIds(steps.flatMap((step) => step.checks), availableChecks);
539
- if (normalizedPlanChecks.invalidCheckIds.length > 0) {
540
- const facade = buildFacadeResult({
541
- status: 'failed',
542
- summary: `PLAN_CHECKS_INVALID: ${normalizedPlanChecks.invalidCheckIds.join(', ')}. Draft repair was not persisted.`,
543
- data: { operation: repairOperation, dryRun: false, planId, repaired: false, normalizedChecks: normalizedPlanChecks, registeredCheckIds: availableChecks.map((check) => check.id).slice(0, 80) },
544
- suggestedNextActions: [],
545
- });
546
- return result(facade as unknown as Record<string, unknown>, true);
198
+ targetScope = explicitRepoId;
199
+ } else {
200
+ const canonicalSemantic = readControlPlaneRecord<unknown>(
201
+ ctx.controllerHome,
202
+ spec.namespace,
203
+ SEMANTIC_SCOPE_KEY,
204
+ id,
205
+ );
206
+ const matches = canonicalSemantic
207
+ ? [{ ...canonicalSemantic, scope: SEMANTIC_SCOPE_KEY }]
208
+ : findControlPlaneRecordsByKey<unknown>(ctx.controllerHome, {
209
+ namespace: spec.namespace,
210
+ key: id,
211
+ limit: 2,
212
+ });
213
+ if (matches.length === 0) {
214
+ return result(buildFacadeResult({
215
+ status: 'not_found',
216
+ summary: `${spec.kind === 'work' ? 'Work' : 'Plan'} ${id} not found.`,
217
+ data: spec.kind === 'work' ? { workId: id } : { planId: id },
218
+ }) as unknown as Record<string, unknown>, true);
547
219
  }
548
- try {
549
- const repaired = await repairDraftPlanContractAsync(store, planId, {
550
- expectedSourceRevision: repairBase.sourceRevision,
551
- scopeKey: typeof args.scope_key === 'string' ? args.scope_key : plan.scopeKey,
552
- sourceRevision: typeof args.source_revision === 'string' ? args.source_revision : repairBase.sourceRevision,
553
- goal: typeof args.objective === 'string' ? args.objective : repairBase.goal,
554
- nonGoals: Array.isArray(args.non_goals) ? args.non_goals.map(String) : repairBase.nonGoals,
555
- assumptions: Array.isArray(args.assumptions) ? args.assumptions.map(String) : repairBase.assumptions,
556
- resolvedDecisions: Array.isArray(args.resolved_decisions) ? args.resolved_decisions.map(String) : repairBase.resolvedDecisions,
557
- stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : repairBase.stopConditions,
558
- replanConditions: Array.isArray(args.replan_conditions) ? args.replan_conditions.map(String) : repairBase.replanConditions,
559
- integrationStrategy: typeof args.integration_strategy === 'string' ? args.integration_strategy : repairBase.integrationStrategy,
560
- obligationDispositions: planObligationDispositionsFromArgs(args.obligation_dispositions) ?? repairBase.obligationDispositions,
561
- steps,
562
- });
563
- const facade = buildFacadeResult({
564
- summary: pendingRevision ? `PlanContract ${repaired.planId} staged revision repaired in place; stable Plan identity and committed authority were preserved.` : `PlanContract ${repaired.planId} draft repaired in place; identity and Requirement authority were preserved.`,
565
- data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(repaired), repaired: true, replacementPlanCreated: false },
566
- suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: repaired.planId }, risk: 'workspace_write', confidence: 'medium' }],
567
- });
568
- return result(facade as unknown as Record<string, unknown>);
569
- } catch (error) {
570
- const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_DRAFT_REPAIR_FAILED', data: { operation: repairOperation, dryRun: false, planId, repaired: false } });
571
- return result(facade as unknown as Record<string, unknown>, true);
220
+ if (matches.length > 1) {
221
+ return result(buildFacadeResult({
222
+ status: 'blocked',
223
+ summary: `SEMANTIC_ID_SCOPE_AMBIGUOUS: ${id} resolves to ${matches.length} scopes.`,
224
+ data: { id, scopes: matches.map((record) => record.scope).sort() },
225
+ }) as unknown as Record<string, unknown>, true);
572
226
  }
227
+ targetScope = matches[0]!.scope;
573
228
  }
574
229
 
575
- if (planStepId && !planId) {
576
- const facade = buildFacadeResult({ status: 'blocked', summary: 'PLAN_STEP_REPAIR_CONTEXT_REQUIRED: plan_id and plan_step_id are both required.', data: { operation: repairOperation, dryRun, repaired: false } });
577
- return result(facade as unknown as Record<string, unknown>, true);
578
- }
230
+ const semanticStore = { controllerHome: ctx.controllerHome, scopeKey: targetScope };
231
+ return spec.kind === 'work'
232
+ ? callRhWorkSemanticOperation(semanticStore, operation, args)
233
+ : callRhWorkPlanOperation(semanticStore, operation, args);
234
+ }
579
235
 
580
- if (planId && planStepId) {
581
- const plan = getPlanContract(store, planId);
582
- const step = plan?.steps.find((candidate) => candidate.id === planStepId);
583
- if (!plan || !step) {
584
- const facade = buildFacadeResult({ status: 'not_found', summary: !plan ? `PlanContract ${planId} not found.` : `PLAN_STEP_NOT_FOUND: ${planStepId}`, data: { operation: repairOperation, dryRun, planId, planStepId, repaired: false } });
585
- return result(facade as unknown as Record<string, unknown>, true);
586
- }
587
- if (!step.workId) {
588
- const facade = buildFacadeResult({ summary: `Plan step ${planId}/${planStepId} has no Work binding to repair.`, data: { operation: repairOperation, dryRun, planId, planStepId, repaired: false, repairRequired: false } });
589
- return result(facade as unknown as Record<string, unknown>);
590
- }
591
- const boundWork = getWorkContract(store, step.workId);
592
- if (boundWork) {
593
- if (['completed', 'failed', 'cancelled'].includes(boundWork.status)) {
594
- if (repairOperation !== 'repair' || dryRun) {
595
- const facade = buildFacadeResult({
596
- summary: `PLAN_STEP_TERMINAL_WORK_RECONCILIABLE: ${planId}/${planStepId} is bound to existing terminal Work ${boundWork.workId}. Explicit repair can project that exact Work without creating a replacement.`,
597
- data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, repairRequired: true, reusedExistingWork: true },
598
- suggestedNextActions: [{ label: 'Project existing terminal Work', tool: 'rh_work', operation: 'repair', payload: { plan_id: planId, plan_step_id: planStepId, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
599
- });
600
- return result(facade as unknown as Record<string, unknown>);
601
- }
602
- try {
603
- const reconciledPlan = completePlanStepForWork(store, { planId, stepId: planStepId, work: boundWork });
604
- const facade = buildFacadeResult({
605
- summary: `Reconciled Plan step ${planId}/${planStepId} from its existing terminal Work ${boundWork.workId}; no replacement Work was created.`,
606
- data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(reconciledPlan), boundWorkId: boundWork.workId, repaired: true, reusedExistingWork: true },
607
- });
608
- return result(facade as unknown as Record<string, unknown>);
609
- } catch (error) {
610
- const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_STEP_TERMINAL_WORK_RECONCILIATION_FAILED', data: { operation: repairOperation, dryRun: false, planId, planStepId, boundWorkId: boundWork.workId, repaired: false } });
611
- return result(facade as unknown as Record<string, unknown>, true);
612
- }
613
- }
614
- const requestedRevisionLabel = typeof args.superseded_by === 'string' ? args.superseded_by.trim() : '';
615
- const requestedAllowedPaths = Array.isArray(args.allowed_paths)
616
- ? [...new Set([...step.allowedPaths, ...args.allowed_paths.map(String).map((value) => value.trim()).filter(Boolean)])]
617
- : step.allowedPaths;
618
- const scopeReplanRequested = Boolean(requestedRevisionLabel) || requestedAllowedPaths.length > step.allowedPaths.length;
619
- if (scopeReplanRequested) {
620
- const requestedSourceRevision = typeof args.source_revision === 'string' ? args.source_revision.trim() : '';
621
- if (!requestedRevisionLabel || !requestedSourceRevision || requestedAllowedPaths.length === step.allowedPaths.length) {
622
- const facade = buildFacadeResult({
623
- status: 'blocked',
624
- summary: 'PLAN_WORK_SCOPE_REPLAN_INPUT_REQUIRED: superseded_by, source_revision, and at least one new allowed_paths entry are required for an active Plan-bound Work scope replan.',
625
- data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, requestedRevisionLabel: requestedRevisionLabel || undefined, requestedSourceRevision: requestedSourceRevision || undefined, requestedAllowedPaths },
626
- });
627
- return result(facade as unknown as Record<string, unknown>, true);
628
- }
629
- if (repairOperation !== 'repair' || dryRun) {
630
- const facade = buildFacadeResult({
631
- summary: `PLAN_WORK_SCOPE_REPLAN_AVAILABLE: ${planId}/${planStepId} can atomically move exact Work ${boundWork.workId} to stable Plan ${planId} revision label ${requestedRevisionLabel} while widening only allowed-path authority.`,
632
- data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, requestedRevisionLabel, requestedSourceRevision, requestedAllowedPaths, repaired: false, repairRequired: true, reusedExistingWork: true },
633
- suggestedNextActions: [{ label: 'Replan exact active Work scope', tool: 'rh_work', operation: 'repair', payload: { plan_id: planId, plan_step_id: planStepId, superseded_by: requestedRevisionLabel, source_revision: requestedSourceRevision, allowed_paths: requestedAllowedPaths, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
634
- });
635
- return result(facade as unknown as Record<string, unknown>);
636
- }
637
- try {
638
- const replanned = replanActivePlanBoundWorkScope(store, {
639
- planId,
640
- stepId: planStepId,
641
- workId: boundWork.workId,
642
- requestedRevisionLabel,
643
- sourceRevision: requestedSourceRevision,
644
- allowedPaths: requestedAllowedPaths,
645
- reason: typeof args.reason === 'string' && args.reason.trim()
646
- ? args.reason.trim()
647
- : 'Explicit Controller repair widened a frozen Plan path fence after current-source evidence proved the existing Plan contract omitted a path required by its own acceptance scope.',
648
- });
649
- const facade = buildFacadeResult({
650
- summary: `Replanned ${planId}/${planStepId} as ${replanned.currentPlan.planId} r${replanned.currentPlan.revision ?? 1} and retained the same active Work ${replanned.work.workId} atomically; semantic acceptance and checks were not widened.`,
651
- data: { operation: repairOperation, dryRun: false, priorPlan: summarizePlanContract(replanned.priorPlan), currentPlan: summarizePlanContract(replanned.currentPlan), work: summarizeWorkContract(replanned.work), repaired: true, replacementWorkCreated: false, reusedExistingWork: true },
652
- });
653
- return result(facade as unknown as Record<string, unknown>);
654
- } catch (error) {
655
- const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_WORK_SCOPE_REPLAN_FAILED', data: { operation: repairOperation, dryRun: false, planId, planStepId, boundWorkId: boundWork.workId, requestedRevisionLabel, repaired: false } });
656
- return result(facade as unknown as Record<string, unknown>, true);
657
- }
658
- }
659
- const facade = buildFacadeResult({
660
- status: 'blocked',
661
- summary: `PLAN_STEP_BOUND_WORK_STILL_EXISTS: ${planId}/${planStepId} is bound to active Work ${boundWork.workId}; continue that exact Work, or explicitly request a scope-only stable Plan revision instead of replacing the Work.`,
662
- data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, repairRequired: false },
663
- suggestedNextActions: [{ label: 'Continue existing Work', tool: 'rh_work', operation: 'continue', payload: { work_id: boundWork.workId }, risk: 'readonly', confidence: 'high' }],
664
- });
665
- return result(facade as unknown as Record<string, unknown>, true);
666
- }
667
- const conflicting = listWorkContracts({ ...store, status: 'active', limit: 200 })
668
- .filter((candidate) => candidate.planId === planId && candidate.planStepId === planStepId && candidate.workId !== step.workId);
669
- if (conflicting.length > 0) {
670
- const facade = buildFacadeResult({
671
- status: 'blocked',
672
- summary: `PLAN_STEP_REPAIR_CONFLICT: ${planId}/${planStepId} is bound to missing Work ${step.workId}, but ${conflicting.length} other active Work record(s) claim the same step. Resolve the conflicting authority before changing the Plan binding.`,
673
- data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: step.workId, conflictingWorkIds: conflicting.map((candidate) => candidate.workId), repaired: false },
674
- });
675
- return result(facade as unknown as Record<string, unknown>, true);
676
- }
677
- if (repairOperation !== 'repair' || dryRun) {
678
- const facade = buildFacadeResult({
679
- summary: `PLAN_STEP_DANGLING_WORK_BINDING: ${planId}/${planStepId} points to missing Work ${step.workId}. Exact repair is available and will clear only this unchanged ghost binding.`,
680
- data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: step.workId, repaired: false, repairRequired: true },
681
- suggestedNextActions: [{ label: 'Repair exact dangling binding', tool: 'rh_work', operation: 'repair', payload: { plan_id: planId, plan_step_id: planStepId, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
682
- });
683
- return result(facade as unknown as Record<string, unknown>);
684
- }
685
- try {
686
- const repairedPlan = repairDanglingPlanStepWorkBinding(store, {
687
- planId,
688
- stepId: planStepId,
689
- expectedWorkId: step.workId,
690
- reason: 'Explicit Controller repair confirmed that the exact bound Work record is absent and no other active primary Work claims this Plan step.',
691
- });
692
- const facade = buildFacadeResult({
693
- summary: `Repaired dangling Plan step binding ${planId}/${planStepId}; ${step.workId} was cleared without creating a replacement Work.`,
694
- data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(repairedPlan), boundWorkId: step.workId, repaired: true, replacementWorkCreated: false },
695
- });
696
- return result(facade as unknown as Record<string, unknown>);
697
- } catch (error) {
698
- const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_STEP_DANGLING_WORK_REPAIR_FAILED', data: { operation: repairOperation, dryRun: false, planId, planStepId, boundWorkId: step.workId, repaired: false } });
699
- return result(facade as unknown as Record<string, unknown>, true);
700
- }
236
+ /**
237
+ * Thin rh_work router.
238
+ *
239
+ * Work is semantic context only. Repository execution, checks, review, Git
240
+ * delivery and cleanup are separate capabilities and never re-enter a Work
241
+ * lifecycle here.
242
+ */
243
+ export async function callWorkAdapter(
244
+ ctx: MultiRepositoryMcpToolContext,
245
+ input: Record<string, unknown>,
246
+ ): Promise<CallToolResult> {
247
+ const wireMigration = normalizeRhWorkInputWireMigration(input);
248
+ if (!wireMigration.ok) {
249
+ return result(buildFacadeResult({
250
+ status: 'blocked',
251
+ summary: wireMigration.summary,
252
+ data: wireMigration.data,
253
+ }) as unknown as Record<string, unknown>, true);
701
254
  }
702
255
 
703
- // The self-healing facade is a policy/planning surface; the authoritative
704
- // maintenance executor owns mutations. Execute it here only for an explicit,
705
- // non-dry-run repair whose entire observed candidate set is already classified
706
- // safe. Unsafe/destructive/remote repair continues through the approval path.
707
- if (
708
- repairOperation === 'repair'
709
- && !dryRun
710
- && !elevatedRepair
711
- && !exactWorkId
712
- && maintenanceSnapshot
713
- && maintenanceSnapshot.candidates.length > 0
714
- ) {
715
- const applied = applyRuntimeMaintenance(repository, ctx.controllerHome, {
716
- actionId: 'full_maintenance_pass',
717
- confirmMaintenance: true,
718
- minAgeMinutes: typeof args.min_age_minutes === 'number' ? args.min_age_minutes : undefined,
719
- maxCandidates: typeof args.max_candidates === 'number' ? args.max_candidates : 20,
720
- });
721
- const actions = applied.applied.slice(0, 20).map((entry) => ({
722
- kind: entry.kind,
723
- id: entry.id,
724
- applied: entry.applied,
725
- result: entry.result,
726
- ...(entry.error ? { error: entry.error.slice(0, 300) } : {}),
727
- }));
728
- const appliedCount = applied.applied.filter((entry) => entry.applied).length;
729
- // Protected stale runtime temp entries are intentionally non-blocking maintenance
730
- // diagnostics. Keep repair completion semantics aligned with rh_status readiness so
731
- // their presence does not falsely report a blocked repair after safe debt is cleared.
732
- const remainingCandidateCount = applied.candidates.filter((candidate) => candidate.kind !== 'stale_runtime_temp_entry').length;
733
- const blocked = remainingCandidateCount > 0;
734
- const facade = buildFacadeResult({
735
- status: blocked ? 'blocked' : 'ok',
736
- summary: blocked
737
- ? `Runtime maintenance applied ${appliedCount} candidate(s); ${remainingCandidateCount} candidate(s) remain after the authoritative executor pass.`
738
- : `Runtime maintenance applied ${appliedCount} candidate(s); no maintenance candidates remain.`,
739
- data: {
740
- operation: 'repair',
741
- dryRun: false,
742
- applied: appliedCount > 0,
743
- actionId: 'full_maintenance_pass',
744
- appliedCount,
745
- remainingCandidateCount,
746
- actions,
747
- classification: 'infrastructure_recovery',
748
- isAcceptanceFailure: false,
749
- },
750
- warnings: applied.warnings.slice(0, 5),
751
- suggestedNextActions: [{
752
- label: 'Verify controller status after repair',
753
- tool: 'rh_status',
754
- operation: 'get',
755
- risk: 'readonly',
756
- confidence: 'high',
757
- }],
758
- rawAvailable: false,
759
- });
760
- return result(facade as unknown as Record<string, unknown>, blocked);
761
- }
256
+ const args = wireMigration.args;
257
+ const operation = wireMigration.operation;
258
+ if (!isRhWorkAcceptedOperation(operation)) return invalidFacadeOperation('rh_work', operation);
762
259
 
763
- let watchdogSummary: string | undefined;
764
- let performanceSummary: string | undefined;
765
- try {
766
- const watchdog = buildWorkflowWatchdogReport(ctx.controllerHome, repository, { includeProcesses: false });
767
- watchdogSummary = `status=${watchdog.status}; findings=${watchdog.findings.length}; stale=${watchdog.staleWork.length}`.slice(0, 240);
768
- } catch {
769
- watchdogSummary = undefined;
260
+ if (operation === 'repair' && isAutomationReceiptCompatibilityCall(args)) {
261
+ return result(buildFacadeResult({
262
+ summary: 'Workflow Supervisor automation receipt accepted for canonical persistence.',
263
+ data: { automationReceiptCompatibility: true },
264
+ }) as unknown as Record<string, unknown>);
770
265
  }
771
- try {
772
- const perf = collectRuntimePerformanceDiagnostics({
773
- repoId: repository.repoId,
774
- repoRoot: repository.canonicalRoot,
775
- includeProcesses: false,
776
- includeTempDirs: false,
777
- });
778
- performanceSummary = perf.summary.slice(0, 240);
779
- } catch {
780
- performanceSummary = undefined;
266
+
267
+ if (isRhWorkRequirementOperation(operation) && operation !== 'requirement_promote_candidate') {
268
+ const requirement = await callRhWorkRequirementOperation(ctx, undefined, operation, args);
269
+ if (requirement) return requirement;
781
270
  }
782
271
 
783
- const daemon = readForgeRuntimeStatus(ctx.controllerHome);
784
- const readiness = await controllerReadinessEvidence(ctx, repository);
785
- const facade = runSelfHealingLoop(
786
- { repoId: repository.repoId, handoffStore: store },
787
- {
788
- operation: repairOperation,
789
- dryRun,
790
- approvalConfirmed: args.approval_confirmed === true,
791
- workId: typeof args.work_id === 'string' ? args.work_id : undefined,
792
- chatgptPullFailed: args.chatgpt_pull_failed === true,
793
- destructive: args.destructive === true,
794
- remoteEffect: args.remote_write === true || args.remote_effect === true,
795
- maintenanceStatus,
796
- diagnostics: {
797
- watchdogSummary,
798
- performanceSummary,
799
- controllerDaemonUnhealthy: daemon.status !== 'ready',
800
- schedulerUnhealthy: readiness.durableScheduler.status !== 'ready',
801
- codexUnavailable: args.codex_available === false,
802
- grokUnavailable: args.grok_available === false || args.target === 'grok',
803
- pluginUnavailable: args.plugin_unavailable === true,
804
- },
805
- },
806
- );
807
- return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked' || facade.status === 'approval_required' || facade.status === 'failed');
808
- }
272
+ const stableSemantic = await routeStableSemanticOperation(ctx, operation, args);
273
+ if (stableSemantic) return stableSemantic;
809
274
 
810
- export function repositoryWorkHandleHasSourceDelta(
811
- repository: ReturnType<typeof selected>,
812
- handle: WorkHandleState,
813
- ): boolean {
814
- try {
815
- const checkout = selectRepositoryCheckout(repository, handle.checkoutId, { allowArchived: true });
816
- const status = repositoryGitStatus(checkout);
817
- const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
818
- return !status.clean || Boolean(head && handle.baseCommit && head !== handle.baseCommit);
819
- } catch (_error) {
820
- // A checkout may already have been removed by an earlier bounded cleanup
821
- // attempt. The WorkHandle's fenced expectedHead remains authoritative for
822
- // whether repository delivery existed and must not be downgraded to an
823
- // effect-only semantic completion.
824
- return Boolean(handle.expectedHead && handle.baseCommit && handle.expectedHead !== handle.baseCommit);
275
+ const repositoryOptionalPlan = await callRhWorkPlanCreateWithoutRepository(ctx, operation, args);
276
+ if (repositoryOptionalPlan) return repositoryOptionalPlan;
277
+
278
+ if (operation === 'start' && !repositoryBoundStartRequested(args)) {
279
+ const semanticStart = await callRhWorkSemanticOperation(
280
+ { controllerHome: ctx.controllerHome, scopeKey: SEMANTIC_SCOPE_KEY },
281
+ operation,
282
+ args,
283
+ );
284
+ if (semanticStart) return semanticStart;
825
285
  }
826
- }
827
286
 
828
- export function finalizeRemoteEffectWorkFromRepositoryProcessReceipt(
829
- ctx: MultiRepositoryMcpToolContext,
830
- repository: ReturnType<typeof selected>,
831
- workId: string,
832
- checkoutId: string,
833
- ) {
287
+ const repository = selected(ctx, args);
288
+ if (operation === 'start') return callRepositoryBoundStart(ctx, repository, args);
834
289
  const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
835
- const evidence = listWorkBoundRepositoryRemoteEffectProcessEvidence({
836
- controllerHome: ctx.controllerHome,
837
- repoId: repository.repoId,
838
- checkoutId,
839
- workId,
840
- })[0];
841
- if (!evidence) return undefined;
842
- return completeRemoteEffectWorkFromProcessReceipt(store, workId, {
843
- processId: evidence.processId,
844
- actionId: evidence.actionId,
845
- requestId: evidence.requestId,
846
- semanticKey: evidence.semanticKey,
847
- resultDigest: evidence.resultDigest,
848
- recordedAt: evidence.finishedAt ?? new Date().toISOString(),
849
- });
850
- }
851
290
 
852
- export function reconcileTerminalFacadeWorkVerifications(
853
- ctx: MultiRepositoryMcpToolContext,
854
- repository: ReturnType<typeof selected>,
855
- workId: string,
856
- ): { sourceRevision?: string; workspaceFingerprint?: string; implementationReviewWorkspaceFingerprint?: string; workspaceChangedPaths?: string[]; reconciledProcessIds: string[]; workBoundProcessEvidenceIds: string[] } {
857
- const resolvedVerification = resolveWorkVerificationContext({ controllerHome: ctx.controllerHome, repository, workId });
858
- if (!resolvedVerification.ok) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
859
- const { store, workContract, repository: verificationRepository, checks: availableChecks } = resolvedVerification.context;
860
- if (!workContract || workContract.completionReceipt) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
861
- const verificationStatus = repositoryGitStatus(verificationRepository);
862
- const sourceRevision = verificationStatus.head ?? undefined;
863
- if (!sourceRevision) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
864
- let verificationHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
865
- const workspaceFingerprint = workspaceValidationFingerprint(verificationRepository.canonicalRoot, verificationStatus);
866
- // Work-bound repository Process evidence remains semantic/result evidence,
867
- // never a typed check receipt. repository_change may use it only when no
868
- // checks are declared. local_effect may bind the same exact durable Process
869
- // ids for Controller semantic review even when checks are declared; the
870
- // normal missing-check gate below still requires typed verification receipts.
871
- const workBoundProcessEvidenceIds = (
872
- workContract.workKind === 'local_effect'
873
- || (workContract.workKind === 'repository_change' && workContract.checks.length === 0)
874
- )
875
- ? listWorkBoundRepositoryProcessEvidence({
876
- controllerHome: ctx.controllerHome,
877
- repoId: repository.repoId,
878
- checkoutId: verificationRepository.activeCheckoutId,
879
- workId,
880
- }).map((evidence) => evidence.processId)
881
- : [];
882
- const workloopCtx = {
883
- workStore: store,
884
- handoffStore: store,
885
- repoId: repository.repoId,
886
- availableChecks,
887
- };
888
- const seenChecks = new Set<string>();
889
- const reconciledProcessIds: string[] = [];
890
- const candidates = listProcessRecords(ctx.controllerHome, repository.repoId, 500)
891
- .filter((record) => (
892
- record.workId === workId
893
- && record.checkoutId === verificationRepository.activeCheckoutId
894
- && !isManagedProcessActive(record)
895
- && record.origin?.workVerificationSnapshot === true
896
- && typeof record.origin?.checkId === 'string'
897
- && typeof record.origin?.requestSemanticFingerprint === 'string'
898
- ));
291
+ const workList = callRhWorkListProjection(store, operation, args);
292
+ if (workList) return workList;
899
293
 
900
- for (const record of candidates) {
901
- const checkId = record.origin?.checkId?.trim() ?? '';
902
- if (!checkId || seenChecks.has(checkId)) continue;
903
- seenChecks.add(checkId);
904
- const classified = classifyVerificationOutcome({ checkId, available: availableChecks });
905
- if (classified.outcome === 'invalid_check_id' || !classified.normalizedCheckId) continue;
906
- const normalizedCheckId = classified.normalizedCheckId;
907
- const requestedChecks = workContract.checks.length ? workContract.checks : [normalizedCheckId];
908
- const currentFingerprint = verificationInputFingerprint({
909
- sourceRevision,
910
- workspaceFingerprint,
911
- checkId: normalizedCheckId,
912
- requestedChecks,
913
- });
914
- if (record.origin?.requestSemanticFingerprint !== currentFingerprint || !record.checkExecution) continue;
915
-
916
- try {
917
- const receipt = processCheckCompletionReceipt(record, {
918
- repoId: verificationRepository.repoId,
919
- checkoutId: verificationRepository.activeCheckoutId,
920
- workId,
921
- checkId: normalizedCheckId,
922
- processId: record.processId,
923
- requestId: record.origin?.requestId,
924
- checkExecution: {
925
- cacheKey: record.checkExecution.cacheKey,
926
- revision: record.checkExecution.revision,
927
- definitionDigest: record.checkExecution.definitionDigest,
928
- environmentFingerprint: record.checkExecution.environmentFingerprint,
929
- timeoutMs: record.checkExecution.timeoutMs,
930
- scopeKey: record.checkExecution.scopeKey,
931
- },
932
- });
933
- const latestContract = getWorkContract(store, workId);
934
- if (latestContract?.checkRefs.some((entry) => entry.receipt?.receiptId === receipt.receiptId)) continue;
935
-
936
- const legacyEvidence = record.origin?.checkResultReceiptPath
937
- ? undefined
938
- : readLatestControllerCheckEvidence(verificationRepository.canonicalRoot, normalizedCheckId);
939
- const evidenceState = classifyPersistedCheckTerminalEvidence(record, normalizedCheckId, { legacyEvidence });
940
- const failureClass = evidenceState.failureClass;
941
- const infrastructureFailed = receipt.timedOut
942
- || receipt.cancelled
943
- || evidenceState.state !== 'matched'
944
- || (!receipt.ok && failureClass !== 'acceptance_failure');
945
- const checkFailed = !receipt.ok && !infrastructureFailed;
946
- verifyGoalWorkloop(workloopCtx, {
947
- workId,
948
- checkId: normalizedCheckId,
949
- sourceRevision,
950
- workspaceFingerprint,
951
- verificationInputFingerprint: currentFingerprint,
952
- commandFingerprint: commandFingerprint(normalizedCheckId, receipt.commandId),
953
- receipt,
954
- infrastructureFailed,
955
- checkFailed,
956
- });
957
- reconciledProcessIds.push(record.processId);
958
- } catch {
959
- // Exact receipt/process identity is mandatory. Any malformed, stale, or
960
- // mismatched terminal Process remains non-authoritative and is ignored.
961
- }
294
+ if (isRhWorkScheduleOperation(operation)) {
295
+ return callRhWorkScheduleAdapter(ctx, repository, operation, args);
962
296
  }
963
297
 
964
- let deliveryBaseRevision = verificationHandle
965
- ? workDeliveryBaseRevision(verificationHandle)
966
- : workContract.baseRevision;
967
- const latestContract = getWorkContract(store, workId) ?? workContract;
968
- if (latestContract.phase === 'review' && verificationHandle && !verificationHandle.managedWorktree && verificationHandle.expectedHead) {
969
- const targetBranch = resolveWorkDeliveryTargetBranch(verificationHandle, verificationRepository.defaultBranch);
970
- const reconciliation = reconcileDirectCanonicalTargetAdvanceCommand({
971
- controllerHome: ctx.controllerHome,
972
- repoId: repository.repoId,
973
- workId,
974
- handle: verificationHandle,
975
- root: verificationRepository.canonicalRoot,
976
- targetBranch,
977
- status: verificationStatus,
978
- scope: { allowedPaths: latestContract.allowedPaths, forbiddenPaths: latestContract.forbiddenPaths },
979
- checkIds: latestContract.checks,
980
- checkRefs: latestContract.checkRefs,
981
- evidenceTitle: 'review target advancement reconciled',
982
- });
983
- if (reconciliation.reconciled) {
984
- verificationHandle = reconciliation.handle;
985
- deliveryBaseRevision = workDeliveryBaseRevision(verificationHandle);
986
- }
298
+ if (operation === 'launcher_start') {
299
+ const controller = await callRhWorkControllerOperation(ctx, repository, operation, args);
300
+ if (controller) return controller;
987
301
  }
988
302
 
989
- deliveryBaseRevision = verificationHandle
990
- ? implementationReviewCommittedBaseRevision(
991
- verificationRepository,
992
- verificationHandle,
993
- workContract.baseRevision,
994
- sourceRevision,
995
- )
996
- : workContract.baseRevision;
997
- const committedPaths = deliveryBaseRevision
998
- ? workChangedPaths(verificationRepository.canonicalRoot, deliveryBaseRevision, sourceRevision)
999
- : workContract.repositoryBaseState === 'unborn'
1000
- ? workChangedPathsFromUnbornBase(verificationRepository.canonicalRoot, sourceRevision)
1001
- : [];
1002
- const workspaceChangedPaths = [...new Set([
1003
- ...committedPaths,
1004
- ...verificationStatus.staged,
1005
- ...verificationStatus.unstaged,
1006
- ...verificationStatus.untracked,
1007
- ])].sort();
1008
- const implementationReviewWorkspaceFingerprint = implementationReviewContentFingerprint(
1009
- verificationRepository.canonicalRoot,
1010
- workspaceChangedPaths,
1011
- );
303
+ const workflow = await callRhWorkWorkflowOperation(ctx, repository, operation, args);
304
+ if (workflow) return workflow;
1012
305
 
1013
- return { sourceRevision, workspaceFingerprint, implementationReviewWorkspaceFingerprint, workspaceChangedPaths, reconciledProcessIds, workBoundProcessEvidenceIds };
1014
- }
306
+ const learning = callRhWorkLearningOperation(ctx, repository, operation, args);
307
+ if (learning) return learning;
1015
308
 
1016
- export async function runFacadeVerify(
1017
- ctx: MultiRepositoryMcpToolContext,
1018
- repository: ReturnType<typeof selected>,
1019
- args: Record<string, unknown>,
1020
- ): Promise<CallToolResult> {
1021
- const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
1022
- const checkId = String(args.check_id ?? args.checkId ?? '').trim();
1023
- const hasBatchInput = Object.prototype.hasOwnProperty.call(args, 'check_ids');
1024
- if (checkId && hasBatchInput) {
1025
- const blocked = buildFacadeResult({
1026
- status: 'blocked',
1027
- summary: 'rh_work verify accepts either check_id or check_ids, not both.',
1028
- data: { workId: workId || undefined, verificationStarted: false },
1029
- warnings: ['WORK_VERIFY_CHECK_INPUT_CONFLICT'],
1030
- });
1031
- return result(blocked as unknown as Record<string, unknown>, true);
1032
- }
1033
- if (hasBatchInput && !Array.isArray(args.check_ids)) {
1034
- const blocked = buildFacadeResult({
1035
- status: 'blocked',
1036
- summary: 'rh_work verify check_ids must be an array.',
1037
- data: { workId: workId || undefined, verificationStarted: false },
1038
- warnings: ['WORK_VERIFY_CHECK_IDS_INVALID'],
1039
- });
1040
- return result(blocked as unknown as Record<string, unknown>, true);
1041
- }
1042
- const commonVerificationInput = {
309
+ const requirement = await callRhWorkRequirementOperation(ctx, repository, operation, args);
310
+ if (requirement) return requirement;
311
+
312
+ const plan = await callRhWorkPlanOperation(store, operation, args);
313
+ if (plan) return plan;
314
+
315
+ const planCreate = await callRhWorkPlanCreateOperation(store, operation, args, {
1043
316
  controllerHome: ctx.controllerHome,
1044
- repository,
1045
- workId: workId || undefined,
1046
- requestId: typeof args.request_id === 'string' && args.request_id.trim() ? args.request_id.trim() : undefined,
1047
- timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
1048
- interactiveWaitMs: 0,
1049
- leaseWaitMs: RH_WORK_VERIFY_LEASE_WAIT_MS,
1050
- simulate: args.simulate_check === true || args.infrastructure_failed === true || args.check_failed === true || args.skipped === true
1051
- ? {
1052
- infrastructureFailed: args.infrastructure_failed === true,
1053
- checkFailed: args.check_failed === true,
1054
- skipped: args.skipped === true,
1055
- }
1056
- : undefined,
1057
- allowDurableCheckExecution: workId ? ({ work }: { work: WorkContract }) => {
1058
- try {
1059
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
1060
- const owner = getControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, work.workId);
1061
- if (!sessionlessFacadeControllerAuthorityMatches(owner, identity)) return false;
1062
- return Boolean(
1063
- owner
1064
- && owner.controllerId === identity.controllerId
1065
- && controllerSessionPrincipalId(owner) === identity.principalId
1066
- && owner.controllerInstanceId === identity.controllerInstanceId,
1067
- );
1068
- } catch {
1069
- return false;
1070
- }
1071
- } : undefined,
1072
- };
1073
- const verification = hasBatchInput
1074
- ? await executeWorkVerificationBatch({
1075
- ...commonVerificationInput,
1076
- checkIds: (args.check_ids as unknown[]).map(String),
1077
- })
1078
- : await executeWorkVerification({ ...commonVerificationInput, checkId });
1079
- return result(verification.facade as unknown as Record<string, unknown>, verification.isError);
1080
- }
317
+ repoId: repository.repoId,
318
+ checks: listControllerChecks(repository.canonicalRoot),
319
+ });
320
+ if (planCreate) return planCreate;
1081
321
 
1082
- /** MCP rh_work transport adapter. Canonical lifecycle semantics remain in Kernel/application services; this layer normalizes ABI input and orchestrates those services. */
1083
- export async function callWorkAdapter(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<CallToolResult> {
1084
- {
1085
- const repository = selected(ctx, args);
1086
- const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
1087
- const requestedOperation = String(args.operation ?? 'start');
1088
- const frozenScheduleDeleteId = requestedOperation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith('schedule.delete:')
1089
- ? args.capability_id.slice('schedule.delete:'.length).trim()
1090
- : '';
1091
- let frozenImplementationReview: { decision: 'approved' | 'changes_required' | 'blocked'; workId: string } | undefined;
1092
- let frozenControllerDisposition: ReturnType<typeof parseControllerDispositionCompatibilityCapability>;
1093
- let frozenControllerRoundOperation: ReturnType<typeof parseControllerRoundCompatibilityCapability>;
1094
- let frozenPlanObligationDispositions: ReturnType<typeof parsePlanObligationCompatibilityCapability>;
1095
- let frozenSemanticOperation: ReturnType<typeof parseFrozenSemanticCompatibilityCapability>;
1096
- try {
1097
- if (requestedOperation === 'repair' && typeof args.capability_id === 'string') {
1098
- const capability = args.capability_id.trim();
1099
- const prefix = 'work.review:';
1100
- if (capability.startsWith(prefix)) {
1101
- const remainder = capability.slice(prefix.length);
1102
- const separator = remainder.indexOf(':');
1103
- if (separator <= 0 || separator === remainder.length - 1) throw new Error('WORK_IMPLEMENTATION_REVIEW_COMPATIBILITY_INVALID');
1104
- const decision = remainder.slice(0, separator);
1105
- const workId = remainder.slice(separator + 1).trim();
1106
- if (!['approved', 'changes_required', 'blocked'].includes(decision) || !workId) throw new Error('WORK_IMPLEMENTATION_REVIEW_COMPATIBILITY_INVALID');
1107
- frozenImplementationReview = { decision: decision as 'approved' | 'changes_required' | 'blocked', workId };
1108
- }
1109
- }
1110
- frozenControllerDisposition = parseControllerDispositionCompatibilityCapability(requestedOperation, args.capability_id);
1111
- frozenControllerRoundOperation = parseControllerRoundCompatibilityCapability(requestedOperation, args.capability_id);
1112
- frozenPlanObligationDispositions = parsePlanObligationCompatibilityCapability(requestedOperation, args.capability_id);
1113
- frozenSemanticOperation = parseFrozenSemanticCompatibilityCapability(requestedOperation, args.capability_id);
1114
- if (frozenControllerDisposition
1115
- && (args.relay_scope_id !== undefined || (frozenControllerDisposition.authorityId && args.controller_authority_id !== undefined))) {
1116
- throw new Error('CONTROLLER_RELAY_DISPOSITION_COMPATIBILITY_CONFLICT');
1117
- }
1118
- if (frozenPlanObligationDispositions && Array.isArray(args.obligation_dispositions)) {
1119
- throw new Error('PLAN_OBLIGATION_COMPATIBILITY_CONFLICT');
1120
- }
1121
- if (frozenSemanticOperation) {
1122
- const requirementScoped = frozenSemanticOperation.operation !== 'work_review';
1123
- if (requirementScoped) {
1124
- const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id.trim() : '';
1125
- if (!requirementId) throw new Error('FROZEN_SEMANTIC_COMPATIBILITY_SCOPE_REQUIRED: requirement_id must remain explicit outside the compatibility envelope');
1126
- }
1127
- for (const key of Object.keys(frozenSemanticOperation.args)) {
1128
- if (args[key] !== undefined) throw new Error(`FROZEN_SEMANTIC_COMPATIBILITY_CONFLICT: native field ${key} is also present`);
1129
- }
1130
- if (frozenSemanticOperation.operation === 'work_review') {
1131
- const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
1132
- if (!explicitWorkId) throw new Error('FROZEN_SEMANTIC_COMPATIBILITY_SCOPE_REQUIRED: work_id must remain explicit outside the work_review envelope');
1133
- if (frozenImplementationReview) throw new Error('WORK_IMPLEMENTATION_REVIEW_COMPATIBILITY_CONFLICT');
1134
- if (args.review_decision !== undefined || args.review_rationale !== undefined) {
1135
- throw new Error('WORK_IMPLEMENTATION_REVIEW_COMPATIBILITY_CONFLICT');
1136
- }
1137
- }
1138
- }
1139
- } catch (error) {
1140
- return result(buildFacadeResult({
1141
- status: 'blocked',
1142
- summary: error instanceof Error ? error.message : 'Controller round compatibility input is invalid.',
1143
- data: {},
1144
- }) as unknown as Record<string, unknown>, true);
1145
- }
1146
- if (frozenControllerRoundOperation) {
1147
- args.relay_scope_id = frozenControllerRoundOperation.relayScopeId;
1148
- args.controller_authority_id = frozenControllerRoundOperation.authorityId;
1149
- if (frozenControllerRoundOperation.operation === 'review') {
1150
- if (args.review_decision !== undefined || args.review_rationale !== undefined) {
1151
- return result(buildFacadeResult({
1152
- status: 'blocked',
1153
- summary: 'CONTROLLER_ROUND_REVIEW_COMPATIBILITY_CONFLICT: native review fields cannot be combined with the frozen review carrier.',
1154
- data: {},
1155
- }) as unknown as Record<string, unknown>, true);
1156
- }
1157
- args.review_decision = frozenControllerRoundOperation.reviewDecision;
1158
- args.review_rationale = typeof args.reason === 'string' ? args.reason : '';
1159
- }
1160
- }
1161
- if (frozenControllerDisposition) {
1162
- args.relay_scope_id = frozenControllerDisposition.relayScopeId;
1163
- if (frozenControllerDisposition.authorityId) args.controller_authority_id = frozenControllerDisposition.authorityId;
1164
- }
1165
- if (frozenPlanObligationDispositions) {
1166
- args.obligation_dispositions = frozenPlanObligationDispositions;
1167
- }
1168
- if (frozenSemanticOperation?.operation === 'plan_create') {
1169
- // Frozen semantic compatibility fills only the field absent from the
1170
- // older schema. Plan identity/scope/source/steps/relation remain native
1171
- // arguments and canonical Plan admission/continuity stays authoritative.
1172
- args.obligation_dispositions = frozenSemanticOperation.args.obligation_dispositions;
1173
- }
1174
- if (frozenSemanticOperation?.operation === 'start') {
1175
- // Older rh_work schemas may lack the technical fields required to admit a
1176
- // terminal Plan successor. The bounded envelope fills only those closed
1177
- // fields; objective, Requirement/Plan binding, route policy, engineering
1178
- // verification, ControllerRound authority and lifecycle remain canonical.
1179
- Object.assign(args, frozenSemanticOperation.args);
1180
- }
1181
- if (frozenSemanticOperation?.operation === 'work_review') {
1182
- args.review_decision = frozenSemanticOperation.args.decision;
1183
- args.review_rationale = typeof args.reason === 'string' ? args.reason : '';
1184
- }
1185
- if (frozenImplementationReview) {
1186
- const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
1187
- if (!explicitWorkId || explicitWorkId !== frozenImplementationReview.workId) {
1188
- return result(buildFacadeResult({
1189
- status: 'blocked',
1190
- summary: `WORK_IMPLEMENTATION_REVIEW_SCOPE_MISMATCH: capability targets ${frozenImplementationReview.workId}; exact work_id is required.`,
1191
- data: { workId: frozenImplementationReview.workId, implementationReviewRecorded: false },
1192
- }) as unknown as Record<string, unknown>, true);
1193
- }
1194
- args.review_decision = frozenImplementationReview.decision;
1195
- args.review_rationale = typeof args.reason === 'string' ? args.reason : '';
1196
- }
1197
- const frozenSemanticFacadeOperation = frozenSemanticOperation?.operation === 'work_review'
1198
- ? 'review'
1199
- : frozenSemanticOperation?.operation;
1200
- const operation = frozenSemanticFacadeOperation ?? frozenControllerRoundOperation?.operation ?? (frozenControllerDisposition
1201
- ? 'controller_disposition'
1202
- : frozenImplementationReview ? 'review'
1203
- : frozenScheduleDeleteId ? 'schedule_delete' : requestedOperation);
1204
- if (!allowedFacadeOperations('rh_work').includes(operation)) {
1205
- return invalidFacadeOperation('rh_work', operation);
1206
- }
1207
- if (isRhWorkScheduleOperation(operation)) {
1208
- return await callRhWorkScheduleAdapter(ctx, repository, operation, args, {
1209
- scheduleIdOverride: frozenScheduleDeleteId || undefined,
1210
- });
1211
- }
1212
- const technicalRetryWorkId = operation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith('plan.step.retry:')
1213
- ? args.capability_id.slice('plan.step.retry:'.length).trim()
1214
- : '';
1215
- if (technicalRetryWorkId) {
1216
- const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
1217
- if (!explicitWorkId || explicitWorkId !== technicalRetryWorkId) {
1218
- return result(buildFacadeResult({
1219
- status: 'blocked',
1220
- summary: `PLAN_STEP_TECHNICAL_RETRY_SCOPE_MISMATCH: capability targets ${technicalRetryWorkId}; exact work_id is required.`,
1221
- data: { workId: technicalRetryWorkId, repaired: false },
1222
- }) as unknown as Record<string, unknown>, true);
1223
- }
1224
- try {
1225
- const work = getWorkContract(store, technicalRetryWorkId);
1226
- if (!work) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_WORK_NOT_FOUND: ${technicalRetryWorkId}`);
1227
- const reason = typeof args.reason === 'string' ? args.reason.trim() : '';
1228
- if (!reason) throw new Error('PLAN_STEP_TECHNICAL_RETRY_REASON_REQUIRED');
1229
- const handle = readWorkHandle(ctx.controllerHome, repository.repoId, technicalRetryWorkId);
1230
- const cleanupComplete = Boolean(handle
1231
- && handle.managedWorktree
1232
- && handle.state === 'cleaned'
1233
- && handle.finalization.branchCleanup === 'done'
1234
- && handle.finalization.worktreeCleanup === 'done'
1235
- && handle.cleanupReceipt?.complete === true
1236
- && handle.baseCommit
1237
- && handle.expectedHead === handle.baseCommit
1238
- && !existsSync(handle.worktreePath));
1239
- if (!cleanupComplete) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_CLEANUP_INCOMPLETE: ${technicalRetryWorkId}`);
1240
- const conflicting = work.planId && work.planStepId
1241
- ? listWorkContracts({ ...store, status: 'active', limit: 200 }).filter((candidate) => candidate.planId === work.planId && candidate.planStepId === work.planStepId)
1242
- : [];
1243
- if (conflicting.length > 0) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_ACTIVE_WORK_CONFLICT: ${conflicting.map((candidate) => candidate.workId).join(',')}`);
1244
- const repairedPlan = repairPlanStepForTechnicalRetry(store, { work, cleanupComplete, reason });
1245
- return result(buildFacadeResult({
1246
- summary: `Plan step ${work.planId}/${work.planStepId} was restored for an explicit technical retry after terminal Work ${work.workId}; no Work was revived or created.`,
1247
- data: { workId: work.workId, plan: summarizePlanContract(repairedPlan), repaired: true, replacementWorkCreated: false },
1248
- }) as unknown as Record<string, unknown>);
1249
- } catch (error) {
1250
- return result(buildFacadeResult({
1251
- status: 'blocked',
1252
- summary: error instanceof Error ? error.message : 'PLAN_STEP_TECHNICAL_RETRY_FAILED',
1253
- data: { workId: technicalRetryWorkId, repaired: false },
1254
- }) as unknown as Record<string, unknown>, true);
1255
- }
1256
- }
1257
-
1258
- const providerRecoveryWorkId = operation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith(CONTROLLER_PROVIDER_RECOVERY_CAPABILITY_PREFIX)
1259
- ? args.capability_id.slice(CONTROLLER_PROVIDER_RECOVERY_CAPABILITY_PREFIX.length).trim()
1260
- : '';
1261
- if (providerRecoveryWorkId) {
1262
- const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
1263
- const relayScopeId = typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : '';
1264
- const authorityId = typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '';
1265
- if (!explicitWorkId || explicitWorkId !== providerRecoveryWorkId) {
1266
- return result(buildFacadeResult({
1267
- status: 'blocked',
1268
- summary: `CONTROLLER_PROVIDER_RECOVERY_SCOPE_MISMATCH: capability targets ${providerRecoveryWorkId}; exact work_id is required.`,
1269
- data: { workId: providerRecoveryWorkId, providerRecovered: false },
1270
- }) as unknown as Record<string, unknown>, true);
1271
- }
1272
- if (!relayScopeId || !authorityId) {
1273
- return result(buildFacadeResult({
1274
- status: 'blocked',
1275
- summary: 'CONTROLLER_PROVIDER_RECOVERY_AUTHORITY_REQUIRED: exact controller_authority_id and relay_scope_id are required.',
1276
- data: { workId: providerRecoveryWorkId, providerRecovered: false },
1277
- }) as unknown as Record<string, unknown>, true);
1278
- }
1279
- try {
1280
- const recovered = await recoverControllerRoundAfterVerifiedProviderRepair({
1281
- controllerHome: ctx.controllerHome,
1282
- repoId: repository.repoId,
1283
- repoRoot: repository.canonicalRoot,
1284
- workId: providerRecoveryWorkId,
1285
- relayScopeId,
1286
- authorityId,
1287
- timeoutMs: typeof args.probe_timeout_ms === 'number' ? args.probe_timeout_ms : undefined,
1288
- });
1289
- return result(buildFacadeResult({
1290
- summary: `Verified ChatGPT provider recovery and rearmed exact ControllerRound for Work ${providerRecoveryWorkId} without changing semantic round identity or recovery budgets.`,
1291
- data: {
1292
- workId: providerRecoveryWorkId,
1293
- providerRecovered: true,
1294
- recoveryAuditId: recovered.audit.id,
1295
- provider: recovered.probe.provider,
1296
- relay: recovered.relay,
1297
- },
1298
- }) as unknown as Record<string, unknown>);
1299
- } catch (error) {
1300
- return result(buildFacadeResult({
1301
- status: 'blocked',
1302
- summary: error instanceof Error ? error.message : 'Controller provider recovery failed.',
1303
- data: { workId: providerRecoveryWorkId, providerRecovered: false },
1304
- }) as unknown as Record<string, unknown>, true);
1305
- }
1306
- }
322
+ if (operation === 'repair') return runFacadeRepair(ctx, repository, args);
1307
323
 
1308
- const authorityRecoveryWorkId = operation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith('controller.authority.recover:')
1309
- ? args.capability_id.slice('controller.authority.recover:'.length).trim()
1310
- : '';
1311
- if (authorityRecoveryWorkId) {
1312
- const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
1313
- if (!explicitWorkId || explicitWorkId !== authorityRecoveryWorkId) {
1314
- return result(buildFacadeResult({
1315
- status: 'blocked',
1316
- summary: `WORK_CONTROLLER_AUTHORITY_RECOVERY_SCOPE_MISMATCH: capability targets ${authorityRecoveryWorkId}; exact work_id is required.`,
1317
- data: { workId: authorityRecoveryWorkId, authorityRecovered: false },
1318
- }) as unknown as Record<string, unknown>, true);
1319
- }
1320
- try {
1321
- const recovered = recoverControllerAuthority({
1322
- controllerHome: ctx.controllerHome,
1323
- repoId: repository.repoId,
1324
- repositoryActiveCheckoutId: repository.activeCheckoutId,
1325
- workId: authorityRecoveryWorkId,
1326
- requestedBy: typeof args.requested_by === 'string' ? args.requested_by : undefined,
1327
- identity: authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true }),
1328
- runtime: runtimeIdentitySnapshot(ctx),
1329
- leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
1330
- });
1331
- return result(buildFacadeResult({
1332
- summary: `Controller authority for exact Work ${authorityRecoveryWorkId} was recovered without changing semantic Work identity, relay scope, or recovery budgets.`,
1333
- data: recovered,
1334
- }) as unknown as Record<string, unknown>);
1335
- } catch (error) {
1336
- return result(buildFacadeResult({
1337
- status: 'blocked',
1338
- summary: error instanceof Error ? error.message : 'Controller authority recovery failed.',
1339
- data: { workId: authorityRecoveryWorkId, authorityRecovered: false },
1340
- }) as unknown as Record<string, unknown>, true);
1341
- }
1342
- }
1343
-
1344
- if (operation === 'workflow_execute' || operation === 'workflow_reconcile') {
1345
- try {
1346
- const workId = String(args.work_id ?? '').trim();
1347
- const workflowId = String(args.workflow_id ?? '').trim();
1348
- const runId = String(args.workflow_run_id ?? '').trim();
1349
- if (!workId || !workflowId || !runId) throw new Error('WORKFLOW_FACADE_IDENTITY_REQUIRED');
1350
- const work = getWorkContract(store, workId);
1351
- if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
1352
- assertFacadeControllerRoundAuthority(ctx, store, workId, args);
1353
- const owner = getControllerSession(store, workId);
1354
- const relay = getControllerRoundRelay(store, workId);
1355
- if (!owner) throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${workId}`);
1356
- const authorityId = relay?.authorityId?.trim() || (typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '');
1357
- if (!authorityId) throw new Error('WORKFLOW_CONTROLLER_AUTHORITY_REQUIRED');
1358
- const workRepository = selectRepositoryCheckout(repository, work.checkoutId);
1359
- const executionIdentity = executionIdentityForRepository(workRepository, { workId });
1360
- if (Object.values(XIAOHONGSHU_WORKFLOW_IDS).includes(workflowId as never)) {
1361
- ensureXiaohongshuWorkflowInstalled(ctx.controllerHome, workflowId);
1362
- }
1363
- const projectId = typeof args.workflow_scope_project_id === 'string' ? args.workflow_scope_project_id.trim() : '';
1364
- const registryScope = projectId ? { kind: 'project' as const, projectId } : { kind: 'controller' as const };
1365
- const workflowInputs = args.workflow_inputs && typeof args.workflow_inputs === 'object' && !Array.isArray(args.workflow_inputs)
1366
- ? args.workflow_inputs as Record<string, import('../../../packages/workflow-runtime/api/index').WorkflowJsonValue>
1367
- : {};
1368
- const base = {
1369
- controllerHome: ctx.controllerHome,
1370
- repository: workRepository,
1371
- executionIdentity,
1372
- workId,
1373
- controller: { controllerId: owner.controllerId, authorityId },
1374
- runId,
1375
- registryScope,
1376
- workflowId,
1377
- inputs: workflowInputs,
1378
- timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
1379
- };
1380
- const learningMetadata = (workflow: Awaited<ReturnType<typeof executeRegisteredWorkflow>>) => {
1381
- const persisted = readWorkflowRun(ctx.controllerHome, workId, runId)?.value;
1382
- let outcomeCollectionSchedule;
1383
- let outcomeCollectionError: string | undefined;
1384
- if (workflow.status === 'succeeded' && workflow.publicationReceipt) {
1385
- try {
1386
- outcomeCollectionSchedule = schedulePublicationOutcomeCollection({ controllerHome: ctx.controllerHome, repoId: repository.repoId, workId, publication: workflow.publicationReceipt, workflowInputs });
1387
- } catch (error) {
1388
- outcomeCollectionError = error instanceof Error ? error.message : 'OUTCOME_COLLECTION_SCHEDULE_FAILED';
1389
- }
1390
- }
1391
- return {
1392
- ...(persisted?.evidenceRef ? { workflowEvidenceRef: persisted.evidenceRef } : {}),
1393
- ...(outcomeCollectionSchedule ? { outcomeCollectionSchedule } : {}),
1394
- ...(outcomeCollectionError ? { outcomeCollectionError } : {}),
1395
- };
1396
- };
1397
- if (operation === 'workflow_reconcile') {
1398
- const reconciliationRequestId = String(args.workflow_reconciliation_request_id ?? '').trim();
1399
- if (!reconciliationRequestId) throw new Error('WORKFLOW_RECONCILIATION_REQUEST_ID_REQUIRED');
1400
- const reconciled = await observeAndReconcileRegisteredWorkflow({ ...base, reconciliationRequestId });
1401
- if (reconciled.status === 'running') {
1402
- const resumed = await executeRegisteredWorkflow(base);
1403
- return result(buildFacadeResult({
1404
- summary: `Workflow ${workflowId}/${runId} reconciled from canonical observation and resumed without replaying the uncertain effect.`,
1405
- data: { workflow: resumed, ...learningMetadata(resumed) },
1406
- }) as unknown as Record<string, unknown>);
1407
- }
1408
- return result(buildFacadeResult({
1409
- status: reconciled.status === 'failed' ? 'blocked' : 'ok',
1410
- summary: `Workflow ${workflowId}/${runId} reconciliation settled as ${reconciled.status}.`,
1411
- data: { workflow: reconciled },
1412
- }) as unknown as Record<string, unknown>, reconciled.status === 'failed');
1413
- }
1414
- const executed = await executeRegisteredWorkflow(base);
1415
- return result(buildFacadeResult({
1416
- status: executed.status === 'failed' || executed.status === 'reconcile_required' ? 'blocked' : 'ok',
1417
- summary: `Workflow ${workflowId}/${runId} is ${executed.status}.`,
1418
- data: { workflow: executed, ...learningMetadata(executed) },
1419
- }) as unknown as Record<string, unknown>, executed.status === 'failed' || executed.status === 'reconcile_required');
1420
- } catch (error) {
1421
- return result(buildFacadeResult({
1422
- status: 'blocked',
1423
- summary: error instanceof Error ? error.message : 'Workflow execution failed.',
1424
- data: { workflowExecuted: false },
1425
- }) as unknown as Record<string, unknown>, true);
1426
- }
1427
- }
1428
-
1429
- if (operation === 'outcome_record' || operation === 'experience_record') {
1430
- try {
1431
- const workId = String(args.work_id ?? '').trim();
1432
- if (!workId) throw new Error('LEARNING_LOOP_WORK_ID_REQUIRED');
1433
- const work = getWorkContract(store, workId);
1434
- if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
1435
- assertFacadeControllerRoundAuthority(ctx, store, workId, args);
1436
- const owner = getControllerSession(store, workId);
1437
- const relay = getControllerRoundRelay(store, workId);
1438
- if (!owner) throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${workId}`);
1439
- const authorityId = relay?.authorityId?.trim() || (typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '');
1440
- if (!authorityId) throw new Error('LEARNING_LOOP_CONTROLLER_AUTHORITY_REQUIRED');
1441
- const identity = { workId, controllerId: owner.controllerId, authorityId };
1442
- if (operation === 'outcome_record') {
1443
- if (!args.outcome_observation || typeof args.outcome_observation !== 'object' || Array.isArray(args.outcome_observation)) {
1444
- throw new Error('OUTCOME_OBSERVATION_REQUIRED');
1445
- }
1446
- const outcome = recordControllerOutcome({
1447
- controllerHome: ctx.controllerHome,
1448
- repoId: repository.repoId,
1449
- identity,
1450
- draft: args.outcome_observation as ControllerOutcomeObservationDraft,
1451
- });
1452
- return result(buildFacadeResult({
1453
- summary: `OutcomeObservation ${outcome.id} recorded from canonical Work/ControllerRound evidence.`,
1454
- data: { outcome },
1455
- }) as unknown as Record<string, unknown>);
1456
- }
1457
- if (!args.experience_draft || typeof args.experience_draft !== 'object' || Array.isArray(args.experience_draft)) {
1458
- throw new Error('EXPERIENCE_DRAFT_REQUIRED');
1459
- }
1460
- const experience = recordControllerExperience({
1461
- controllerHome: ctx.controllerHome,
1462
- repoId: repository.repoId,
1463
- identity,
1464
- draft: args.experience_draft as ControllerExperienceDraft,
1465
- qualityAdjustmentFingerprint: typeof args.quality_adjustment_fingerprint === 'string' ? args.quality_adjustment_fingerprint.trim() || undefined : undefined,
1466
- });
1467
- return result(buildFacadeResult({
1468
- summary: `Experience ${experience.id} recorded from canonical evidence for reuse by the next ControllerRound.`,
1469
- data: { experience },
1470
- }) as unknown as Record<string, unknown>);
1471
- } catch (error) {
1472
- return result(buildFacadeResult({
1473
- status: 'blocked',
1474
- summary: error instanceof Error ? error.message : 'Learning-loop record failed.',
1475
- data: { recorded: false },
1476
- }) as unknown as Record<string, unknown>, true);
1477
- }
1478
- }
1479
-
1480
- if (operation === 'controller_get_owner') {
1481
- const owner = getControllerSession(store, String(args.work_id ?? '').trim());
1482
- return result(buildFacadeResult({ summary: owner ? `Work is claimed by ${owner.controllerId}.` : 'Work has no active controller owner.', data: { owner } }) as unknown as Record<string, unknown>);
1483
- }
1484
- if (operation === 'controller_claim') {
1485
- try {
1486
- const workId = String(args.work_id ?? '').trim();
1487
- const work = getWorkContract(store, workId);
1488
- if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
1489
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
1490
- const activeLaunchReservation = identity.controllerType === 'codex'
1491
- ? getExternalControllerLaunchReservation(store, workId)
1492
- : undefined;
1493
- if (activeLaunchReservation?.controllerType === 'codex') {
1494
- const expectedLaunchIdentity = providerMcpReservationIdentity('codex', activeLaunchReservation.reservationId);
1495
- if (identity.principalId !== expectedLaunchIdentity.principalId || identity.sessionId !== expectedLaunchIdentity.sessionId) {
1496
- throw new Error(`WORK_CONTROLLER_LAUNCH_IDENTITY_MISMATCH: ${workId}; active Codex launch reservation requires its exact reservation-scoped MCP identity.`);
1497
- }
1498
- }
1499
- let authorizedRelay = assertFacadeControllerRoundAuthority(ctx, store, workId, args);
1500
- const observedOwner = getControllerSession(store, workId);
1501
- const dispatchedRelay = getControllerRoundRelay(store, workId);
1502
- const requestedRelayScopeId = typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : '';
1503
- if (!authorizedRelay && identity.controllerAuthorityId && requestedRelayScopeId) {
1504
- authorizedRelay = resolveRequirementControllerRoundRelayForWork(store, {
1505
- workId,
1506
- authorityId: identity.controllerAuthorityId,
1507
- relayScopeId: requestedRelayScopeId,
1508
- });
1509
- if (!authorizedRelay) {
1510
- throw new Error(`WORK_CONTROLLER_ROUND_AUTHORITY_UNBOUND: ${workId}:${requestedRelayScopeId}`);
1511
- }
1512
- }
1513
- if (observedOwner && observedOwner.authorityDigest
1514
- && (observedOwner.sessionId !== identity.sessionId || (observedOwner.controllerInstanceId?.trim() || '') !== identity.controllerInstanceId)
1515
- && !dispatchedRelay?.authorityId?.trim()
1516
- && !controllerSessionAuthorityMatches(observedOwner, identity.controllerAuthorityId)) {
1517
- throw new Error(`WORK_CONTROLLER_SCOPE_MISMATCH: ${workId}; controller_claim requires the existing Work-bound controller authority after transport rotation.`);
1518
- }
1519
- const inheritedRequirementAuthority = !dispatchedRelay?.authorityId?.trim() && authorizedRelay?.authorityId?.trim()
1520
- ? {
1521
- authorityId: authorizedRelay.authorityId!.trim(),
1522
- authorityDigest: controllerSessionAuthorityDigest(authorizedRelay.authorityId!),
1523
- }
1524
- : undefined;
1525
- const directAuthority = dispatchedRelay?.authorityId?.trim() || inheritedRequirementAuthority
1526
- ? undefined
1527
- : mintControllerSessionAuthority();
1528
- const sessionAuthority = inheritedRequirementAuthority ?? directAuthority;
1529
- const crossOwnerRecovery = Boolean(observedOwner)
1530
- && (
1531
- observedOwner!.controllerId !== identity.controllerId
1532
- || controllerSessionPrincipalId(observedOwner!) !== identity.principalId
1533
- )
1534
- && dispatchedChatgptRelayAuthorizesStaleControllerRecovery(store, workId, dispatchedRelay, identity.controllerType);
1535
- const session = resumeControllerSession(store, {
1536
- workId,
1537
- controllerId: identity.controllerId,
1538
- controllerType: identity.controllerType,
1539
- sessionId: identity.sessionId,
1540
- ...(sessionAuthority ? { authorityDigest: sessionAuthority.authorityDigest } : {}),
1541
- principalId: identity.principalId,
1542
- controllerInstanceId: identity.controllerInstanceId,
1543
- ...(crossOwnerRecovery
1544
- ? {
1545
- expectedClaimGeneration: observedOwner!.claimGeneration,
1546
- allowStaleRecovery: true,
1547
- }
1548
- : {}),
1549
- leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
1550
- });
1551
- const permissionSnapshotVersion = currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId);
1552
- const executionSession = startExecutionSession(ctx.controllerHome, {
1553
- sessionId: identity.sessionId,
1554
- principalId: identity.principalId,
1555
- controllerInstanceId: identity.controllerInstanceId,
1556
- permissionSnapshotVersion,
1557
- });
1558
- updateExecutionSession(ctx.controllerHome, {
1559
- sessionId: executionSession.sessionId,
1560
- principalId: executionSession.principalId,
1561
- controllerInstanceId: executionSession.controllerInstanceId,
1562
- }, {
1563
- activeRepositoryId: repository.repoId,
1564
- activeCheckoutId: work.checkoutId || repository.activeCheckoutId,
1565
- activeWorkId: work.workId,
1566
- permissionSnapshotVersion,
1567
- lastValidatedAt: new Date().toISOString(),
1568
- });
1569
- const assistantContextBundle = prepareControllerAssistantContextBundle(store, workId);
1570
- const relay = acknowledgeControllerRoundClaim(
1571
- { controllerHome: ctx.controllerHome, repoId: repository.repoId },
1572
- { workId, session, assistantContextSnapshot: assistantContextBundle?.snapshot ?? null },
1573
- );
1574
- return result(buildFacadeResult({
1575
- summary: relay?.status === 'claimed'
1576
- ? `Controller ${session.controllerId} claimed ${session.workId}; the dispatched ChatGPT round is mechanically acknowledged and still requires an explicit semantic disposition.`
1577
- : `Controller ${session.controllerId} claimed ${session.workId}.`,
1578
- data: {
1579
- session,
1580
- relay,
1581
- assistantContext: assistantContextBundle?.rendered ?? prepareControllerAssistantContext(store, workId),
1582
- assistantContextSnapshot: assistantContextBundle?.snapshot,
1583
- controllerAuthorityId: dispatchedRelay?.authorityId?.trim() || inheritedRequirementAuthority?.authorityId || directAuthority?.authorityId,
1584
- controllerAuthorityCarrier: dispatchedRelay?.authorityId?.trim() || inheritedRequirementAuthority
1585
- ? 'controller_authority_id'
1586
- : 'controller_authority_id_or_session_id_compat',
1587
- },
1588
- }) as unknown as Record<string, unknown>);
1589
- } catch (error) {
1590
- return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller claim failed.', data: {} }) as unknown as Record<string, unknown>, true);
1591
- }
1592
- }
1593
- if (operation === 'controller_disposition') {
1594
- try {
1595
- const workId = String(args.work_id ?? '').trim();
1596
- const disposition = frozenControllerDisposition?.disposition ?? String(args.disposition ?? '').trim();
1597
- if (!['continue_immediately', 'wait', 'wait_for_user', 'goal_complete'].includes(disposition)) {
1598
- throw new Error('CONTROLLER_RELAY_DISPOSITION_INVALID');
1599
- }
1600
- const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
1601
- const work = getWorkContract(store, workId);
1602
- if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
1603
- const currentRelay = getControllerRoundRelay(store, workId);
1604
- const terminalGoalComplete = work.status === 'completed' && disposition === 'goal_complete';
1605
- const terminalSuccessorContinuation = work.status === 'completed'
1606
- && disposition === 'continue_immediately'
1607
- && Boolean(currentRelay?.successorWorkId);
1608
- const terminalRoundClosure = terminalGoalComplete || terminalSuccessorContinuation;
1609
- const currentOwner = getControllerSession(store, workId);
1610
- if (!currentOwner && terminalRoundClosure) {
1611
- // Live ownership remains the compatibility fence for ordinary rounds.
1612
- // Once a terminal predecessor lease is gone, only the exact opaque
1613
- // ControllerRound capability may authorize the semantic close.
1614
- assertFacadeControllerRoundAuthority(ctx, store, workId, args);
1615
- }
1616
- if (!currentOwner && !terminalRoundClosure) {
1617
- if (work.status === 'failed' || work.status === 'cancelled' || work.status === 'completed') {
1618
- throw new Error(`CONTROLLER_RELAY_WORK_TERMINAL: ${work.status}`);
1619
- }
1620
- throw new Error(`CONTROLLER_RELAY_ACTIVE_CLAIM_REQUIRED: ${workId}`);
1621
- }
1622
- if (currentOwner) {
1623
- if (currentOwner.controllerType !== 'chatgpt') throw new Error(`CONTROLLER_RELAY_CHATGPT_ONLY: ${workId}`);
1624
- if (currentOwner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
1625
- if ((controllerSessionPrincipalId(currentOwner)) !== identity.principalId) {
1626
- throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
1627
- }
1628
- // Finalization may terminalize the Work before the prompt-required
1629
- // goal_complete disposition is submitted. A frozen MCP client may also
1630
- // rotate transport sessions between those two calls. Never resume or
1631
- // rewrite a terminal Work lease here; terminal authorization is fenced
1632
- // by the already-claimed relay lineage in submitControllerRoundDisposition.
1633
- if (!terminalRoundClosure) {
1634
- bindFacadeControllerOwnership(ctx, store, workId, { ...identity, controllerType: 'chatgpt' }, {
1635
- relayScopeId: typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : undefined,
1636
- });
1637
- }
1638
- }
1639
- const explicitRequirementId = typeof args.requirement_id === 'string' ? args.requirement_id.trim() : '';
1640
- if (explicitRequirementId && work.requirementId && explicitRequirementId !== work.requirementId) {
1641
- throw new Error(`CONTROLLER_RELAY_REQUIREMENT_MISMATCH: Work ${workId} belongs to ${work.requirementId}, not ${explicitRequirementId}`);
1642
- }
1643
- const rationale = typeof args.reason === 'string' ? args.reason.trim() : '';
1644
- const chatgptBinding = chatgptControllerRoundBinding(store, workId);
1645
- let relay: ControllerRoundRelayRecord;
1646
- let requirementAcceptance;
1647
- if (disposition === 'goal_complete' && work.requirementId) {
1648
- if (!rationale) throw new Error('REQUIREMENT_ACCEPTANCE_METADATA_REQUIRED: goal_complete for a Requirement-bound Work requires reason');
1649
- const completedGoal = completeRequirementGoal({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
1650
- workId,
1651
- identity,
1652
- requirementId: work.requirementId,
1653
- rationale,
1654
- relayScopeId: frozenControllerDisposition?.relayScopeId ?? (typeof args.relay_scope_id === 'string' ? args.relay_scope_id : undefined),
1655
- handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
1656
- stateFingerprint: typeof args.state_fingerprint === 'string' ? args.state_fingerprint : undefined,
1657
- bindingId: chatgptBinding?.bindingId,
1658
- maxRounds: typeof args.max_rounds === 'number' ? args.max_rounds : undefined,
1659
- maxRepeatedState: typeof args.max_repeated_state === 'number' ? args.max_repeated_state : undefined,
1660
- maxFailures: typeof args.max_failures === 'number' ? args.max_failures : undefined,
1661
- });
1662
- relay = completedGoal.relay;
1663
- requirementAcceptance = completedGoal.requirementAcceptance;
1664
- } else {
1665
- const existingRelay = disposition === 'goal_complete' ? getControllerRoundRelay(store, workId) : undefined;
1666
- if (existingRelay?.status === 'goal_complete' && existingRelay.disposition === 'goal_complete') {
1667
- relay = existingRelay;
1668
- } else {
1669
- try {
1670
- relay = submitControllerRoundDisposition({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
1671
- workId,
1672
- identity,
1673
- disposition: disposition as ControllerRoundDisposition,
1674
- executionQualityDecisions: args.execution_quality_decisions as Parameters<typeof submitControllerRoundDisposition>[1]['executionQualityDecisions'],
1675
- executionQualityAdjustmentResults: args.execution_quality_adjustment_results as Parameters<typeof submitControllerRoundDisposition>[1]['executionQualityAdjustmentResults'],
1676
- assistantContextDigest: typeof args.assistant_context_digest === 'string' ? args.assistant_context_digest : undefined,
1677
- assistantContextUsage: args.assistant_context_usage as Parameters<typeof submitControllerRoundDisposition>[1]['assistantContextUsage'],
1678
- relayScopeId: frozenControllerDisposition?.relayScopeId ?? (typeof args.relay_scope_id === 'string' ? args.relay_scope_id : undefined),
1679
- requirementId: typeof args.requirement_id === 'string' ? args.requirement_id : undefined,
1680
- handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
1681
- stateFingerprint: typeof args.state_fingerprint === 'string' ? args.state_fingerprint : undefined,
1682
- reason: typeof args.reason === 'string' ? args.reason : undefined,
1683
- bindingId: chatgptBinding?.bindingId,
1684
- maxRounds: typeof args.max_rounds === 'number' ? args.max_rounds : undefined,
1685
- maxRepeatedState: typeof args.max_repeated_state === 'number' ? args.max_repeated_state : undefined,
1686
- maxFailures: typeof args.max_failures === 'number' ? args.max_failures : undefined,
1687
- });
1688
- } catch (error) {
1689
- const raced = disposition === 'goal_complete' ? getControllerRoundRelay(store, workId) : undefined;
1690
- if (!raced || raced.status !== 'goal_complete' || raced.disposition !== 'goal_complete') throw error;
1691
- relay = raced;
1692
- }
1693
- }
1694
- }
1695
- const continuationSchedule = ensureControllerDispositionContinuation(
1696
- ctx.controllerHome,
1697
- repository.repoId,
1698
- relay,
1699
- );
1700
- return result(buildFacadeResult({
1701
- status: relay.status === 'blocked' ? 'blocked' : 'ok',
1702
- summary: relay.status === 'pending_release'
1703
- ? `Controller disposition ${relay.disposition} recorded; relay will dispatch only after the current lease is released.`
1704
- : continuationSchedule
1705
- ? `Controller disposition ${relay.disposition} recorded with status ${relay.status}; exact-Work continuation is now event-driven by ${continuationSchedule.trigger.eventName}.`
1706
- : `Controller disposition ${relay.disposition} recorded with status ${relay.status}.`,
1707
- data: { relay, ...(requirementAcceptance ? { requirementAcceptance } : {}), ...(continuationSchedule ? { continuationSchedule } : {}) },
1708
- }) as unknown as Record<string, unknown>, relay.status === 'blocked');
1709
- } catch (error) {
1710
- return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller disposition failed.', data: {} }) as unknown as Record<string, unknown>, true);
1711
- }
1712
- }
1713
- if (operation === 'controller_release') {
1714
- try {
1715
- const workId = String(args.work_id ?? '').trim();
1716
- const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
1717
- try {
1718
- assertFacadeControllerRoundAuthority(ctx, store, workId, args);
1719
- } catch (error) {
1720
- const observed = getControllerSession(store, workId);
1721
- const runtime = runtimeIdentitySnapshot(ctx);
1722
- const samePrincipalCanonicalRuntimeMigration = Boolean(
1723
- observed
1724
- && observed.controllerId === identity.controllerId
1725
- && observed.controllerType === identity.controllerType
1726
- && controllerSessionPrincipalId(observed) === identity.principalId
1727
- && (observed.controllerInstanceId?.trim() || '') !== identity.controllerInstanceId
1728
- && runtime.running
1729
- && runtime.runtimeInstanceId === identity.controllerInstanceId,
1730
- );
1731
- if (!samePrincipalCanonicalRuntimeMigration) throw error;
1732
- }
1733
- const observedOwner = getControllerSession(store, workId);
1734
- const work = getWorkContract(store, workId);
1735
- const terminalWork = work ? ['completed', 'failed', 'cancelled'].includes(work.status) : false;
1736
- const pendingTerminalRelay = !observedOwner && work?.status === 'completed'
1737
- ? getControllerRoundRelay(store, workId)
1738
- : undefined;
1739
- const retainedReleaseWitness = pendingTerminalRelay?.status === 'pending_release'
1740
- ? getRetainedControllerSession(store, workId)
1741
- : undefined;
1742
- if (retainedReleaseWitness && pendingTerminalRelay) {
1743
- const retainedPrincipal = controllerSessionPrincipalId(retainedReleaseWitness);
1744
- if (retainedReleaseWitness.controllerId !== identity.controllerId
1745
- || retainedReleaseWitness.controllerType !== identity.controllerType
1746
- || retainedPrincipal !== identity.principalId
1747
- || retainedReleaseWitness.claimGeneration !== pendingTerminalRelay.claimGeneration
1748
- || pendingTerminalRelay.controllerId !== identity.controllerId
1749
- || pendingTerminalRelay.principalId !== identity.principalId) {
1750
- throw new Error(`CONTROLLER_RELAY_RELEASE_FENCE_MISMATCH: ${workId}`);
1751
- }
1752
- }
1753
- const owner = observedOwner
1754
- ? terminalWork
1755
- ? (() => {
1756
- // A terminal Work cannot be rebound to a replacement MCP transport.
1757
- // Authenticate the caller against the observed durable controller epoch
1758
- // without mutating it; releaseControllerSessionWithAuthority remains the
1759
- // exact claim-generation CAS authority for physical lease release.
1760
- const ownerPrincipal = observedOwner.principalId?.trim() || observedOwner.controllerId;
1761
- const ownerInstanceId = observedOwner.controllerInstanceId?.trim() || '';
1762
- if (observedOwner.controllerId !== identity.controllerId) {
1763
- throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
1764
- }
1765
- if (observedOwner.controllerType !== identity.controllerType) {
1766
- throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${workId}`);
1767
- }
1768
- if (ownerPrincipal !== identity.principalId) {
1769
- throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
1770
- }
1771
- if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) {
1772
- throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
1773
- }
1774
- if (typeof observedOwner.claimGeneration !== 'number' || observedOwner.claimGeneration < 1) {
1775
- throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
1776
- }
1777
- return observedOwner;
1778
- })()
1779
- : bindFacadeControllerOwnership(ctx, store, workId, identity, {
1780
- relayScopeId: typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : undefined,
1781
- })
1782
- : undefined;
1783
- if (owner) {
1784
- const ownerPrincipal = controllerSessionPrincipalId(owner);
1785
- const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
1786
- if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
1787
- throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
1788
- }
1789
- const released = releaseControllerSessionWithAuthority(store, {
1790
- workId,
1791
- actor: `controller-release:${identity.controllerId}:${identity.controllerInstanceId}`,
1792
- authority: {
1793
- controllerId: owner.controllerId,
1794
- controllerType: owner.controllerType,
1795
- principalId: ownerPrincipal,
1796
- controllerInstanceId: ownerInstanceId,
1797
- claimGeneration: owner.claimGeneration,
1798
- },
1799
- });
1800
- if (!released.allowed) {
1801
- throw new Error(`WORK_CONTROLLER_RELEASE_FENCED: ${workId}:${released.reason}`);
1802
- }
1803
- }
1804
- const executionSession = readExecutionSession(ctx.controllerHome, identity);
1805
- if (executionSession?.activeWorkId === workId) {
1806
- updateExecutionSession(ctx.controllerHome, identity, { activeWorkId: undefined, lastValidatedAt: new Date().toISOString() });
1807
- }
1808
- const relayStore = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
1809
- const releasedSession = owner ?? retainedReleaseWitness;
1810
- const relay = releasedSession ? beginControllerRoundRelayAfterRelease(
1811
- relayStore,
1812
- { workId, releasedSession },
1813
- ) : undefined;
1814
- const abandonedRelay = owner && !relay
1815
- ? reconcileControllerRoundAfterAbandonedRelease(relayStore, { workId, releasedSession: owner })
1816
- : undefined;
1817
- if (relay?.status === 'dispatching') {
1818
- const relayWorkId = relay.originWorkId;
1819
- try {
1820
- assertAutomatedOperationAllowed('external_controller_wake', {
1821
- controller_type: 'chatgpt',
1822
- relay_scope_id: relay.relayScopeId,
1823
- requirement_id: relay.requirementId,
1824
- });
1825
- const prompt = renderChatgptControllerRoundPrompt(relayStore, relay);
1826
- const relayBinding = chatgptControllerRoundBinding(relayStore, relayWorkId);
1827
- const predecessorBinding = relayWorkId !== workId ? chatgptControllerRoundBinding(relayStore, workId) : undefined;
1828
- const dispatched = await runWorkChatgptContinuation({
1829
- controllerHome: ctx.controllerHome,
1830
- repoId: repository.repoId,
1831
- repoRoot: repository.canonicalRoot,
1832
- workId: relayWorkId,
1833
- prompt,
1834
- controllerAuthorityId: relay.authorityId,
1835
- relayScopeId: relay.relayScopeId,
1836
- browserSessionId: relayBinding?.browserSessionId ?? predecessorBinding?.browserSessionId,
1837
- conversationUrl: relayBinding?.conversationUrl ?? predecessorBinding?.conversationUrl,
1838
- tabPolicy: 'reuse',
1839
- });
1840
- if (dispatched.status === 'failed') throw new Error(`${dispatched.error?.code ?? 'CONTROLLER_RELAY_DISPATCH_FAILED'}:${dispatched.error?.message ?? 'Controller relay dispatch failed'}`);
1841
- // The next prompt is externally committed before this transition is
1842
- // recorded. Provider conversation context may be reused across the
1843
- // handoff, but the successor Work receives its own binding/authority.
1844
- recordChatgptControllerRoundTabSettlement(relayStore, {
1845
- workId: relayWorkId,
1846
- relayScopeId: relay.relayScopeId,
1847
- status: 'retained_for_immediate_continuation',
1848
- });
1849
- const updatedBinding = chatgptControllerRoundBinding(relayStore, relayWorkId);
1850
- const completed = finishControllerRoundRelayDispatch(
1851
- relayStore,
1852
- { workId: relayWorkId, ok: true, bindingId: updatedBinding?.bindingId },
1853
- );
1854
- return result(buildFacadeResult({
1855
- summary: relayWorkId === workId
1856
- ? `Controller lease released and immediate relay ${relay.relayScopeId} dispatched through the canonical ChatGPT launcher.`
1857
- : `Controller lease released; relay ${relay.relayScopeId} handed off from completed ${workId} to successor ${relayWorkId} and dispatched through the canonical ChatGPT launcher.`,
1858
- data: { relay: completed, dispatch: dispatched, ...(relayWorkId !== workId ? { predecessorWorkId: workId, successorWorkId: relayWorkId } : {}) },
1859
- }) as unknown as Record<string, unknown>);
1860
- } catch (relayError) {
1861
- const relayFailure = relayError instanceof Error ? relayError.message : String(relayError);
1862
- const failed = finishControllerRoundRelayDispatch(
1863
- relayStore,
1864
- { workId: relayWorkId, ok: false, error: relayFailure, outcomeUnknown: /OUTCOME_UNKNOWN/i.test(relayFailure) },
1865
- );
1866
- return result(buildFacadeResult({
1867
- status: 'blocked',
1868
- summary: `Controller lease released, but immediate relay dispatch failed: ${relayError instanceof Error ? relayError.message : String(relayError)}`,
1869
- data: { relay: failed },
1870
- }) as unknown as Record<string, unknown>, true);
1871
- }
1872
- }
1873
- const settledRelay = getControllerRoundRelay(relayStore, workId);
1874
- let tabSettlement;
1875
- if (
1876
- owner?.controllerType === 'chatgpt'
1877
- && settledRelay
1878
- && ['waiting', 'waiting_for_user', 'goal_complete', 'blocked', 'failed'].includes(settledRelay.status)
1879
- ) {
1880
- const binding = chatgptControllerRoundBinding(store, workId);
1881
- const browserSessionId = binding?.browserSessionId;
1882
- if (browserSessionId) {
1883
- tabSettlement = await settleWorkChatgptAutomationTab({
1884
- controllerHome: ctx.controllerHome,
1885
- workId,
1886
- browserSessionId,
1887
- });
1888
- recordChatgptControllerRoundTabSettlement(relayStore, {
1889
- workId,
1890
- relayScopeId: settledRelay.relayScopeId,
1891
- status: tabSettlement.status,
1892
- error: tabSettlement.error?.message,
1893
- });
1894
- }
1895
- }
1896
- return result(buildFacadeResult({
1897
- summary: abandonedRelay
1898
- ? 'Controller lease released; the claimed round was mechanically marked abandoned without semantic completion and can be relaunched through the bounded launcher path.'
1899
- : 'Controller lease released.',
1900
- data: { relay: getControllerRoundRelay(relayStore, workId) ?? abandonedRelay ?? relay, tabSettlement },
1901
- }) as unknown as Record<string, unknown>);
1902
- } catch (error) {
1903
- return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller release failed.', data: {} }) as unknown as Record<string, unknown>, true);
1904
- }
1905
- }
1906
- if (operation === 'launcher_start') {
1907
- try {
1908
- const controllerType = String(args.controller_type ?? 'codex');
1909
- if (!['chatgpt', 'codex', 'grok', 'claude'].includes(controllerType)) throw new Error('CONTROLLER_TYPE_INVALID');
1910
- const workId = String(args.work_id ?? '').trim();
1911
- const launchArgs = Array.isArray(args.launch_args) ? args.launch_args.map(String) : [];
1912
- if (controllerType === 'chatgpt') {
1913
- const work = getWorkContract(store, workId);
1914
- if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
1915
- const handoffId = typeof args.handoff_id === 'string' ? args.handoff_id.trim() : '';
1916
- const handoff = handoffId ? getHandoffItem(store, handoffId) : undefined;
1917
- const valueForFlag = (flag: string): string | undefined => {
1918
- const index = launchArgs.indexOf(flag);
1919
- if (index < 0) return undefined;
1920
- const value = launchArgs[index + 1];
1921
- if (!value || value.startsWith('--')) throw new Error(`CHATGPT_LAUNCH_ARG_VALUE_REQUIRED: ${flag}`);
1922
- return value;
1923
- };
1924
- const supportedFlags = new Set(['--model', '--reasoning', '--tab-policy', '--timeout-ms']);
1925
- for (let index = 0; index < launchArgs.length; index += 2) {
1926
- const flag = launchArgs[index];
1927
- if (!flag || !supportedFlags.has(flag)) throw new Error(`CHATGPT_LAUNCH_ARG_UNSUPPORTED: ${flag ?? ''}`);
1928
- if (!launchArgs[index + 1] || launchArgs[index + 1]!.startsWith('--')) throw new Error(`CHATGPT_LAUNCH_ARG_VALUE_REQUIRED: ${flag}`);
1929
- }
1930
- const reasoning = valueForFlag('--reasoning') ?? 'high';
1931
- if (!['medium', 'high', 'xhigh'].includes(reasoning)) throw new Error(`CHATGPT_LAUNCH_REASONING_INVALID: ${reasoning}`);
1932
- const tabPolicy = valueForFlag('--tab-policy') ?? 'auto';
1933
- if (!['auto', 'reuse', 'new'].includes(tabPolicy)) throw new Error(`CHATGPT_LAUNCH_TAB_POLICY_INVALID: ${tabPolicy}`);
1934
- const timeoutValue = valueForFlag('--timeout-ms');
1935
- const timeoutMs = timeoutValue === undefined ? undefined : Number(timeoutValue);
1936
- if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) throw new Error(`CHATGPT_LAUNCH_TIMEOUT_INVALID: ${timeoutValue}`);
1937
- const continuationPrompt = typeof args.continuation_prompt === 'string' ? args.continuation_prompt.trim() : '';
1938
- const relayStore = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
1939
- const existingBinding = chatgptControllerRoundBinding(relayStore, workId);
1940
- const relay = beginInitialControllerRoundDispatch(
1941
- relayStore,
1942
- {
1943
- workId,
1944
- identity: authenticatedFacadeControllerIdentity(ctx, args),
1945
- requirementId: work.requirementId,
1946
- bindingId: existingBinding?.bindingId,
1947
- },
1948
- );
1949
- if (relay.status === 'blocked') {
1950
- throw new Error(`CONTROLLER_RELAY_LAUNCH_BLOCKED: ${relay.blockedReason ?? relay.relayScopeId}`);
1951
- }
1952
- const prompt = [
1953
- renderChatgptControllerRoundPrompt(store, relay, { exactOriginWork: true }),
1954
- `Controller round: ${relay.relayScopeId}. launcher_start opened this durable round; dispatch success is not semantic completion.`,
1955
- handoff ? `Handoff: ${handoff.summary}\nNext: ${handoff.recommendedContinuationPrompt ?? handoff.recommendedPrompt}` : '',
1956
- continuationPrompt ? `Continuation: ${continuationPrompt}` : '',
1957
- ].filter(Boolean).join('\n');
1958
- let dispatched: Awaited<ReturnType<typeof runWorkChatgptContinuation>>;
1959
- try {
1960
- dispatched = await runWorkChatgptContinuation({
1961
- controllerHome: ctx.controllerHome,
1962
- repoId: repository.repoId,
1963
- repoRoot: repository.canonicalRoot,
1964
- workId,
1965
- prompt,
1966
- controllerAuthorityId: relay.authorityId,
1967
- relayScopeId: relay.relayScopeId,
1968
- browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
1969
- conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
1970
- model: valueForFlag('--model') ?? 'gpt-5.6',
1971
- reasoning: reasoning as 'medium' | 'high' | 'xhigh',
1972
- tabPolicy: tabPolicy as 'auto' | 'reuse' | 'new',
1973
- timeoutMs,
1974
- });
1975
- if (dispatched.status === 'failed') throw new Error(`${dispatched.error?.code ?? 'CHATGPT_WORK_CONTINUATION_FAILED'}:${dispatched.error?.message ?? 'ChatGPT Work continuation failed'}`);
1976
- } catch (launchError) {
1977
- const launchFailure = launchError instanceof Error ? launchError.message : String(launchError);
1978
- finishControllerRoundRelayDispatch(
1979
- { controllerHome: ctx.controllerHome, repoId: repository.repoId },
1980
- { workId, ok: false, error: launchFailure, outcomeUnknown: /OUTCOME_UNKNOWN/i.test(launchFailure) },
1981
- );
1982
- throw launchError;
1983
- }
1984
- const updatedBinding = chatgptControllerRoundBinding(relayStore, workId);
1985
- const completedRelay = finishControllerRoundRelayDispatch(
1986
- relayStore,
1987
- { workId, ok: true, bindingId: updatedBinding?.bindingId },
1988
- );
1989
- return result(buildFacadeResult({
1990
- summary: 'ChatGPT continuation dispatched; wake completion remains pending until the new ChatGPT Controller claims the Work, and semantic closure still requires an explicit disposition.',
1991
- data: {
1992
- workId,
1993
- relay: completedRelay,
1994
- browserSessionId: dispatched.browserSessionId,
1995
- conversationUrl: dispatched.conversationUrl,
1996
- executionPreferenceVerified: dispatched.executionPreferenceVerified,
1997
- },
1998
- }) as unknown as Record<string, unknown>);
1999
- }
2000
- const launched = await launchSuperController({ work: store, handoff: store }, {
2001
- controllerType: controllerType as 'codex' | 'grok' | 'claude',
2002
- executable: typeof args.executable === 'string' && args.executable.trim() ? args.executable.trim() : undefined,
2003
- args: launchArgs,
2004
- workId,
2005
- launchReservationMs: typeof args.launch_reservation_ms === 'number' ? args.launch_reservation_ms : typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
2006
- handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
2007
- browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
2008
- conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
2009
- continuationPrompt: typeof args.continuation_prompt === 'string' ? args.continuation_prompt : undefined,
2010
- cwd: repository.canonicalRoot,
2011
- });
2012
- return result(buildFacadeResult({ summary: `Thin Launcher started ${launched.controllerType}.`, data: { pid: launched.pid, executable: launched.executable, workId, reservationId: launched.reservationId } }) as unknown as Record<string, unknown>);
2013
- } catch (error) {
2014
- return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Launcher failed.', data: {} }) as unknown as Record<string, unknown>, true);
2015
- }
2016
- }
2017
- if (operation === 'plan_list') {
2018
- const plans = listPlanContracts({ ...store, status: 'active', limit: typeof args.limit === 'number' ? args.limit : 20 });
2019
- const facade = buildFacadeResult({
2020
- summary: `${plans.length} active PlanContract(s) in this repository.`,
2021
- data: { plans: plans.map(summarizePlanContract), bounded: true },
2022
- });
2023
- return result(facade as unknown as Record<string, unknown>);
2024
- }
2025
- if (operation === 'plan_get') {
2026
- const plan = getPlanContract(store, String(args.plan_id ?? ''));
2027
- const facade = plan
2028
- ? buildFacadeResult({ summary: `PlanContract ${plan.planId} retrieved.`, data: { plan: args.detail_level === 'detail' ? plan : summarizePlanContract(plan) }, detailLevel: args.detail_level === 'detail' ? 'detail' : 'summary' })
2029
- : buildFacadeResult({ status: 'not_found', summary: `PlanContract ${String(args.plan_id ?? '')} not found.`, data: { planId: String(args.plan_id ?? '') } });
2030
- return result(facade as unknown as Record<string, unknown>, !plan);
2031
- }
2032
-
2033
- const checks = listControllerChecks(repository.canonicalRoot);
2034
- const workloopSource = freshGitIdentity(repository.canonicalRoot);
2035
- const workloopStatus = repositoryGitStatus(repository);
2036
- const workloopChangedPaths = [...new Set([
2037
- ...workloopStatus.staged,
2038
- ...workloopStatus.unstaged,
2039
- ...workloopStatus.untracked,
2040
- ])].sort();
2041
- const workloopCtx = {
2042
- workStore: store,
2043
- handoffStore: store,
2044
- planStore: store,
2045
- repoId: repository.repoId,
2046
- checkoutId: repository.activeCheckoutId,
2047
- principalId: ctx.principalId,
2048
- controllerInstanceId: ctx.controllerInstanceId,
2049
- availableChecks: checks,
2050
- sourceRevision: workloopSource.head ?? undefined,
2051
- sourceBaseState: workloopSource.head ? 'revision' as const : 'unborn' as const,
2052
- workspaceDirty: !workloopStatus.clean,
2053
- workspaceChangedPaths: workloopChangedPaths,
2054
- materializeIsolatedWorkspace: ({ workId, title, baseRef, needsDependencies }: { workId: string; title: string; baseRef?: string; needsDependencies?: boolean }) => {
2055
- const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
2056
- requestId: workId,
2057
- title,
2058
- baseRef,
2059
- prepareDependencies: needsDependencies === true,
2060
- });
2061
- if (!workspace.managed || !workspace.checkoutId || !workspace.root) throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
2062
- return { checkoutId: workspace.checkoutId, root: workspace.root, baseRevision: workspace.baseRevision, managed: true as const };
2063
- },
2064
- };
2065
-
2066
- if (operation === 'requirement_continue') {
2067
- const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id.trim() : '';
2068
- if (!requirementId) {
2069
- return result(buildFacadeResult({
2070
- status: 'blocked',
2071
- summary: 'REQUIREMENT_CONTINUE_INPUT_REQUIRED: requirement_id is required.',
2072
- data: { requirementResumed: false },
2073
- }) as unknown as Record<string, unknown>, true);
2074
- }
2075
- try {
2076
- const continued = continueRequirement({ controllerHome: ctx.controllerHome }, requirementId);
2077
- return result(buildFacadeResult({
2078
- summary: continued.resumed
2079
- ? `Requirement ${continued.requirement.requirementId} resumed from waiting_for_user to active by explicit semantic continue.`
2080
- : `REQUIREMENT_ALREADY_ACTIVE: ${continued.requirement.requirementId}. Explicit continue is idempotent.`,
2081
- data: {
2082
- requirement: continued.requirement,
2083
- requirementResumed: continued.resumed,
2084
- semanticDecision: 'continue',
2085
- },
2086
- suggestedNextActions: [],
2087
- }) as unknown as Record<string, unknown>);
2088
- } catch (error) {
2089
- return result(buildFacadeResult({
2090
- status: 'blocked',
2091
- summary: error instanceof Error ? error.message : String(error),
2092
- data: { requirementResumed: false },
2093
- suggestedNextActions: [],
2094
- }) as unknown as Record<string, unknown>, true);
2095
- }
2096
- }
2097
-
2098
- if (operation === 'requirement_create') {
2099
- const compatibilityArgs = frozenSemanticOperation?.operation === 'requirement_create' ? frozenSemanticOperation.args : undefined;
2100
- const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id : '';
2101
- const title = compatibilityArgs?.requirement_title ?? (typeof args.requirement_title === 'string' ? args.requirement_title : '');
2102
- const outcomeStatement = compatibilityArgs?.requirement_outcome ?? (typeof args.requirement_outcome === 'string' ? args.requirement_outcome : '');
2103
- if (!requirementId.trim() || !title.trim() || !outcomeStatement.trim()) {
2104
- return result(buildFacadeResult({
2105
- status: 'blocked',
2106
- summary: 'REQUIREMENT_CREATE_INPUT_REQUIRED: requirement_id, requirement_title, and requirement_outcome are required.',
2107
- data: { requirementCreated: false },
2108
- }) as unknown as Record<string, unknown>, true);
2109
- }
2110
- let admission;
2111
- try {
2112
- admission = admitRequirement({ controllerHome: ctx.controllerHome }, {
2113
- requirementId,
2114
- title,
2115
- outcomeStatement,
2116
- acceptanceCriteria: compatibilityArgs?.requirement_acceptance_criteria
2117
- ?? (Array.isArray(args.requirement_acceptance_criteria) ? args.requirement_acceptance_criteria.map(String) : []),
2118
- requiredDeliveryReferences: compatibilityArgs?.requirement_delivery_references
2119
- ?? (Array.isArray(args.requirement_delivery_references) ? args.requirement_delivery_references.map(String) : []),
2120
- legacyAliases: compatibilityArgs?.requirement_legacy_aliases
2121
- ?? (Array.isArray(args.requirement_legacy_aliases) ? args.requirement_legacy_aliases.map(String) : []),
2122
- });
2123
- } catch (error) {
2124
- return result(buildFacadeResult({
2125
- status: 'blocked',
2126
- summary: error instanceof Error ? error.message : String(error),
2127
- data: { requirementCreated: false },
2128
- }) as unknown as Record<string, unknown>, true);
2129
- }
2130
- if (admission.decision === 'existing_conflict') {
2131
- return result(buildFacadeResult({
2132
- status: 'blocked',
2133
- summary: `REQUIREMENT_ALREADY_EXISTS_CONFLICT: ${admission.requirement.requirementId}. Existing Requirement authority was not changed.`,
2134
- data: { requirement: admission.requirement, requirementCreated: false, admissionDecision: admission.decision },
2135
- suggestedNextActions: [],
2136
- }) as unknown as Record<string, unknown>, true);
2137
- }
2138
- return result(buildFacadeResult({
2139
- summary: admission.decision === 'created'
2140
- ? `Requirement ${admission.requirement.requirementId} created. Requirement authority does not imply a Plan; Controller chooses the next action.`
2141
- : `REQUIREMENT_AUTHORITY_REUSED: ${admission.requirement.requirementId}. Requirement authority does not imply a Plan; Controller chooses the next action.`,
2142
- data: { requirement: admission.requirement, requirementCreated: admission.created, admissionDecision: admission.decision },
2143
- suggestedNextActions: [],
2144
- }) as unknown as Record<string, unknown>);
2145
- }
2146
-
2147
- if (operation.startsWith('plan_')) {
2148
- try {
2149
- if (operation === 'plan_create') {
2150
- const rawSteps = Array.isArray(args.plan_steps) ? args.plan_steps : [];
2151
- const requestedPlanId = String(args.plan_id ?? '').trim();
2152
- const requestedRequirementId = typeof args.requirement_id === 'string' && args.requirement_id.trim() ? args.requirement_id.trim() : undefined;
2153
- const requestedPlanRelation: 'extend' | 'parallel' | undefined = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
2154
- ? args.plan_relation
2155
- : undefined;
2156
- const relatedPlanId = typeof args.related_plan_id === 'string' && args.related_plan_id.trim() ? args.related_plan_id.trim() : undefined;
2157
- if (requestedRequirementId && !readRequirement({ controllerHome: ctx.controllerHome }, requestedRequirementId)) {
2158
- const facade = buildFacadeResult({
2159
- status: 'failed',
2160
- summary: `PLAN_REQUIREMENT_NOT_FOUND: ${requestedRequirementId}. Plan was not persisted; create or reconcile the Requirement authority first.`,
2161
- data: { executionStarted: false, planContractCreated: false, admissionDecision: 'missing_requirement', requirementId: requestedRequirementId },
2162
- });
2163
- return result(facade as unknown as Record<string, unknown>, true);
2164
- }
2165
- const admissionInput = {
2166
- requirementId: requestedRequirementId,
2167
- scopeKey: String(args.scope_key ?? ''),
2168
- planRelation: requestedPlanRelation,
2169
- relatedPlanId,
2170
- };
2171
- const renderPlanAdmission = (admission: ReturnType<typeof resolvePlanAdmission>): CallToolResult | undefined => {
2172
- if (admission.admissionDecision === 'create_new') return undefined;
2173
- if (admission.reason === 'exact_scope_authority' && admission.plan) {
2174
- const exactDraftRepair = admission.plan.status === 'draft' && requestedPlanId === admission.plan.planId;
2175
- const facade = buildFacadeResult({
2176
- summary: exactDraftRepair
2177
- ? `PLAN_DRAFT_REPAIR_REQUIRED: draft Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; preserve that authority and amend it through rh_work repair.`
2178
- : `PLAN_AUTHORITY_REUSED: active Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; no duplicate draft was created.`,
2179
- data: {
2180
- plan: summarizePlanContract(admission.plan),
2181
- executionStarted: false,
2182
- planContractCreated: false,
2183
- admissionDecision: 'reuse_existing',
2184
- resolutionRequired: false,
2185
- ...(exactDraftRepair ? { repairRequired: true } : {}),
2186
- },
2187
- suggestedNextActions: exactDraftRepair
2188
- ? [{
2189
- label: 'Repair this exact draft Plan',
2190
- tool: 'rh_work',
2191
- operation: 'repair',
2192
- payload: {
2193
- plan_id: admission.plan.planId,
2194
- repair_operation: 'repair',
2195
- dry_run: false,
2196
- scope_key: args.scope_key,
2197
- source_revision: args.source_revision,
2198
- objective: args.objective,
2199
- plan_steps: args.plan_steps,
2200
- non_goals: args.non_goals,
2201
- assumptions: args.assumptions,
2202
- resolved_decisions: args.resolved_decisions,
2203
- stop_conditions: args.stop_conditions,
2204
- replan_conditions: args.replan_conditions,
2205
- integration_strategy: args.integration_strategy,
2206
- },
2207
- risk: 'workspace_write',
2208
- confidence: 'high',
2209
- }]
2210
- : [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: admission.plan.planId }, risk: 'readonly', confidence: 'high' }],
2211
- });
2212
- return result(facade as unknown as Record<string, unknown>);
2213
- }
2214
- if (admission.reason === 'extension_target_required') {
2215
- const facade = buildFacadeResult({
2216
- summary: `PLAN_EXTENSION_TARGET_REQUIRED: select related_plan_id from the active Plan slices for Requirement ${requestedRequirementId}.`,
2217
- data: { executionStarted: false, planContractCreated: false, admissionDecision: 'resolution_required', resolutionRequired: true, candidates: admission.candidates.map(summarizePlanContract) },
2218
- });
2219
- return result(facade as unknown as Record<string, unknown>);
2220
- }
2221
- if (admission.reason === 'extend_existing' && admission.plan) {
2222
- // plan_create + plan_relation=extend is a compatibility transport
2223
- // for revising the explicitly related stable Plan identity. Preflight
2224
- // must continue into atomic admission; no successor Plan is minted.
2225
- return undefined;
2226
- }
2227
- throw new Error(`PLAN_ADMISSION_RESULT_INVALID: ${admission.admissionDecision}:${admission.reason}`);
2228
- };
2229
- const plans = listPlanContracts({ ...store, status: 'all', limit: 100 });
2230
- const preflightAdmission = resolvePlanAdmission(plans, admissionInput);
2231
- const preflightResult = renderPlanAdmission(preflightAdmission);
2232
- if (preflightResult) return preflightResult;
2233
- const requestedPlanCheckIds = rawSteps
2234
- .filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step))
2235
- .flatMap((step) => Array.isArray(step.check_ids) ? step.check_ids.map(String) : []);
2236
- const normalizedPlanChecks = normalizeCheckIds(requestedPlanCheckIds, checks);
2237
- if (normalizedPlanChecks.invalidCheckIds.length > 0) {
2238
- const facade = buildFacadeResult({
2239
- status: 'failed',
2240
- summary: `PLAN_CHECKS_INVALID: ${normalizedPlanChecks.invalidCheckIds.join(', ')}. Plan was not persisted; select replacement IDs from registeredCheckIds in this response, then request readiness only for the checks you choose.`,
2241
- data: {
2242
- executionStarted: false,
2243
- planContractCreated: false,
2244
- admissionDecision: 'invalid_checks',
2245
- normalizedChecks: normalizedPlanChecks,
2246
- registeredCheckIds: checks.map((check) => check.id).slice(0, 80),
2247
- },
2248
- suggestedNextActions: [],
2249
- });
2250
- return result(facade as unknown as Record<string, unknown>, true);
2251
- }
2252
- const admitted = await admitPlanContractAsync(store, {
2253
- planId: String(args.plan_id ?? ''),
2254
- repoId: repository.repoId,
2255
- requirementId: requestedRequirementId,
2256
- scopeKey: String(args.scope_key ?? ''),
2257
- planRelation: requestedPlanRelation,
2258
- relatedPlanId,
2259
- sourceRevision: String(args.source_revision ?? ''),
2260
- goal: String(args.objective ?? ''),
2261
- nonGoals: Array.isArray(args.non_goals) ? args.non_goals.map(String) : undefined,
2262
- assumptions: Array.isArray(args.assumptions) ? args.assumptions.map(String) : undefined,
2263
- resolvedDecisions: Array.isArray(args.resolved_decisions) ? args.resolved_decisions.map(String) : undefined,
2264
- stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : undefined,
2265
- replanConditions: Array.isArray(args.replan_conditions) ? args.replan_conditions.map(String) : undefined,
2266
- integrationStrategy: typeof args.integration_strategy === 'string' ? args.integration_strategy : undefined,
2267
- obligationDispositions: planObligationDispositionsFromArgs(args.obligation_dispositions),
2268
- steps: rawSteps.filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step)).map((step) => ({
2269
- id: String(step.id ?? ''),
2270
- objective: String(step.objective ?? ''),
2271
- dependencies: Array.isArray(step.dependencies) ? step.dependencies.map(String) : [],
2272
- authoritativeFiles: Array.isArray(step.authoritative_files) ? step.authoritative_files.map(String) : [],
2273
- allowedPaths: Array.isArray(step.allowed_paths) ? step.allowed_paths.map(String) : [],
2274
- forbiddenPaths: Array.isArray(step.forbidden_paths) ? step.forbidden_paths.map(String) : [],
2275
- checks: Array.isArray(step.check_ids) ? step.check_ids.map(String) : [],
2276
- acceptanceCriteria: Array.isArray(step.acceptance_criteria) ? step.acceptance_criteria.map(String) : [],
2277
- })),
2278
- });
2279
- if (admitted.reason === 'extend_existing' && admitted.plan) {
2280
- const plan = admitted.plan;
2281
- const requestedLabel = requestedPlanId && requestedPlanId !== plan.planId ? ` Requested compatibility plan_id ${requestedPlanId} was retained only as revision audit metadata.` : '';
2282
- const facade = buildFacadeResult({
2283
- summary: `PLAN_REVISION_REUSED_AUTHORITY: Plan ${plan.planId} was revised in place; no successor PlanContract was created.${requestedLabel}`,
2284
- data: {
2285
- plan: summarizePlanContract(plan),
2286
- executionStarted: false,
2287
- planContractCreated: false,
2288
- admissionDecision: 'reuse_existing',
2289
- resolutionRequired: false,
2290
- },
2291
- suggestedNextActions: [{ label: 'Approve revised Plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: plan.planId }, risk: 'workspace_write', confidence: 'high' }],
2292
- });
2293
- return result(facade as unknown as Record<string, unknown>);
2294
- }
2295
- const racedAdmissionResult = renderPlanAdmission(admitted);
2296
- if (racedAdmissionResult) return racedAdmissionResult;
2297
- if (!admitted.plan) throw new Error('PLAN_ADMISSION_CREATE_MISSING_PLAN');
2298
- const plan = admitted.plan;
2299
- const facade = buildFacadeResult({
2300
- summary: `PlanContract ${plan.planId} created as draft after atomic authority admission; no execution was started.`,
2301
- data: { plan: summarizePlanContract(plan), executionStarted: false, planContractCreated: true, admissionDecision: 'create_new' },
2302
- suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: plan.planId }, risk: 'workspace_write', confidence: 'medium' }],
2303
- });
2304
- return result(facade as unknown as Record<string, unknown>);
2305
- }
2306
- if (operation === 'plan_approve') {
2307
- const plan = await approvePlanContractAsync(store, String(args.plan_id ?? ''));
2308
- const facade = buildFacadeResult({
2309
- summary: `PlanContract ${plan.planId} approved at source revision ${plan.sourceRevision}; execution remains explicit.`,
2310
- data: { plan: summarizePlanContract(plan), executionStarted: false },
2311
- });
2312
- return result(facade as unknown as Record<string, unknown>);
2313
- }
2314
- if (operation === 'plan_accept_step') {
2315
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
2316
- const planId = String(args.plan_id ?? '').trim();
2317
- const stepId = String(args.plan_step_id ?? '').trim();
2318
- const rationale = String(args.acceptance_rationale ?? '').trim();
2319
- const before = getPlanContract(store, planId);
2320
- const beforeStep = before?.steps.find((candidate) => candidate.id === stepId);
2321
- const predecessorWorkId = beforeStep?.workId?.trim();
2322
- const predecessorWork = predecessorWorkId ? getWorkContract(store, predecessorWorkId) : undefined;
2323
- const claimedRelay = predecessorWorkId ? getControllerRoundRelay(store, predecessorWorkId) : undefined;
2324
- const currentOwner = predecessorWorkId ? getControllerSession(store, predecessorWorkId) : undefined;
2325
- const claimedTerminalRound = Boolean(
2326
- predecessorWork
2327
- && predecessorWork.status === 'completed'
2328
- && claimedRelay?.status === 'claimed'
2329
- );
2330
- if (claimedTerminalRound && predecessorWorkId) {
2331
- // Semantic acceptance may occur after MCP/Runtime replacement or
2332
- // physical Work-lease release, but a still-open terminal round is
2333
- // fenced by its exact opaque capability. Never reclaim the Work.
2334
- assertFacadeControllerRoundAuthority(ctx, store, predecessorWorkId, args);
2335
- if (currentOwner) {
2336
- if (currentOwner.controllerType !== identity.controllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${predecessorWorkId}`);
2337
- if (currentOwner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${predecessorWorkId}`);
2338
- if (controllerSessionPrincipalId(currentOwner) !== identity.principalId) throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${predecessorWorkId}`);
2339
- }
2340
- }
2341
- const plan = acceptPlanStepEvidence(store, {
2342
- planId,
2343
- stepId,
2344
- reviewer: identity.principalId,
2345
- rationale,
2346
- acceptedSourceRevision: workloopCtx.sourceRevision,
2347
- });
2348
- const facade = buildFacadeResult({
2349
- summary: `Plan step ${stepId} semantically accepted by the current Controller. Successor execution remains an explicit Controller start.`,
2350
- data: {
2351
- plan: summarizePlanContract(plan),
2352
- semanticAcceptanceRecorded: true,
2353
- reviewer: identity.principalId,
2354
- ...(predecessorWorkId ? { predecessorWorkId } : {}),
2355
- successorAdmissionRequired: plan.status !== 'finalized',
2356
- },
2357
- suggestedNextActions: plan.status === 'finalized'
2358
- ? []
2359
- : [{ label: 'Read the next approved Plan step', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: plan.planId }, risk: 'readonly', confidence: 'high' }],
2360
- });
2361
- return result(facade as unknown as Record<string, unknown>);
2362
- }
2363
- const plan = supersedePlanContract(store, String(args.plan_id ?? ''), String(args.superseded_by ?? ''));
2364
- const facade = buildFacadeResult({ summary: `PlanContract ${plan.planId} superseded by ${plan.supersededBy}.`, data: { plan: summarizePlanContract(plan) } });
2365
- return result(facade as unknown as Record<string, unknown>);
2366
- } catch (error) {
2367
- const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PlanContract operation failed.', data: { operation, executionStarted: false } });
2368
- return result(facade as unknown as Record<string, unknown>, true);
2369
- }
2370
- }
2371
-
2372
- if (operation === 'repair' && args.capability_id === 'recovery.migrate_controller_home') {
2373
- const workId = String(args.work_id ?? '').trim();
2374
- if (!workId) {
2375
- const blocked = buildFacadeResult({ status: 'blocked', summary: 'RECOVERY_CONTROLLER_HOME_MIGRATION_WORK_REQUIRED', data: { executionStarted: false } });
2376
- return result(blocked as unknown as Record<string, unknown>, true);
2377
- }
2378
- try {
2379
- const work = getWorkContract(store, workId);
2380
- if (!work || ['completed', 'failed', 'cancelled'].includes(work.status)) {
2381
- throw new Error(`RECOVERY_CONTROLLER_HOME_MIGRATION_ACTIVE_WORK_REQUIRED: ${workId}`);
2382
- }
2383
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
2384
- const owner = getControllerSession(store, workId);
2385
- if (!owner || controllerSessionPrincipalId(owner) !== identity.principalId || owner.sessionId !== identity.sessionId) {
2386
- throw new Error(`RECOVERY_CONTROLLER_HOME_MIGRATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
2387
- }
2388
- const liveGit = gitSnapshot(repository.canonicalRoot);
2389
- if (!liveGit.head) throw new Error('RECOVERY_CONTROLLER_HOME_MIGRATION_SOURCE_REVISION_REQUIRED');
2390
- const migrationRequestId = typeof args.request_id === 'string' && args.request_id.trim()
2391
- ? args.request_id.trim()
2392
- : `controller-home-migration:${workId}`;
2393
- const scheduled = await callStandaloneRecoveryTool(ctx.controllerHome, 'migrate_controller_home', {
2394
- request_id: migrationRequestId,
2395
- canonical_source_root: repository.canonicalRoot,
2396
- expected_source_revision: liveGit.head,
2397
- });
2398
- const facade = buildFacadeResult({
2399
- summary: `Standalone Recovery accepted the Controller Home migration transaction for Work ${workId}.`,
2400
- data: { workId, migration: scheduled, executionStarted: true },
2401
- });
2402
- return result(facade as unknown as Record<string, unknown>);
2403
- } catch (error) {
2404
- const facade = buildFacadeResult({
2405
- status: 'blocked',
2406
- summary: error instanceof Error ? error.message : 'Controller Home migration scheduling failed.',
2407
- data: { workId, executionStarted: false },
2408
- });
2409
- return result(facade as unknown as Record<string, unknown>, true);
2410
- }
2411
- }
2412
-
2413
- if (operation === 'repair') {
2414
- return await runFacadeRepair(ctx, repository, args);
2415
- }
2416
-
2417
- if (operation === 'verify') {
2418
- const workId = String(args.work_id ?? '').trim();
2419
- try {
2420
- if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
2421
- } catch (error) {
2422
- const blocked = buildFacadeResult({
2423
- status: 'blocked',
2424
- summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`,
2425
- data: { workId, verificationStarted: false },
2426
- });
2427
- return result(blocked as unknown as Record<string, unknown>, true);
2428
- }
2429
- return await runFacadeVerify(ctx, repository, args);
2430
- }
2431
-
2432
- if (operation === 'review') {
2433
- const workId = String(args.work_id ?? '').trim();
2434
- try {
2435
- if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
2436
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
2437
- const reconciled = workId ? reconcileTerminalFacadeWorkVerifications(ctx, repository, workId) : undefined;
2438
- if (!workId || !reconciled?.sourceRevision || !reconciled.workspaceFingerprint || !reconciled.implementationReviewWorkspaceFingerprint) {
2439
- throw new Error(`WORK_IMPLEMENTATION_REVIEW_SOURCE_IDENTITY_REQUIRED: ${workId || 'work_id_missing'}`);
2440
- }
2441
- const facade = runGoalWorkloop({
2442
- ...workloopCtx,
2443
- principalId: identity.principalId,
2444
- controllerInstanceId: identity.controllerInstanceId,
2445
- sourceRevision: reconciled.sourceRevision,
2446
- workspaceFingerprint: reconciled.workspaceFingerprint,
2447
- implementationReviewWorkspaceFingerprint: reconciled.implementationReviewWorkspaceFingerprint,
2448
- workspaceChangedPaths: reconciled.workspaceChangedPaths,
2449
- workBoundProcessEvidenceIds: reconciled.workBoundProcessEvidenceIds,
2450
- }, 'review', args);
2451
- return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked' || facade.status === 'failed' || facade.status === 'not_found');
2452
- } catch (error) {
2453
- const blocked = buildFacadeResult({
2454
- status: 'blocked',
2455
- summary: error instanceof Error ? error.message : `Work ${workId} implementation review failed.`,
2456
- data: { workId, implementationReviewRecorded: false },
2457
- });
2458
- return result(blocked as unknown as Record<string, unknown>, true);
2459
- }
2460
- }
2461
-
2462
- if (operation === 'delegate') {
2463
- const facade = delegateToCodexCerebellum(
2464
- { repoId: repository.repoId },
2465
- {
2466
- workId: typeof args.work_id === 'string' ? args.work_id : undefined,
2467
- target: args.target === 'grok' || args.target === 'claude' || args.target === 'codex' ? args.target : 'codex',
2468
- objective: typeof args.objective === 'string' ? args.objective : 'Delegated cerebellum work',
2469
- acceptanceCriteria: Array.isArray(args.acceptance_criteria) ? args.acceptance_criteria.map(String) : undefined,
2470
- allowedPaths: Array.isArray(args.allowed_paths) ? args.allowed_paths.map(String) : undefined,
2471
- forbiddenPaths: Array.isArray(args.forbidden_paths) ? args.forbidden_paths.map(String) : undefined,
2472
- available: typeof args.available === 'boolean' ? args.available : undefined,
2473
- codexAvailable: args.codex_available !== false,
2474
- workerOutput: args.worker_output && typeof args.worker_output === 'object' && !Array.isArray(args.worker_output)
2475
- ? args.worker_output as { uncertain?: boolean; summary?: string; patchProposal?: string; evidenceSummary?: string }
2476
- : undefined,
2477
- },
2478
- );
2479
- return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked');
2480
- }
2481
-
2482
- if (operation === 'stop') {
2483
- const workId = String(args.work_id ?? '').trim();
2484
- const existingWork = workId ? getWorkContract(store, workId) : undefined;
2485
- const terminalCleanupOnly = Boolean(
2486
- existingWork
2487
- && ['completed', 'failed', 'cancelled'].includes(existingWork.status)
2488
- && args.cleanup !== false,
2489
- );
2490
- if (terminalCleanupOnly && existingWork) {
2491
- // Terminal resource cleanup is not semantic terminalization. Never
2492
- // reacquire/reopen Controller ownership merely to settle an outcome
2493
- // that is already durable. Active ownership/rounds still fence the
2494
- // cleanup path, while the lower terminal cleanup authority preserves
2495
- // dirty/unique source before removing Work-owned resources.
2496
- const owner = getControllerSession(store, workId);
2497
- let cleanupAuthority: ControllerTerminalizationAuthority | undefined;
2498
- if (owner) {
2499
- try {
2500
- cleanupAuthority = currentTerminalCleanupAuthority(ctx, store, workId, args);
2501
- } catch (error) {
2502
- return result(buildFacadeResult({
2503
- status: 'blocked',
2504
- summary: error instanceof Error ? error.message : `Work ${workId} terminal cleanup authority check failed.`,
2505
- data: { workId, terminalizationApplied: false, cleanupOnly: true },
2506
- }) as unknown as Record<string, unknown>, true);
2507
- }
2508
- }
2509
- const relay = getControllerRoundRelay(store, workId);
2510
- if (relay && !['goal_complete', 'handed_off', 'failed'].includes(relay.status)) {
2511
- return result(buildFacadeResult({
2512
- status: 'blocked',
2513
- summary: `WORK_TERMINAL_CLEANUP_ACTIVE_ROUND: ${workId}:${relay.status}.`,
2514
- data: { workId, terminalizationApplied: false, cleanupOnly: true, relayStatus: relay.status },
2515
- }) as unknown as Record<string, unknown>, true);
2516
- }
2517
- try {
2518
- const cleanup = await reconcileSingleTerminalWorkCleanup(
2519
- ctx.controllerHome,
2520
- repository.repoId,
2521
- workId,
2522
- {
2523
- targetBranch: typeof args.target_branch === 'string' ? args.target_branch : undefined,
2524
- deleteBranch: args.delete_branch !== false,
2525
- controllerAuthority: cleanupAuthority,
2526
- },
2527
- );
2528
- const cleanupCompleted = cleanup.status === 'cleaned';
2529
- const cleanupRetained = cleanup.status === 'retained';
2530
- const cleanupSettled = cleanupCompleted || cleanupRetained || cleanup.status === 'no_handle';
2531
- return result(buildFacadeResult({
2532
- status: cleanupSettled ? 'ok' : 'blocked',
2533
- summary: cleanupCompleted
2534
- ? `Terminal Work ${workId} outcome was preserved; managed repository cleanup completed without reopening Controller ownership.`
2535
- : cleanupRetained
2536
- ? `Terminal Work ${workId} outcome was preserved; managed repository retention was recorded durably.`
2537
- : cleanup.status === 'no_handle'
2538
- ? `Terminal Work ${workId} has no managed repository resources requiring cleanup.`
2539
- : `Terminal Work ${workId} outcome was preserved; managed repository cleanup remains incomplete and visible for retry.`,
2540
- data: {
2541
- work: summarizeWorkContract(existingWork),
2542
- finalStatus: existingWork.status,
2543
- terminalizationApplied: false,
2544
- cleanupOnly: true,
2545
- worktreeDeleted: cleanupCompleted,
2546
- cleanupPending: !cleanupSettled,
2547
- cleanupRetained,
2548
- lifecycleCleanup: cleanup,
2549
- },
2550
- }) as unknown as Record<string, unknown>, !cleanupSettled);
2551
- } catch (error) {
2552
- return result(buildFacadeResult({
2553
- status: 'blocked',
2554
- summary: `Terminal Work ${workId} cleanup-only reconciliation failed: ${error instanceof Error ? error.message : String(error)}`,
2555
- data: {
2556
- work: summarizeWorkContract(existingWork),
2557
- finalStatus: existingWork.status,
2558
- terminalizationApplied: false,
2559
- cleanupOnly: true,
2560
- worktreeDeleted: false,
2561
- cleanupPending: true,
2562
- },
2563
- }) as unknown as Record<string, unknown>, true);
2564
- }
2565
- }
2566
- const observedOwner = workId ? getControllerSession(store, workId) : undefined;
2567
- const observedRelay = workId ? getControllerRoundRelay(store, workId) : undefined;
2568
- const activeWorkProcess = workId
2569
- ? processRuntimeResourceDiagnostics().activeProcessIds
2570
- .map((processId) => getProcessRecord(ctx.controllerHome, repository.repoId, processId))
2571
- .some((process) => Boolean(process && process.workId === workId && isManagedProcessActive(process)))
2572
- : false;
2573
- // Maintenance stop is intentionally narrower than ControllerRound recovery.
2574
- // Once no Controller owns the Work, no Process is active, and the relay is
2575
- // absent or terminally failed/handed-off, the obsolete round capability must
2576
- // not make the durable Work immortal. The ControllerSession task lock below
2577
- // still fences a concurrent fresh claim before Work terminalization.
2578
- const ownerlessMaintenanceStop = Boolean(
2579
- workId
2580
- && !observedOwner
2581
- && !activeWorkProcess
2582
- && Boolean(observedRelay)
2583
- && ['failed', 'handed_off'].includes(observedRelay!.status),
2584
- );
2585
- if (!ownerlessMaintenanceStop) {
2586
- try {
2587
- if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
2588
- } catch (error) {
2589
- const blocked = buildFacadeResult({
2590
- status: 'blocked',
2591
- summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`,
2592
- data: { workId, terminalizationApplied: false },
2593
- });
2594
- return result(blocked as unknown as Record<string, unknown>, true);
2595
- }
2596
- }
2597
- let terminalizationAuthority: ControllerTerminalizationAuthority | undefined;
2598
- if (!ownerlessMaintenanceStop) {
2599
- try {
2600
- terminalizationAuthority = currentFacadeTerminalizationAuthority(ctx, store, workId, args);
2601
- } catch (error) {
2602
- const blocked = buildFacadeResult({
2603
- status: 'blocked',
2604
- summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`,
2605
- data: { workId, terminalizationApplied: false },
2606
- });
2607
- return result(blocked as unknown as Record<string, unknown>, true);
2608
- }
2609
- }
2610
-
2611
- const fenced = withControllerSessionTerminalizationFence(
2612
- store,
2613
- {
2614
- workId,
2615
- actor: `rh-work-stop:${terminalizationAuthority?.controllerId ?? String(args.requested_by ?? 'explicit')}`,
2616
- authority: terminalizationAuthority,
2617
- },
2618
- () => runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'stop', args),
2619
- );
2620
- if (!fenced.allowed) {
2621
- const blocked = buildFacadeResult({
2622
- status: 'blocked',
2623
- summary: `WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`,
2624
- data: {
2625
- workId,
2626
- terminalizationApplied: false,
2627
- currentClaimGeneration: fenced.owner?.claimGeneration,
2628
- },
2629
- });
2630
- return result(blocked as unknown as Record<string, unknown>, true);
2631
- }
2632
- const facade = fenced.value;
2633
- if (facade.status !== 'ok') {
2634
- return result(facade as unknown as Record<string, unknown>, true);
2635
- }
2636
- const reconcileStoppedRound = () => {
2637
- const stopped = getWorkContract(store, workId);
2638
- if (!stopped || !['failed', 'cancelled'].includes(stopped.status) || getControllerSession(store, workId)) return undefined;
2639
- const terminalRelay = reconcileControllerRoundAfterTerminalWork(store, { workId, actor: 'rh-work-stop-terminal-reconcile' });
2640
- if (terminalRelay) return terminalRelay;
2641
- const retained = getRetainedControllerSession(store, workId);
2642
- if (!retained) return undefined;
2643
- return reconcileControllerRoundAfterAbandonedRelease(
2644
- { controllerHome: ctx.controllerHome, repoId: repository.repoId },
2645
- { workId, releasedSession: retained },
2646
- );
2647
- };
2648
- try {
2649
- const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'stop');
2650
- if (!physical) {
2651
- const relay = reconcileStoppedRound();
2652
- return result({
2653
- ...facade,
2654
- data: { ...(facade.data && typeof facade.data === 'object' ? facade.data : {}), ...(relay ? { relay } : {}) },
2655
- } as unknown as Record<string, unknown>);
2656
- }
2657
- const cleanup = contextRecord(physical.structuredContent);
2658
- const cleanupCompleted = cleanup.cleanupCompleted === true || contextRecord(cleanup.work).state === 'cleaned';
2659
- const cleanupRetained = cleanup.cleanupRetained === true;
2660
- const cleanupSettled = cleanupCompleted || cleanupRetained;
2661
- const stoppedRelay = reconcileStoppedRound();
2662
- const response = {
2663
- ...facade,
2664
- status: cleanupSettled ? 'ok' : 'blocked',
2665
- summary: cleanupCompleted
2666
- ? `${facade.summary} Managed worktree and branch cleanup completed automatically.`
2667
- : cleanupRetained
2668
- ? `${facade.summary} Managed worktree and branch retention was recorded durably; automatic cleanup is disabled for this terminal Work.`
2669
- : `${facade.summary} Automatic managed-resource cleanup is incomplete and remains visible for retry.`,
2670
- data: {
2671
- ...(facade.data && typeof facade.data === 'object' ? facade.data : {}),
2672
- worktreeDeleted: cleanupCompleted,
2673
- cleanupPending: !cleanupSettled,
2674
- cleanupRetained,
2675
- lifecycleCleanup: cleanup,
2676
- ...(stoppedRelay ? { relay: stoppedRelay } : {}),
2677
- },
2678
- };
2679
- return result(response as unknown as Record<string, unknown>, !cleanupSettled || physical.isError === true);
2680
- } catch (error) {
2681
- const response = {
2682
- ...facade,
2683
- status: 'blocked',
2684
- summary: `${facade.summary} Automatic managed-resource cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
2685
- data: {
2686
- ...(facade.data && typeof facade.data === 'object' ? facade.data : {}),
2687
- worktreeDeleted: false,
2688
- cleanupPending: true,
2689
- },
2690
- };
2691
- return result(response as unknown as Record<string, unknown>, true);
2692
- }
2693
- }
2694
-
2695
- if (operation === 'finalize') {
2696
- const workId = String(args.work_id ?? '').trim();
2697
- try {
2698
- if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
2699
- } catch (error) {
2700
- const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`, data: { workId, lifecycleClosed: false } });
2701
- return result(blocked as unknown as Record<string, unknown>, true);
2702
- }
2703
- const finalizeReconciliation = workId
2704
- ? reconcileTerminalFacadeWorkVerifications(ctx, repository, workId)
2705
- : undefined;
2706
- const semanticFinalizeContext = {
2707
- ...workloopCtx,
2708
- sourceRevision: finalizeReconciliation?.sourceRevision ?? workloopCtx.sourceRevision ?? undefined,
2709
- workspaceFingerprint: finalizeReconciliation?.workspaceFingerprint,
2710
- implementationReviewWorkspaceFingerprint: finalizeReconciliation?.implementationReviewWorkspaceFingerprint,
2711
- workspaceChangedPaths: finalizeReconciliation?.workspaceChangedPaths,
2712
- workBoundProcessEvidenceIds: finalizeReconciliation?.workBoundProcessEvidenceIds,
2713
- };
2714
- let before = workId ? getWorkContract(store, workId) : undefined;
2715
- let terminalizationAuthority: ControllerTerminalizationAuthority | undefined;
2716
- // Finalize may commit, merge, clean resources, and complete the Work.
2717
- // It therefore shares the same exact-claim authority as stop. Transport
2718
- // or Runtime recovery remains an explicit controller_claim operation;
2719
- // terminalization itself must never rebind an unrelated controller scope.
2720
- if (before && !['completed', 'failed', 'cancelled'].includes(before.status)) {
2721
- try {
2722
- terminalizationAuthority = currentFacadeTerminalizationAuthority(ctx, store, workId, args);
2723
- } catch (error) {
2724
- const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`, data: { workId, terminalizationApplied: false, lifecycleClosed: false } });
2725
- return result(blocked as unknown as Record<string, unknown>, true);
2726
- }
2727
- }
2728
- if (before && !before.completionReceipt && args.reconcile_historical_delivery === true) {
2729
- try {
2730
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
2731
- const owner = getControllerSession(store, workId);
2732
- if (!owner || (controllerSessionPrincipalId(owner)) !== identity.principalId) {
2733
- throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
2734
- }
2735
- const historicalHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
2736
- const explicitTargetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
2737
- ? args.target_branch.trim()
2738
- : undefined;
2739
- const reconciliationTargetBranch = historicalHandle
2740
- ? resolveWorkDeliveryTargetBranch(historicalHandle, repository.defaultBranch, explicitTargetBranch)
2741
- : explicitTargetBranch || repository.defaultBranch || 'main';
2742
- if (historicalHandle?.managedWorktree) {
2743
- const managedCleanupComplete = historicalHandle.finalization.merge === 'done'
2744
- && historicalHandle.finalization.branchCleanup === 'done'
2745
- && historicalHandle.finalization.worktreeCleanup === 'done'
2746
- && !existsSync(historicalHandle.worktreePath);
2747
- if (!managedCleanupComplete) {
2748
- let managedCheckout: ReturnType<typeof selectRepositoryCheckout>;
2749
- try {
2750
- managedCheckout = selectRepositoryCheckout(repository, historicalHandle.checkoutId, { allowArchived: true });
2751
- } catch {
2752
- throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_MANAGED_CLEANUP_REQUIRED: ${workId}`);
2753
- }
2754
- const managedStatus = repositoryGitStatus(managedCheckout);
2755
- const targetRevision = String(args.reconcile_target_revision ?? '').trim();
2756
- const exactCleanCandidate = existsSync(historicalHandle.worktreePath)
2757
- && managedStatus.clean
2758
- && managedStatus.head === targetRevision
2759
- && historicalHandle.expectedHead?.trim() === targetRevision;
2760
- if (!exactCleanCandidate) throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_MANAGED_CLEANUP_REQUIRED: ${workId}`);
2761
- }
2762
- }
2763
- acceptReviewedDirectEditWorkReconciliation({
2764
- controllerHome: ctx.controllerHome,
2765
- repoId: repository.repoId,
2766
- checkoutId: before.checkoutId ?? repository.activeCheckoutId,
2767
- repoRoot: repository.canonicalRoot,
2768
- workId,
2769
- targetBranch: reconciliationTargetBranch,
2770
- targetRevision: String(args.reconcile_target_revision ?? ''),
2771
- comparedPaths: Array.isArray(args.reconcile_compared_paths) ? args.reconcile_compared_paths.map(String) : [],
2772
- reviewer: identity.principalId,
2773
- rationale: String(args.reconcile_rationale ?? ''),
2774
- cleanupOwnershipProof: String(args.reconcile_cleanup_proof ?? ''),
2775
- });
2776
- const released = releaseObservedControllerSession(store, {
2777
- workId,
2778
- actor: `direct-edit-reconciliation:${identity.principalId}`,
2779
- owner,
2780
- });
2781
- if (!released.allowed) {
2782
- throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_RELEASE_FENCED: ${workId}:${released.reason}`);
2783
- }
2784
- before = getWorkContract(store, workId);
2785
- } catch (error) {
2786
- const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Historical Work delivery reconciliation failed.', data: { workId, lifecycleClosed: false } });
2787
- return result(blocked as unknown as Record<string, unknown>, true);
2788
- }
2789
- }
2790
- const repositoryDeliveryHandle = workId
2791
- ? readWorkHandle(ctx.controllerHome, repository.repoId, workId)
2792
- : undefined;
2793
- const effectHasRepositoryDelta = Boolean(
2794
- before
2795
- && ['local_effect', 'remote_effect'].includes(before.workKind)
2796
- && (
2797
- (repositoryDeliveryHandle && repositoryWorkHandleHasSourceDelta(repository, repositoryDeliveryHandle))
2798
- || (finalizeReconciliation?.workspaceChangedPaths?.length ?? 0) > 0
2799
- )
2800
- );
2801
- if (effectHasRepositoryDelta && !repositoryDeliveryHandle) {
2802
- const blocked = buildFacadeResult({
2803
- status: 'blocked',
2804
- summary: `WORK_EFFECT_REPOSITORY_DELIVERY_HANDLE_REQUIRED: ${workId} has repository source delta and cannot use effect-only terminalization without a physical WorkHandle.`,
2805
- data: { workId, lifecycleClosed: false },
2806
- });
2807
- return result(blocked as unknown as Record<string, unknown>, true);
2808
- }
2809
- // Pure remote effects may complete from either the canonical plugin
2810
- // action receipt or the deliberately narrow trusted repository
2811
- // Process authority (`git push` + exact remote lease). A remote_effect
2812
- // that acquired source delta is no longer pure and must use physical
2813
- // repository delivery instead.
2814
- if (before?.workKind === 'remote_effect' && !before.completionReceipt && !effectHasRepositoryDelta) {
2815
- try {
2816
- before = finalizeRemoteEffectWorkFromActionReceipt(ctx.controllerHome, repository.repoId, workId);
2817
- } catch (pluginError) {
2818
- const checkoutId = before.checkoutId ?? repositoryDeliveryHandle?.checkoutId ?? repository.activeCheckoutId;
2819
- const processCompleted = finalizeRemoteEffectWorkFromRepositoryProcessReceipt(ctx, repository, workId, checkoutId);
2820
- if (processCompleted) {
2821
- before = processCompleted;
2822
- } else {
2823
- const blocked = buildFacadeResult({
2824
- status: 'blocked',
2825
- summary: pluginError instanceof Error ? pluginError.message : 'Remote-effect semantic finalization failed.',
2826
- data: { workId, lifecycleClosed: false },
2827
- });
2828
- return result(blocked as unknown as Record<string, unknown>, true);
2829
- }
2830
- }
2831
- }
2832
- const completedCleanupPending = Boolean(
2833
- before?.completionReceipt
2834
- && args.cleanup !== false
2835
- && before.worktreeRef?.trim()
2836
- && existsSync(before.worktreeRef),
2837
- );
2838
- const repositoryDeliveryRequired = Boolean(
2839
- before
2840
- && before.workKind !== 'read_only_review'
2841
- && (
2842
- !['local_effect', 'remote_effect'].includes(before.workKind)
2843
- || effectHasRepositoryDelta
2844
- )
2845
- );
2846
- if (before && ((repositoryDeliveryRequired && !before.completionReceipt) || completedCleanupPending)) {
2847
- try {
2848
- const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'finalize');
2849
- if (physical?.isError === true) return physical;
2850
- const refreshed = getWorkContract(store, workId);
2851
- if (physical && !refreshed?.completionReceipt) return physical;
2852
- } catch (error) {
2853
- const blocked = buildFacadeResult({
2854
- status: 'blocked',
2855
- summary: error instanceof Error ? error.message : 'Work delivery/finalization failed.',
2856
- data: { workId, lifecycleClosed: false },
2857
- });
2858
- return result(blocked as unknown as Record<string, unknown>, true);
2859
- }
2860
- }
2861
- const semanticWork = getWorkContract(store, workId);
2862
- let facade;
2863
- if (semanticWork && !['completed', 'failed', 'cancelled'].includes(semanticWork.status)) {
2864
- try {
2865
- const authority = terminalizationAuthority ?? currentFacadeTerminalizationAuthority(ctx, store, workId, args);
2866
- const fenced = withControllerSessionTerminalizationFence(
2867
- store,
2868
- {
2869
- workId,
2870
- actor: `rh-work-finalize:${authority.controllerId}:${authority.controllerInstanceId}`,
2871
- authority,
2872
- },
2873
- () => runGoalWorkloop(semanticFinalizeContext, 'finalize', args),
2874
- );
2875
- if (!fenced.allowed) {
2876
- throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`);
2877
- }
2878
- facade = fenced.value;
2879
- } catch (error) {
2880
- const blocked = buildFacadeResult({
2881
- status: 'blocked',
2882
- summary: error instanceof Error ? error.message : `Work ${workId} semantic finalization authority check failed.`,
2883
- data: { workId, terminalizationApplied: false, lifecycleClosed: false },
2884
- });
2885
- return result(blocked as unknown as Record<string, unknown>, true);
2886
- }
2887
- } else {
2888
- facade = runGoalWorkloop(semanticFinalizeContext, 'finalize', args);
2889
- }
2890
- let completed = getWorkContract(store, workId);
2891
- const postSemanticCleanupPending = Boolean(
2892
- completed?.completionReceipt
2893
- && args.cleanup !== false
2894
- && completed.worktreeRef?.trim()
2895
- && existsSync(completed.worktreeRef),
2896
- );
2897
- if (postSemanticCleanupPending) {
2898
- try {
2899
- const physical = await finalizeFacadeWorkHandle(ctx, repository, { ...args, commit: false, merge: false }, 'finalize');
2900
- if (physical?.isError === true) return physical;
2901
- completed = getWorkContract(store, workId);
2902
- } catch (error) {
2903
- const blocked = buildFacadeResult({
2904
- status: 'blocked',
2905
- summary: error instanceof Error ? error.message : `Work ${workId} terminal cleanup reconciliation failed.`,
2906
- data: { workId, terminalizationApplied: true, lifecycleClosed: false },
2907
- });
2908
- return result(blocked as unknown as Record<string, unknown>, true);
2909
- }
2910
- }
2911
- // Finalizing a Work proves the Work lifecycle only. A Plan step may aggregate
2912
- // acceptance criteria that are broader than this Work (for example, a canary
2913
- // plus a later stabilization soak), so finalize must never synthesize semantic
2914
- // Plan acceptance. Only the explicit plan_accept_step operation may promote a
2915
- // validating step to completed after the Controller reviews all criteria.
2916
- const completedHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
2917
- const lifecycleClosed = Boolean(completed?.completionReceipt)
2918
- && (!completedHandle || completedHandle.finalization.worktreeCleanup !== 'pending');
2919
- let blockerResolutionOccurrences: Array<{ scheduleId: string; occurrenceId?: string; status?: string }> = [];
2920
- if (lifecycleClosed && facade.status === 'ok') {
2921
- const explicitTargetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
2922
- ? args.target_branch.trim()
2923
- : undefined;
2924
- const targetBranch = completedHandle
2925
- ? resolveWorkDeliveryTargetBranch(completedHandle, repository.defaultBranch, explicitTargetBranch)
2926
- : explicitTargetBranch || repository.defaultBranch || 'main';
2927
- const targetStatus = repositoryGitStatus(repository);
2928
- if (targetStatus.clean && targetStatus.branch === targetBranch && targetStatus.head) {
2929
- blockerResolutionOccurrences = await triggerWorkContinuationRepositoryEvent(
2930
- ctx.controllerHome,
2931
- repository.repoId,
2932
- repositoryCleanContinuationEventName(targetBranch),
2933
- `repository-clean:${targetBranch}:${targetStatus.head}`,
2934
- { data: { targetBranch, targetRevision: targetStatus.head, sourceWorkId: workId } },
2935
- );
2936
- }
2937
- }
2938
- const response = {
2939
- ...facade,
2940
- data: {
2941
- ...(facade.data && typeof facade.data === 'object' ? facade.data : {}),
2942
- lifecycleClosed,
2943
- ...(blockerResolutionOccurrences.length > 0 ? { blockerResolutionOccurrences } : {}),
2944
- },
2945
- };
2946
- return result(response as unknown as Record<string, unknown>, response.status === 'blocked' || response.status === 'failed' || response.status === 'not_found');
2947
- }
2948
-
2949
- let resumedControllerSession: ReturnType<typeof resumeControllerSession> | undefined;
2950
- let cancelledWorkReauthorized = false;
2951
- let reconstructedCancelledCheckout = false;
2952
- let reconstructedRunningCheckout = false;
2953
- let continuationSourceRevision = workloopCtx.sourceRevision;
2954
- let continuationWorkspaceFingerprint: string | undefined;
2955
- let continuationImplementationReviewWorkspaceFingerprint: string | undefined;
2956
- let continuationWorkspaceChangedPaths: string[] | undefined;
2957
- let continuationWorkBoundProcessEvidenceIds: string[] | undefined;
2958
- if (operation === 'continue') {
2959
- try {
2960
- const workId = String(args.work_id ?? '').trim();
2961
- if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
2962
- let work = getWorkContract(store, workId);
2963
- if (work?.status === 'cancelled') {
2964
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
2965
- const resumed = reauthorizeRetainedCancelledRepositoryWork({
2966
- controllerHome: ctx.controllerHome,
2967
- repository,
2968
- workId,
2969
- identity,
2970
- requestedBy: typeof args.requested_by === 'string' ? args.requested_by : undefined,
2971
- approvalConfirmed: args.approval_confirmed === true,
2972
- prepareDependencies: args.needs_dependencies === true,
2973
- });
2974
- cancelledWorkReauthorized = true;
2975
- reconstructedCancelledCheckout = resumed.reconstructedCheckout;
2976
- work = getWorkContract(store, workId);
2977
- }
2978
- if (work && !['cancelled', 'completed', 'failed'].includes(work.status)) {
2979
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
2980
- // Continue uses the same Kernel rebind authority as terminalization.
2981
- // MCP transport identity is replaceable; principal/controller and
2982
- // canonical Runtime ownership remain fenced by the ControllerSession.
2983
- resumedControllerSession = bindFacadeControllerOwnership(ctx, store, workId, identity, {
2984
- allowClaimIfMissing: true,
2985
- leaseMs: 3_600_000,
2986
- relayScopeId: typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : undefined,
2987
- });
2988
- rebindRepositoryWorkHandleControllerIdentity({
2989
- controllerHome: ctx.controllerHome,
2990
- repositoryId: repository.repoId,
2991
- workId,
2992
- identity: { sessionId: identity.sessionId, principalId: identity.principalId },
2993
- });
2994
- reconcileRepositoryWorkHandlePlacement({
2995
- controllerHome: ctx.controllerHome,
2996
- repositoryId: repository.repoId,
2997
- workId,
2998
- });
2999
- const recovered = ensureRunningRepositoryWorkCheckout({
3000
- controllerHome: ctx.controllerHome,
3001
- repository,
3002
- workId,
3003
- identity,
3004
- prepareDependencies: args.needs_dependencies === true,
3005
- });
3006
- reconstructedRunningCheckout = recovered.reconstructedCheckout;
3007
- if (reconstructedRunningCheckout) {
3008
- work = getWorkContract(store, workId);
3009
- if (!work) throw new Error(`WORK_CONTINUE_RECONSTRUCTION_CONTRACT_MISSING: ${workId}`);
3010
- }
3011
- if (work.worktreePolicy.required === true && !work.worktreeRef) {
3012
- materializeFacadeWorkPlacement(ctx, repository, workId, args);
3013
- }
3014
- }
3015
- if (workId) {
3016
- const reconciled = reconcileTerminalFacadeWorkVerifications(ctx, repository, workId);
3017
- continuationSourceRevision = reconciled.sourceRevision ?? continuationSourceRevision;
3018
- continuationWorkspaceFingerprint = reconciled.workspaceFingerprint ?? continuationWorkspaceFingerprint;
3019
- continuationImplementationReviewWorkspaceFingerprint = reconciled.implementationReviewWorkspaceFingerprint ?? continuationImplementationReviewWorkspaceFingerprint;
3020
- continuationWorkspaceChangedPaths = reconciled.workspaceChangedPaths ?? continuationWorkspaceChangedPaths;
3021
- continuationWorkBoundProcessEvidenceIds = reconciled.workBoundProcessEvidenceIds;
3022
- }
3023
- } catch (error) {
3024
- const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller resume failed.', data: { operation, executionStarted: false, ownershipResumed: false } });
3025
- return result(facade as unknown as Record<string, unknown>, true);
3026
- }
3027
- }
3028
-
3029
- let terminalSuccessorAdmission: {
3030
- predecessorWorkId: string;
3031
- relay: ControllerRoundRelayRecord;
3032
- identity: ReturnType<typeof authenticatedFacadeControllerIdentity>;
3033
- } | undefined;
3034
- if (operation === 'start' && String(args.work_relation ?? '').trim() === 'continue') {
3035
- const predecessorWorkId = String(args.related_work_id ?? '').trim();
3036
- const predecessorWork = predecessorWorkId ? getWorkContract(store, predecessorWorkId) : undefined;
3037
- if (predecessorWork?.status === 'completed' && predecessorWork.planId) {
3038
- const relay = getControllerRoundRelay(store, predecessorWorkId);
3039
- if (!relay || !['claimed', 'pending_release'].includes(relay.status)) {
3040
- const facade = buildFacadeResult({ status: 'blocked', summary: `TERMINAL_SUCCESSOR_CONTROLLER_ROUND_REQUIRED: ${predecessorWorkId}:${relay?.status ?? 'missing'}`, data: { operation, executionStarted: false, predecessorWorkId } });
3041
- return result(facade as unknown as Record<string, unknown>, true);
3042
- }
3043
- const workKind = typeof args.work_kind === 'string' ? args.work_kind.trim() : '';
3044
- if (!workKind) {
3045
- const facade = buildFacadeResult({ status: 'blocked', summary: `TERMINAL_SUCCESSOR_WORK_KIND_REQUIRED: ${predecessorWorkId}`, data: { operation, executionStarted: false, predecessorWorkId } });
3046
- return result(facade as unknown as Record<string, unknown>, true);
3047
- }
3048
- const explicitAuthorityId = typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '';
3049
- const explicitRelayScopeId = typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : '';
3050
- if (!explicitAuthorityId || !explicitRelayScopeId) {
3051
- const facade = buildFacadeResult({ status: 'blocked', summary: `TERMINAL_SUCCESSOR_CONTROLLER_ROUND_AUTHORITY_REQUIRED: ${predecessorWorkId}`, data: { operation, executionStarted: false, predecessorWorkId } });
3052
- return result(facade as unknown as Record<string, unknown>, true);
3053
- }
3054
- try {
3055
- assertFacadeControllerRoundAuthority(ctx, store, predecessorWorkId, args);
3056
- const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
3057
- if (relay.controllerId !== identity.controllerId || relay.principalId !== identity.principalId || relay.controllerType !== identity.controllerType) {
3058
- throw new Error(`TERMINAL_SUCCESSOR_CONTROLLER_ROUND_IDENTITY_MISMATCH: ${predecessorWorkId}`);
3059
- }
3060
- terminalSuccessorAdmission = { predecessorWorkId, relay, identity };
3061
- } catch (error) {
3062
- const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Terminal successor ControllerRound authority validation failed.', data: { operation, executionStarted: false, predecessorWorkId } });
3063
- return result(facade as unknown as Record<string, unknown>, true);
3064
- }
3065
- }
3066
- }
3067
-
3068
- const semanticAdmissionRequired = operation === 'start' && Boolean(
3069
- (typeof args.plan_id === 'string' && args.plan_id.trim())
3070
- || (typeof args.plan_step_id === 'string' && args.plan_step_id.trim())
3071
- || (typeof args.requirement_id === 'string' && args.requirement_id.trim())
3072
- || (typeof args.related_work_id === 'string' && args.related_work_id.trim())
3073
- || (typeof args.work_relation === 'string' && args.work_relation.trim()),
3074
- );
3075
- const startContext = {
3076
- ...workloopCtx,
3077
- sourceRevision: continuationSourceRevision ?? undefined,
3078
- workspaceFingerprint: continuationWorkspaceFingerprint,
3079
- implementationReviewWorkspaceFingerprint: continuationImplementationReviewWorkspaceFingerprint,
3080
- workspaceChangedPaths: continuationWorkspaceChangedPaths,
3081
- workBoundProcessEvidenceIds: continuationWorkBoundProcessEvidenceIds,
3082
- semanticAdmissionLocked: semanticAdmissionRequired,
3083
- };
3084
- let trustedEngineeringEvidence: ReturnType<typeof mintEngineeringAdmissionEvidence> | undefined;
3085
- if ((operation === 'start' || operation === 'continue') && args.engineering_preconditions !== undefined) {
3086
- try {
3087
- const sourceRevision = startContext.sourceRevision?.trim();
3088
- if (!sourceRevision) throw new Error('ENGINEERING_PRECONDITIONS_SOURCE_REQUIRED');
3089
- const existingWork = operation === 'continue'
3090
- ? getWorkContract(store, String(args.work_id ?? '').trim())
3091
- : undefined;
3092
- const requirementContext = existingWork
3093
- ? { objective: existingWork.objective, acceptanceCriteria: existingWork.acceptanceCriteria }
3094
- : {
3095
- objective: String(args.objective ?? ''),
3096
- acceptanceCriteria: Array.isArray(args.acceptance_criteria) ? args.acceptance_criteria.map(String) : [],
3097
- };
3098
- const verified = mintEngineeringAdmissionEvidence({
3099
- repoRoot: repository.canonicalRoot,
3100
- sourceRevision,
3101
- draft: args.engineering_preconditions,
3102
- requirementContext,
3103
- existingProjectContractReceipt: existingWork?.engineeringContext?.projectContractReceipt,
3104
- });
3105
- trustedEngineeringEvidence = verified;
3106
- } catch (error) {
3107
- const facade = buildFacadeResult({
3108
- status: 'blocked',
3109
- summary: error instanceof Error ? error.message : 'ENGINEERING_PRECONDITIONS_INVALID',
3110
- data: { operation, executionStarted: false, engineeringPreconditionsAccepted: false },
3111
- });
3112
- return result(facade as unknown as Record<string, unknown>, true);
3113
- }
3114
- }
3115
- const facade = semanticAdmissionRequired
3116
- ? await withPrimaryWorkAdmissionLockAsync(store, () => runGoalWorkloop(startContext, 'start', args, { verifiedEngineeringEvidence: trustedEngineeringEvidence }))
3117
- : runGoalWorkloop(startContext, operation as 'start' | 'continue', args, { verifiedEngineeringEvidence: trustedEngineeringEvidence });
3118
- const facadeData = facade.data && typeof facade.data === 'object' ? facade.data as Record<string, unknown> : {};
3119
- const facadeWorkId = contextText(contextRecord(facadeData.work).workId, 200);
3120
- if (facade.status === 'ok' && facadeWorkId) {
3121
- try {
3122
- if (facadeData.workContractCreated === true) {
3123
- materializeFacadeWorkPlacement(ctx, repository, facadeWorkId, args);
3124
- }
3125
- const handle = ensureFacadeWorkHandle(ctx, repository, facadeWorkId, args);
3126
- if (handle) facadeData.executionHandle = { workId: handle.workId, checkoutId: handle.checkoutId, managedWorktree: handle.managedWorktree, state: handle.state };
3127
- if (facadeData.workContractCreated === true && !terminalSuccessorAdmission) {
3128
- const claimed = claimNewFacadeWork(ctx, repository, facadeWorkId, args);
3129
- facadeData.controllerSession = claimed.session;
3130
- facadeData.controllerAuthorityId = claimed.controllerAuthorityId;
3131
- facadeData.controllerAuthorityCarrier = 'controller_authority_id_or_session_id_compat';
3132
- facadeData.ownershipClaimed = true;
3133
- }
3134
- if (terminalSuccessorAdmission) {
3135
- const currentRelay = getControllerRoundRelay(store, terminalSuccessorAdmission.predecessorWorkId);
3136
- let boundRelay: ControllerRoundRelayRecord;
3137
- if (currentRelay?.status === 'pending_release') {
3138
- // Idempotent retry after the Controller already submitted its
3139
- // disposition. Never create/rebind another successor.
3140
- if (currentRelay.successorWorkId !== facadeWorkId) {
3141
- throw new Error(`CONTROLLER_RELAY_SUCCESSOR_ALREADY_BOUND: ${terminalSuccessorAdmission.predecessorWorkId}:${currentRelay.successorWorkId ?? 'none'}`);
3142
- }
3143
- boundRelay = currentRelay;
3144
- } else {
3145
- boundRelay = bindControllerRoundSuccessorWork(store, {
3146
- workId: terminalSuccessorAdmission.predecessorWorkId,
3147
- successorWorkId: facadeWorkId,
3148
- identity: terminalSuccessorAdmission.identity,
3149
- controllerAuthorityId: terminalSuccessorAdmission.relay.authorityId,
3150
- });
3151
- }
3152
- facadeData.predecessorWorkId = terminalSuccessorAdmission.predecessorWorkId;
3153
- facadeData.successorWorkId = facadeWorkId;
3154
- facadeData.terminalSuccessorRelay = boundRelay;
3155
- facadeData.ownershipClaimed = false;
3156
- }
3157
- } catch (error) {
3158
- const blocked = buildFacadeResult({ status: 'blocked', summary: `WORK_HANDLE_MATERIALIZATION_FAILED: ${error instanceof Error ? error.message : String(error)}`, data: { ...facadeData, workId: facadeWorkId, executionStarted: false, canonicalWorkRetained: true } });
3159
- return result(blocked as unknown as Record<string, unknown>, true);
3160
- }
3161
- }
3162
- const response = resumedControllerSession
3163
- ? {
3164
- ...facade,
3165
- ...(cancelledWorkReauthorized
3166
- ? {
3167
- status: 'ok' as const,
3168
- summary: `Explicit current-user reauthorization resumed ${resumedControllerSession.workId}; implementation may continue on the exact Work identity.`,
3169
- }
3170
- : { summary: `Controller ownership resumed for ${resumedControllerSession.workId}. ${facade.summary}` }),
3171
- data: {
3172
- ...facadeData,
3173
- ownershipResumed: true,
3174
- controllerSession: resumedControllerSession,
3175
- ...(reconstructedRunningCheckout ? { reconstructedRunningCheckout: true } : {}),
3176
- ...(cancelledWorkReauthorized ? {
3177
- cancelledWorkReauthorized: true,
3178
- reconstructedCancelledCheckout,
3179
- nextStep: 'execute',
3180
- } : {}),
3181
- },
3182
- }
3183
- : facade;
3184
- return result(response as unknown as Record<string, unknown>, response.status === 'blocked' || response.status === 'failed' || response.status === 'not_found');
3185
- }
324
+ return invalidFacadeOperation('rh_work', operation);
3186
325
  }