@moretea-labs/forge 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -0
  4. package/README.md +2 -2
  5. package/README.zh-CN.md +1 -1
  6. package/SKILL.md +2 -2
  7. package/THIRD_PARTY_NOTICES.md +1 -1
  8. package/agents/openai.yaml +1 -1
  9. package/assets/hooks/hook-input.sh +11 -8
  10. package/assets/hooks/lib/session-state.sh +10 -7
  11. package/assets/hooks/lib/workflow-state.sh +13 -177
  12. package/assets/hooks/post-tool-observer.sh +8 -2
  13. package/assets/hooks/prompt-guard.sh +5 -81
  14. package/assets/hooks/session-start-context.sh +56 -1
  15. package/assets/partials/05-workflow.partial.md +1 -1
  16. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  17. package/assets/plugin-registry.v1.json +1 -1
  18. package/assets/reference-configs/document-generation.md +1 -1
  19. package/assets/reference-configs/harness-overview.md +1 -1
  20. package/assets/reference-configs/sprint-contracts.md +3 -3
  21. package/assets/skill-version.json +2 -2
  22. package/assets/templates/contract.template.md +2 -7
  23. package/assets/templates/helpers/capture-plan.sh +2 -2
  24. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  25. package/assets/templates/helpers/contract-worktree.sh +1 -21
  26. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  27. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  28. package/assets/templates/helpers/new-plan.sh +1 -1
  29. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  30. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  31. package/assets/templates/helpers/verify-contract.sh +1 -111
  32. package/assets/templates/helpers/verify-sprint.sh +3 -180
  33. package/assets/templates/plan.template.md +1 -1
  34. package/assets/templates/review.template.md +9 -34
  35. package/assets/workflow-contract.v1.json +10 -19
  36. package/docs/README.md +10 -10
  37. package/docs/architecture/CURRENT.md +170 -0
  38. package/docs/architecture/EVOLUTION.md +42 -0
  39. package/docs/architecture/current/README.md +4 -156
  40. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  41. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  42. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  43. package/docs/architecture/history/README.md +7 -0
  44. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  45. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  46. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  47. package/docs/architecture/history.md +3 -34
  48. package/docs/architecture/index.md +15 -98
  49. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  50. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  51. package/docs/architecture/versions/1.6.md +38 -0
  52. package/docs/architecture/versions/1.7.md +41 -0
  53. package/docs/forge-chatgpt-browser-engine.md +2 -2
  54. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  55. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  56. package/docs/forge-github-issue-launcher.md +1 -1
  57. package/docs/forge-runtime-self-healing-loop.md +1 -1
  58. package/docs/operations/controller-browser-plugin.md +26 -6
  59. package/docs/operations/controller-performance-and-502.md +124 -0
  60. package/docs/operations/controller-reliability-runbook.md +1 -1
  61. package/docs/operations/features.md +2 -2
  62. package/docs/operations/github-repository.md +1 -1
  63. package/docs/operations/homebrew.md +2 -2
  64. package/docs/operations/mcp-tool-exposure.md +5 -3
  65. package/docs/wiki/Architecture.md +6 -6
  66. package/docs/wiki/Implementation.md +2 -2
  67. package/docs/wiki/Integrations.md +2 -2
  68. package/docs/wiki/Work-Lifecycle.md +1 -1
  69. package/package.json +29 -10
  70. package/scripts/activate-source-baseline.ts +17 -13
  71. package/scripts/benchmark-background-check-overlap.ts +228 -0
  72. package/scripts/benchmark-mcp-host.sh +117 -0
  73. package/scripts/benchmark-mcp-transport.ts +170 -0
  74. package/scripts/benchmark-quality-harness.ts +146 -0
  75. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  76. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  77. package/scripts/benchmark-typescript-navigation.ts +186 -0
  78. package/scripts/benchmark-work-efficiency.ts +334 -0
  79. package/scripts/capture-plan.sh +3 -3
  80. package/scripts/check-agent-tooling.sh +8 -35
  81. package/scripts/check-architecture-sync.sh +17 -86
  82. package/scripts/check-mcp-compatibility.ts +14 -0
  83. package/scripts/check-runtime-architecture.mjs +107 -64
  84. package/scripts/check-stable-baseline.ts +92 -0
  85. package/scripts/check-task-workflow.sh +4 -4
  86. package/scripts/contract-worktree.sh +1 -21
  87. package/scripts/ensure-task-workflow.sh +22 -55
  88. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  89. package/scripts/install-standalone-recovery.ts +24 -0
  90. package/scripts/lib/project-init-lib.sh +15 -44
  91. package/scripts/migrate-project-template.sh +23 -1179
  92. package/scripts/new-plan.sh +1 -1
  93. package/scripts/plan-to-todo.sh +11 -36
  94. package/scripts/run-governed-gate.ts +2 -1
  95. package/scripts/ship-worktrees.sh +2 -29
  96. package/scripts/smoke-ios-physical-device.ts +4 -2
  97. package/scripts/smoke-runtime-recovery.ts +1 -35
  98. package/scripts/smoke-schedule-engine.ts +1 -20
  99. package/scripts/verify-contract.sh +1 -111
  100. package/scripts/verify-forge-runtime.sh +0 -1
  101. package/scripts/verify-sprint.sh +3 -180
  102. package/scripts/workflow-contract.ts +1 -5
  103. package/src/cli/commands/init-hook.ts +3 -3
  104. package/src/cli/commands/mcp.ts +9 -5
  105. package/src/cli/commands/plugin.ts +1 -1
  106. package/src/cli/commands/recovery.ts +25 -3
  107. package/src/cli/commands/runtime.ts +55 -3
  108. package/src/cli/commands/security.ts +12 -1
  109. package/src/cli/commands/setup-profile.ts +31 -21
  110. package/src/cli/controller/check-runner.ts +106 -3
  111. package/src/cli/controller/composite-result.ts +1 -1
  112. package/src/cli/controller/context/focus.ts +51 -0
  113. package/src/cli/controller/context/instruction-context.ts +114 -0
  114. package/src/cli/controller/context/known-paths.ts +76 -0
  115. package/src/cli/controller/context/query-planning.ts +110 -0
  116. package/src/cli/controller/context/rule-resolution.ts +532 -0
  117. package/src/cli/controller/context/source-materializer.ts +278 -0
  118. package/src/cli/controller/context/structural.ts +16 -0
  119. package/src/cli/controller/context/types.ts +286 -0
  120. package/src/cli/controller/context-pack.ts +785 -469
  121. package/src/cli/controller/launch-agents.ts +77 -2
  122. package/src/cli/controller/operational-plan.ts +2 -2
  123. package/src/cli/controller/work-mode.ts +6 -4
  124. package/src/cli/editing/edit-session.ts +60 -7
  125. package/src/cli/github/github.ts +133 -0
  126. package/src/cli/installer/managed-entries.ts +15 -1
  127. package/src/cli/local-bridge/console-automations.ts +54 -78
  128. package/src/cli/local-bridge/console-view-models.ts +1 -35
  129. package/src/cli/local-bridge/facade-api.ts +0 -217
  130. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  131. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  132. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  133. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  134. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  135. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  136. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  137. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  138. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  139. package/src/cli/local-bridge/job-store.ts +1 -490
  140. package/src/cli/local-bridge/server.ts +0 -513
  141. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  142. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  143. package/src/cli/mcp/auth.ts +5 -5
  144. package/src/cli/mcp/instructions.ts +3 -2
  145. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  146. package/src/cli/mcp/multi-repository.ts +16 -5
  147. package/src/cli/mcp/oauth.ts +175 -26
  148. package/src/cli/mcp/repository-tools.ts +247 -107
  149. package/src/cli/mcp/server.ts +537 -111
  150. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  151. package/src/cli/mcp/toolset-selection.ts +1 -1
  152. package/src/cli/mcp/toolset.ts +42 -5
  153. package/src/cli/mcp/transports/http.ts +101 -14
  154. package/src/cli/mcp/transports/session-registry.ts +43 -5
  155. package/src/cli/mcp/types.ts +3 -3
  156. package/src/cli/repositories/command-classifier.ts +101 -19
  157. package/src/cli/repositories/command-executor.ts +55 -521
  158. package/src/cli/repositories/command-process.ts +108 -0
  159. package/src/cli/repositories/command-scope.ts +99 -41
  160. package/src/cli/repositories/controller-home.ts +7 -8
  161. package/src/cli/repositories/git-command-executor.ts +2 -0
  162. package/src/cli/repositories/locks.ts +53 -16
  163. package/src/cli/repositories/repository-snapshot.ts +121 -0
  164. package/src/cli/repositories/selected-path-actions.ts +17 -9
  165. package/src/cli/repositories/structured-git.ts +61 -3
  166. package/src/cli/repository/inspector.ts +553 -63
  167. package/src/cli/repository/session-cache.ts +58 -17
  168. package/src/cli/v81-entry.ts +1 -4
  169. package/src/effects/fs-transaction.ts +85 -17
  170. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  171. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  172. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  173. package/src/runtime/context/lsp-client.ts +283 -0
  174. package/src/runtime/context/semantic-navigation.ts +275 -0
  175. package/src/runtime/context/swift-navigation.ts +565 -0
  176. package/src/runtime/context/typescript-navigation.ts +226 -0
  177. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  178. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +47 -8
  179. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  180. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  181. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  182. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  183. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  184. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  185. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  186. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +143 -8
  187. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  188. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +85 -8
  189. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  190. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  191. package/src/runtime/control-plane/facade/controller-session-store.ts +132 -1
  192. package/src/runtime/control-plane/facade/goal-workloop-access.ts +2 -0
  193. package/src/runtime/control-plane/facade/goal-workloop.ts +686 -129
  194. package/src/runtime/control-plane/facade/index.ts +1 -0
  195. package/src/runtime/control-plane/facade/plan-contract-store.ts +185 -20
  196. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  197. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  198. package/src/runtime/control-plane/facade/types.ts +22 -5
  199. package/src/runtime/control-plane/facade/work-contract-store.ts +175 -73
  200. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  201. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  202. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  203. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  204. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  205. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  206. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  207. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  208. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  209. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  210. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  211. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  212. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  213. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  214. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  215. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  216. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  217. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  218. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  219. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +355 -116
  220. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  221. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  222. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  223. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  224. package/src/runtime/control-plane/release-retention.ts +62 -2
  225. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  226. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  227. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  228. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  229. package/src/runtime/diagnostics/performance.ts +24 -1
  230. package/src/runtime/evidence/event-ledger.ts +1 -1
  231. package/src/runtime/execution/jobs/store.ts +58 -4
  232. package/src/runtime/execution/jobs/types.ts +1 -1
  233. package/src/runtime/execution/managed-workspace.ts +203 -1
  234. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  235. package/src/runtime/execution/process-runtime/check-result.ts +53 -0
  236. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  237. package/src/runtime/execution/process-runtime/command-facade.ts +345 -40
  238. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  239. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  240. package/src/runtime/execution/process-runtime/persisted-check.ts +47 -4
  241. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  242. package/src/runtime/execution/process-runtime/runtime.ts +43 -3
  243. package/src/runtime/execution/process-runtime/store.ts +1 -0
  244. package/src/runtime/execution/process-runtime/types.ts +2 -0
  245. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  246. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  247. package/src/runtime/execution/workers/executor.ts +0 -48
  248. package/src/runtime/gateway/mcp/execution-tools.ts +1192 -114
  249. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  250. package/src/runtime/gateway/mcp/router.ts +184 -82
  251. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  252. package/src/runtime/gateway/mcp/runtime-tools.ts +2224 -1589
  253. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  254. package/src/runtime/plugins/browser-adapter.ts +2901 -241
  255. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  256. package/src/runtime/plugins/browser-handoff.ts +92 -2
  257. package/src/runtime/plugins/browser-macos-bridge.ts +582 -49
  258. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  259. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  260. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  261. package/src/runtime/plugins/browser-runtime.ts +183 -0
  262. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  263. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  264. package/src/runtime/plugins/desktop-operator-registration.ts +8 -2
  265. package/src/runtime/plugins/external-adapter.ts +324 -3
  266. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  267. package/src/runtime/plugins/first-party-registry.ts +2 -0
  268. package/src/runtime/plugins/github-adapter.ts +125 -0
  269. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  270. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  271. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  272. package/src/runtime/plugins/ios-adapter.ts +106 -16
  273. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  274. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  275. package/src/runtime/plugins/lightweight-action.ts +153 -0
  276. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  277. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  278. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  279. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  280. package/src/runtime/plugins/resend-adapter.ts +1 -1
  281. package/src/runtime/plugins/store.ts +269 -29
  282. package/src/runtime/plugins/types.ts +35 -2
  283. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  284. package/src/runtime/projections/git-status-sampler.ts +16 -16
  285. package/src/runtime/projections/materialized-view.ts +28 -10
  286. package/src/runtime/recovery/maintenance-executor.ts +393 -28
  287. package/src/runtime/root/mcp-transport.ts +180 -10
  288. package/src/runtime/root/package-connector-service.ts +186 -8
  289. package/src/runtime/root/package-runtime-release.ts +143 -17
  290. package/src/runtime/root/package-runtime-service.ts +430 -22
  291. package/src/runtime/root/release-manifest.ts +7 -2
  292. package/src/runtime/root/release-materialize.ts +19 -0
  293. package/src/runtime/root/release-store.ts +48 -11
  294. package/src/runtime/root/runtime.ts +23 -5
  295. package/src/runtime/root/service.ts +100 -29
  296. package/src/runtime/root/types.ts +2 -0
  297. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  298. package/src/runtime/shared/trusted-executable.ts +62 -0
  299. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  300. package/src/runtime/standalone-recovery/core.ts +700 -92
  301. package/src/runtime/standalone-recovery/entry.ts +99 -23
  302. package/src/runtime/standalone-recovery/installer.ts +19 -3
  303. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  304. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  305. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  306. package/src/runtime/workflow/schedules/engine.ts +287 -151
  307. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  308. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  309. package/src/runtime/workflow/schedules/store.ts +131 -10
  310. package/src/runtime/workflow/schedules/types.ts +2 -4
  311. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  312. package/src/testing/test-governance.ts +8 -40
  313. package/assets/reference-configs/heartbeat-triage.md +0 -57
  314. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  315. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  316. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  317. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  318. package/docs/architecture/current/architecture-invariants.md +0 -331
  319. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  320. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  321. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  322. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  323. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  324. package/docs/architecture/current/entity-model.md +0 -612
  325. package/docs/architecture/current/external-plugin-governance.md +0 -396
  326. package/docs/architecture/current/failure-recovery.md +0 -447
  327. package/docs/architecture/current/governance.md +0 -213
  328. package/docs/architecture/current/human-interaction-plane.md +0 -87
  329. package/docs/architecture/current/implementation-status.md +0 -128
  330. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  331. package/docs/architecture/current/legacy-governance.md +0 -58
  332. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  333. package/docs/architecture/current/migration-roadmap.md +0 -105
  334. package/docs/architecture/current/multi-repository-execution.md +0 -371
  335. package/docs/architecture/current/operations-runbook.md +0 -45
  336. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  337. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  338. package/docs/architecture/current/repository-access-modes.md +0 -102
  339. package/docs/architecture/current/route-policy.md +0 -15
  340. package/docs/architecture/current/route-session-concurrency.md +0 -85
  341. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  342. package/docs/architecture/current/runtime-directory-map.md +0 -49
  343. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  344. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  345. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  346. package/docs/architecture/current/system-overview.md +0 -169
  347. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  348. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  349. package/docs/architecture/current/thin-harness-v1.md +0 -321
  350. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  351. package/docs/forge-autonomous-goal-loop.md +0 -9
  352. package/docs/forge-provider-configuration.md +0 -7
  353. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  354. package/docs/operations/gmail-assistant-routines.md +0 -186
  355. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  356. package/docs/operations/provider-configuration.md +0 -31
  357. package/scripts/harness-trace-grade.sh +0 -150
  358. package/scripts/heartbeat-triage.sh +0 -361
  359. package/scripts/maintenance-triage.sh +0 -167
  360. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  361. package/src/cli/repositories/goal-registry.ts +0 -239
  362. package/src/runtime/assistant/action-proposals.ts +0 -279
  363. package/src/runtime/assistant/intent.ts +0 -395
  364. package/src/runtime/assistant/model-provider.ts +0 -460
  365. package/src/runtime/assistant/openapi.ts +0 -223
  366. package/src/runtime/assistant/policy.ts +0 -89
  367. package/src/runtime/assistant/readiness.ts +0 -258
  368. package/src/runtime/assistant/routine-runtime.ts +0 -477
  369. package/src/runtime/assistant/schedule-binding.ts +0 -171
  370. package/src/runtime/assistant/standing-grants.ts +0 -375
  371. package/src/runtime/assistant/store.ts +0 -243
  372. package/src/runtime/assistant/types.ts +0 -135
  373. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  374. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  375. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  376. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  377. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  378. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  379. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  380. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  381. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  382. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  383. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  384. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  385. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  386. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  387. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  388. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  389. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  390. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  391. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  392. package/src/runtime/workflow/findings/store.ts +0 -204
  393. package/src/runtime/workflow/findings/types.ts +0 -58
  394. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  395. package/src/runtime/workflow/portfolio/store.ts +0 -185
  396. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -5,9 +5,12 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
5
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
6
6
  import type { CallToolResult as SdkCallToolResult } from '@modelcontextprotocol/sdk/types.js';
7
7
  import { collectRuntimePerformanceDiagnostics, inferLocalControllerProcess } from '../../diagnostics/performance';
8
+ import { defaultSemanticProviderRegistry, type SemanticNavigationKind, type SemanticNavigationRequest } from '../../context/semantic-navigation';
8
9
  import type { McpToolDefinition, CallToolResult } from '../../../cli/mcp/tools';
9
10
  import type { MultiRepositoryMcpToolContext } from '../../../cli/mcp/multi-repository';
10
11
  import { repositoryScopedToolArgs } from '../../../cli/mcp/multi-repository';
12
+ import { resolveMcpPath } from '../../../cli/mcp/paths';
13
+ import { freshGitIdentity } from '../../../cli/repository/inspector';
11
14
  import { reconcileReadinessProjectionSource } from '../../../cli/mcp/readiness-projection';
12
15
  import { listRepositories, repositorySummary, resolveRepositorySelection, selectRepositoryCheckout } from '../../../cli/repositories/registry';
13
16
  import { repositoryGitStatus } from '../../../cli/repositories/structured-git';
@@ -15,10 +18,14 @@ import { repositoryControllerRoot } from '../../../cli/repositories/controller-h
15
18
  import { cancelExecutionJob, findExecutionJob, getExecutionJob, getExecutionJobByRequestId, listExecutionJobs } from '../../execution/jobs/store';
16
19
  import { waitForExecutionJob } from '../../execution/jobs/wait';
17
20
  import type { ExecutionJob } from '../../execution/jobs/types';
18
- import { getProcessHandle, getProcessRecord, isManagedProcessActive, listProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics, readPersistedCheckResultReceipt, runPersistedCheckViaProcessRuntime, waitForProcess } from '../../execution/process-runtime';
21
+ import { DEFAULT_WORK_CHECK_LEASE_WAIT_MS, checkRequiresDurableWorkflow, getProcessHandle, getProcessRecord, isManagedProcessActive, listProcessRecords, listRecoverableProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics, readPersistedCheckResultReceipt, runPersistedCheckViaProcessRuntime, waitForProcess } from '../../execution/process-runtime';
22
+ import { getRepositoryCommandProcess, waitRepositoryCommandProcess } from '../../execution/process-runtime/command-facade';
19
23
  import { buildJobOperationDigest } from '../../control-plane/facade/operation-digest';
20
- import { readWorkHandle, transitionWorkHandle, writeWorkHandle, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
24
+ import { readWorkHandle, transitionWorkHandle, workDeliveryBaseRevision, writeWorkHandle, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
25
+ import { recoverTerminalWorkHandle } from '../../control-plane/execution/work-terminal-cleanup';
26
+ import { gitCommitAtRef, gitWorktreeSnapshot } from '../../control-plane/execution/work-lifecycle-audit';
21
27
  import { executionIdentityForRepository } from '../../control-plane/execution/execution-identity';
28
+ import { commandFingerprint, verificationInputFingerprint, workspaceValidationFingerprint } from '../../control-plane/execution/verification-evidence';
22
29
  import { acceptReviewedDirectEditWorkReconciliation, reconcileFinalizedDirectEditWorksAfterCommit } from '../../control-plane/execution/direct-edit-work-completion';
23
30
  import { readJobEvents } from '../../evidence/event-ledger';
24
31
  import { readExecutionArtifact } from '../../evidence/artifact-store';
@@ -41,26 +48,23 @@ import {
41
48
  type RuntimeOperationalView,
42
49
  type GradedObservation,
43
50
  } from '../../health';
44
- import { applyScheduleDedupe, buildScheduleDedupeReport, createSchedule, getSchedule, getScheduleDecision, listOccurrences, listSchedules, saveSchedule } from '../../workflow/schedules/store';
45
- import { evaluateSchedule } from '../../workflow/schedules/engine';
51
+ import { applyScheduleDedupe, buildScheduleDedupeReport, deleteSchedule } from '../../workflow/schedules/store';
46
52
  import {
47
53
  createWorkContinuationSchedule,
54
+ getWorkContinuationSchedule,
55
+ listWorkContinuationSchedules,
56
+ pauseWorkContinuationSchedule,
57
+ resumeWorkContinuationSchedule,
58
+ triggerWorkContinuationSchedule,
48
59
  type ContinuationControllerType,
49
60
  } from '../../workflow/schedules/work-continuation';
50
- import { createPortfolioWorkflow, getPortfolioWorkflow, listPortfolioWorkflows } from '../../workflow/portfolio/store';
51
- import { claimsForMcpOperation } from './resource-policy';
52
61
  import {
53
62
  gatewayRouteBehaviorSnapshot,
54
63
  getMcpToolDefinition,
55
- hashMcpToolArguments,
56
- injectDurableCommandFields,
57
- operationMetadataForTool,
58
64
  RETIRED_AGENT_OPERATIONS,
59
65
  routeDurableMcpCall,
60
- validateMcpToolArguments,
61
66
  } from './router';
62
67
  import { assertAutomatedOperationAllowed } from '../../control-plane/governance/external-effects';
63
- import { getCandidateFinding, listCandidateFindings, recordCandidateFinding } from '../../workflow/findings/store';
64
68
  import { ensureRepositoryRuntimeStorage } from '../../../cli/repositories/runtime-storage';
65
69
  import { assessWorkMode, parseExplicitTaskMode } from '../../../cli/controller/work-mode';
66
70
  import { projectBoard } from '../../../cli/controller/issue-store';
@@ -68,10 +72,11 @@ import {
68
72
  buildControllerTaskLedgerProjection,
69
73
  writeControllerTaskLedgerArtifacts,
70
74
  } from '../../../cli/controller/task-ledger';
71
- import { buildControllerContextPack, CONTROLLER_CONTEXT_IMPACT_DOMAINS, type ControllerContextImpactDomain } from '../../../cli/controller/context-pack';
75
+ import { buildControllerContextPack, buildControllerContextPackAsync, CONTROLLER_CONTEXT_IMPACT_DOMAINS, type ControllerContextImpactDomain } from '../../../cli/controller/context-pack';
72
76
  import { legacyIssueAuthorityRetired } from '../../../cli/controller/legacy-issue-cutover';
73
77
  import { buildControllerOperationalPlan } from '../../../cli/controller/operational-plan';
74
78
  import { listControllerChecks, readLatestControllerCheckEvidence } from '../../../cli/controller/check-runner';
79
+ import { buildCheckExecutionSchedule } from '../../execution/process-runtime/check-scheduling';
75
80
  import { repositoryChangeVerify } from '../../../cli/controller/composite-operations';
76
81
  import { listActiveAgentJobSnapshots } from '../../../cli/agent-jobs/job-manager';
77
82
  import { readAgentExecutableReadinessSnapshot } from '../../../cli/agent-jobs/executable-resolver';
@@ -102,7 +107,8 @@ import {
102
107
  } from '../../../cli/controller/runtime-config';
103
108
  import { redactMcpText } from '../../../cli/mcp/redaction';
104
109
  import { resolveLocalBridgeSurface, summarizeRecentJobs } from '../../shared/local-bridge-surface';
105
- import { assistantPluginScope, controllerPluginRepository, executeAssistantPluginReadDirect, getAssistantPluginManifest, isDirectPluginReadAction, listAssistantPluginManifests, submitAssistantPluginAction } from '../../plugins/store';
110
+ import { assistantPluginScope, controllerPluginRepository, executeAssistantPluginReadDirect, finalizeRemoteEffectWorkFromActionReceipt, getAssistantPluginManifest, isDirectPluginReadAction, listAssistantPluginManifests, submitAssistantPluginAction } from '../../plugins/store';
111
+ import { startLightweightPluginAction, waitLightweightPluginAction } from '../../plugins/lightweight-action';
106
112
  import { mcpPluginExecutionOrigin } from '../../plugins/execution-origin';
107
113
  import {
108
114
  summarizeExecutionJobForMcp,
@@ -114,28 +120,12 @@ import {
114
120
  prepareWorkspaceAuthLogin,
115
121
  previewExternalFilesystemGrant,
116
122
  readExternalFilesystemSnapshot,
117
- iosXcodeStatus,
118
- iosSimulatorsList,
119
- iosProjectDiscover,
120
- iosSchemesList,
121
- iosSimulatorBoot,
122
- iosAppBuild,
123
- iosAppInstall,
124
- iosAppLaunch,
125
- iosSimulatorScreenshot,
126
- iosSimulatorLogTail,
127
- iosUiSmokeTest,
128
123
  buildReviewArtifactIndex,
129
124
  ensureReviewArtifactRoots,
130
125
  prepareBrowserReviewPacket,
131
126
  prepareIosReviewPacket,
132
127
  } from '../../safe-tooling';
133
128
  import { buildModelClientSummary, buildModelControlPlaneSummary, deepSeekControllerManifest, deepSeekFunctionToolManifest, prepareDeepSeekControllerHandoff, prepareDeepSeekControllerRequest, prepareDeepSeekToolCall } from '../../model-clients';
134
- import { buildAssistantReadinessReport } from '../../assistant/readiness';
135
- import { approveAssistantActionProposal, getAssistantActionProposal, listAssistantActionProposals, rejectAssistantActionProposal } from '../../assistant/action-proposals';
136
- import { assistantModelReadiness } from '../../assistant/model-provider';
137
- import { createAssistantStandingGrant, listAssistantStandingGrants, revokeAssistantStandingGrant } from '../../assistant/standing-grants';
138
- import { buildGmailTriagePlan, readGmailTriageRules, upsertGmailTriageRule } from '../../personal-assistant/gmail-triage-manager';
139
129
  import { sessionCacheGlobalDiagnostics } from '../../../cli/repository/session-cache';
140
130
  import { cachedGitIdentity, gitIdentityPerformanceSnapshot, gitSnapshot, gitSnapshotPerformanceSnapshot } from '../../../cli/repository/inspector';
141
131
  import { buildWorkflowWatchdogReport } from '../../watchdog/workflow-watchdog';
@@ -170,7 +160,9 @@ import {
170
160
  dismissHandoffItem,
171
161
  getHandoffItem,
172
162
  listCapabilityDescriptors,
163
+ getCapabilityDescriptor,
173
164
  getPluginActionCapabilitySchema,
165
+ searchCapabilityDescriptors,
174
166
  summarizeCapabilityGroups,
175
167
  listHandoffItems,
176
168
  normalizeCheckIds,
@@ -182,12 +174,18 @@ import {
182
174
  listWorkContracts,
183
175
  getWorkContract,
184
176
  getWorkContractByRequestId,
177
+ updateWorkContract,
178
+ resumeRetainedCancelledWorkContract,
185
179
  buildWorkContinuationSnapshot,
186
- acceptSubmittedWorkContract,
187
- approvePlanContract,
188
- createPlanContract,
180
+ acceptPlanStepEvidence,
181
+ admitPlanContractAsync,
182
+ approvePlanContractAsync,
189
183
  getPlanContract,
190
184
  listPlanContracts,
185
+ resolvePlanAdmission,
186
+ withPrimaryWorkAdmissionLockAsync,
187
+ repairDanglingPlanStepWorkBinding,
188
+ completePlanStepForWork,
191
189
  summarizePlanContract,
192
190
  supersedePlanContract,
193
191
  verifyGoalWorkloop,
@@ -195,764 +193,44 @@ import {
195
193
  claimControllerSession,
196
194
  getControllerSession,
197
195
  releaseControllerSession,
196
+ releaseControllerSessionWithAuthority,
198
197
  resumeControllerSession,
198
+ withControllerSessionTerminalizationFence,
199
+ type ControllerTerminalizationAuthority,
199
200
  } from '../../control-plane/facade';
200
201
  import { currentControllerInstanceId, readExecutionSession, startExecutionSession, updateExecutionSession } from '../../control-plane/execution/session-store';
202
+ import { changedPaths as workChangedPaths } from '../../control-plane/execution/work-task-receipt';
203
+ import { createRequirement, readRequirement } from '../../control-plane/persistence/requirement-store';
201
204
  import { ensureManagedWorkspace } from '../../execution/managed-workspace';
202
205
  import { currentPermissionSnapshotVersion } from '../../control-plane/execution/validation';
203
206
  import { observeRuntimeStatus } from '../../root/status';
204
207
  import { reconcileWorkValidation } from './work-validation-reconciler';
205
208
  import { callExecutionTool } from './execution-tools';
206
209
  import { launchSuperController } from '../../control-plane/launcher/thin-launcher';
210
+ import { runStandaloneChatgptPrompt, runWorkChatgptContinuation, settleWorkChatgptAutomationTab } from '../../control-plane/launcher/chatgpt-work-continuation';
211
+ import { getChatgptWorkConversationBinding } from '../../control-plane/launcher/chatgpt-work-binding-store';
207
212
  import {
208
- executorDispatch,
209
- executorRoutePreview,
210
- goalContinue,
211
- goalCreate,
212
- goalFinalize,
213
- goalGet,
214
- goalHandoffPacketCreate,
215
- goalHandoffPacketGet,
216
- goalList,
217
- goalStart,
218
- goalStatus,
219
- goalStop,
220
- goalTickOnce,
221
- providerConfigStatusAction,
222
- providerHealthAction,
223
- providerListAction,
224
- repairContinue,
225
- repairPlan,
226
- summarizeGoalContract,
227
- tickActiveGoals,
228
- type GoalContract,
229
- type GoalLoopContext,
230
- type GoalStatus,
231
- type TaskIntent,
232
- } from '../../control-plane/goal-loop';
233
-
234
- function summarizeGoalPublic(goal: GoalContract) {
235
- return summarizeGoalContract(goal);
236
- }
213
+ acknowledgeControllerRoundClaim,
214
+ beginControllerRoundRelayAfterRelease,
215
+ beginInitialControllerRoundDispatch,
216
+ buildControllerRoundRelayPrompt,
217
+ finishControllerRoundRelayDispatch,
218
+ getControllerRoundRelay,
219
+ parseControllerDispositionCompatibilityCapability,
220
+ recordControllerRoundTabSettlement,
221
+ submitControllerRoundDisposition,
222
+ type ControllerRoundDisposition,
223
+ } from '../../control-plane/facade/controller-round-relay';
237
224
 
238
- function definition(name: string, description: string, properties: Record<string, unknown>, required: string[] = [], readOnly = true): McpToolDefinition {
239
- return {
240
- name,
241
- description,
242
- inputSchema: { type: 'object', properties, ...(required.length ? { required } : {}), additionalProperties: false },
243
- annotations: { readOnlyHint: readOnly, openWorldHint: false, destructiveHint: false },
244
- };
245
- }
246
- const repoId = { type: 'string', description: 'Stable repository id.' };
247
- export const runtimeToolDefinitions: McpToolDefinition[] = [
248
- definition('rh_status', 'Preferred ChatGPT facade: bounded controller status, capability readiness, and self-healing diagnose/repair.', {
249
- repo_id: repoId,
250
- operation: { type: 'string', enum: ['list', 'get', 'repair'], description: 'Defaults to get.' },
251
- detail_level: { type: 'string', enum: ['summary', 'detail'], description: 'Defaults to summary.' },
252
- repair_operation: { type: 'string', enum: ['diagnose', 'repair', 'verify', 'handoff'] },
253
- dry_run: { type: 'boolean', description: 'Defaults to true for repair/diagnose.' },
254
- approval_confirmed: { type: 'boolean' },
255
- chatgpt_pull_failed: { type: 'boolean' },
256
- destructive: { type: 'boolean' },
257
- }),
258
- definition('rh_inbox', 'Preferred ChatGPT facade: pending handoff decisions only (not logs).', {
259
- repo_id: repoId,
260
- operation: { type: 'string', enum: ['list', 'get', 'ack', 'accept', 'resolve', 'dismiss', 'create'], description: 'Defaults to list (pending).' },
261
- handoff_id: { type: 'string' },
262
- work_id: { type: 'string' },
263
- title: { type: 'string' },
264
- reason: { type: 'string' },
265
- summary: { type: 'string' },
266
- recommended_decision: { type: 'string' },
267
- recommended_prompt: { type: 'string' },
268
- decision: { type: 'string', description: 'Recorded on resolve/dismiss.' },
269
- resolver: { type: 'string', description: 'Who resolved or dismissed the handoff.' },
270
- detail_level: { type: 'string', enum: ['summary', 'detail'] },
271
- limit: { type: 'number' },
272
- }),
273
- definition('rh_context', 'Preferred ChatGPT facade: bounded repository context and the default code retrieval router, with optional CodeGraph structural augmentation.', {
274
- repo_id: repoId,
275
- checkout_id: { type: 'string', description: 'Optional checkout identity for repositories with multiple worktrees.' },
276
- operation: { type: 'string', enum: ['list', 'get', 'search'], description: 'Defaults to get. Use search as the default code-location path when an exact file is unknown.' },
277
- query: { type: 'string', description: 'Code/content intent for operation=search.' },
278
- known_paths: { type: 'array', items: { type: 'string' }, description: 'Optional exact paths or globs that should receive highest retrieval priority.' },
279
- include_globs: { type: 'array', items: { type: 'string' } },
280
- exclude_globs: { type: 'array', items: { type: 'string' } },
281
- retrieval_mode: { type: 'string', enum: ['implementation', 'plan', 'debug', 'review'], description: 'Defaults to implementation. Implementation returns current raw impact evidence for ChatGPT to judge; plan/debug/review intentionally widen evidence.' },
282
- impact_domains: { type: 'array', items: { type: 'string', enum: [...CONTROLLER_CONTEXT_IMPACT_DOMAINS] }, description: 'Optional GPT-selected cross-cutting evidence dimensions. Forge expands these mechanically in the same retrieval call; it never treats them as proof of semantic completeness.' },
283
- structural_context: { type: 'string', enum: ['off', 'auto', 'required'], description: 'Optional override. Defaults to auto for implementation/review and required for plan/debug.' },
284
- max_files: { type: 'number' },
285
- max_snippets: { type: 'number' },
286
- requested_check_ids: { type: 'array', items: { type: 'string' } },
287
- capability_id: { type: 'string', description: 'Optional capability identity. For plugin.<pluginId>.<actionId>, returns the exact typed action schema/policy for plugin_action_execute.' },
288
- work_id: { type: 'string' },
289
- detail_level: { type: 'string', enum: ['summary', 'detail', 'raw'], description: 'Defaults to summary; raw is still bounded.' },
290
- }),
291
- definition('rh_work', 'Preferred ChatGPT facade: bounded planning, direct control, controller ownership, and external SuperController launch.', {
292
- repo_id: repoId,
293
- operation: { type: 'string', enum: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_trigger'], description: 'Defaults to start. Complex starts require plan_id and plan_step_id. schedule_* manages generic recurring ChatGPT workflow schedules plus Work-bound continuation/browser subtypes.' },
294
- objective: { type: 'string' },
295
- work_id: { type: 'string' },
296
- related_work_id: { type: 'string', description: 'Optional explicit existing Work ownership target for continue/extend semantics. Unrelated active Work is never inferred as a semantic target.' },
297
- work_relation: { type: 'string', enum: ['continue', 'extend', 'parallel', 'new_goal'], description: 'Optional semantic relation to an explicitly related Work. This field never selects Direct/Process/Durable execution depth; checkout conflicts are resolved by placement.' },
298
- related_plan_id: { type: 'string', description: 'Existing active Plan selected when resolving another slice under the same Requirement.' },
299
- plan_relation: { type: 'string', enum: ['extend', 'parallel'], description: 'Relationship to another active Plan under the same Requirement. Omit to receive candidates first. parallel explicitly permits a distinct Plan slice; extend reuses the existing Plan authority and requires replanning/supersession instead of creating another draft.' },
300
- requested_by: { type: 'string', enum: ['chatgpt', 'user', 'system', 'scheduler'], description: 'Origin of the request. Scheduler-origin starts are continuation-only and cannot create new durable Work.' },
301
- requirement_id: { type: 'string', description: 'Stable Requirement authority used to find related active Work before creation.' },
302
- handoff_id: { type: 'string', description: 'Optional existing HandoffItem used to resume controller context.' },
303
- controller_id: { type: 'string', description: 'Stable external SuperController identity for controller_claim/release.' },
304
- controller_type: { type: 'string', enum: ['chatgpt', 'codex', 'grok', 'claude', 'human'], description: 'Controller kind for claims and launcher_start.' },
305
- session_id: { type: 'string', description: 'Controller-owned session identity used for an exclusive Work lease.' },
306
- lease_ms: { type: 'number', description: 'Authenticated Controller ownership lease duration, bounded to one hour. Legacy launcher callers may still use this as a launch-reservation TTL.' },
307
- launch_reservation_ms: { type: 'number', description: 'Short duplicate-spawn reservation TTL for launcher_start or schedule_create; does not grant Work ownership.' },
308
- executable: { type: 'string', description: 'External controller executable for launcher_start. This is owned by the Launcher, not the Kernel.' },
309
- launch_args: { type: 'array', items: { type: 'string' }, description: 'Provider-specific arguments for launcher_start.' },
310
- browser_session_id: { type: 'string', description: 'Saved Forge ChatGPT browser session to continue when controller_type=chatgpt.' },
311
- conversation_url: { type: 'string', description: 'Explicit https://chatgpt.com conversation URL used when no saved Forge browser session exists.' },
312
- continuation_prompt: { type: 'string', description: 'Bounded Work continuation instruction, or the recurring ChatGPT prompt when schedule_mode=workflow and objective is omitted.' },
313
- schedule_id: { type: 'string', description: 'Schedule identity for schedule_get/pause/resume/trigger.' },
314
- schedule_name: { type: 'string', description: 'Human-readable schedule name; generic workflow schedules use the objective/prompt when omitted.' },
315
- schedule_request_id: { type: 'string', description: 'Stable idempotency key for schedule_create.' },
316
- schedule_mode: { type: 'string', enum: ['workflow', 'continuation', 'browser_watch', 'browser_keepalive'], description: 'Defaults to workflow when work_id is omitted and continuation when work_id is present. workflow dispatches one bounded ChatGPT prompt per finite Occurrence without requiring Work; continuation/browser modes remain Work-bound.' },
317
- probe_url: { type: 'string', description: 'Allowed URL to navigate or refresh for browser_watch/browser_keepalive.' },
318
- probe_browser_session_id: { type: 'string', description: 'Existing Forge browser session used as the external observation target.' },
319
- probe_selector: { type: 'string', description: 'Optional selector limiting text extraction for browser_watch.' },
320
- probe_max_chars: { type: 'number', description: 'Bounded extracted text size for browser_watch.' },
321
- probe_timeout_ms: { type: 'number', description: 'Bounded browser probe timeout.' },
322
- include_terms: { type: 'array', items: { type: 'string' }, description: 'Optional case-insensitive terms; only matching normalized lines contribute to the observation fingerprint.' },
323
- ignore_patterns: { type: 'array', items: { type: 'string' }, description: 'Optional regex patterns stripped before fingerprinting volatile text such as relative timestamps.' },
324
- login_url_terms: { type: 'array', items: { type: 'string' }, description: 'Configured URL markers that classify the browser observation as authentication-required.' },
325
- login_text_terms: { type: 'array', items: { type: 'string' }, description: 'Configured page-text markers that classify the browser observation as authentication-required.' },
326
- wake_on_first_observation: { type: 'boolean', description: 'Defaults false so the first browser_watch occurrence records a silent baseline.' },
327
- wake_on_auth_required: { type: 'boolean', description: 'Defaults true for Work-bound browser schedules.' },
328
- auth_required_prompt: { type: 'string', description: 'Optional ChatGPT continuation instruction when the probe detects a login marker.' },
329
- trigger_type: { type: 'string', enum: ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'] },
330
- every_minutes: { type: 'number', description: 'Interval trigger cadence in minutes.' },
331
- cron_expression: { type: 'string' },
332
- schedule_timezone: { type: 'string' },
333
- catch_up_minutes: { type: 'number' },
334
- calendar_at: { type: 'string' },
335
- condition: { type: 'object' },
336
- event_name: { type: 'string' },
337
- event_id: { type: 'string' },
338
- event_data: { type: 'object' },
339
- dependency_job_ids: { type: 'array', items: { type: 'string' } },
340
- max_failures: { type: 'number' },
341
- cooldown_minutes: { type: 'number' },
342
- daily_budget_minutes: { type: 'number' },
343
- shadow_mode: { type: 'boolean', description: 'Defaults to true. Set false only after the continuation path has been validated.' },
344
- backoff_base_minutes: { type: 'number' },
345
- backoff_max_minutes: { type: 'number' },
346
- include_occurrences: { type: 'boolean' },
347
- plan_id: { type: 'string' },
348
- plan_step_id: { type: 'string', description: 'Approved PlanContract step to bind to a complex Goal Workloop start.' },
349
- scope_key: { type: 'string', description: 'Stable normalized scope identity used to prevent overlapping active plans.' },
350
- source_revision: { type: 'string', description: 'Repository revision observed during read-only planning preflight.' },
351
- plan_steps: { type: 'array', items: { type: 'object' }, description: 'Bounded plan steps with id, objective, dependencies, authoritative_files, allowed_paths, forbidden_paths, check_ids, and acceptance_criteria.' },
352
- non_goals: { type: 'array', items: { type: 'string' } },
353
- assumptions: { type: 'array', items: { type: 'string' } },
354
- resolved_decisions: { type: 'array', items: { type: 'string' } },
355
- stop_conditions: { type: 'array', items: { type: 'string' } },
356
- replan_conditions: { type: 'array', items: { type: 'string' } },
357
- integration_strategy: { type: 'string' },
358
- superseded_by: { type: 'string' },
359
- expected_files: { type: 'number' },
360
- expected_changed_lines: { type: 'number' },
361
- scope_clear: { type: 'boolean' },
362
- requires_investigation: { type: 'boolean' },
363
- requires_long_running_checks: { type: 'boolean' },
364
- requires_parallelism: { type: 'boolean' },
365
- needs_dependencies: { type: 'boolean' },
366
- requires_recovery: { type: 'boolean' },
367
- requires_worker: { type: 'boolean', description: 'Opt-in only. Set true only when the user explicitly requests Codex/Claude or another agent worker; never infer it from task complexity.' },
368
- requires_external_effect: { type: 'boolean' },
369
- requires_approval: { type: 'boolean' },
370
- requires_user_approval: { type: 'boolean' },
371
- destructive: { type: 'boolean' },
372
- remote_write: { type: 'boolean' },
373
- secret_access: { type: 'boolean' },
374
- capability_id: { type: 'string' },
375
- approval_confirmed: { type: 'boolean' },
376
- dry_run: { type: 'boolean', description: 'Defaults to true for repair.' },
377
- check_ids: { type: 'array', items: { type: 'string' } },
378
- check_id: { type: 'string' },
379
- acceptance_criteria: { type: 'array', items: { type: 'string' } },
380
- allowed_paths: { type: 'array', items: { type: 'string' } },
381
- forbidden_paths: { type: 'array', items: { type: 'string' } },
382
- constraints: { type: 'object' },
383
- repair_operation: { type: 'string', enum: ['diagnose', 'repair', 'verify', 'handoff'] },
384
- target: { type: 'string', enum: ['codex', 'grok', 'claude'], description: 'Explicit small-brain delegate target for operation=delegate. Delegation is never the default execution path.' },
385
- available: { type: 'boolean', description: 'Whether the delegate target is currently available.' },
386
- codex_available: { type: 'boolean' },
387
- simulate_check: { type: 'boolean', description: 'Test hook: skip real check execution and use infrastructure_failed/check_failed flags.' },
388
- infrastructure_failed: { type: 'boolean' },
389
- check_failed: { type: 'boolean' },
390
- authorize_destructive_cleanup: { type: 'boolean' },
391
- commit: { type: 'boolean', description: 'Finalize override. Defaults to true only when the Work owns uncommitted changes.' },
392
- merge: { type: 'boolean', description: 'Finalize override. Defaults to true for managed Work delivery.' },
393
- cleanup: { type: 'boolean', description: 'Finalize/stop override. Defaults to true so managed worktrees close automatically.' },
394
- target_branch: { type: 'string', description: 'Delivery branch; defaults to the repository default branch.' },
395
- delete_branch: { type: 'boolean', description: 'Defaults to true after target-branch containment is proven.' },
396
- no_ff: { type: 'boolean' },
397
- completion_outcome: { type: 'string', enum: ['completed_changed', 'completed_no_change'] },
398
- no_change_evidence: { type: 'string', description: 'Optional explicit proof for a no-change completion; otherwise the lifecycle coordinator derives proof only for an unchanged validated HEAD.' },
399
- reconcile_historical_delivery: { type: 'boolean', description: 'Explicitly request reviewed reconciliation for a historically delivered Direct Edit Work whose original Work binding/receipt is missing.' },
400
- reconcile_target_revision: { type: 'string', description: 'Exact already-integrated commit accepted by the reviewer.' },
401
- reconcile_compared_paths: { type: 'array', items: { type: 'string' }, description: 'Complete exact path set changed from the Work base revision to reconcile_target_revision.' },
402
- reconcile_rationale: { type: 'string', description: 'Bounded reviewer rationale for accepting the historical delivery.' },
403
- reconcile_cleanup_proof: { type: 'string', description: 'Concrete proof that no owned cleanup remains or that cleanup is complete.' },
404
- detail_level: { type: 'string', enum: ['summary', 'detail'] },
405
- reason: { type: 'string', description: 'Bounded reason for a work stop or repair record.' },
406
- }, [], false),
407
- definition('work_submit', 'Submit one durable repository operation and return a resumable Work handle.', {
408
- repo_id: repoId,
409
- request_id: { type: 'string', description: 'Stable idempotency key used to resume the same execution-child Work after reconnecting.' },
410
- parent_work_id: { type: 'string', description: 'Optional active primary Work that owns this low-level execution child. work_submit never creates a second business-workflow authority.' },
411
- operation: { type: 'string', description: 'Existing durable controller operation, for example run_check, dispatch_task, or create_issue.' },
412
- arguments: { type: 'object', description: 'Arguments passed to the durable operation.' },
413
- timeout_ms: { type: 'number' },
414
- }, ['request_id', 'operation'], false),
415
- definition('work_get', 'Resume one Work by work_id or request_id. Repository scope is always enforced.', {
416
- repo_id: repoId,
417
- work_id: { type: 'string' },
418
- request_id: { type: 'string' },
419
- include_events: { type: 'boolean' },
420
- wait: { type: 'boolean', description: 'When true, wait for terminal status before returning digest.' },
421
- wait_ms: { type: 'number' },
422
- }),
423
- definition('work_list', 'List recent resumable Work for one repository.', {
424
- repo_id: repoId,
425
- limit: { type: 'number' },
426
- }),
427
- definition('work_cancel', 'Cancel one queued or running Work by work_id or request_id.', {
428
- repo_id: repoId,
429
- work_id: { type: 'string' },
430
- request_id: { type: 'string' },
431
- reason: { type: 'string' },
432
- }, [], false),
433
- definition('work_wait', 'Wait for one Work/ExecutionJob/managed process to reach a terminal status and return a bounded operation digest.', {
434
- repo_id: repoId,
435
- work_id: { type: 'string' },
436
- request_id: { type: 'string' },
437
- wait_ms: { type: 'number', description: 'Max wait in ms. Default 15000, max 120000.' },
438
- }),
439
- definition('git_diff_paths', 'Return a bounded Git diff and status for explicit repository-relative paths only.', {
440
- repo_id: repoId,
441
- paths: { type: 'array', items: { type: 'string' } },
442
- staged: { type: 'boolean' },
443
- max_bytes: { type: 'number' },
444
- }, ['paths']),
445
- definition('git_stage_paths', 'Stage explicit repository-relative paths only using git add --all -- <paths>.', {
446
- repo_id: repoId,
447
- paths: { type: 'array', items: { type: 'string' } },
448
- }, ['paths'], false),
449
- definition('git_commit_paths', 'Stage and commit explicit repository-relative paths only, leaving unrelated staged changes untouched.', {
450
- repo_id: repoId,
451
- paths: { type: 'array', items: { type: 'string' } },
452
- message: { type: 'string' },
453
- }, ['paths', 'message'], false),
454
- definition('prepare_transfer_artifacts', 'Refresh rebuildable session continuation plus a selected-path patch transfer artifact when recovery context is needed.', {
455
- repo_id: repoId,
456
- reason: { type: 'string' },
457
- }, [], false),
458
- definition('get_job', 'Read one durable Execution Job. Summary is the default; opt in to full state only when needed.', {
459
- job_id: { type: 'string' },
460
- repo_id: repoId,
461
- include_events: { type: 'boolean' },
462
- detail_level: { type: 'string', enum: ['summary', 'full'] },
463
- wait: { type: 'boolean', description: 'When true, wait for terminal status before returning digest.' },
464
- wait_ms: { type: 'number' },
465
- }, ['job_id']),
466
- definition('get_artifact', 'Read one bounded Evidence Plane artifact by id. Large content remains bounded.', { artifact_id: { type: 'string' }, repo_id: repoId, max_bytes: { type: 'number' } }, ['artifact_id', 'repo_id']),
467
- definition('repository_change_verify', 'Composite: verify checkout/SHA, apply bounded patch, run checks, return first failure inline without get_job/get_artifact follow-ups.', {
468
- repo_id: repoId,
469
- expected_branch: { type: 'string' },
470
- expected_head: { type: 'string' },
471
- expected_file_shas: { type: 'object', additionalProperties: { type: 'string' } },
472
- patch: { type: 'string', description: 'Unified diff applied with git apply.' },
473
- allowed_paths: { type: 'array', items: { type: 'string' } },
474
- checks: { type: 'array', items: { type: 'string' } },
475
- check_timeout_ms: { type: 'number' },
476
- }, [], false),
477
- definition('list_jobs', 'List durable Execution Jobs for one repository. Summary is the default.', {
478
- repo_id: repoId,
479
- limit: { type: 'number' },
480
- detail_level: { type: 'string', enum: ['summary', 'full'] },
481
- }),
482
- definition('cancel_job', 'Cancel one queued or running durable Execution Job.', { job_id: { type: 'string' }, repo_id: repoId, reason: { type: 'string' } }, ['job_id'], false),
483
- definition('controller_ready', 'Return one binary Runtime readiness decision with bounded diagnostic evidence.', {
484
- repo_id: repoId,
485
- }),
486
- definition('repository_runtime_snapshot', 'Read the materialized runtime projection without scanning historical state.', { repo_id: repoId }),
487
- definition('schedule_dedupe_report', 'Read-only report that groups duplicate schedules by semantic trigger/action identity.', {
488
- repo_id: repoId,
489
- }, ['repo_id']),
490
- definition('schedule_dedupe_apply', 'Pause duplicate schedules after explicit authorization, keeping one canonical enabled schedule per semantic identity.', {
491
- repo_id: repoId,
492
- dry_run: { type: 'boolean', description: 'When true, only returns the proposed changes.' },
493
- confirm_authorization: { type: 'boolean', description: 'Must be true unless dry_run is true.' },
494
- }, ['repo_id']),
495
- definition('runtime_performance_diagnostics', 'Diagnose forge host performance, orphan workers, Local Controller presence, and cleanup candidates.', {
496
- repo_id: repoId,
497
- include_processes: { type: 'boolean', description: 'Include bounded forge related process samples. Defaults to true.' },
498
- include_temp_dirs: { type: 'boolean', description: 'Include bounded forge temporary directory scan. Defaults to true.' },
499
- cleanup_preview: { type: 'boolean', description: 'Return a no-side-effect cleanup plan for orphan workers and stale temp entries.' },
500
- }),
501
- definition('capability_recovery_probe', 'Read-only Runtime diagnostic evidence. This tool never starts, stops, restarts, repairs, or replaces Runtime modules.', {
502
- repo_id: repoId,
503
- recent_errors: { type: 'array', items: { type: 'string' } },
504
- command_preview_available: { type: 'boolean' },
505
- command_execute_available: { type: 'boolean' },
506
- issue_tools_available: { type: 'boolean' },
507
- job_tools_available: { type: 'boolean' },
508
- }),
509
- definition('capability_recovery_plan', 'Return a diagnostic handoff plan without Runtime lifecycle actions or automatic source repair.', {
510
- repo_id: repoId,
511
- recent_errors: { type: 'array', items: { type: 'string' } },
512
- }),
513
- definition('capability_recovery_apply', 'Apply one currently supported bounded recovery action after explicit authorization. Runtime lifecycle actions are outside this surface.', {
514
- repo_id: repoId,
515
- action_id: { type: 'string' },
516
- confirm_authorization: { type: 'boolean' },
517
- authorization: { type: 'string', description: 'Must equal action_id for actions that require authorization.' },
518
- reason: { type: 'string' },
519
- min_age_minutes: { type: 'number' },
520
- max_candidates: { type: 'number' },
521
- }, ['action_id'], false),
522
- definition('runtime_maintenance_status', 'Read the self-contained runtime maintenance plan without using repository_command_execute or Local Job tickets.', {
523
- repo_id: repoId,
524
- min_age_minutes: { type: 'number' },
525
- max_candidates: { type: 'number' },
526
- cancel_pending_approvals: { type: 'boolean' },
527
- }),
528
- definition('runtime_maintenance_apply', 'Apply one bounded runtime maintenance action without repository_command_execute or Local Job tickets.', {
529
- repo_id: repoId,
530
- action_id: { type: 'string', enum: ['local_jobs_reconcile', 'quarantine_unreadable_local_jobs', 'runtime_storage_finalize_relocation', 'rebuild_projection', 'full_maintenance_pass'] },
531
- confirm_maintenance: { type: 'boolean' },
532
- authorization: { type: 'string', description: 'Must equal action_id.' },
533
- min_age_minutes: { type: 'number' },
534
- max_candidates: { type: 'number' },
535
- cancel_pending_approvals: { type: 'boolean' },
536
- }, ['action_id', 'confirm_maintenance', 'authorization'], false),
537
- definition('goal_create', 'Deprecated historical GoalContract creation. Use rh_work PlanContract/WorkContract instead.', {
538
- repo_id: repoId,
539
- title: { type: 'string' },
540
- objective: { type: 'string' },
541
- mode: { type: 'string', enum: ['manual', 'supervised', 'autonomous'] },
542
- issue_id: { type: 'string' },
543
- task_ids: { type: 'array', items: { type: 'string' } },
544
- acceptance_criteria: { type: 'array', items: { type: 'string' } },
545
- check_ids: { type: 'array', items: { type: 'string' } },
546
- allowed_executors: { type: 'array', items: { type: 'string' } },
547
- forbidden_executors: { type: 'array', items: { type: 'string' } },
548
- retry_budget: { type: 'number' },
549
- }, ['title', 'objective'], false),
550
- definition('goal_list', 'List GoalContracts for a repository.', {
551
- repo_id: repoId,
552
- status: { type: 'string', enum: ['active', 'all', 'created', 'planning', 'ready', 'dispatching', 'running', 'verifying', 'repairing', 'waiting_for_user', 'handoff_ready', 'finalized', 'failed', 'stopped'] },
553
- limit: { type: 'number' },
554
- }),
555
- definition('goal_get', 'Get one GoalContract by id.', {
556
- repo_id: repoId,
557
- goal_id: { type: 'string' },
558
- }, ['goal_id']),
559
- definition('goal_start', 'Deprecated historical GoalContract execution. Use controller_claim and launcher_start.', {
560
- repo_id: repoId,
561
- goal_id: { type: 'string' },
562
- }, ['goal_id'], false),
563
- definition('goal_continue', 'Deprecated historical GoalContract execution. Use WorkContract and HandoffItem.', {
564
- repo_id: repoId,
565
- goal_id: { type: 'string' },
566
- }, ['goal_id'], false),
567
- definition('goal_stop', 'Stop a GoalContract.', {
568
- repo_id: repoId,
569
- goal_id: { type: 'string' },
570
- reason: { type: 'string' },
571
- }, ['goal_id'], false),
572
- definition('goal_finalize', 'Finalize a GoalContract when verification evidence exists.', {
573
- repo_id: repoId,
574
- goal_id: { type: 'string' },
575
- force: { type: 'boolean' },
576
- }, ['goal_id'], false),
577
- definition('goal_status', 'User-facing goal loop status (active goals, stage, provider health, handoff).', {
578
- repo_id: repoId,
579
- goal_id: { type: 'string' },
580
- }),
581
- definition('goal_tick_once', 'Deprecated. Kernel no longer ticks or dispatches Goal providers.', {
582
- repo_id: repoId,
583
- goal_id: { type: 'string' },
584
- task_intent: { type: 'string', enum: ['deterministic_edit', 'code_implementation', 'code_repair', 'architecture_planning', 'ios_build_or_sim', 'browser_automation', 'verification_repair', 'review', 'unknown'] },
585
- verification_ok: { type: 'boolean' },
586
- verification_check_id: { type: 'string' },
587
- provider_failure: { type: 'boolean' },
588
- external_write: { type: 'boolean' },
589
- approval_confirmed: { type: 'boolean' },
590
- }, [], false),
591
- definition('goal_handoff_packet_create', 'Create a ChatGPT/supervisor continuation packet (handoff-only path).', {
592
- repo_id: repoId,
593
- goal_id: { type: 'string' },
594
- blockers: { type: 'array', items: { type: 'string' } },
595
- required_user_decision: { type: 'string' },
596
- recommended_provider: { type: 'string' },
597
- }, ['goal_id'], false),
598
- definition('goal_handoff_packet_get', 'Read a durable goal handoff/continuation packet.', {
599
- repo_id: repoId,
600
- packet_id: { type: 'string' },
601
- }, ['packet_id']),
602
- definition('provider_list', 'List model/code executor providers (invokable vs handoff-only).', {
603
- repo_id: repoId,
604
- }),
605
- definition('provider_health', 'Bounded redacted provider health (never returns tokens).', {
606
- repo_id: repoId,
607
- provider_id: { type: 'string' },
608
- }),
609
- definition('provider_config_status', 'Summarize invokable vs missing_auth vs handoff-only providers.', {
610
- repo_id: repoId,
611
- }),
612
- definition('executor_route_preview', 'Preview which provider the ExecutorRouter would select.', {
613
- repo_id: repoId,
614
- goal_id: { type: 'string' },
615
- task_intent: { type: 'string', enum: ['deterministic_edit', 'code_implementation', 'code_repair', 'architecture_planning', 'ios_build_or_sim', 'browser_automation', 'verification_repair', 'review', 'unknown'] },
616
- risk: { type: 'string', enum: ['readonly', 'local_repo_write', 'workspace_write', 'remote_write', 'destructive', 'raw_secret_config'] },
617
- objective: { type: 'string' },
618
- }),
619
- definition('executor_dispatch', 'Policy-gated dispatch to an invokable provider; forge applies/verifies patches (no raw shell exposure).', {
620
- repo_id: repoId,
621
- goal_id: { type: 'string' },
622
- provider_id: { type: 'string' },
623
- task_intent: { type: 'string', enum: ['deterministic_edit', 'code_implementation', 'code_repair', 'architecture_planning', 'ios_build_or_sim', 'browser_automation', 'verification_repair', 'review', 'unknown'] },
624
- risk: { type: 'string', enum: ['readonly', 'local_repo_write', 'workspace_write', 'remote_write', 'destructive', 'raw_secret_config'] },
625
- approval_confirmed: { type: 'boolean' },
626
- external_write: { type: 'boolean' },
627
- strong_confirmation_text: { type: 'string' },
628
- }, ['goal_id'], false),
629
- definition('repair_plan', 'Classify goal failure and recommend repair provider/status.', {
630
- repo_id: repoId,
631
- goal_id: { type: 'string' },
632
- }, ['goal_id']),
633
- definition('repair_continue', 'Continue the self-healing repair loop for a goal (one bounded transition).', {
634
- repo_id: repoId,
635
- goal_id: { type: 'string' },
636
- force_failure_class: { type: 'string' },
637
- }, ['goal_id'], false),
638
- definition('workspace_auth_status', 'Summarize Workspace/Gmail auth readiness without returning or persisting secrets.', {
639
- repo_id: repoId,
640
- }),
641
- definition('workspace_auth_login_prepare', 'Prepare a state-protected local Google Workspace/Gmail OAuth login with PKCE and Keychain persistence.', {
642
- repo_id: repoId,
643
- service: { type: 'string', enum: ['gmail', 'calendar', 'tasks', 'google-workspace'] },
644
- scopes: { type: 'array', items: { type: 'string' } },
645
- redirect_uri: { type: 'string' },
646
- }),
647
- definition('assistant_model_readiness', 'Read the optional bounded Assistant model provider configuration without returning secrets.', {
648
- repo_id: repoId,
649
- }),
650
- definition('assistant_standing_grants', 'List scoped, expiring Assistant Standing Grants.', {
651
- repo_id: repoId,
652
- status: { type: 'string', enum: ['active', 'revoked', 'expired'] },
653
- limit: { type: 'number' },
654
- }),
655
- definition('assistant_standing_grant_create', 'Create an explicitly authorized Standing Grant for a hardcoded low-risk action.', {
656
- repo_id: repoId,
657
- name: { type: 'string' },
658
- plugin_id: { type: 'string' },
659
- action_id: { type: 'string' },
660
- routine_ids: { type: 'array', items: { type: 'string' } },
661
- sender_allowlist: { type: 'array', items: { type: 'string' } },
662
- subject_contains: { type: 'array', items: { type: 'string' } },
663
- min_confidence: { type: 'number' },
664
- max_per_run: { type: 'number' },
665
- expires_in_days: { type: 'number' },
666
- confirm_authorization: { type: 'boolean' },
667
- }, ['plugin_id', 'action_id', 'confirm_authorization'], false),
668
- definition('assistant_standing_grant_revoke', 'Revoke a Standing Grant with explicit authorization.', {
669
- repo_id: repoId,
670
- grant_id: { type: 'string' },
671
- reason: { type: 'string' },
672
- confirm_authorization: { type: 'boolean' },
673
- }, ['grant_id', 'confirm_authorization'], false),
674
- definition('assistant_action_proposals', 'List or get structured Assistant action proposals and execution status.', {
675
- repo_id: repoId,
676
- proposal_id: { type: 'string' },
677
- status: { type: 'string', enum: ['proposed', 'approved', 'rejected', 'executed', 'failed', 'expired'] },
678
- limit: { type: 'number' },
679
- }),
680
- definition('assistant_action_proposal_resolve', 'Approve or reject one Assistant action proposal. Approval submits a separate user-authorized plugin Job.', {
681
- repo_id: repoId,
682
- proposal_id: { type: 'string' },
683
- decision: { type: 'string', enum: ['approve', 'reject'] },
684
- request_id: { type: 'string' },
685
- reason: { type: 'string' },
686
- confirm_authorization: { type: 'boolean' },
687
- confirmation_text: { type: 'string' },
688
- }, ['proposal_id', 'decision'], false),
689
- definition('external_filesystem_targets_list', 'List pre-authorized external filesystem targets. Does not expose arbitrary absolute-path access.', {
690
- repo_id: repoId,
691
- }),
692
- definition('external_filesystem_grant_preview', 'Preview a read-only external filesystem grant for a narrow absolute directory without writing config.', {
693
- repo_id: repoId,
694
- grant_key: { type: 'string' },
695
- root_path: { type: 'string' },
696
- mode: { type: 'string', enum: ['read', 'copy_into_repo'] },
697
- expires_in_minutes: { type: 'number', description: 'Optional expiry in minutes, capped at 24h. Defaults to 8h.' },
698
- reason: { type: 'string' },
699
- }, ['grant_key', 'root_path', 'reason']),
700
- definition('external_filesystem_grant_apply', 'Apply a previously previewed read-only external filesystem grant with explicit authorization.', {
701
- repo_id: repoId,
702
- grant_key: { type: 'string' },
703
- root_path: { type: 'string' },
704
- mode: { type: 'string', enum: ['read', 'copy_into_repo'] },
705
- expires_in_minutes: { type: 'number', description: 'Optional expiry in minutes, capped at 24h. Defaults to 8h.' },
706
- reason: { type: 'string' },
707
- preview_ticket_id: { type: 'string' },
708
- confirm_authorization: { type: 'boolean' },
709
- }, ['grant_key', 'root_path', 'reason', 'preview_ticket_id', 'confirm_authorization'], false),
710
- definition('external_filesystem_text_snapshot', 'Read a bounded text/directory snapshot from a pre-authorized external filesystem target key.', {
711
- repo_id: repoId,
712
- target_key: { type: 'string' },
713
- path: { type: 'string' },
714
- max_chars: { type: 'number' },
715
- }, ['target_key']),
716
- definition('runtime_storage_repair_preview', 'Preview repo-scoped local-jobs runtime storage repair candidates without mutation.', {
717
- repo_id: repoId,
718
- min_age_minutes: { type: 'number' },
719
- max_candidates: { type: 'number' },
720
- }),
721
- definition('runtime_storage_repair_apply', 'Apply safe repo-scoped local-jobs runtime storage repairs after explicit confirmation.', {
722
- repo_id: repoId,
723
- candidate_ids: { type: 'array', items: { type: 'string' } },
724
- min_age_minutes: { type: 'number' },
725
- max_candidates: { type: 'number' },
726
- confirm_repair: { type: 'boolean' },
727
- }, ['confirm_repair'], false),
728
- definition('list_plugins', 'List repository plugins, or controller-scoped plugins when repo_id is omitted.', {
729
- repo_id: repoId,
730
- }),
731
- definition('get_plugin', 'Read one repository or controller-scoped plugin manifest including action schemas and policy requirements.', {
732
- repo_id: repoId,
733
- plugin_id: { type: 'string' },
734
- }, ['plugin_id']),
735
- definition('plugin_action_execute', 'Submit one typed repository or controller-scoped plugin action through the durable execution layer.', {
736
- repo_id: repoId,
737
- plugin_id: { type: 'string' },
738
- action_id: { type: 'string' },
739
- request_id: { type: 'string' },
740
- work_id: { type: 'string', description: 'Optional existing remote-effect Work that should own the successful typed plugin receipt.' },
741
- arguments: { type: 'object' },
742
- timeout_ms: { type: 'number' },
743
- confirm_authorization: { type: 'boolean' },
744
- confirmation_text: { type: 'string' },
745
- }, ['plugin_id', 'action_id', 'request_id'], false),
746
- definition('toolchain_plugin_summary', 'Return a redacted, low-interception plugin summary without raw config files or full action schemas.', {
747
- repo_id: repoId,
748
- plugin_id: { type: 'string' },
749
- }, ['plugin_id']),
750
- definition('work_result_summary', 'Return a redacted job result summary with failure class and suggested next actions. Does not return raw stdout or stderr.', {
751
- repo_id: repoId,
752
- job_id: { type: 'string' },
753
- }, ['job_id']),
754
- definition('work_status_digest', 'Return a redacted work status digest using a neutral work_ref. Does not return raw stdout or stderr.', {
755
- repo_id: repoId,
756
- work_ref: { type: 'string' },
757
- }, ['work_ref']),
758
- definition('model_clients_summary', 'List enabled model clients and DeepSeek adapter configuration state. Policy remains enforced by forge.', {
759
- repo_id: repoId,
760
- }),
761
- definition('model_control_plane_summary', 'Summarize primary and backup model controllers, handoff modes, and concurrency policy.', {
762
- repo_id: repoId,
763
- }),
764
- definition('deepseek_tool_manifest', 'Return DeepSeek function-calling tool manifests for the low-interception forge surface.', {
765
- repo_id: repoId,
766
- }),
767
- definition('deepseek_tool_call_prepare', 'Translate one DeepSeek function call into a forge operation without executing it.', {
768
- repo_id: repoId,
769
- function_name: { type: 'string' },
770
- function_arguments: { type: 'object' },
771
- }, ['function_name', 'function_arguments']),
772
- definition('deepseek_controller_manifest', 'Return DeepSeek backup-controller manifest, boundaries, and function tools.', {
773
- repo_id: repoId,
774
- }),
775
- definition('deepseek_controller_handoff_prepare', 'Prepare a safe DeepSeek backup-controller handoff packet without calling the external model.', {
776
- repo_id: repoId,
777
- reason: { type: 'string', enum: ['manual', 'chatgpt_platform_blocked', 'chatgpt_unavailable', 'parallel_review', 'local_gui_request'] },
778
- objective: { type: 'string' },
779
- current_controller: { type: 'string' },
780
- blocked_tool_name: { type: 'string' },
781
- recent_safe_error: { type: 'string' },
782
- }),
783
- definition('deepseek_controller_request_prepare', 'Prepare a DeepSeek chat-completions request for backup controller mode without sending it.', {
784
- repo_id: repoId,
785
- reason: { type: 'string', enum: ['manual', 'chatgpt_platform_blocked', 'chatgpt_unavailable', 'parallel_review', 'local_gui_request'] },
786
- objective: { type: 'string' },
787
- user_message: { type: 'string' },
788
- current_controller: { type: 'string' },
789
- blocked_tool_name: { type: 'string' },
790
- recent_safe_error: { type: 'string' },
791
- model: { type: 'string' },
792
- }),
793
- definition('assistant_readiness', 'Summarize personal-assistant readiness, Google plugin state, routines, inbox, and recommended next actions.', {
794
- repo_id: repoId,
795
- }),
796
- definition('gmail_triage_rules', 'Read repository-local Gmail triage rules used by the personal assistant manager.', { repo_id: repoId }),
797
- definition('gmail_triage_rule_upsert', 'Create or update one repository-local Gmail triage rule. This only changes repo-local assistant configuration.', {
798
- repo_id: repoId,
799
- id: { type: 'string' },
800
- enabled: { type: 'boolean' },
801
- order: { type: 'number' },
802
- match: { type: 'object' },
803
- decision: { type: 'object' },
804
- }, ['id', 'match', 'decision'], false),
805
- definition('gmail_triage_plan', 'Build a safe Gmail triage plan from supplied message summaries and current Gmail plugin readiness. Does not mutate Gmail.', {
806
- repo_id: repoId,
807
- query: { type: 'string' },
808
- items: { type: 'array', items: { type: 'object' } },
809
- }),
810
- definition('review_artifacts_prepare', 'Create bounded repo-local review artifact roots for browser/iOS screenshot review workflows.', { repo_id: repoId }, [], false),
811
- definition('review_artifacts_index', 'Index bounded repo-local browser/iOS screenshots, logs, and build reports for visual review.', { repo_id: repoId, limit: { type: 'number' } }),
812
- definition('browser_review_packet', 'Build a browser visual review packet from indexed browser screenshots.', { repo_id: repoId, limit: { type: 'number' } }),
813
- definition('ios_review_packet', 'Build or capture an iOS visual review packet from simulator screenshots and logs.', {
814
- repo_id: repoId,
815
- udid: { type: 'string' },
816
- label: { type: 'string' },
817
- capture: { type: 'boolean' },
818
- limit: { type: 'number' },
819
- }, [], false),
820
- definition('workflow_watchdog_report', 'Diagnose stuck repository workflows across execution jobs, local bridge jobs, schedules, and runtime processes.', {
821
- repo_id: repoId,
822
- stale_minutes: { type: 'number' },
823
- include_processes: { type: 'boolean' },
824
- }),
825
- definition('ios_xcode_status', 'Check local Xcode, xcodebuild, and simctl availability without mutation.', { repo_id: repoId }),
826
- definition('ios_simulators_list', 'List available iOS Simulator devices using structured simctl JSON output.', {
827
- repo_id: repoId,
828
- runtime: { type: 'string' },
829
- name: { type: 'string' },
830
- }),
831
- definition('ios_project_discover', 'Discover iOS workspaces, projects, Package.swift, and Info.plist files inside the repository.', { repo_id: repoId }),
832
- definition('ios_schemes_list', 'List Xcode schemes for a repository-bounded workspace or project.', {
833
- repo_id: repoId,
834
- workspace: { type: 'string' },
835
- project: { type: 'string' },
836
- }),
837
- definition('ios_simulator_boot', 'Boot an explicitly selected iOS Simulator UDID. Requires authorization.', {
838
- repo_id: repoId,
839
- udid: { type: 'string' },
840
- open_simulator: { type: 'boolean' },
841
- confirm_authorization: { type: 'boolean' },
842
- timeout_ms: { type: 'number' },
843
- }, ['udid', 'confirm_authorization'], false),
844
- definition('ios_app_build', 'Build an iOS app into .forge/ios/DerivedData using xcodebuild structured arguments.', {
845
- repo_id: repoId,
846
- scheme: { type: 'string' },
847
- udid: { type: 'string' },
848
- workspace: { type: 'string' },
849
- project: { type: 'string' },
850
- configuration: { type: 'string' },
851
- timeout_ms: { type: 'number' },
852
- }, ['scheme'], false),
853
- definition('ios_app_install', 'Install a bounded .app product from .forge/ios/DerivedData into a booted simulator. Requires authorization.', {
854
- repo_id: repoId,
855
- udid: { type: 'string' },
856
- app_path: { type: 'string' },
857
- confirm_authorization: { type: 'boolean' },
858
- }, ['udid', 'app_path', 'confirm_authorization'], false),
859
- definition('ios_app_launch', 'Launch a simulator app by bundle id with structured launch arguments. Requires authorization.', {
860
- repo_id: repoId,
861
- udid: { type: 'string' },
862
- bundle_id: { type: 'string' },
863
- arguments: { type: 'array', items: { type: 'string' } },
864
- confirm_authorization: { type: 'boolean' },
865
- }, ['udid', 'bundle_id', 'confirm_authorization'], false),
866
- definition('ios_simulator_screenshot', 'Capture a simulator screenshot to .forge/ios/screenshots as a bounded artifact.', {
867
- repo_id: repoId,
868
- udid: { type: 'string' },
869
- label: { type: 'string' },
870
- }, ['udid'], false),
871
- definition('ios_simulator_log_tail', 'Collect a bounded recent simulator log tail into .forge/ios/logs.', {
872
- repo_id: repoId,
873
- udid: { type: 'string' },
874
- process: { type: 'string' },
875
- last: { type: 'string' },
876
- max_bytes: { type: 'number' },
877
- }),
878
- definition('ios_ui_smoke_test', 'Compose build, boot, launch, screenshot, and bounded logs for simulator UI review. Requires authorization.', {
879
- repo_id: repoId,
880
- udid: { type: 'string' },
881
- scheme: { type: 'string' },
882
- bundle_id: { type: 'string' },
883
- workspace: { type: 'string' },
884
- project: { type: 'string' },
885
- configuration: { type: 'string' },
886
- app_path: { type: 'string' },
887
- screenshot_label: { type: 'string' },
888
- confirm_authorization: { type: 'boolean' },
889
- }, ['udid', 'scheme', 'bundle_id', 'confirm_authorization'], false),
890
- definition('runtime_cleanup_preview', 'Preview stale forge temp directories, terminal local jobs, and historical attention cleanup candidates.', {
891
- repo_id: repoId,
892
- min_age_minutes: { type: 'number' },
893
- include_temp_dirs: { type: 'boolean' },
894
- include_terminal_local_jobs: { type: 'boolean' },
895
- include_legacy_runs: { type: 'boolean' },
896
- include_historical_attention: { type: 'boolean' },
897
- max_candidates: { type: 'number' },
898
- }),
899
- definition('runtime_cleanup_apply', 'Apply safe cleanup candidates. Requires confirm_cleanup=true and remains limited to explicit forge candidates.', {
900
- repo_id: repoId,
901
- min_age_minutes: { type: 'number' },
902
- include_temp_dirs: { type: 'boolean' },
903
- include_terminal_local_jobs: { type: 'boolean' },
904
- include_legacy_runs: { type: 'boolean' },
905
- include_historical_attention: { type: 'boolean' },
906
- max_candidates: { type: 'number' },
907
- confirm_cleanup: { type: 'boolean' },
908
- }, [], false),
909
- definition('create_schedule', 'Create a bounded repository Schedule. Shadow mode defaults to true.', {
910
- repo_id: repoId,
911
- request_id: { type: 'string' },
912
- name: { type: 'string' },
913
- trigger_type: { type: 'string', enum: ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'] },
914
- every_minutes: { type: 'number' },
915
- cron_expression: { type: 'string' },
916
- calendar_at: { type: 'string' },
917
- condition: { type: 'object' },
918
- event_name: { type: 'string' },
919
- dependency_job_ids: { type: 'array', items: { type: 'string' } },
920
- operation: { type: 'string' },
921
- arguments: { type: 'object' },
922
- shadow_mode: { type: 'boolean' },
923
- daily_budget_minutes: { type: 'number' },
924
- cooldown_minutes: { type: 'number' },
925
- backoff_base_minutes: { type: 'number' },
926
- backoff_max_minutes: { type: 'number' },
927
- max_failures: { type: 'number' },
928
- stop_conditions: { type: 'array', items: { type: 'string' } },
929
- }, ['name', 'operation'], false),
930
- definition('list_schedules', 'List repository Schedules and recent bounded Occurrences.', { repo_id: repoId, include_occurrences: { type: 'boolean' } }),
931
- definition('pause_schedule', 'Pause a repository Schedule.', { repo_id: repoId, schedule_id: { type: 'string' }, reason: { type: 'string' } }, ['schedule_id'], false),
932
- definition('trigger_schedule', 'Create one idempotent bounded Occurrence for a Schedule or deliver a repository event.', {
933
- repo_id: repoId,
934
- schedule_id: { type: 'string' },
935
- event_name: { type: 'string' },
936
- event_id: { type: 'string' },
937
- event_data: { type: 'object' },
938
- }, ['schedule_id'], false),
939
- definition('request_release_gate', 'Return an explicit external-Controller handoff for release evidence. Push, tag, and publish remain separately authorized.', { repo_id: repoId, request_id: { type: 'string' } }, [], false),
940
- definition('create_portfolio_workflow', 'Create a cross-repository DAG with deterministic Saga stop/compensation semantics.', {
941
- name: { type: 'string' }, request_id: { type: 'string' }, failure_policy: { type: 'string', enum: ['stop', 'compensate'] }, steps: { type: 'array', items: { type: 'object' } },
942
- }, ['name', 'request_id', 'steps'], false),
943
- definition('list_portfolio_workflows', 'List cross-repository Portfolio workflows.', { limit: { type: 'number' } }),
944
- definition('get_portfolio_workflow', 'Read one Portfolio workflow and its repository DAG state.', { workflow_id: { type: 'string' } }, ['workflow_id']),
945
- definition('record_candidate_finding', 'Record or refresh a deduplicated candidate requirement without creating an Issue automatically.', {
946
- repo_id: repoId, request_id: { type: 'string' }, semantic_key: { type: 'string' }, title: { type: 'string' }, summary: { type: 'string' }, severity: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] }, reference: { type: 'string' }, evidence: { type: 'object' },
947
- }, ['semantic_key', 'title'], false),
948
- definition('list_candidate_findings', 'List bounded candidate findings awaiting human promotion or dismissal.', { repo_id: repoId, include_terminal: { type: 'boolean' }, limit: { type: 'number' } }),
949
- definition('promote_candidate_finding', 'Return the candidate finding and an explicit external-Controller handoff for Issue creation.', {
950
- repo_id: repoId, finding_id: { type: 'string' }, request_id: { type: 'string' }, kind: { type: 'string', enum: ['bug', 'feature', 'governance', 'investigation'] }, goals: { type: 'array', items: { type: 'string' } }, acceptance_criteria: { type: 'array', items: { type: 'string' } },
951
- }, ['finding_id'], false),
952
- ];
953
-
954
- export const connectorExposedTools = runtimeToolDefinitions.map((tool) => tool.name);
955
- export const currentCallableTools = new Set(connectorExposedTools);
225
+ export {
226
+ connectorExposedTools,
227
+ currentCallableTools,
228
+ runtimeToolDefinitions,
229
+ } from './runtime-tool-definitions';
230
+ import {
231
+ currentCallableTools,
232
+ runtimeToolDefinitions,
233
+ } from './runtime-tool-definitions';
956
234
 
957
235
  const RH_CONTEXT_CURRENT_WINDOW_MS = 24 * 60 * 60 * 1_000;
958
236
 
@@ -971,6 +249,233 @@ function isCurrentRhContextWork(
971
249
  return timestampIsCurrent(contract.updatedAt, cutoffMs);
972
250
  }
973
251
 
252
+ function rhContextReadSessionId(ctx: MultiRepositoryMcpToolContext): string | undefined {
253
+ const principal = ctx.principalId?.trim();
254
+ if (principal) {
255
+ const controllerInstance = ctx.controllerInstanceId?.trim() || currentControllerInstanceId();
256
+ return `controller:${principal}:${controllerInstance}`;
257
+ }
258
+ const transportSession = ctx.sessionId?.trim();
259
+ return transportSession ? `transport:${transportSession}` : undefined;
260
+ }
261
+
262
+ const RH_CONTEXT_SEMANTIC_QUERY_LIMIT = 8;
263
+ const RH_CONTEXT_SEMANTIC_LOCATION_LIMIT = 200;
264
+ const RH_CONTEXT_LEGACY_SEMANTIC_SYNTAX = '@tsnav references <repo-path>:<line>:<column> | @swiftnav references <repo-path>:<line>:<column>';
265
+
266
+ type RhContextSemanticNavigationRequest = {
267
+ navigation: SemanticNavigationKind;
268
+ path: string;
269
+ line: number;
270
+ column: number;
271
+ tsconfig_path?: string;
272
+ language?: string;
273
+ };
274
+
275
+ function rhContextLegacySemanticQuery(query: string): {
276
+ retrievalQuery: string;
277
+ requests: RhContextSemanticNavigationRequest[];
278
+ } {
279
+ const requests: RhContextSemanticNavigationRequest[] = [];
280
+ const directive = /(?:^|\s)@(tsnav|swiftnav)\s+(definition|references|implementations)\s+([^\s]+):(\d+):(\d+)(?:\s+tsconfig=([^\s]+))?/gi;
281
+ const retrievalQuery = query.replace(directive, (_match, directiveKind, navigation, path, line, column, tsconfigPath) => {
282
+ const language = String(directiveKind).toLowerCase() === 'swiftnav' ? 'swift' : 'typescript';
283
+ requests.push({
284
+ navigation: String(navigation).toLowerCase() as SemanticNavigationKind,
285
+ path: String(path),
286
+ line: Number(line),
287
+ column: Number(column),
288
+ language,
289
+ ...(tsconfigPath ? { tsconfig_path: String(tsconfigPath) } : {}),
290
+ });
291
+ return ' ';
292
+ }).replace(/\s+/g, ' ').trim();
293
+ const fallback = requests.length > 0
294
+ ? `${requests[0]!.language === 'swift' ? 'Swift' : 'TypeScript'} ${requests[0]!.navigation} ${requests[0]!.path}`
295
+ : query;
296
+ return { retrievalQuery: retrievalQuery || fallback, requests };
297
+ }
298
+
299
+ interface RhContextSemanticNavigationProjection {
300
+ requested: number;
301
+ executed: number;
302
+ results: Record<string, unknown>[];
303
+ errors: Array<{ index: number; code: string; message: string }>;
304
+ providers: unknown[];
305
+ policyDeniedLocations: number;
306
+ policyDeniedReads: number;
307
+ policyDeniedReadSamples: string[];
308
+ requestTruncated: boolean;
309
+ freshness: 'not_requested' | 'changed_during_query' | 'current_at_query';
310
+ sourceIdentity?: Record<string, unknown>;
311
+ staticClosure: { scope: string; status: 'not_requested' | 'incomplete' | 'complete_for_requested_symbols'; limitations: string[] };
312
+ }
313
+
314
+ async function rhContextSemanticNavigation(
315
+ repoRoot: string,
316
+ policy: MultiRepositoryMcpToolContext['policy'],
317
+ value: unknown,
318
+ repositoryIdentity: { repoId: string; checkoutId: string },
319
+ ): Promise<RhContextSemanticNavigationProjection> {
320
+ const raw = Array.isArray(value) ? value : [];
321
+ const requests = raw.slice(0, RH_CONTEXT_SEMANTIC_QUERY_LIMIT);
322
+ const results: Record<string, unknown>[] = [];
323
+ const errors: Array<{ index: number; code: string; message: string }> = [];
324
+ let anyLocationTruncated = false;
325
+ let policyDeniedLocations = 0;
326
+ let policyDeniedReads = 0;
327
+ const policyDeniedReadSamples = new Set<string>();
328
+ const semanticAccessScope = createHash('sha256')
329
+ .update(JSON.stringify({ profile: policy.profile, readGlobs: policy.readGlobs, denyGlobs: policy.denyGlobs }))
330
+ .digest('hex')
331
+ .slice(0, 20);
332
+
333
+ const fingerprintOf = (identity: ReturnType<typeof freshGitIdentity> | undefined): string | undefined => identity
334
+ ? identity.workingTreeFingerprint
335
+ ?? createHash('sha256').update(`${identity.head ?? ''}\n${identity.branch ?? ''}`).digest('hex').slice(0, 24)
336
+ : undefined;
337
+ // Semantic providers are targeted/optional, so pay the stronger fresh identity
338
+ // sampling cost only when semantic evidence was explicitly requested.
339
+ const sourceBefore = requests.length > 0 ? freshGitIdentity(repoRoot) : undefined;
340
+ const sourceFingerprintBefore = fingerprintOf(sourceBefore);
341
+ const indexedRequests: Array<{ index: number; request: SemanticNavigationRequest }> = [];
342
+
343
+ requests.forEach((entry, index) => {
344
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
345
+ errors.push({ index, code: 'SEMANTIC_NAVIGATION_REQUEST_INVALID', message: 'semantic_navigation entries must be objects.' });
346
+ return;
347
+ }
348
+ const item = entry as Record<string, unknown>;
349
+ const navigation = String(item.navigation ?? '') as SemanticNavigationKind;
350
+ const path = String(item.path ?? '').trim();
351
+ const line = Number(item.line);
352
+ const column = Number(item.column);
353
+ const tsconfigPath = typeof item.tsconfig_path === 'string' && item.tsconfig_path.trim() ? item.tsconfig_path.trim() : undefined;
354
+ const language = typeof item.language === 'string' && item.language.trim() ? item.language.trim().toLowerCase() : undefined;
355
+ if (!['definition', 'references', 'implementations'].includes(navigation) || !path || !Number.isInteger(line) || line < 1 || !Number.isInteger(column) || column < 1) {
356
+ errors.push({ index, code: 'SEMANTIC_NAVIGATION_REQUEST_INVALID', message: 'navigation, path, and positive 1-based line/column are required.' });
357
+ return;
358
+ }
359
+ const targetDecision = resolveMcpPath(repoRoot, path, policy, 'read');
360
+ if (!targetDecision.ok) {
361
+ errors.push({ index, code: 'SEMANTIC_NAVIGATION_TARGET_DENIED', message: targetDecision.reason ?? 'target path denied by MCP read policy.' });
362
+ return;
363
+ }
364
+ indexedRequests.push({
365
+ index,
366
+ request: { navigation, path, line, column, ...(tsconfigPath ? { tsconfigPath } : {}), ...(language ? { language } : {}) },
367
+ });
368
+ });
369
+
370
+ const allowRepositoryPath = (relativePath: string): boolean => {
371
+ const decision = resolveMcpPath(repoRoot, relativePath, policy, 'read');
372
+ if (decision.ok) return true;
373
+ policyDeniedReads += 1;
374
+ if (policyDeniedReadSamples.size < 20) policyDeniedReadSamples.add(relativePath);
375
+ return false;
376
+ };
377
+
378
+ const navigationOutcomes = await defaultSemanticProviderRegistry.navigate(repoRoot, indexedRequests, {
379
+ cacheScope: `mcp:${semanticAccessScope}`,
380
+ sourceIdentity: sourceFingerprintBefore,
381
+ profile: policy.profile,
382
+ allowRepositoryPath,
383
+ });
384
+
385
+ for (const { index, outcome } of navigationOutcomes) {
386
+ if (!outcome.ok) {
387
+ errors.push({ index, code: outcome.code, message: outcome.message });
388
+ continue;
389
+ }
390
+ const semantic = outcome.result;
391
+ const allowedLocations = semantic.locations.filter((location) => {
392
+ const decision = resolveMcpPath(repoRoot, location.path, policy, 'read');
393
+ if (decision.ok) return true;
394
+ policyDeniedLocations += 1;
395
+ return false;
396
+ });
397
+ const truncated = allowedLocations.length > RH_CONTEXT_SEMANTIC_LOCATION_LIMIT;
398
+ anyLocationTruncated ||= truncated;
399
+ results.push({
400
+ provider: semantic.providerId,
401
+ ...(semantic.providerIdentity ? { providerIdentity: semantic.providerIdentity } : {}),
402
+ language: semantic.language,
403
+ navigation: semantic.navigation,
404
+ target: semantic.target,
405
+ locations: allowedLocations.slice(0, RH_CONTEXT_SEMANTIC_LOCATION_LIMIT),
406
+ totalLocations: allowedLocations.length,
407
+ returnedLocations: Math.min(allowedLocations.length, RH_CONTEXT_SEMANTIC_LOCATION_LIMIT),
408
+ truncated,
409
+ policyDeniedReads: semantic.policyDeniedReads ?? 0,
410
+ ...(semantic.details ?? {}),
411
+ });
412
+ }
413
+
414
+ results.sort((left, right) => {
415
+ const leftTarget = left.target as { path?: string; line?: number; column?: number } | undefined;
416
+ const rightTarget = right.target as { path?: string; line?: number; column?: number } | undefined;
417
+ return String(leftTarget?.path ?? '').localeCompare(String(rightTarget?.path ?? ''))
418
+ || Number(leftTarget?.line ?? 0) - Number(rightTarget?.line ?? 0)
419
+ || Number(leftTarget?.column ?? 0) - Number(rightTarget?.column ?? 0);
420
+ });
421
+
422
+ const sourceAfter = requests.length > 0 ? freshGitIdentity(repoRoot) : undefined;
423
+ const sourceFingerprintAfter = fingerprintOf(sourceAfter);
424
+ const sourceChangedDuringQuery = Boolean(
425
+ sourceBefore
426
+ && sourceAfter
427
+ && (sourceBefore.head !== sourceAfter.head || sourceFingerprintBefore !== sourceFingerprintAfter),
428
+ );
429
+ if (sourceChangedDuringQuery) {
430
+ errors.push({
431
+ index: -1,
432
+ code: 'SEMANTIC_SOURCE_CHANGED_DURING_QUERY',
433
+ message: 'Repository source identity changed while semantic providers were running. Returned locations are retained only as hints for the sampled source and are not proof for the newer source state.',
434
+ });
435
+ }
436
+
437
+ const requestTruncated = raw.length > RH_CONTEXT_SEMANTIC_QUERY_LIMIT;
438
+ const incomplete = requestTruncated || anyLocationTruncated || policyDeniedLocations > 0 || policyDeniedReads > 0 || errors.length > 0 || sourceChangedDuringQuery;
439
+ const languages = new Set(results.map((entry) => String(entry.language ?? '')).filter(Boolean));
440
+ const singleLanguage = languages.size === 1 ? [...languages][0] : undefined;
441
+ const scope = singleLanguage === 'typescript'
442
+ ? 'requested_typescript_static_relationships'
443
+ : singleLanguage === 'swift'
444
+ ? 'requested_swift_static_relationships'
445
+ : singleLanguage
446
+ ? `requested_${singleLanguage.replace(/[^a-z0-9_-]+/gi, '_').toLowerCase()}_static_relationships`
447
+ : languages.size > 1
448
+ ? 'requested_multilanguage_static_relationships'
449
+ : 'requested_semantic_static_relationships';
450
+ return {
451
+ requested: raw.length,
452
+ executed: requests.length,
453
+ results,
454
+ errors,
455
+ providers: defaultSemanticProviderRegistry.list(),
456
+ policyDeniedLocations,
457
+ policyDeniedReads,
458
+ policyDeniedReadSamples: Array.from(policyDeniedReadSamples),
459
+ requestTruncated,
460
+ freshness: raw.length === 0 ? 'not_requested' : sourceChangedDuringQuery ? 'changed_during_query' : 'current_at_query',
461
+ ...(sourceBefore ? {
462
+ sourceIdentity: {
463
+ repoId: repositoryIdentity.repoId,
464
+ checkoutId: repositoryIdentity.checkoutId,
465
+ branch: sourceBefore.branch,
466
+ head: sourceBefore.head,
467
+ workingTreeFingerprint: sourceFingerprintBefore,
468
+ sampledAt: new Date(sourceBefore.sampledAt).toISOString(),
469
+ },
470
+ } : {}),
471
+ staticClosure: {
472
+ scope,
473
+ status: raw.length === 0 ? 'not_requested' : incomplete ? 'incomplete' : 'complete_for_requested_symbols',
474
+ limitations: ['dynamic_registration', 'string_or_config_edges', 'reflection', 'runtime_dispatch', 'stale_or_missing_language_index'],
475
+ },
476
+ };
477
+ }
478
+
974
479
  export function summarizeControllerReadyPayload(fullPayload: Record<string, unknown>): Record<string, unknown> {
975
480
  const health = (fullPayload.health ?? {}) as Record<string, unknown>;
976
481
  const workerLoop = (fullPayload.workerLoop ?? {}) as Record<string, unknown>;
@@ -1110,6 +615,32 @@ async function callStandaloneRecoveryTool(
1110
615
  }
1111
616
  }
1112
617
 
618
+ export const RH_WORK_VERIFY_LEASE_WAIT_MS = DEFAULT_WORK_CHECK_LEASE_WAIT_MS;
619
+
620
+ type TerminalCheckEvidenceState = 'matched' | 'process_runtime_failed_before_result' | 'missing' | 'mismatch';
621
+
622
+ export function classifyTerminalCheckEvidence(input: {
623
+ processError?: { code: string; message: string };
624
+ structuredPresent: boolean;
625
+ structuredMatches: boolean;
626
+ legacyPresent: boolean;
627
+ legacyMatches: boolean;
628
+ }): { state: TerminalCheckEvidenceState; warning?: string; infrastructureReason?: string } {
629
+ if (input.structuredMatches || input.legacyMatches) return { state: 'matched' };
630
+ if (!input.structuredPresent && !input.legacyPresent && input.processError?.message?.trim()) {
631
+ const reason = input.processError.message.trim().slice(0, 512);
632
+ return {
633
+ state: 'process_runtime_failed_before_result',
634
+ warning: `check process failed before structured result receipt: ${reason}`,
635
+ infrastructureReason: reason,
636
+ };
637
+ }
638
+ if (input.structuredPresent || input.legacyPresent) {
639
+ return { state: 'mismatch', warning: 'check result receipt did not match the terminal Process semantic identity' };
640
+ }
641
+ return { state: 'missing', warning: 'check result receipt is missing for the terminal Check Process' };
642
+ }
643
+
1113
644
  function result(value: Record<string, unknown>, isError = false): CallToolResult {
1114
645
  // Compact text channel by default (no pretty-print bloat).
1115
646
  return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value, ...(isError ? { isError: true } : {}) };
@@ -1293,6 +824,17 @@ function summarizePluginActionReceipt(manifest: ReturnType<typeof getAssistantPl
1293
824
  };
1294
825
  }
1295
826
 
827
+ function compactSubmittedPluginActionResult(value: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
828
+ if (!value) return undefined;
829
+ const nested = value.result;
830
+ if (!nested || typeof nested !== 'object' || Array.isArray(nested)) return value;
831
+ const work = value.work;
832
+ return {
833
+ ...(nested as Record<string, unknown>),
834
+ ...(work && typeof work === 'object' && !Array.isArray(work) ? { work } : {}),
835
+ };
836
+ }
837
+
1296
838
 
1297
839
  function stringList(value: unknown): string[] {
1298
840
  return Array.isArray(value) ? value.map(String).map((entry) => entry.trim()).filter(Boolean) : [];
@@ -1636,6 +1178,7 @@ function ensureFacadeWorkHandle(
1636
1178
  managedWorktree,
1637
1179
  workContractId: contract.workId,
1638
1180
  baseCommit: contract.baseRevision ?? status.head ?? undefined,
1181
+ deliveryBaseCommit: contract.baseRevision ?? status.head ?? undefined,
1639
1182
  expectedHead: status.head ?? contract.baseRevision,
1640
1183
  permissionSnapshotVersion: currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId),
1641
1184
  state: 'prepared',
@@ -1646,6 +1189,206 @@ function ensureFacadeWorkHandle(
1646
1189
  });
1647
1190
  }
1648
1191
 
1192
+ function materializeFacadeWorkPlacement(
1193
+ ctx: MultiRepositoryMcpToolContext,
1194
+ repository: ReturnType<typeof selected>,
1195
+ workId: string,
1196
+ args: Record<string, unknown>,
1197
+ ) {
1198
+ const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
1199
+ const contract = getWorkContract(store, workId);
1200
+ if (!contract || contract.worktreePolicy.required !== true || contract.worktreeRef) return contract;
1201
+ const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
1202
+ requestId: workId,
1203
+ title: contract.objective,
1204
+ baseRef: contract.baseRevision,
1205
+ prepareDependencies: args.needs_dependencies === true,
1206
+ });
1207
+ if (!workspace.managed || !workspace.checkoutId || !workspace.root || workspace.checkoutId === repository.activeCheckoutId) {
1208
+ throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
1209
+ }
1210
+ return updateWorkContract(store, workId, {
1211
+ checkoutId: workspace.checkoutId,
1212
+ baseRevision: workspace.baseRevision ?? contract.baseRevision,
1213
+ worktreeRef: workspace.root,
1214
+ driver: { ...contract.driver, preferred: 'isolated_worktree', allowDirectEdit: false },
1215
+ });
1216
+ }
1217
+
1218
+ function reauthorizeCancelledFacadeWork(
1219
+ ctx: MultiRepositoryMcpToolContext,
1220
+ repository: ReturnType<typeof selected>,
1221
+ workId: string,
1222
+ args: Record<string, unknown>,
1223
+ ): { reconstructedCheckout: boolean } {
1224
+ const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
1225
+ let work = getWorkContract(store, workId);
1226
+ if (!work || work.status !== 'cancelled') return { reconstructedCheckout: false };
1227
+ if (args.approval_confirmed !== true || args.requested_by !== 'user') {
1228
+ throw new Error(`WORK_CANCELLED_RESUME_REAUTHORIZATION_REQUIRED: ${workId}; use exact work_id with requested_by=user and approval_confirmed=true`);
1229
+ }
1230
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
1231
+ if (!work.principalId?.trim() || work.principalId.trim() !== identity.principalId) {
1232
+ throw new Error(`WORK_CANCELLED_RESUME_PRINCIPAL_MISMATCH: ${workId}`);
1233
+ }
1234
+ const owner = getControllerSession(store, workId);
1235
+ if (owner && (owner.principalId?.trim() || owner.controllerId) !== identity.principalId) {
1236
+ throw new Error(`WORK_CANCELLED_RESUME_CONTROLLER_OWNERSHIP_MISMATCH: ${workId}`);
1237
+ }
1238
+ if (work.completionReceipt || work.completionOutcome) throw new Error(`WORK_CANCELLED_RESUME_COMPLETION_CONFLICT: ${workId}`);
1239
+ if (work.reconciliations.length > 0) throw new Error(`WORK_CANCELLED_RESUME_DELIVERY_HISTORY_AMBIGUOUS: ${workId}`);
1240
+ if (work.phase !== 'cleanup' || work.phaseEvidence.cleanup.state !== 'skipped' || work.phaseEvidence.cleanup.source !== 'recorded') {
1241
+ throw new Error(`WORK_CANCELLED_RESUME_HISTORY_AMBIGUOUS: ${workId}`);
1242
+ }
1243
+ if (work.workKind !== 'repository_change' || work.worktreePolicy.required !== true) {
1244
+ throw new Error(`WORK_CANCELLED_RESUME_ISOLATED_REPOSITORY_WORK_REQUIRED: ${workId}`);
1245
+ }
1246
+
1247
+ let handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
1248
+ if (handle) {
1249
+ if (handle.repositoryId !== repository.repoId || (handle.workContractId && handle.workContractId !== workId)) {
1250
+ throw new Error(`WORK_CANCELLED_RESUME_HANDLE_IDENTITY_MISMATCH: ${workId}`);
1251
+ }
1252
+ if (handle.principalId !== identity.principalId) throw new Error(`WORK_CANCELLED_RESUME_HANDLE_PRINCIPAL_MISMATCH: ${workId}`);
1253
+ if (['committed', 'merged', 'cleaned', 'failed_terminal_cleanup'].includes(handle.state)
1254
+ || handle.finalization.commit === 'done'
1255
+ || handle.finalization.merge === 'done'
1256
+ || handle.cleanupReceipt?.complete === true) {
1257
+ throw new Error(`WORK_CANCELLED_RESUME_DELIVERY_OR_CLEANUP_CONFLICT: ${workId}`);
1258
+ }
1259
+ }
1260
+
1261
+ const registryRepository = listRepositories(ctx.controllerHome, { includeRemoved: true })
1262
+ .find((candidate) => candidate.repoId === repository.repoId);
1263
+ if (!registryRepository) throw new Error(`WORK_CANCELLED_RESUME_REPOSITORY_MISSING: ${workId}`);
1264
+ const recordedCheckoutId = work.checkoutId?.trim();
1265
+ const recordedWorktree = work.worktreeRef?.trim();
1266
+ const checkoutRecord = recordedCheckoutId
1267
+ ? registryRepository.checkouts.find((candidate) => candidate.checkoutId === recordedCheckoutId)
1268
+ : undefined;
1269
+ const checkoutActive = checkoutRecord?.lifecycle === 'active';
1270
+ const worktreePresent = Boolean(recordedWorktree && existsSync(recordedWorktree));
1271
+
1272
+ if (checkoutActive && worktreePresent && recordedCheckoutId && recordedWorktree) {
1273
+ const retained = selectRepositoryCheckout(registryRepository, recordedCheckoutId);
1274
+ if (resolve(retained.canonicalRoot) !== resolve(recordedWorktree)) {
1275
+ throw new Error(`WORK_CANCELLED_RESUME_CHECKOUT_REUSED: ${workId}`);
1276
+ }
1277
+ if (!handle) throw new Error(`WORK_CANCELLED_RESUME_HANDLE_REQUIRED: ${workId}`);
1278
+ if (handle.checkoutId !== recordedCheckoutId || resolve(handle.worktreePath) !== resolve(recordedWorktree)) {
1279
+ throw new Error(`WORK_CANCELLED_RESUME_CHECKOUT_OWNERSHIP_MISMATCH: ${workId}`);
1280
+ }
1281
+ const status = repositoryGitStatus(retained);
1282
+ if (!status.branch || status.branch !== handle.branch) throw new Error(`WORK_CANCELLED_RESUME_BRANCH_OWNERSHIP_MISMATCH: ${workId}`);
1283
+ writeWorkHandle(ctx.controllerHome, {
1284
+ ...handle,
1285
+ principalId: identity.principalId,
1286
+ sessionId: identity.sessionId,
1287
+ updatedAt: new Date().toISOString(),
1288
+ });
1289
+ resumeRetainedCancelledWorkContract(store, workId, {
1290
+ principalId: identity.principalId,
1291
+ controllerInstanceId: identity.controllerInstanceId,
1292
+ summary: 'Explicit current user re-authorized the same retained cancelled Work; exact managed checkout ownership was revalidated.',
1293
+ });
1294
+ return { reconstructedCheckout: false };
1295
+ }
1296
+
1297
+ if (checkoutActive) throw new Error(`WORK_CANCELLED_RESUME_PRESERVATION_AMBIGUOUS: ${workId}`);
1298
+ if (!recordedCheckoutId || !recordedWorktree || !handle) throw new Error(`WORK_CANCELLED_RESUME_HANDLE_REQUIRED: ${workId}`);
1299
+
1300
+ const actualChangedPaths = work.scopeEvidence?.actualChangedPaths;
1301
+ if (!actualChangedPaths || actualChangedPaths.length !== 0
1302
+ || work.checks.length !== 0
1303
+ || work.checkRefs.length !== 0
1304
+ || work.evidenceState !== 'none') {
1305
+ throw new Error(`WORK_CANCELLED_RESUME_ZERO_DELTA_PROOF_REQUIRED: ${workId}`);
1306
+ }
1307
+ const recordedBase = work.baseRevision?.trim();
1308
+ if (!recordedBase) throw new Error(`WORK_CANCELLED_RESUME_BASE_REVISION_REQUIRED: ${workId}`);
1309
+ const baseRevision = gitCommitAtRef(repository.canonicalRoot, recordedBase);
1310
+ if (!baseRevision) throw new Error(`WORK_CANCELLED_RESUME_BASE_REVISION_MISSING: ${workId}`);
1311
+ const handleBase = handle.baseCommit ? gitCommitAtRef(repository.canonicalRoot, handle.baseCommit) : undefined;
1312
+ const handleHead = handle.expectedHead ? gitCommitAtRef(repository.canonicalRoot, handle.expectedHead) : undefined;
1313
+ if ((handle.baseCommit && handleBase !== baseRevision) || (handle.expectedHead && handleHead !== baseRevision)) {
1314
+ throw new Error(`WORK_CANCELLED_RESUME_REVISION_AMBIGUOUS: ${workId}`);
1315
+ }
1316
+ if (handle.checkoutId !== recordedCheckoutId || resolve(handle.worktreePath) !== resolve(recordedWorktree)) {
1317
+ throw new Error(`WORK_CANCELLED_RESUME_CHECKOUT_OWNERSHIP_MISMATCH: ${workId}`);
1318
+ }
1319
+ if (handle.branch) {
1320
+ const historicalBranchHead = gitCommitAtRef(repository.canonicalRoot, `refs/heads/${handle.branch}`);
1321
+ if (historicalBranchHead && historicalBranchHead !== baseRevision) {
1322
+ throw new Error(`WORK_CANCELLED_RESUME_UNIQUE_COMMIT_CONFLICT: ${workId}`);
1323
+ }
1324
+ }
1325
+ if (worktreePresent) {
1326
+ const retainedSnapshot = gitWorktreeSnapshot(recordedWorktree);
1327
+ if (!retainedSnapshot || !retainedSnapshot.clean || retainedSnapshot.head !== baseRevision || (handle.branch && retainedSnapshot.branch !== handle.branch)) {
1328
+ throw new Error(`WORK_CANCELLED_RESUME_ZERO_DELTA_PHYSICAL_CONFLICT: ${workId}`);
1329
+ }
1330
+ }
1331
+
1332
+ const branchName = `work/resume-${workId.replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 42)}-${baseRevision.slice(0, 12)}`;
1333
+ const existingResumeBranch = gitCommitAtRef(repository.canonicalRoot, `refs/heads/${branchName}`);
1334
+ if (existingResumeBranch && existingResumeBranch !== baseRevision) {
1335
+ throw new Error(`WORK_CANCELLED_RESUME_RECONSTRUCTION_BRANCH_CONFLICT: ${workId}`);
1336
+ }
1337
+ const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
1338
+ requestId: `${workId}:explicit-user-resume:${baseRevision}`,
1339
+ title: `${work.objective} explicit user resume`,
1340
+ branchName,
1341
+ baseRef: baseRevision,
1342
+ prepareDependencies: args.needs_dependencies === true,
1343
+ });
1344
+ if (!workspace.managed || !workspace.checkoutId || !workspace.root || !workspace.branch) {
1345
+ throw new Error(`WORK_CANCELLED_RESUME_RECONSTRUCTION_FAILED: ${workId}`);
1346
+ }
1347
+ const refreshedRepository = listRepositories(ctx.controllerHome, { includeRemoved: true })
1348
+ .find((candidate) => candidate.repoId === repository.repoId);
1349
+ if (!refreshedRepository) throw new Error(`WORK_CANCELLED_RESUME_REPOSITORY_MISSING: ${workId}`);
1350
+ const reconstructed = selectRepositoryCheckout(refreshedRepository, workspace.checkoutId);
1351
+ const reconstructedStatus = repositoryGitStatus(reconstructed);
1352
+ if (!reconstructedStatus.clean || reconstructedStatus.head !== baseRevision || reconstructedStatus.branch !== workspace.branch) {
1353
+ throw new Error(`WORK_CANCELLED_RESUME_RECONSTRUCTION_REVISION_MISMATCH: ${workId}`);
1354
+ }
1355
+
1356
+ work = updateWorkContract(store, workId, {
1357
+ checkoutId: workspace.checkoutId,
1358
+ worktreeRef: workspace.root,
1359
+ baseRevision,
1360
+ controllerInstanceId: identity.controllerInstanceId,
1361
+ });
1362
+ const now = new Date().toISOString();
1363
+ handle = writeWorkHandle(ctx.controllerHome, {
1364
+ ...handle,
1365
+ principalId: identity.principalId,
1366
+ sessionId: identity.sessionId,
1367
+ checkoutId: workspace.checkoutId,
1368
+ worktreePath: workspace.root,
1369
+ branch: workspace.branch,
1370
+ sourceCheckoutId: repository.activeCheckoutId,
1371
+ managedWorktree: true,
1372
+ baseCommit: baseRevision,
1373
+ deliveryBaseCommit: baseRevision,
1374
+ expectedHead: baseRevision,
1375
+ state: 'prepared',
1376
+ validatedInputFingerprint: undefined,
1377
+ failureReason: undefined,
1378
+ cleanupReceipt: undefined,
1379
+ finalization: { validation: 'pending', commit: 'pending', merge: 'pending', branchCleanup: 'pending', worktreeCleanup: 'pending' },
1380
+ updatedAt: now,
1381
+ });
1382
+ resumeRetainedCancelledWorkContract(store, workId, {
1383
+ principalId: identity.principalId,
1384
+ controllerInstanceId: identity.controllerInstanceId,
1385
+ summary: 'Explicit current user re-authorized the same cancelled Work; prior managed checkout was absent and a fresh isolated checkout was reconstructed at the exact zero-delta base revision.',
1386
+ checkoutId: workspace.checkoutId,
1387
+ worktreeRef: workspace.root,
1388
+ });
1389
+ return { reconstructedCheckout: true };
1390
+ }
1391
+
1649
1392
  function claimNewFacadeWork(
1650
1393
  ctx: MultiRepositoryMcpToolContext,
1651
1394
  repository: ReturnType<typeof selected>,
@@ -1698,7 +1441,8 @@ async function finalizeFacadeWorkHandle(
1698
1441
  ): Promise<CallToolResult | undefined> {
1699
1442
  const workId = String(args.work_id ?? '').trim();
1700
1443
  if (!workId) return undefined;
1701
- const handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
1444
+ let handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId)
1445
+ ?? recoverTerminalWorkHandle(ctx.controllerHome, repository.repoId, workId);
1702
1446
  if (!handle) return undefined;
1703
1447
  const session = bindFacadeExecutionSession(ctx, repository, handle, args);
1704
1448
  const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
@@ -1726,11 +1470,45 @@ async function finalizeFacadeWorkHandle(
1726
1470
  const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
1727
1471
  const committedDelta = Boolean(head && handle.baseCommit && head !== handle.baseCommit);
1728
1472
  const commit = typeof args.commit === 'boolean' ? args.commit : !status.clean;
1729
- const merge = typeof args.merge === 'boolean' ? args.merge : true;
1473
+ const workContract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
1474
+ if (
1475
+ status.clean
1476
+ && committedDelta
1477
+ && commit === false
1478
+ && (workContract?.allowedPaths.length ?? 0) > 0
1479
+ && head
1480
+ && handle.expectedHead
1481
+ && head !== handle.expectedHead
1482
+ && (handle.state === 'prepared' || handle.state === 'editing')
1483
+ ) {
1484
+ const contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
1485
+ if ((contract?.allowedPaths.length ?? 0) > 0 || (contract?.forbiddenPaths.length ?? 0) > 0) {
1486
+ const adopted = await callExecutionTool(ctx, 'work_prepare', {
1487
+ session_id: session.sessionId,
1488
+ repo_id: repository.repoId,
1489
+ checkout_id: handle.checkoutId,
1490
+ work_id: workId,
1491
+ expected_previous_head: handle.expectedHead,
1492
+ adopt_candidate_head: head,
1493
+ });
1494
+ if (!adopted || adopted.isError === true) return adopted;
1495
+ handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId) ?? handle;
1496
+ } else if (handle.state === 'prepared') {
1497
+ // A Work without explicit path allow/deny constraints historically permits
1498
+ // repository-scoped committed progress. Preserve that contract while moving
1499
+ // the handle onto a legal lifecycle edge before the physical finalizer
1500
+ // validates and adopts the exact delivery HEAD.
1501
+ handle = transitionWorkHandle(ctx.controllerHome, handle, 'editing', { failureReason: undefined });
1502
+ }
1503
+ }
1730
1504
  const requestedOutcome = args.completion_outcome === 'completed_no_change' || args.completion_outcome === 'completed_changed'
1731
1505
  ? args.completion_outcome
1732
1506
  : undefined;
1733
1507
  const completionOutcome = requestedOutcome ?? (!commit && !committedDelta && status.clean ? 'completed_no_change' : 'completed_changed');
1508
+ // A proven no-change completion has nothing to merge. Defaulting merge=true
1509
+ // forced the generic Git delivery path to do unnecessary work and conflicted
1510
+ // with work_finalize's explicit no-change contract.
1511
+ const merge = typeof args.merge === 'boolean' ? args.merge : completionOutcome !== 'completed_no_change';
1734
1512
  const explicitNoChangeEvidence = typeof args.no_change_evidence === 'string' ? args.no_change_evidence.trim() : '';
1735
1513
  const noChangeEvidence = completionOutcome === 'completed_no_change'
1736
1514
  ? explicitNoChangeEvidence || `Validated Work ${workId} has no repository delta from base ${handle.baseCommit ?? 'unknown'} at clean HEAD ${head ?? 'unknown'}.`
@@ -1740,6 +1518,7 @@ async function finalizeFacadeWorkHandle(
1740
1518
  commit,
1741
1519
  merge,
1742
1520
  no_ff: args.no_ff === true,
1521
+ remote_write: args.remote_write === true,
1743
1522
  completion_outcome: completionOutcome,
1744
1523
  ...(noChangeEvidence ? { no_change_evidence: noChangeEvidence } : {}),
1745
1524
  };
@@ -1796,27 +1575,6 @@ function selected(ctx: MultiRepositoryMcpToolContext, args: Record<string, unkno
1796
1575
  });
1797
1576
  }
1798
1577
 
1799
- function schemaAwareWorkSubmitArguments(
1800
- name: string,
1801
- explicitArguments: Record<string, unknown>,
1802
- repository: ReturnType<typeof selected>,
1803
- definition: McpToolDefinition,
1804
- timeoutMs: unknown,
1805
- ): Record<string, unknown> {
1806
- const schema = definition.inputSchema as { properties?: Record<string, unknown> };
1807
- const declared = new Set(Object.keys(schema.properties ?? {}));
1808
- const candidates = repositoryScopedToolArgs(name, {
1809
- ...explicitArguments,
1810
- ...(declared.has('repo_id') ? { repo_id: repository.repoId } : {}),
1811
- ...(declared.has('timeout_ms') && typeof timeoutMs === 'number' ? { timeout_ms: timeoutMs } : {}),
1812
- }, repository);
1813
- const scoped: Record<string, unknown> = { ...explicitArguments };
1814
- for (const [key, value] of Object.entries(candidates)) {
1815
- if (Object.hasOwn(explicitArguments, key) || declared.has(key)) scoped[key] = value;
1816
- }
1817
- return scoped;
1818
- }
1819
-
1820
1578
  function pluginRepository(
1821
1579
  ctx: MultiRepositoryMcpToolContext,
1822
1580
  args: Record<string, unknown>,
@@ -1827,6 +1585,32 @@ function pluginRepository(
1827
1585
  : selected(ctx, args);
1828
1586
  }
1829
1587
 
1588
+ function definedArguments(value: Record<string, unknown>): Record<string, unknown> {
1589
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
1590
+ }
1591
+
1592
+ function legacyIosPluginAction(
1593
+ ctx: MultiRepositoryMcpToolContext,
1594
+ legacyTool: string,
1595
+ args: Record<string, unknown>,
1596
+ actionId: string,
1597
+ actionArgs: Record<string, unknown>,
1598
+ ): Promise<CallToolResult | undefined> {
1599
+ const repository = selected(ctx, args);
1600
+ const requestId = typeof args.request_id === 'string' && args.request_id.trim()
1601
+ ? args.request_id.trim()
1602
+ : `legacy-${legacyTool}-${Date.now()}`;
1603
+ return callRuntimeTool(ctx, 'plugin_action_execute', {
1604
+ repo_id: repository.repoId,
1605
+ checkout_id: repository.activeCheckoutId,
1606
+ plugin_id: 'ios',
1607
+ action_id: actionId,
1608
+ request_id: requestId,
1609
+ arguments: definedArguments(actionArgs),
1610
+ ...(args.confirm_authorization === true ? { confirm_authorization: true } : {}),
1611
+ });
1612
+ }
1613
+
1830
1614
  function expectedRevision(args: Record<string, unknown>, key = 'expected_revision'): number | undefined {
1831
1615
  return typeof args[key] === 'number' ? Math.trunc(args[key] as number) : undefined;
1832
1616
  }
@@ -2447,14 +2231,10 @@ function managedProcessOperationDigest(
2447
2231
  summary: terminal
2448
2232
  ? `Managed process ${handle.processId} completed with status ${handle.status}.`
2449
2233
  : `Managed process ${handle.processId} is still ${handle.status}.`,
2450
- suggestedNextActions: terminal ? [] : [{
2451
- label: 'Poll managed process',
2452
- tool: 'work_status_digest',
2453
- operation: 'get',
2454
- payload: { work_ref: handle.processId },
2455
- risk: 'readonly',
2456
- confidence: 'high',
2457
- }],
2234
+ // A running Process is not itself a request to poll. The controller should
2235
+ // keep making independent progress, then join only at its real dependency
2236
+ // boundary through the Process lifecycle surface.
2237
+ suggestedNextActions: [],
2458
2238
  };
2459
2239
  }
2460
2240
 
@@ -2525,6 +2305,90 @@ async function runFacadeRepair(
2525
2305
  : 'diagnose';
2526
2306
  const dryRun = args.dry_run === undefined ? true : args.dry_run === true;
2527
2307
  const elevatedRepair = args.destructive === true || args.remote_write === true || args.remote_effect === true;
2308
+ const planId = typeof args.plan_id === 'string' ? args.plan_id.trim() : '';
2309
+ const planStepId = typeof args.plan_step_id === 'string' ? args.plan_step_id.trim() : '';
2310
+
2311
+ if (planId || planStepId) {
2312
+ if (!planId || !planStepId) {
2313
+ 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 } });
2314
+ return result(facade as unknown as Record<string, unknown>, true);
2315
+ }
2316
+ const plan = getPlanContract(store, planId);
2317
+ const step = plan?.steps.find((candidate) => candidate.id === planStepId);
2318
+ if (!plan || !step) {
2319
+ 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 } });
2320
+ return result(facade as unknown as Record<string, unknown>, true);
2321
+ }
2322
+ if (!step.workId) {
2323
+ const facade = buildFacadeResult({ summary: `Plan step ${planId}/${planStepId} has no Work binding to repair.`, data: { operation: repairOperation, dryRun, planId, planStepId, repaired: false, repairRequired: false } });
2324
+ return result(facade as unknown as Record<string, unknown>);
2325
+ }
2326
+ const boundWork = getWorkContract(store, step.workId);
2327
+ if (boundWork) {
2328
+ if (['completed', 'failed', 'cancelled'].includes(boundWork.status)) {
2329
+ if (repairOperation !== 'repair' || dryRun) {
2330
+ const facade = buildFacadeResult({
2331
+ 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.`,
2332
+ data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, repairRequired: true, reusedExistingWork: true },
2333
+ 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' }],
2334
+ });
2335
+ return result(facade as unknown as Record<string, unknown>);
2336
+ }
2337
+ try {
2338
+ const reconciledPlan = completePlanStepForWork(store, { planId, stepId: planStepId, work: boundWork });
2339
+ const facade = buildFacadeResult({
2340
+ summary: `Reconciled Plan step ${planId}/${planStepId} from its existing terminal Work ${boundWork.workId}; no replacement Work was created.`,
2341
+ data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(reconciledPlan), boundWorkId: boundWork.workId, repaired: true, reusedExistingWork: true },
2342
+ });
2343
+ return result(facade as unknown as Record<string, unknown>);
2344
+ } catch (error) {
2345
+ 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 } });
2346
+ return result(facade as unknown as Record<string, unknown>, true);
2347
+ }
2348
+ }
2349
+ const facade = buildFacadeResult({
2350
+ status: 'blocked',
2351
+ summary: `PLAN_STEP_BOUND_WORK_STILL_EXISTS: ${planId}/${planStepId} is bound to active Work ${boundWork.workId}; continue that exact Work instead of repairing or replacing it.`,
2352
+ data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, repairRequired: false },
2353
+ suggestedNextActions: [{ label: 'Continue existing Work', tool: 'rh_work', operation: 'continue', payload: { work_id: boundWork.workId }, risk: 'readonly', confidence: 'high' }],
2354
+ });
2355
+ return result(facade as unknown as Record<string, unknown>, true);
2356
+ }
2357
+ const conflicting = listWorkContracts({ ...store, status: 'active', limit: 200 })
2358
+ .filter((candidate) => candidate.planId === planId && candidate.planStepId === planStepId && candidate.workId !== step.workId);
2359
+ if (conflicting.length > 0) {
2360
+ const facade = buildFacadeResult({
2361
+ status: 'blocked',
2362
+ 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.`,
2363
+ data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: step.workId, conflictingWorkIds: conflicting.map((candidate) => candidate.workId), repaired: false },
2364
+ });
2365
+ return result(facade as unknown as Record<string, unknown>, true);
2366
+ }
2367
+ if (repairOperation !== 'repair' || dryRun) {
2368
+ const facade = buildFacadeResult({
2369
+ 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.`,
2370
+ data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: step.workId, repaired: false, repairRequired: true },
2371
+ 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' }],
2372
+ });
2373
+ return result(facade as unknown as Record<string, unknown>);
2374
+ }
2375
+ try {
2376
+ const repairedPlan = repairDanglingPlanStepWorkBinding(store, {
2377
+ planId,
2378
+ stepId: planStepId,
2379
+ expectedWorkId: step.workId,
2380
+ reason: 'Explicit Controller repair confirmed that the exact bound Work record is absent and no other active primary Work claims this Plan step.',
2381
+ });
2382
+ const facade = buildFacadeResult({
2383
+ summary: `Repaired dangling Plan step binding ${planId}/${planStepId}; ${step.workId} was cleared without creating a replacement Work.`,
2384
+ data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(repairedPlan), boundWorkId: step.workId, repaired: true, replacementWorkCreated: false },
2385
+ });
2386
+ return result(facade as unknown as Record<string, unknown>);
2387
+ } catch (error) {
2388
+ 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 } });
2389
+ return result(facade as unknown as Record<string, unknown>, true);
2390
+ }
2391
+ }
2528
2392
 
2529
2393
  // The self-healing facade is a policy/planning surface; the authoritative
2530
2394
  // maintenance executor owns mutations. Execute it here only for an explicit,
@@ -2632,6 +2496,144 @@ async function runFacadeRepair(
2632
2496
  return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked' || facade.status === 'approval_required' || facade.status === 'failed');
2633
2497
  }
2634
2498
 
2499
+ function reconcileTerminalFacadeWorkVerifications(
2500
+ ctx: MultiRepositoryMcpToolContext,
2501
+ repository: ReturnType<typeof selected>,
2502
+ workId: string,
2503
+ ): { sourceRevision?: string; workspaceFingerprint?: string; workspaceChangedPaths?: string[]; reconciledProcessIds: string[] } {
2504
+ const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
2505
+ const workContract = getWorkContract(store, workId);
2506
+ if (!workContract || workContract.completionReceipt) return { reconciledProcessIds: [] };
2507
+
2508
+ let verificationRepository: ReturnType<typeof selectRepositoryCheckout>;
2509
+ try {
2510
+ verificationRepository = workContract.checkoutId
2511
+ ? selectRepositoryCheckout(repository, workContract.checkoutId, { allowArchived: true })
2512
+ : repository;
2513
+ } catch {
2514
+ return { reconciledProcessIds: [] };
2515
+ }
2516
+ const verificationStatus = repositoryGitStatus(verificationRepository);
2517
+ const sourceRevision = verificationStatus.head ?? undefined;
2518
+ if (!sourceRevision) return { reconciledProcessIds: [] };
2519
+ const verificationHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
2520
+ const deliveryBaseRevision = verificationHandle
2521
+ ? workDeliveryBaseRevision(verificationHandle)
2522
+ : workContract.baseRevision;
2523
+ const committedPaths = deliveryBaseRevision
2524
+ ? workChangedPaths(verificationRepository.canonicalRoot, deliveryBaseRevision, sourceRevision)
2525
+ : [];
2526
+ const workspaceChangedPaths = [...new Set([
2527
+ ...committedPaths,
2528
+ ...verificationStatus.staged,
2529
+ ...verificationStatus.unstaged,
2530
+ ...verificationStatus.untracked,
2531
+ ])].sort();
2532
+ const workspaceFingerprint = workspaceValidationFingerprint(verificationRepository.canonicalRoot, verificationStatus);
2533
+ const availableChecks = listControllerChecks(repository.canonicalRoot);
2534
+ const workloopCtx = {
2535
+ workStore: store,
2536
+ handoffStore: store,
2537
+ repoId: repository.repoId,
2538
+ availableChecks,
2539
+ };
2540
+ const seenChecks = new Set<string>();
2541
+ const reconciledProcessIds: string[] = [];
2542
+ const candidates = listProcessRecords(ctx.controllerHome, repository.repoId, 500)
2543
+ .filter((record) => (
2544
+ record.workId === workId
2545
+ && record.checkoutId === verificationRepository.activeCheckoutId
2546
+ && !isManagedProcessActive(record)
2547
+ && record.origin?.workVerificationSnapshot === true
2548
+ && typeof record.origin?.checkId === 'string'
2549
+ && typeof record.origin?.requestSemanticFingerprint === 'string'
2550
+ ));
2551
+
2552
+ for (const record of candidates) {
2553
+ const checkId = record.origin?.checkId?.trim() ?? '';
2554
+ if (!checkId || seenChecks.has(checkId)) continue;
2555
+ seenChecks.add(checkId);
2556
+ const classified = classifyVerificationOutcome({ checkId, available: availableChecks });
2557
+ if (classified.outcome === 'invalid_check_id' || !classified.normalizedCheckId) continue;
2558
+ const normalizedCheckId = classified.normalizedCheckId;
2559
+ const requestedChecks = workContract.checks.length ? workContract.checks : [normalizedCheckId];
2560
+ const currentFingerprint = verificationInputFingerprint({
2561
+ sourceRevision,
2562
+ workspaceFingerprint,
2563
+ checkId: normalizedCheckId,
2564
+ requestedChecks,
2565
+ });
2566
+ if (record.origin?.requestSemanticFingerprint !== currentFingerprint || !record.checkExecution) continue;
2567
+
2568
+ try {
2569
+ const receipt = processCheckCompletionReceipt(record, {
2570
+ repoId: verificationRepository.repoId,
2571
+ checkoutId: verificationRepository.activeCheckoutId,
2572
+ workId,
2573
+ checkId: normalizedCheckId,
2574
+ processId: record.processId,
2575
+ requestId: record.origin?.requestId,
2576
+ checkExecution: {
2577
+ cacheKey: record.checkExecution.cacheKey,
2578
+ revision: record.checkExecution.revision,
2579
+ definitionDigest: record.checkExecution.definitionDigest,
2580
+ environmentFingerprint: record.checkExecution.environmentFingerprint,
2581
+ timeoutMs: record.checkExecution.timeoutMs,
2582
+ scopeKey: record.checkExecution.scopeKey,
2583
+ },
2584
+ });
2585
+ const latestContract = getWorkContract(store, workId);
2586
+ if (latestContract?.checkRefs.some((entry) => entry.receipt?.receiptId === receipt.receiptId)) continue;
2587
+
2588
+ const structuredCheckResult = readPersistedCheckResultReceipt(record.origin?.checkResultReceiptPath);
2589
+ const structuredResultMatchesProcess = Boolean(
2590
+ structuredCheckResult
2591
+ && structuredCheckResult.checkId === normalizedCheckId
2592
+ && structuredCheckResult.cacheKey === record.checkExecution.cacheKey,
2593
+ );
2594
+ const legacyEvidence = record.origin?.checkResultReceiptPath
2595
+ ? undefined
2596
+ : readLatestControllerCheckEvidence(verificationRepository.canonicalRoot, normalizedCheckId);
2597
+ const legacyEvidenceMatchesProcess = Boolean(
2598
+ legacyEvidence?.cacheKey
2599
+ && legacyEvidence.cacheKey === record.checkExecution.cacheKey,
2600
+ );
2601
+ const evidenceState = classifyTerminalCheckEvidence({
2602
+ processError: record.error,
2603
+ structuredPresent: Boolean(structuredCheckResult),
2604
+ structuredMatches: structuredResultMatchesProcess,
2605
+ legacyPresent: Boolean(legacyEvidence),
2606
+ legacyMatches: legacyEvidenceMatchesProcess,
2607
+ });
2608
+ const failureClass = structuredResultMatchesProcess
2609
+ ? structuredCheckResult?.failureClass
2610
+ : legacyEvidenceMatchesProcess ? legacyEvidence?.failureClass : undefined;
2611
+ const infrastructureFailed = receipt.timedOut
2612
+ || receipt.cancelled
2613
+ || evidenceState.state !== 'matched'
2614
+ || (!receipt.ok && failureClass !== 'acceptance_failure');
2615
+ const checkFailed = !receipt.ok && !infrastructureFailed;
2616
+ verifyGoalWorkloop(workloopCtx, {
2617
+ workId,
2618
+ checkId: normalizedCheckId,
2619
+ sourceRevision,
2620
+ workspaceFingerprint,
2621
+ verificationInputFingerprint: currentFingerprint,
2622
+ commandFingerprint: commandFingerprint(normalizedCheckId, receipt.commandId),
2623
+ receipt,
2624
+ infrastructureFailed,
2625
+ checkFailed,
2626
+ });
2627
+ reconciledProcessIds.push(record.processId);
2628
+ } catch {
2629
+ // Exact receipt/process identity is mandatory. Any malformed, stale, or
2630
+ // mismatched terminal Process remains non-authoritative and is ignored.
2631
+ }
2632
+ }
2633
+
2634
+ return { sourceRevision, workspaceFingerprint, workspaceChangedPaths, reconciledProcessIds };
2635
+ }
2636
+
2635
2637
  async function runFacadeVerify(
2636
2638
  ctx: MultiRepositoryMcpToolContext,
2637
2639
  repository: ReturnType<typeof selected>,
@@ -2646,7 +2648,30 @@ async function runFacadeVerify(
2646
2648
  availableChecks: checks,
2647
2649
  };
2648
2650
  const workId = typeof args.work_id === 'string' ? args.work_id : '';
2649
- const checkId = String(args.check_id ?? args.checkId ?? '');
2651
+ const checkId = String(args.check_id ?? args.checkId ?? '').trim();
2652
+ if (!checkId) {
2653
+ return result(buildFacadeResult({
2654
+ status: 'blocked',
2655
+ summary: 'rh_work verify requires a registered check_id.',
2656
+ data: {
2657
+ verification: {
2658
+ outcome: 'check_id_required',
2659
+ isAcceptanceFailure: false,
2660
+ isInfrastructureIssue: true,
2661
+ doesNotRequestTaskChanges: true,
2662
+ },
2663
+ registeredCheckCount: checks.length,
2664
+ },
2665
+ warnings: ['CHECK_ID_REQUIRED: pass check_id for one registered repository check.'],
2666
+ suggestedNextActions: normalizeCheckIds(checks.slice(0, 3).map((check) => check.id), checks).suggestedNextActions,
2667
+ }) as unknown as Record<string, unknown>, true);
2668
+ }
2669
+ const workContract = workId ? getWorkContract(store, workId) : undefined;
2670
+ if (workId && (!workContract || workContract.status === 'completed' || workContract.status === 'cancelled' || workContract.status === 'failed')) {
2671
+ const facade = verifyGoalWorkloop(workloopCtx, { workId, checkId });
2672
+ return result(facade as unknown as Record<string, unknown>, facade.status === 'failed');
2673
+ }
2674
+
2650
2675
  const classified = classifyVerificationOutcome({
2651
2676
  checkId,
2652
2677
  available: checks,
@@ -2708,12 +2733,39 @@ async function runFacadeVerify(
2708
2733
  const requestId = typeof args.request_id === 'string' && args.request_id.trim()
2709
2734
  ? args.request_id.trim()
2710
2735
  : undefined;
2711
- const workContract = workId ? getWorkContract(store, workId) : undefined;
2712
- if (workId && !workContract) throw new Error(`WORK_NOT_FOUND: ${workId}`);
2713
2736
  const verificationRepository = workContract?.checkoutId
2714
2737
  ? selectRepositoryCheckout(repository, workContract.checkoutId, { allowArchived: true })
2715
2738
  : repository;
2716
- const observedGitHead = repositoryGitStatus(verificationRepository).head;
2739
+ const verificationStatus = repositoryGitStatus(verificationRepository);
2740
+ const observedGitHead = verificationStatus.head;
2741
+ const workspaceFingerprint = workspaceValidationFingerprint(verificationRepository.canonicalRoot, verificationStatus);
2742
+ const requestedChecks = workContract?.checks.length ? workContract.checks : [normalizedCheckId];
2743
+ const verificationRequestFingerprint = observedGitHead ? verificationInputFingerprint({
2744
+ sourceRevision: observedGitHead,
2745
+ workspaceFingerprint,
2746
+ checkId: normalizedCheckId,
2747
+ requestedChecks,
2748
+ }) : undefined;
2749
+ const registeredCheck = checks.find((entry) => entry.id === normalizedCheckId);
2750
+ const durableClassCheck = checkRequiresDurableWorkflow(normalizedCheckId, registeredCheck);
2751
+ let allowDurableCheckExecution = false;
2752
+ if (durableClassCheck && workContract?.checks.includes(normalizedCheckId) && !workContract.completionReceipt && workContract.status === 'running') {
2753
+ try {
2754
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
2755
+ const owner = getControllerSession(store, workId);
2756
+ const ownerPrincipal = owner?.principalId?.trim() || owner?.controllerId;
2757
+ allowDurableCheckExecution = Boolean(
2758
+ owner
2759
+ && owner.controllerId === identity.controllerId
2760
+ && ownerPrincipal === identity.principalId
2761
+ && owner.controllerInstanceId === identity.controllerInstanceId,
2762
+ );
2763
+ } catch {
2764
+ // Durable-class checks remain deferred when the authenticated claim cannot
2765
+ // be proven exactly. The lower Process Runtime never infers authority.
2766
+ allowDurableCheckExecution = false;
2767
+ }
2768
+ }
2717
2769
  const executed = await runPersistedCheckViaProcessRuntime({
2718
2770
  controllerHome: ctx.controllerHome,
2719
2771
  repoId: verificationRepository.repoId,
@@ -2724,8 +2776,13 @@ async function runFacadeVerify(
2724
2776
  timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
2725
2777
  // rh_work.verify is a control-plane continuation primitive: callers should
2726
2778
  // be able to keep working and later reattach to the exact same Process.
2779
+ // Absorb brief shared build-cache/resource contention during admission so
2780
+ // a lifecycle verification does not turn a few seconds of contention into
2781
+ // a terminal failed Check that the controller must manually retry.
2727
2782
  interactiveWaitMs: 0,
2783
+ leaseWaitMs: RH_WORK_VERIFY_LEASE_WAIT_MS,
2728
2784
  requestId,
2785
+ requestSemanticFingerprint: verificationRequestFingerprint,
2729
2786
  workId: workId || undefined,
2730
2787
  commandId: requestId,
2731
2788
  verificationSnapshot: workContract ? {
@@ -2733,6 +2790,7 @@ async function runFacadeVerify(
2733
2790
  allowedPaths: workContract.allowedPaths,
2734
2791
  forbiddenPaths: workContract.forbiddenPaths,
2735
2792
  } : undefined,
2793
+ allowDurableCheckExecution,
2736
2794
  });
2737
2795
 
2738
2796
  if (executed.mode === 'durable') {
@@ -2825,7 +2883,14 @@ async function runFacadeVerify(
2825
2883
  && record.checkExecution?.cacheKey
2826
2884
  && legacyEvidence.cacheKey === record.checkExecution.cacheKey,
2827
2885
  );
2828
- const resultMatchesProcess = structuredResultMatchesProcess || legacyEvidenceMatchesProcess;
2886
+ const evidenceState = classifyTerminalCheckEvidence({
2887
+ processError: record.error,
2888
+ structuredPresent: Boolean(structuredCheckResult),
2889
+ structuredMatches: structuredResultMatchesProcess,
2890
+ legacyPresent: Boolean(legacyEvidence),
2891
+ legacyMatches: legacyEvidenceMatchesProcess,
2892
+ });
2893
+ const resultMatchesProcess = evidenceState.state === 'matched';
2829
2894
  const failureClass = structuredResultMatchesProcess
2830
2895
  ? structuredCheckResult?.failureClass
2831
2896
  : legacyEvidenceMatchesProcess ? legacyEvidence?.failureClass : undefined;
@@ -2858,12 +2923,21 @@ async function runFacadeVerify(
2858
2923
  checkResultReceiptId: structuredCheckResult?.receiptId,
2859
2924
  verificationIsolation: record.origin?.workVerificationSnapshot ? 'work_snapshot' : 'shared_checkout',
2860
2925
  boundedStatus,
2926
+ evidenceState: evidenceState.state,
2927
+ ...(evidenceState.infrastructureReason ? { infrastructureReason: evidenceState.infrastructureReason } : {}),
2928
+ ...(record.error?.code ? { processErrorCode: record.error.code } : {}),
2861
2929
  };
2862
2930
 
2863
2931
  if (workId) {
2932
+ const sourceRevision = observedGitHead ?? undefined;
2864
2933
  const facade = verifyGoalWorkloop(workloopCtx, {
2865
2934
  workId,
2866
2935
  checkId: normalizedCheckId,
2936
+ sourceRevision,
2937
+ workspaceFingerprint,
2938
+ verificationInputFingerprint: sourceRevision ? verificationRequestFingerprint : undefined,
2939
+ commandFingerprint: commandFingerprint(normalizedCheckId, receipt.commandId),
2940
+ receipt,
2867
2941
  infrastructureFailed,
2868
2942
  checkFailed,
2869
2943
  });
@@ -2878,7 +2952,7 @@ async function runFacadeVerify(
2878
2952
  },
2879
2953
  },
2880
2954
  warnings: infrastructureFailed
2881
- ? [...facade.warnings, resultMatchesProcess ? 'infrastructure_failure is distinct from acceptance failure' : 'check result receipt did not match the terminal Process semantic identity']
2955
+ ? [...facade.warnings, evidenceState.warning ?? 'infrastructure_failure is distinct from acceptance failure']
2882
2956
  : facade.warnings,
2883
2957
  } as unknown as Record<string, unknown>, facade.status === 'failed');
2884
2958
  }
@@ -2892,7 +2966,7 @@ async function runFacadeVerify(
2892
2966
  : `Check ${normalizedCheckId} failed acceptance.`,
2893
2967
  data: { verification: commonVerification },
2894
2968
  warnings: infrastructureFailed
2895
- ? [resultMatchesProcess ? 'infrastructure_failure is distinct from acceptance failure' : 'check result receipt did not match the terminal Process semantic identity']
2969
+ ? [evidenceState.warning ?? 'infrastructure_failure is distinct from acceptance failure']
2896
2970
  : [],
2897
2971
  rawAvailable: false,
2898
2972
  }) as unknown as Record<string, unknown>, checkFailed);
@@ -2957,6 +3031,90 @@ export function runtimeSourceSnapshotStatus(
2957
3031
  };
2958
3032
  }
2959
3033
 
3034
+ export function repositoryExecutionReadiness(
3035
+ repoRoot: string,
3036
+ availableChecks: ReturnType<typeof listControllerChecks>,
3037
+ requestedCheckIds: string[] = [],
3038
+ schedulingScope: { repoId?: string; checkoutId?: string } = {},
3039
+ ): Record<string, unknown> {
3040
+ const git = gitSnapshot(repoRoot);
3041
+ const registeredCheckIds = availableChecks.map((check) => check.id);
3042
+ const normalizedChecks = normalizeCheckIds(requestedCheckIds, availableChecks);
3043
+ const hasPackageJson = existsSync(join(repoRoot, 'package.json'));
3044
+ const nodeModulesReady = !hasPackageJson || existsSync(join(repoRoot, 'node_modules'));
3045
+ const lockCandidates = [
3046
+ ['bun', 'bun.lock'],
3047
+ ['bun', 'bun.lockb'],
3048
+ ['pnpm', 'pnpm-lock.yaml'],
3049
+ ['npm', 'package-lock.json'],
3050
+ ['yarn', 'yarn.lock'],
3051
+ ] as const;
3052
+ const detectedLock = lockCandidates.find(([, path]) => existsSync(join(repoRoot, path)));
3053
+ const packageManager = detectedLock?.[0];
3054
+ const bootstrapCommand = hasPackageJson && !nodeModulesReady
3055
+ ? packageManager === 'bun' ? ['bun', 'install', '--frozen-lockfile']
3056
+ : packageManager === 'pnpm' ? ['pnpm', 'install', '--frozen-lockfile']
3057
+ : packageManager === 'npm' ? ['npm', 'ci']
3058
+ : packageManager === 'yarn' ? ['yarn', 'install', '--frozen-lockfile']
3059
+ : undefined
3060
+ : undefined;
3061
+ const hasPythonManifest = existsSync(join(repoRoot, 'pyproject.toml'))
3062
+ || existsSync(join(repoRoot, 'requirements.txt'))
3063
+ || existsSync(join(repoRoot, 'requirements-dev.txt'));
3064
+ const localPythonReady = !hasPythonManifest
3065
+ || existsSync(join(repoRoot, '.venv', 'bin', 'python'))
3066
+ || existsSync(join(repoRoot, '.venv', 'Scripts', 'python.exe'));
3067
+ const checkScheduling = buildCheckExecutionSchedule({
3068
+ checks: availableChecks,
3069
+ requestedCheckIds,
3070
+ repoId: schedulingScope.repoId?.trim() || 'selected-repository',
3071
+ checkoutId: schedulingScope.checkoutId?.trim() || 'active',
3072
+ });
3073
+ const blockers = [
3074
+ ...(!nodeModulesReady ? [{ code: 'NODE_DEPENDENCIES_MISSING', message: 'package.json is present but node_modules is not materialized in this checkout.' }] : []),
3075
+ ...normalizedChecks.invalidCheckIds.map((checkId) => ({ code: 'CHECK_NOT_REGISTERED', message: `Requested check is not registered: ${checkId}`, checkId })),
3076
+ ];
3077
+ return {
3078
+ readyForFocusedExecution: blockers.length === 0,
3079
+ git: { head: git.head, branch: git.branch, dirty: git.dirty },
3080
+ checks: {
3081
+ registeredCount: registeredCheckIds.length,
3082
+ registeredCheckIds: registeredCheckIds.slice(0, 80),
3083
+ requestedCheckIds: requestedCheckIds.slice(0, 40),
3084
+ normalized: normalizedChecks,
3085
+ },
3086
+ checkScheduling: {
3087
+ waveCount: checkScheduling.waves.length,
3088
+ maxParallel: checkScheduling.maxParallel,
3089
+ waveSummaries: checkScheduling.waves.map((wave) => `wave ${wave.wave}: ${wave.checkIds.join(', ')}`),
3090
+ conflictSummaries: checkScheduling.conflicts.map((conflict) => {
3091
+ const resources = [...new Set(conflict.resources.flatMap(({ left, right }) => [left.resourceKey, right.resourceKey]))];
3092
+ return `${conflict.leftCheckId} <> ${conflict.rightCheckId}: ${resources.join(', ')}`;
3093
+ }),
3094
+ invalidCheckIds: checkScheduling.invalidCheckIds,
3095
+ guidance: checkScheduling.guidance,
3096
+ },
3097
+ dependencies: {
3098
+ node: {
3099
+ applicable: hasPackageJson,
3100
+ ready: nodeModulesReady,
3101
+ packageManager: packageManager ?? null,
3102
+ lockfile: detectedLock?.[1] ?? null,
3103
+ ...(bootstrapCommand ? { bootstrapCommand } : {}),
3104
+ },
3105
+ python: {
3106
+ applicable: hasPythonManifest,
3107
+ localVirtualEnvReady: localPythonReady,
3108
+ advisoryOnly: true,
3109
+ },
3110
+ },
3111
+ blockers,
3112
+ guidance: bootstrapCommand
3113
+ ? [`Materialize checkout dependencies before tests/builds: ${bootstrapCommand.join(' ')}`]
3114
+ : [],
3115
+ };
3116
+ }
3117
+
2960
3118
  export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>): Promise<CallToolResult | undefined> {
2961
3119
  try {
2962
3120
  switch (name) {
@@ -2970,16 +3128,167 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
2970
3128
  if (operation === 'repair') {
2971
3129
  return await runFacadeRepair(ctx, repository, args);
2972
3130
  }
2973
- const readiness = await controllerReadinessEvidence(ctx, repository);
2974
- const liveGit = gitSnapshot(repository.canonicalRoot);
2975
- // Compare startup Runtime Source against the Controller package authority —
2976
- // never against the selected execution repository.
3131
+ const detailLevel = args.detail_level === 'detail' ? 'detail' : 'summary';
3132
+ if (detailLevel === 'summary') {
3133
+ const startedAt = performance.now();
3134
+ const observation = observeRuntimeStatus(ctx.controllerHome);
3135
+ // Summary answers only whether this repository can work now. Reuse one
3136
+ // porcelain-v2 sample for branch/HEAD/dirty and avoid the full Git
3137
+ // status/diff-stat, access-policy, and inventory construction paths.
3138
+ const repositoryIdentity = cachedGitIdentity(repository.canonicalRoot);
3139
+ const runtimeGeneration = readRuntimeGeneration(ctx.controllerHome);
3140
+ const runtimeSource = runtimeSourceSnapshotStatus(
3141
+ runtimeGeneration?.source,
3142
+ ctx.runtimeSourceRoot,
3143
+ );
3144
+ const sourceSnapshotStale = runtimeSource.restartRequired;
3145
+ const toolset = await import('../../../cli/mcp/toolset');
3146
+ const exposure = toolset.controllerToolSurfaceStatus(ctx);
3147
+ const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
3148
+ const ready = observation.ready && toolSurfaceReady && !sourceSnapshotStale;
3149
+ const reasonCodes = [...observation.reasonCodes];
3150
+ if (!toolSurfaceReady) reasonCodes.push('MCP_TOOL_SURFACE_INCOMPLETE');
3151
+ if (sourceSnapshotStale) reasonCodes.push(runtimeSource.code === 'RUNTIME_SOURCE_SNAPSHOT_MISSING'
3152
+ ? 'RUNTIME_SOURCE_SNAPSHOT_MISSING'
3153
+ : runtimeSource.code === 'RUNTIME_SOURCE_CURRENT_UNAVAILABLE'
3154
+ ? 'RUNTIME_SOURCE_CURRENT_UNAVAILABLE'
3155
+ : 'RUNTIME_SOURCE_SNAPSHOT_STALE');
3156
+ const runtimeReadiness = observation.snapshot?.readiness;
3157
+ const releaseDiagnostic = runtimeReadiness?.diagnostics.releaseCoherence;
3158
+ const activeWorkSnapshot = listWorkContracts({ ...store, status: 'active', limit: 3 }).map((entry) => ({
3159
+ workId: entry.workId,
3160
+ status: entry.status,
3161
+ mode: entry.mode,
3162
+ objective: entry.objective.slice(0, 160),
3163
+ semantics: buildWorkContinuationSnapshot(entry).semantics,
3164
+ nextSafeAction: buildWorkContinuationSnapshot(entry).nextSafeAction,
3165
+ }));
3166
+ const activePlanSnapshot = listPlanContracts({ ...store, status: 'active', limit: 3 }).map(summarizePlanContract);
3167
+ const pendingHandoffSnapshot = listHandoffItems({ ...store, status: 'pending', limit: 4 });
3168
+ const preferredFacadeTools = ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'] as const;
3169
+ const facade = buildFacadeResult({
3170
+ status: ready ? 'ok' : 'blocked',
3171
+ summary: ready ? 'Controller and MCP tool surface are ready for bounded work.' : 'Controller or MCP tool surface needs attention before work.',
3172
+ data: {
3173
+ operation,
3174
+ repoId: repository.repoId,
3175
+ readiness: {
3176
+ ready,
3177
+ reasonCodes: [...new Set(reasonCodes)],
3178
+ diagnostics: {
3179
+ runtime: { ready: observation.ready },
3180
+ runtimeReadiness: runtimeReadiness ? {
3181
+ observedAt: runtimeReadiness.observedAt,
3182
+ database: runtimeReadiness.diagnostics.database,
3183
+ scheduler: runtimeReadiness.diagnostics.scheduler,
3184
+ releaseCoherence: runtimeReadiness.diagnostics.releaseCoherence,
3185
+ mcpEndToEnd: runtimeReadiness.diagnostics.mcpEndToEnd,
3186
+ } : undefined,
3187
+ toolSurface: {
3188
+ ready: toolSurfaceReady,
3189
+ expectedToolCount: exposure.expectedToolNames.length,
3190
+ actualToolCount: exposure.actualToolNames.length,
3191
+ observation: 'computed',
3192
+ missingTools: exposure.missingToolNames,
3193
+ unexpectedTools: exposure.unexpectedToolNames,
3194
+ duplicateTools: exposure.duplicateToolNames,
3195
+ fingerprint: exposure.fingerprint,
3196
+ schemaStableAcrossAccessModes: exposure.schemaStableAcrossAccessModes,
3197
+ },
3198
+ semantics: {
3199
+ executionReady: observation.ready,
3200
+ maintenanceHealthy: null,
3201
+ maintenanceCandidateCount: 0,
3202
+ releaseReady: releaseDiagnostic?.outcome === 'pass',
3203
+ executionBlockers: observation.ready ? [] : observation.reasonCodes,
3204
+ releaseBlockers: releaseDiagnostic?.outcome === 'fail' && releaseDiagnostic.reasonCode ? [releaseDiagnostic.reasonCode] : [],
3205
+ },
3206
+ sourceCoherence: { ready: !sourceSnapshotStale, reasons: runtimeSource.reasons },
3207
+ },
3208
+ observedAt: observation.observedAt,
3209
+ },
3210
+ repositoryState: {
3211
+ branch: repositoryIdentity.branch,
3212
+ head: repositoryIdentity.head,
3213
+ dirty: repositoryIdentity.dirty,
3214
+ observedAt: new Date().toISOString(),
3215
+ sourceSnapshotAgeMs: runtimeGeneration?.source.observedAt
3216
+ ? Math.max(0, Date.now() - Date.parse(runtimeGeneration.source.observedAt))
3217
+ : undefined,
3218
+ sourceSnapshotStale,
3219
+ sourceSnapshotReasons: runtimeSource.reasons,
3220
+ runtimeSourceDirty: runtimeSource.current?.dirty === true,
3221
+ },
3222
+ toolArchitecture: {
3223
+ facadeTools: [...preferredFacadeTools],
3224
+ domainSchemaLoading: 'status_summary_runtime_snapshot',
3225
+ },
3226
+ toolSurface: preferredFacadeTools.filter((tool) => exposure.actualToolNames.includes(tool)),
3227
+ toolSurfaceStatus: {
3228
+ ready: toolSurfaceReady,
3229
+ expectedToolCount: exposure.expectedToolNames.length,
3230
+ actualToolCount: exposure.actualToolNames.length,
3231
+ observation: 'computed',
3232
+ missingTools: exposure.missingToolNames,
3233
+ unexpectedTools: exposure.unexpectedToolNames,
3234
+ duplicateTools: exposure.duplicateToolNames,
3235
+ fingerprint: exposure.fingerprint,
3236
+ schemaStableAcrossAccessModes: exposure.schemaStableAcrossAccessModes,
3237
+ },
3238
+ controllerSnapshot: {
3239
+ activeWork: activeWorkSnapshot,
3240
+ activePlans: activePlanSnapshot,
3241
+ pendingHandoffCount: pendingHandoffSnapshot.length,
3242
+ pendingHandoffs: pendingHandoffSnapshot.slice(0, 3).map((item) => ({
3243
+ id: item.id,
3244
+ workId: item.workId,
3245
+ title: item.title.slice(0, 96),
3246
+ severity: item.severity,
3247
+ updatedAt: item.updatedAt,
3248
+ })),
3249
+ bounded: true,
3250
+ nextDetail: 'Use rh_context(work_id=...) or rh_work(plan_get) only when the next decision requires more detail.',
3251
+ },
3252
+ },
3253
+ suggestedNextActions: [{
3254
+ label: 'Read repository context',
3255
+ tool: 'rh_context',
3256
+ operation: 'get',
3257
+ risk: 'readonly',
3258
+ confidence: 'medium',
3259
+ }],
3260
+ rawAvailable: false,
3261
+ detailLevel,
3262
+ });
3263
+ const payload = facade as unknown as Record<string, unknown>;
3264
+ payload.responseMeta = {
3265
+ serverDurationMs: Number((performance.now() - startedAt).toFixed(2)),
3266
+ structuredPayloadBytes: 0,
3267
+ };
3268
+ (payload.responseMeta as { structuredPayloadBytes: number }).structuredPayloadBytes = Buffer.byteLength(JSON.stringify(payload), 'utf8');
3269
+ return result(payload, facade.status !== 'ok');
3270
+ }
3271
+ const detailTimingStartedAt = performance.now();
3272
+ let detailTimingMark = detailTimingStartedAt;
3273
+ const detailPhaseTimingsMs: Record<string, number> = {};
3274
+ const markDetailPhase = (phase: string): void => {
3275
+ const now = performance.now();
3276
+ detailPhaseTimingsMs[phase] = Number((now - detailTimingMark).toFixed(2));
3277
+ detailTimingMark = now;
3278
+ };
3279
+ const readiness = await controllerReadinessEvidence(ctx, repository);
3280
+ markDetailPhase('readiness');
3281
+ const liveGit = gitSnapshot(repository.canonicalRoot);
3282
+ markDetailPhase('git');
3283
+ // Compare startup Runtime Source against the Controller package authority —
3284
+ // never against the selected execution repository.
2977
3285
  const runtimeSource = runtimeSourceSnapshotStatus(readiness.daemon.source, ctx.runtimeSourceRoot);
2978
3286
  const sourceSnapshotStale = runtimeSource.restartRequired;
2979
3287
  // Dynamic import avoids a static cycle: toolset.ts composes runtimeToolDefinitions.
2980
3288
  const toolset = await import('../../../cli/mcp/toolset');
2981
3289
  const exposure = toolset.controllerExposureSnapshot(ctx);
2982
3290
  const localRegisteredToolNames = toolset.allControllerToolDefinitions(ctx).map((tool) => tool.name).sort();
3291
+ markDetailPhase('tool_surface');
2983
3292
  const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
2984
3293
  // Ordinary interactive execution depends on the execution axis only.
2985
3294
  // Maintenance debt / durable queue debt are reported as separate
@@ -2999,6 +3308,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
2999
3308
  maintenanceHealthy = null;
3000
3309
  }
3001
3310
  }
3311
+ markDetailPhase('maintenance');
3002
3312
  const effectiveReady = executionReady && toolSurfaceReady && !sourceSnapshotStale;
3003
3313
  const readinessReasons = [...readiness.reasons];
3004
3314
  if (!toolSurfaceReady) {
@@ -3056,22 +3366,29 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3056
3366
  },
3057
3367
  observedAt: new Date().toISOString(),
3058
3368
  };
3059
- const detailLevel = args.detail_level === 'detail' ? 'detail' : 'summary';
3060
- // Always prefer stored plugin manifests on rh_status get. Live host probes
3369
+ // Always prefer stored plugin manifests on rh_status detail. Live host probes
3061
3370
  // (Xcode/simctl, etc.) must not stall Managed MCP gateways on reconnect/status.
3062
3371
  const manifests = listAssistantPluginManifests(ctx.controllerHome, repository, {
3063
3372
  preferStored: true,
3064
3373
  });
3065
3374
  const capabilities = listCapabilityDescriptors(manifests);
3375
+ markDetailPhase('plugins');
3066
3376
  const pendingHandoffs = listHandoffItems({ ...store, status: 'pending', limit: 20 });
3067
3377
  const activeContracts = listWorkContracts({ ...store, status: 'active', limit: 200 });
3378
+ markDetailPhase('work_state');
3068
3379
  const activePrimaryWork = activeContracts.filter((contract) => (contract.lifecycleRole ?? 'primary') === 'primary');
3069
3380
  const activeExecutionChildren = activeContracts.filter((contract) => contract.lifecycleRole === 'execution_child');
3070
- const activeRuntimeInstanceId = currentControllerInstanceId();
3071
- const activeProcessRecords = listProcessRecords(ctx.controllerHome, repository.repoId, 500).filter((process) => isManagedProcessActive(process) && (!process.runtimeInstanceId || process.runtimeInstanceId === activeRuntimeInstanceId));
3381
+ // Current Runtime activity is owned by the in-memory Process monitor set.
3382
+ // Do not route an interactive status read through the retired cross-restart
3383
+ // recovery index, which may contain historical starting/running records.
3384
+ const activeProcessRecords = processRuntimeResourceDiagnostics().activeProcessIds
3385
+ .map((processId) => getProcessRecord(ctx.controllerHome, repository.repoId, processId))
3386
+ .filter((process): process is NonNullable<typeof process> => Boolean(process && isManagedProcessActive(process)));
3387
+ markDetailPhase('process_state');
3072
3388
  const activeProcessWorkIds = new Set(activeProcessRecords.map((process) => process.workId).filter((workId): workId is string => Boolean(workId)));
3073
3389
  const activeControllerWorkIds = new Set(activePrimaryWork.filter((contract) => Boolean(getControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, contract.workId))).map((contract) => contract.workId));
3074
3390
  const executingPrimaryWorkIds = new Set([...activeProcessWorkIds, ...activeControllerWorkIds].filter((workId) => activePrimaryWork.some((contract) => contract.workId === workId)));
3391
+ markDetailPhase('controller_sessions');
3075
3392
  const preferredFacadeTools = ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'] as const;
3076
3393
  const facade = buildFacadeResult({
3077
3394
  status: effectiveReady ? 'ok' : 'blocked',
@@ -3132,7 +3449,13 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3132
3449
  rawAvailable: detailLevel === 'detail',
3133
3450
  detailLevel,
3134
3451
  });
3135
- return result(facade as unknown as Record<string, unknown>, facade.status !== 'ok');
3452
+ markDetailPhase('response_build');
3453
+ const payload = facade as unknown as Record<string, unknown>;
3454
+ payload.responseMeta = {
3455
+ serverDurationMs: Number((performance.now() - detailTimingStartedAt).toFixed(2)),
3456
+ phaseTimingsMs: detailPhaseTimingsMs,
3457
+ };
3458
+ return result(payload, facade.status !== 'ok');
3136
3459
  }
3137
3460
  case 'rh_inbox': {
3138
3461
  const repository = selected(ctx, args);
@@ -3234,6 +3557,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3234
3557
  });
3235
3558
  return result(facade as unknown as Record<string, unknown>, true);
3236
3559
  }
3560
+ const legacySemanticQuery = rhContextLegacySemanticQuery(query);
3561
+ const retrievalQuery = legacySemanticQuery.retrievalQuery;
3237
3562
  const list = (value: unknown): string[] => Array.isArray(value)
3238
3563
  ? value.map(String).map((entry) => entry.trim()).filter(Boolean)
3239
3564
  : [];
@@ -3247,9 +3572,27 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3247
3572
  : retrievalMode === 'plan' || retrievalMode === 'debug'
3248
3573
  ? 'required'
3249
3574
  : 'auto';
3250
- const pack = buildControllerContextPack(repository.canonicalRoot, ctx.policy, {
3251
- description: query,
3252
- searchTerms: [query],
3575
+ const requestedCheckIds = list(args.requested_check_ids);
3576
+ // Source retrieval is the default Context responsibility. Check
3577
+ // readiness is only meaningful when the controller has named the
3578
+ // checks it is about to use, so do not pay its Git/dependency/schedule
3579
+ // preflight cost for every broad search.
3580
+ const checks = requestedCheckIds.length > 0
3581
+ ? listControllerChecks(repository.canonicalRoot)
3582
+ : [];
3583
+ const executionReadiness = requestedCheckIds.length > 0
3584
+ ? repositoryExecutionReadiness(repository.canonicalRoot, checks, requestedCheckIds, {
3585
+ repoId: repository.repoId,
3586
+ checkoutId: repository.activeCheckoutId,
3587
+ })
3588
+ : undefined;
3589
+ const pack = await buildControllerContextPackAsync(repository.canonicalRoot, ctx.policy, {
3590
+ description: retrievalQuery,
3591
+ // Short code-like queries remain useful exact lexical needles. Long
3592
+ // semantic prompts are already tokenized from description; adding the
3593
+ // whole prompt as an exact needle prevents batch-search early exit and
3594
+ // forces a full bounded repository scan for a phrase that will not match.
3595
+ searchTerms: retrievalQuery.length <= 160 ? [retrievalQuery] : undefined,
3253
3596
  knownPaths: list(args.known_paths),
3254
3597
  includeGlobs: list(args.include_globs),
3255
3598
  excludeGlobs: list(args.exclude_globs),
@@ -3259,7 +3602,55 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3259
3602
  structuralIndexRoot: structuralContext === 'off' ? undefined : structuralIndexRoot(repository),
3260
3603
  retrievalMode,
3261
3604
  impactDomains,
3605
+ session: rhContextReadSessionId(ctx)
3606
+ ? { sessionId: rhContextReadSessionId(ctx)!, repoId: repository.repoId, checkoutId: repository.activeCheckoutId }
3607
+ : undefined,
3262
3608
  });
3609
+ const explicitSemanticNavigation = Array.isArray(args.semantic_navigation) ? args.semantic_navigation : [];
3610
+ const semanticRequests = [...explicitSemanticNavigation, ...legacySemanticQuery.requests];
3611
+ const semanticNavigation = {
3612
+ ...await rhContextSemanticNavigation(repository.canonicalRoot, ctx.policy, semanticRequests, { repoId: repository.repoId, checkoutId: repository.activeCheckoutId }),
3613
+ requestSource: explicitSemanticNavigation.length > 0 && legacySemanticQuery.requests.length > 0
3614
+ ? 'schema_and_query'
3615
+ : explicitSemanticNavigation.length > 0
3616
+ ? 'semantic_navigation'
3617
+ : legacySemanticQuery.requests.length > 0
3618
+ ? 'query_compatibility'
3619
+ : 'none',
3620
+ compatibilityQuerySyntax: RH_CONTEXT_LEGACY_SEMANTIC_SYNTAX,
3621
+ };
3622
+ const semanticReasonCodes = Array.from(new Set([
3623
+ ...semanticNavigation.errors.map((entry) => `semantic.${String(entry.code ?? 'provider_error').toLowerCase()}`),
3624
+ ...(semanticNavigation.requestTruncated ? ['semantic.request_truncated'] : []),
3625
+ ...(semanticNavigation.policyDeniedLocations > 0 || semanticNavigation.policyDeniedReads > 0 ? ['semantic.policy_denied'] : []),
3626
+ ...(semanticNavigation.freshness === 'changed_during_query' ? ['semantic.source_changed_during_query'] : []),
3627
+ ...(semanticNavigation.requested > 0 && semanticNavigation.staticClosure.status !== 'complete_for_requested_symbols' ? ['semantic.static_closure_incomplete'] : []),
3628
+ ]));
3629
+ const semanticUnavailable = semanticNavigation.requested > 0
3630
+ && semanticNavigation.results.length === 0
3631
+ && semanticNavigation.errors.length > 0
3632
+ && semanticNavigation.errors.every((entry) => /UNAVAILABLE|NOT_AVAILABLE|NOT_READY|MISSING|NOT_FOUND|BUILD_SETTINGS|BUILD_SERVER/i.test(String(entry.code ?? '')));
3633
+ const semanticReadinessStatus = semanticNavigation.requested === 0
3634
+ ? 'not_requested' as const
3635
+ : semanticNavigation.results.length === 0 && semanticNavigation.errors.length > 0
3636
+ ? semanticUnavailable ? 'unavailable' as const : 'error' as const
3637
+ : semanticNavigation.staticClosure.status === 'complete_for_requested_symbols' && semanticReasonCodes.length === 0
3638
+ ? 'ready' as const
3639
+ : 'partial' as const;
3640
+ const readinessStatus = pack.readiness.status === 'insufficient'
3641
+ ? 'insufficient' as const
3642
+ : semanticReadinessStatus === 'unavailable' || semanticReadinessStatus === 'error'
3643
+ ? 'insufficient' as const
3644
+ : pack.readiness.status === 'degraded' || semanticReadinessStatus === 'partial'
3645
+ ? 'degraded' as const
3646
+ : 'ready' as const;
3647
+ const readiness = {
3648
+ ...pack.readiness,
3649
+ status: readinessStatus,
3650
+ semantic: { status: semanticReadinessStatus, reasonCodes: semanticReasonCodes },
3651
+ unresolvedReasonCodes: Array.from(new Set([...pack.readiness.unresolvedReasonCodes, ...semanticReasonCodes])).slice(0, 80),
3652
+ readyForHighConfidenceMutation: readinessStatus === 'ready',
3653
+ };
3263
3654
  const warnings = structuralContext === 'required' && !pack.structuralContext.requiredSatisfied
3264
3655
  ? [pack.structuralContext.fallbackReason ?? 'Required structural context is not ready; lexical retrieval results are returned as degraded evidence.']
3265
3656
  : [];
@@ -3275,16 +3666,27 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3275
3666
  search: pack.search,
3276
3667
  structuralContext: pack.structuralContext,
3277
3668
  impactContext: pack.impactContext,
3669
+ semanticNavigation,
3670
+ readiness,
3671
+ expansion: pack.expansion,
3278
3672
  files: pack.files,
3673
+ coverage: pack.coverage,
3674
+ cache: pack.cache,
3675
+ timingsMs: pack.timingsMs,
3279
3676
  deniedPaths: pack.deniedPaths,
3280
3677
  omitted: pack.omitted,
3281
3678
  limits: pack.limits,
3282
3679
  contextContract: pack.contextContract,
3680
+ ...(executionReadiness ? {
3681
+ executionReadiness,
3682
+ registeredChecks: checks.slice(0, 80).map((check) => ({ id: check.id, description: check.description, source: check.source, effects: check.effects })),
3683
+ } : {}),
3283
3684
  retrievalPolicy: {
3284
3685
  defaultBackend: 'bounded_lexical',
3285
3686
  structuralBackend: 'codegraph',
3286
3687
  rawReadTool: 'read_repository_file',
3287
3688
  shellSearchFallbackOnly: true,
3689
+ executionReadiness: 'requested_check_ids_only',
3288
3690
  },
3289
3691
  },
3290
3692
  warnings,
@@ -3294,8 +3696,116 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3294
3696
  });
3295
3697
  return result(facade as unknown as Record<string, unknown>);
3296
3698
  }
3297
- const checks = listControllerChecks(repository.canonicalRoot);
3699
+ const startedAt = performance.now();
3700
+ const requestedCapabilityId = typeof args.capability_id === 'string' ? args.capability_id.trim() : '';
3701
+ if (requestedCapabilityId) {
3702
+ const pluginMatch = /^plugin\.([^.]+)\.(.+)$/.exec(requestedCapabilityId);
3703
+ const manifests = pluginMatch ? (() => {
3704
+ const pluginId = pluginMatch[1];
3705
+ const targets = [repository];
3706
+ const controllerRepository = controllerPluginRepository(ctx.controllerHome);
3707
+ if (controllerRepository.repoId !== repository.repoId) targets.push(controllerRepository);
3708
+ for (const target of targets) {
3709
+ try {
3710
+ return [getAssistantPluginManifest(ctx.controllerHome, target, pluginId)];
3711
+ } catch (error) {
3712
+ if (error instanceof Error && error.message.startsWith('PLUGIN_NOT_FOUND:')) continue;
3713
+ throw error;
3714
+ }
3715
+ }
3716
+ return [];
3717
+ })() : [];
3718
+ const descriptor = getCapabilityDescriptor(requestedCapabilityId, manifests);
3719
+ const pluginAction = getPluginActionCapabilitySchema(requestedCapabilityId, manifests);
3720
+ const facade = buildFacadeResult({
3721
+ status: 'ok',
3722
+ summary: descriptor
3723
+ ? `Exact capability ${requestedCapabilityId} is available.`
3724
+ : `Capability ${requestedCapabilityId} was not found.`,
3725
+ data: {
3726
+ operation,
3727
+ repoId: repository.repoId,
3728
+ repository: {
3729
+ repoId: repository.repoId,
3730
+ displayName: repository.displayName,
3731
+ defaultBranch: repository.defaultBranch,
3732
+ repositoryType: repository.repositoryType,
3733
+ },
3734
+ capabilityLookup: {
3735
+ requestedCapabilityId,
3736
+ found: Boolean(descriptor),
3737
+ descriptor,
3738
+ pluginAction,
3739
+ },
3740
+ toolArchitecture: {
3741
+ facadeTools: ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'],
3742
+ domainSchemaLoading: 'exact_capability_fast_path',
3743
+ },
3744
+ bounded: true,
3745
+ },
3746
+ warnings: [],
3747
+ suggestedNextActions: [],
3748
+ detailLevel: args.detail_level === 'detail' || args.detail_level === 'raw' ? args.detail_level : 'summary',
3749
+ rawAvailable: false,
3750
+ });
3751
+ const payload = facade as unknown as Record<string, unknown>;
3752
+ payload.responseMeta = {
3753
+ serverDurationMs: Number((performance.now() - startedAt).toFixed(2)),
3754
+ structuredPayloadBytes: 0,
3755
+ };
3756
+ (payload.responseMeta as { structuredPayloadBytes: number }).structuredPayloadBytes = Buffer.byteLength(JSON.stringify(payload), 'utf8');
3757
+ return result(payload);
3758
+ }
3759
+ const capabilityIntentQuery = operation === 'list' && typeof args.query === 'string'
3760
+ ? args.query.trim()
3761
+ : '';
3762
+ if (capabilityIntentQuery) {
3763
+ const manifestOptions = { preferStored: true };
3764
+ const repositoryManifests = listAssistantPluginManifests(ctx.controllerHome, repository, manifestOptions);
3765
+ const controllerRepository = controllerPluginRepository(ctx.controllerHome);
3766
+ const controllerManifests = repository.repoId === controllerRepository.repoId
3767
+ ? []
3768
+ : listAssistantPluginManifests(ctx.controllerHome, controllerRepository, manifestOptions);
3769
+ const manifests = [...new Map(
3770
+ [...repositoryManifests, ...controllerManifests].map((manifest) => [manifest.pluginId, manifest] as const),
3771
+ ).values()];
3772
+ const matches = searchCapabilityDescriptors(capabilityIntentQuery, manifests, 12)
3773
+ .map((match) => ({
3774
+ ...match,
3775
+ pluginAction: getPluginActionCapabilitySchema(match.capabilityId, manifests),
3776
+ }));
3777
+ const facade = buildFacadeResult({
3778
+ status: 'ok',
3779
+ summary: matches.length > 0
3780
+ ? `Found ${matches.length} capability candidate(s) for intent: ${capabilityIntentQuery}`
3781
+ : `No capability candidates matched intent: ${capabilityIntentQuery}`,
3782
+ data: {
3783
+ operation,
3784
+ repoId: repository.repoId,
3785
+ capabilitySearch: {
3786
+ query: capabilityIntentQuery,
3787
+ matches,
3788
+ readOnlyDiscovery: true,
3789
+ executeWith: 'plugin_action_execute',
3790
+ },
3791
+ toolArchitecture: {
3792
+ facadeTools: ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'],
3793
+ domainSchemaLoading: 'intent_ranked_capability_search',
3794
+ exactCapabilityLookupStillAvailable: true,
3795
+ },
3796
+ bounded: true,
3797
+ },
3798
+ warnings: [],
3799
+ suggestedNextActions: [],
3800
+ detailLevel: args.detail_level === 'detail' || args.detail_level === 'raw' ? args.detail_level : 'summary',
3801
+ rawAvailable: false,
3802
+ });
3803
+ return result(facade as unknown as Record<string, unknown>);
3804
+ }
3298
3805
  const requested = Array.isArray(args.requested_check_ids) ? args.requested_check_ids.map(String) : [];
3806
+ const detailLevel = args.detail_level === 'detail' || args.detail_level === 'raw' ? args.detail_level : 'summary';
3807
+ const isSummary = detailLevel === 'summary';
3808
+ const checks = requested.length > 0 || !isSummary ? listControllerChecks(repository.canonicalRoot) : [];
3299
3809
  const normalizedChecks = normalizeCheckIds(requested, checks);
3300
3810
  const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
3301
3811
  const workId = typeof args.work_id === 'string' ? args.work_id : undefined;
@@ -3310,9 +3820,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3310
3820
  });
3311
3821
  return result(facade as unknown as Record<string, unknown>, true);
3312
3822
  }
3313
- const startedAt = performance.now();
3314
- const detailLevel = args.detail_level === 'detail' || args.detail_level === 'raw' ? args.detail_level : 'summary';
3315
- const isSummary = detailLevel === 'summary';
3316
3823
  const activeContractScan = operation === 'list' || !workId
3317
3824
  ? listWorkContracts({ ...store, status: 'active', limit: 20 })
3318
3825
  : [];
@@ -3326,36 +3833,29 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3326
3833
  .filter((job) => timestampIsCurrent(job.updatedAt, currentCutoffMs))
3327
3834
  .slice(0, 5)
3328
3835
  : [];
3329
- const processScan = listProcessRecords(ctx.controllerHome, repository.repoId, workId ? 100 : 50);
3836
+ const processScan = isSummary
3837
+ ? listRecoverableProcessRecords(ctx.controllerHome, repository.repoId)
3838
+ : listProcessRecords(ctx.controllerHome, repository.repoId, workId ? 100 : 50);
3330
3839
  const relevantProcesses = processScan.filter((process) => workId ? process.workId === workId : timestampIsCurrent(process.updatedAt, currentCutoffMs));
3331
- const activeRuntimeInstanceId = currentControllerInstanceId();
3332
- const activeProcesses = relevantProcesses.filter((process) => isManagedProcessActive(process) && (!process.runtimeInstanceId || process.runtimeInstanceId === activeRuntimeInstanceId));
3840
+ const liveProcessIds = new Set(processRuntimeResourceDiagnostics().activeProcessIds);
3841
+ const activeProcesses = relevantProcesses.filter((process) => liveProcessIds.has(process.processId) && isManagedProcessActive(process));
3333
3842
  const workController = work ? getControllerSession(store, work.workId) : undefined;
3334
- const repositoryManifests = listAssistantPluginManifests(ctx.controllerHome, repository, {
3335
- preferStored: true,
3336
- });
3337
- const controllerRepository = controllerPluginRepository(ctx.controllerHome);
3338
- const controllerManifests = repository.repoId === controllerRepository.repoId
3843
+ const manifestOptions = { preferStored: true };
3844
+ const manifests = isSummary
3339
3845
  ? []
3340
- : listAssistantPluginManifests(ctx.controllerHome, controllerRepository, { preferStored: true });
3341
- const manifests = [...new Map(
3342
- [...repositoryManifests, ...controllerManifests].map((manifest) => [manifest.pluginId, manifest] as const),
3343
- ).values()];
3344
- const capabilities = listCapabilityDescriptors(manifests);
3345
- const capabilityGroups = summarizeCapabilityGroups(manifests);
3346
- const requestedCapabilityId = typeof args.capability_id === 'string' ? args.capability_id.trim() : '';
3347
- const selectedCapability = requestedCapabilityId
3348
- ? capabilities.find((descriptor) => descriptor.capabilityId === requestedCapabilityId)
3349
- : undefined;
3350
- const pluginActionSchema = requestedCapabilityId
3351
- ? getPluginActionCapabilitySchema(requestedCapabilityId, manifests)
3352
- : undefined;
3353
- const capabilityLookup = requestedCapabilityId ? {
3354
- requestedCapabilityId,
3355
- found: Boolean(selectedCapability),
3356
- descriptor: selectedCapability,
3357
- pluginAction: pluginActionSchema,
3358
- } : undefined;
3846
+ : (() => {
3847
+ const repositoryManifests = listAssistantPluginManifests(ctx.controllerHome, repository, manifestOptions);
3848
+ const controllerRepository = controllerPluginRepository(ctx.controllerHome);
3849
+ const controllerManifests = repository.repoId === controllerRepository.repoId
3850
+ ? []
3851
+ : listAssistantPluginManifests(ctx.controllerHome, controllerRepository, manifestOptions);
3852
+ return [...new Map(
3853
+ [...repositoryManifests, ...controllerManifests].map((manifest) => [manifest.pluginId, manifest] as const),
3854
+ ).values()];
3855
+ })();
3856
+ const capabilities = isSummary ? [] : listCapabilityDescriptors(manifests);
3857
+ const capabilityGroups = isSummary ? [] : summarizeCapabilityGroups(manifests);
3858
+ const capabilityLookup = undefined;
3359
3859
  const selectedChecks = normalizedChecks.validCheckIds
3360
3860
  .map((id) => checks.find((check) => check.id === id))
3361
3861
  .filter((check): check is (typeof checks)[number] => Boolean(check))
@@ -3407,8 +3907,11 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3407
3907
  requestedCheckIds: requested,
3408
3908
  normalizedChecks,
3409
3909
  invalidCheckIdsAreNotFailures: true,
3410
- capabilityCount: capabilities.length,
3411
- capabilityGroups: capabilityGroups.map((group) => ({ group: group.group, capabilityCount: group.capabilityCount })),
3910
+ capabilityInventory: {
3911
+ mode: 'detail_only',
3912
+ deferred: true,
3913
+ reason: 'Work/repository summary does not hydrate plugin manifests; request capability_id or detail/raw only when schema/policy detail is needed.',
3914
+ },
3412
3915
  capabilityLookup,
3413
3916
  toolArchitecture: {
3414
3917
  facadeTools: ['rh_access', 'rh_status', 'rh_inbox', 'rh_context', 'rh_work'],
@@ -3441,8 +3944,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3441
3944
  counts: {
3442
3945
  availableChecks: checks.length,
3443
3946
  selectedChecks: selectedChecks.length,
3444
- capabilities: capabilities.length,
3445
- capabilityGroups: capabilityGroups.length,
3947
+ capabilityInventoryDeferred: true,
3446
3948
  activeWork: currentContractScan.length,
3447
3949
  activeWorkShown: activeContracts.length,
3448
3950
  storedNonTerminalWork: activeContractScan.length,
@@ -3452,6 +3954,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3452
3954
  currentAttentionShown: attention.length,
3453
3955
  activeProcesses: activeProcesses.length,
3454
3956
  recentProcesses: relevantProcesses.length,
3957
+ historicalProcessScanDeferred: true,
3455
3958
  pendingAttentionScanned: pendingAttention.length,
3456
3959
  historicalPendingAttention: Math.max(0, pendingAttention.length - currentAttentionScan.length),
3457
3960
  omittedCurrentAttention: Math.max(0, currentAttentionScan.length - attention.length),
@@ -3547,14 +4050,30 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3547
4050
  case 'rh_work': {
3548
4051
  const repository = selected(ctx, args);
3549
4052
  const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
3550
- const operation = String(args.operation ?? 'start');
4053
+ const requestedOperation = String(args.operation ?? 'start');
4054
+ const frozenScheduleDeleteId = requestedOperation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith('schedule.delete:')
4055
+ ? args.capability_id.slice('schedule.delete:'.length).trim()
4056
+ : '';
4057
+ let frozenControllerDisposition: ReturnType<typeof parseControllerDispositionCompatibilityCapability>;
4058
+ try {
4059
+ frozenControllerDisposition = parseControllerDispositionCompatibilityCapability(requestedOperation, args.capability_id);
4060
+ } catch (error) {
4061
+ return result(buildFacadeResult({
4062
+ status: 'blocked',
4063
+ summary: error instanceof Error ? error.message : 'Controller disposition compatibility input is invalid.',
4064
+ data: {},
4065
+ }) as unknown as Record<string, unknown>, true);
4066
+ }
4067
+ const operation = frozenControllerDisposition
4068
+ ? 'controller_disposition'
4069
+ : frozenScheduleDeleteId ? 'schedule_delete' : requestedOperation;
3551
4070
  if (!allowedFacadeOperations('rh_work').includes(operation)) {
3552
4071
  return invalidFacadeOperation('rh_work', operation);
3553
4072
  }
3554
4073
  if (operation.startsWith('schedule_')) {
3555
4074
  try {
3556
4075
  const workId = String(args.work_id ?? '').trim();
3557
- const scheduleId = String(args.schedule_id ?? '').trim();
4076
+ const scheduleId = frozenScheduleDeleteId || String(args.schedule_id ?? '').trim();
3558
4077
  if (operation === 'schedule_create') {
3559
4078
  const controllerType = String(args.controller_type ?? 'chatgpt').trim();
3560
4079
  if (!['chatgpt', 'codex', 'claude', 'grok'].includes(controllerType)) throw new Error('CONTROLLER_TYPE_INVALID');
@@ -3562,56 +4081,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3562
4081
  const triggerType = ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'].includes(triggerTypeRaw)
3563
4082
  ? triggerTypeRaw as 'interval' | 'cron' | 'calendar' | 'condition' | 'repository-event' | 'dependency-checkpoint' | 'manual'
3564
4083
  : undefined;
3565
- const scheduleModeRaw = String(args.schedule_mode ?? (workId ? 'continuation' : 'workflow')).trim();
3566
- if (!['workflow', 'continuation', 'browser_watch', 'browser_keepalive'].includes(scheduleModeRaw)) throw new Error('SCHEDULE_MODE_INVALID');
3567
- if (scheduleModeRaw === 'workflow') {
3568
- if (controllerType !== 'chatgpt') throw new Error('WORKFLOW_SCHEDULE_CONTROLLER_TYPE_UNSUPPORTED');
3569
- const prompt = (typeof args.objective === 'string' ? args.objective : typeof args.continuation_prompt === 'string' ? args.continuation_prompt : '').trim();
3570
- if (!prompt) throw new Error('WORKFLOW_SCHEDULE_PROMPT_REQUIRED');
3571
- const trigger = {
3572
- type: triggerType ?? (typeof args.every_minutes === 'number' ? 'interval' : 'manual'),
3573
- everyMinutes: typeof args.every_minutes === 'number' ? Math.max(1, Math.trunc(args.every_minutes)) : undefined,
3574
- cronExpression: typeof args.cron_expression === 'string' ? args.cron_expression : undefined,
3575
- timezone: typeof args.schedule_timezone === 'string' ? args.schedule_timezone : undefined,
3576
- catchUpMinutes: typeof args.catch_up_minutes === 'number' ? Math.max(0, Math.trunc(args.catch_up_minutes)) : undefined,
3577
- calendarAt: typeof args.calendar_at === 'string' ? args.calendar_at : undefined,
3578
- condition: args.condition && typeof args.condition === 'object' && !Array.isArray(args.condition) ? args.condition as never : undefined,
3579
- eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
3580
- dependencyJobIds: Array.isArray(args.dependency_job_ids) ? args.dependency_job_ids.map(String) : undefined,
3581
- };
3582
- const policy = {
3583
- maxActiveOccurrences: 1,
3584
- maxFailures: typeof args.max_failures === 'number' ? Math.max(1, Math.trunc(args.max_failures)) : 3,
3585
- cooldownMinutes: typeof args.cooldown_minutes === 'number' ? Math.max(0, Math.trunc(args.cooldown_minutes)) : 120,
3586
- dailyBudgetMinutes: typeof args.daily_budget_minutes === 'number' ? Math.max(1, Math.trunc(args.daily_budget_minutes)) : 180,
3587
- shadowMode: args.shadow_mode !== false,
3588
- backoffBaseMinutes: typeof args.backoff_base_minutes === 'number' ? Math.max(1, Math.trunc(args.backoff_base_minutes)) : 5,
3589
- backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? Math.max(1, Math.trunc(args.backoff_max_minutes)) : 24 * 60,
3590
- };
3591
- const actionArguments = {
3592
- prompt,
3593
- ...(typeof args.browser_session_id === 'string' && args.browser_session_id.trim() ? { browser_session_id: args.browser_session_id.trim() } : {}),
3594
- ...(typeof args.conversation_url === 'string' && args.conversation_url.trim() ? { conversation_url: args.conversation_url.trim() } : {}),
3595
- };
3596
- assertAutomatedOperationAllowed('chatgpt_browser_prompt', actionArguments);
3597
- const name = typeof args.schedule_name === 'string' && args.schedule_name.trim() ? args.schedule_name.trim() : `Workflow: ${prompt.slice(0, 80)}`;
3598
- const stopConditions = Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : [];
3599
- const semantic = JSON.stringify({ repoId: repository.repoId, name, trigger, policy, actionArguments, stopConditions });
3600
- const requestId = typeof args.schedule_request_id === 'string' && args.schedule_request_id.trim()
3601
- ? args.schedule_request_id.trim()
3602
- : `workflow-schedule:${repository.repoId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 20)}`;
3603
- const schedule = createSchedule(ctx.controllerHome, {
3604
- requestId,
3605
- repoId: repository.repoId,
3606
- name,
3607
- enabled: true,
3608
- trigger,
3609
- policy,
3610
- action: { operation: 'chatgpt_browser_prompt', target: 'runtime', arguments: actionArguments, resourceClaims: [] },
3611
- stopConditions,
3612
- });
3613
- return result(buildFacadeResult({ summary: `Workflow schedule ${schedule.scheduleId} is configured without a durable Work.`, data: { schedule } }) as unknown as Record<string, unknown>);
3614
- }
4084
+ const scheduleModeRaw = String(args.schedule_mode ?? (workId ? 'continuation' : '')).trim();
4085
+ if (!['continuation', 'browser_watch', 'browser_keepalive'].includes(scheduleModeRaw)) throw new Error('SCHEDULE_MODE_REQUIRES_WORK_OR_EXPLICIT_BROWSER_KEEPALIVE');
3615
4086
  const created = createWorkContinuationSchedule(ctx.controllerHome, repository.repoId, {
3616
4087
  workId,
3617
4088
  scheduleMode: scheduleModeRaw as 'continuation' | 'browser_watch' | 'browser_keepalive',
@@ -3654,38 +4125,53 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3654
4125
  backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? args.backoff_max_minutes : undefined,
3655
4126
  stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : undefined,
3656
4127
  });
3657
- return result(buildFacadeResult({ summary: `Work schedule ${created.schedule.scheduleId} is configured for Work ${workId}.`, data: { schedule: created.schedule, work: buildWorkContinuationSnapshot(created.work) } }) as unknown as Record<string, unknown>);
4128
+ return result(buildFacadeResult({
4129
+ summary: created.work
4130
+ ? `Work schedule ${created.schedule.scheduleId} is configured for Work ${created.work.workId}.`
4131
+ : `Browser keepalive schedule ${created.schedule.scheduleId} is configured without a durable Work.`,
4132
+ data: {
4133
+ schedule: created.schedule,
4134
+ ...(created.work ? { work: buildWorkContinuationSnapshot(created.work) } : {}),
4135
+ },
4136
+ }) as unknown as Record<string, unknown>);
3658
4137
  }
3659
4138
  if (operation === 'schedule_list') {
3660
- const schedules = listSchedules(ctx.controllerHome, repository.repoId)
3661
- .filter((entry) => !workId || entry.action.arguments?.work_id === workId);
3662
- const data = args.include_occurrences === true
3663
- ? { schedules, occurrences: listOccurrences(ctx.controllerHome, repository.repoId, undefined, 100).filter((entry) => schedules.some((schedule) => schedule.scheduleId === entry.scheduleId)) }
3664
- : { schedules };
3665
- return result(buildFacadeResult({ summary: `Found ${schedules.length} schedule(s).`, data }) as unknown as Record<string, unknown>);
4139
+ const schedules = listWorkContinuationSchedules(ctx.controllerHome, repository.repoId, {
4140
+ workId: workId || undefined,
4141
+ includeOccurrences: args.include_occurrences === true,
4142
+ });
4143
+ const data = args.include_occurrences === true ? schedules : { schedules: schedules.schedules };
4144
+ return result(buildFacadeResult({ summary: `Found ${schedules.schedules.length} schedule(s).`, data }) as unknown as Record<string, unknown>);
3666
4145
  }
3667
4146
  if (!scheduleId) throw new Error('SCHEDULE_ID_REQUIRED');
3668
4147
  if (operation === 'schedule_get') {
3669
- const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3670
- const data = args.include_occurrences === true ? { schedule, occurrences: listOccurrences(ctx.controllerHome, repository.repoId, scheduleId, 50) } : { schedule };
4148
+ const data = getWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId, args.include_occurrences === true);
3671
4149
  return result(buildFacadeResult({ summary: `Schedule ${scheduleId}.`, data }) as unknown as Record<string, unknown>);
3672
4150
  }
3673
4151
  if (operation === 'schedule_pause') {
3674
- const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3675
- const saved = saveSchedule(ctx.controllerHome, { ...schedule, enabled: false, pausedReason: typeof args.reason === 'string' && args.reason.trim() ? args.reason.trim() : 'Paused by ChatGPT.' });
4152
+ const saved = pauseWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId, typeof args.reason === 'string' ? args.reason : undefined);
3676
4153
  return result(buildFacadeResult({ summary: `Schedule ${scheduleId} is paused.`, data: { schedule: saved } }) as unknown as Record<string, unknown>);
3677
4154
  }
3678
4155
  if (operation === 'schedule_resume') {
3679
- const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3680
- const saved = saveSchedule(ctx.controllerHome, { ...schedule, enabled: true, pausedReason: undefined, consecutiveFailures: 0, nextEligibleAt: undefined });
4156
+ const saved = resumeWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3681
4157
  return result(buildFacadeResult({ summary: `Schedule ${scheduleId} is resumed.`, data: { schedule: saved } }) as unknown as Record<string, unknown>);
3682
4158
  }
4159
+ if (operation === 'schedule_delete') {
4160
+ const schedule = getWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId).schedule;
4161
+ deleteSchedule(ctx.controllerHome, repository.repoId, scheduleId);
4162
+ return result(buildFacadeResult({
4163
+ summary: `Schedule ${scheduleId} is deleted; historical occurrences and evidence are retained.`,
4164
+ data: { scheduleId, deleted: true, requestId: schedule.requestId },
4165
+ }) as unknown as Record<string, unknown>);
4166
+ }
3683
4167
  if (operation === 'schedule_trigger') {
3684
- const schedule = getSchedule(ctx.controllerHome, repository.repoId, scheduleId);
3685
- const occurrence = await evaluateSchedule(ctx.controllerHome, schedule, true, {
3686
- source: typeof args.event_name === 'string' && args.event_name.trim() ? 'repository-event' : 'manual',
4168
+ const repositoryEvent = typeof args.event_name === 'string' && args.event_name.trim().length > 0;
4169
+ const explicitEventId = typeof args.event_id === 'string' ? args.event_id.trim() : '';
4170
+ const manualRequestId = !repositoryEvent && typeof args.request_id === 'string' ? args.request_id.trim() : '';
4171
+ const occurrence = await triggerWorkContinuationSchedule(ctx.controllerHome, repository.repoId, scheduleId, {
4172
+ source: repositoryEvent ? 'repository-event' : 'manual',
3687
4173
  eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
3688
- eventId: typeof args.event_id === 'string' ? args.event_id : undefined,
4174
+ eventId: explicitEventId || manualRequestId || undefined,
3689
4175
  data: args.event_data && typeof args.event_data === 'object' && !Array.isArray(args.event_data) ? args.event_data as Record<string, unknown> : undefined,
3690
4176
  });
3691
4177
  return result(buildFacadeResult({ summary: occurrence ? `Schedule ${scheduleId} produced ${occurrence.decision}.` : `Schedule ${scheduleId} produced no occurrence.`, data: { occurrence } }) as unknown as Record<string, unknown>);
@@ -3731,25 +4217,210 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3731
4217
  permissionSnapshotVersion,
3732
4218
  lastValidatedAt: new Date().toISOString(),
3733
4219
  });
3734
- return result(buildFacadeResult({ summary: `Controller ${session.controllerId} claimed ${session.workId}.`, data: { session } }) as unknown as Record<string, unknown>);
4220
+ const relay = acknowledgeControllerRoundClaim(
4221
+ { controllerHome: ctx.controllerHome, repoId: repository.repoId },
4222
+ { workId, session },
4223
+ );
4224
+ return result(buildFacadeResult({
4225
+ summary: relay?.status === 'claimed'
4226
+ ? `Controller ${session.controllerId} claimed ${session.workId}; the dispatched ChatGPT round is mechanically acknowledged and still requires an explicit semantic disposition.`
4227
+ : `Controller ${session.controllerId} claimed ${session.workId}.`,
4228
+ data: { session, relay },
4229
+ }) as unknown as Record<string, unknown>);
3735
4230
  } catch (error) {
3736
4231
  return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller claim failed.', data: {} }) as unknown as Record<string, unknown>, true);
3737
4232
  }
3738
4233
  }
4234
+ if (operation === 'controller_disposition') {
4235
+ try {
4236
+ const workId = String(args.work_id ?? '').trim();
4237
+ const disposition = frozenControllerDisposition?.disposition ?? String(args.disposition ?? '').trim();
4238
+ if (!['continue_immediately', 'wait', 'wait_for_user', 'goal_complete'].includes(disposition)) {
4239
+ throw new Error('CONTROLLER_RELAY_DISPOSITION_INVALID');
4240
+ }
4241
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
4242
+ const work = getWorkContract(store, workId);
4243
+ if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
4244
+ const terminalGoalComplete = work.status === 'completed' && disposition === 'goal_complete';
4245
+ const currentOwner = getControllerSession(store, workId);
4246
+ if (!currentOwner && !terminalGoalComplete) {
4247
+ if (work.status === 'failed' || work.status === 'cancelled' || work.status === 'completed') {
4248
+ throw new Error(`CONTROLLER_RELAY_WORK_TERMINAL: ${work.status}`);
4249
+ }
4250
+ throw new Error(`CONTROLLER_RELAY_ACTIVE_CLAIM_REQUIRED: ${workId}`);
4251
+ }
4252
+ if (currentOwner) {
4253
+ if (currentOwner.controllerType !== 'chatgpt') throw new Error(`CONTROLLER_RELAY_CHATGPT_ONLY: ${workId}`);
4254
+ if (currentOwner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
4255
+ if ((currentOwner.principalId?.trim() || currentOwner.controllerId) !== identity.principalId) {
4256
+ throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
4257
+ }
4258
+ // Finalization may terminalize the Work before the prompt-required
4259
+ // goal_complete disposition is submitted. A frozen MCP client may also
4260
+ // rotate transport sessions between those two calls. Never resume or
4261
+ // rewrite a terminal Work lease here; terminal authorization is fenced
4262
+ // by the already-claimed relay lineage in submitControllerRoundDisposition.
4263
+ if (!terminalGoalComplete) {
4264
+ if (!currentOwner.controllerInstanceId?.trim() || currentOwner.controllerInstanceId.trim() !== identity.controllerInstanceId) {
4265
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
4266
+ }
4267
+ if (currentOwner.sessionId !== identity.sessionId) {
4268
+ resumeControllerSession(store, {
4269
+ workId,
4270
+ controllerId: identity.controllerId,
4271
+ controllerType: 'chatgpt',
4272
+ sessionId: identity.sessionId,
4273
+ principalId: identity.principalId,
4274
+ controllerInstanceId: identity.controllerInstanceId,
4275
+ expectedClaimGeneration: currentOwner.claimGeneration,
4276
+ leaseMs: 3_600_000,
4277
+ });
4278
+ }
4279
+ }
4280
+ }
4281
+ const chatgptBinding = getChatgptWorkConversationBinding(store, workId);
4282
+ const relay = submitControllerRoundDisposition({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
4283
+ workId,
4284
+ identity,
4285
+ disposition: disposition as ControllerRoundDisposition,
4286
+ relayScopeId: frozenControllerDisposition?.relayScopeId ?? (typeof args.relay_scope_id === 'string' ? args.relay_scope_id : undefined),
4287
+ requirementId: typeof args.requirement_id === 'string' ? args.requirement_id : undefined,
4288
+ handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
4289
+ stateFingerprint: typeof args.state_fingerprint === 'string' ? args.state_fingerprint : undefined,
4290
+ reason: typeof args.reason === 'string' ? args.reason : undefined,
4291
+ browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : chatgptBinding?.latestBrowserSessionId,
4292
+ conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : chatgptBinding?.conversationUrl,
4293
+ maxRounds: typeof args.max_rounds === 'number' ? args.max_rounds : undefined,
4294
+ maxRepeatedState: typeof args.max_repeated_state === 'number' ? args.max_repeated_state : undefined,
4295
+ maxFailures: typeof args.max_failures === 'number' ? args.max_failures : undefined,
4296
+ });
4297
+ return result(buildFacadeResult({
4298
+ status: relay.status === 'blocked' ? 'blocked' : 'ok',
4299
+ summary: relay.status === 'pending_release'
4300
+ ? `Controller disposition ${relay.disposition} recorded; relay will dispatch only after the current lease is released.`
4301
+ : `Controller disposition ${relay.disposition} recorded with status ${relay.status}.`,
4302
+ data: { relay },
4303
+ }) as unknown as Record<string, unknown>, relay.status === 'blocked');
4304
+ } catch (error) {
4305
+ return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller disposition failed.', data: {} }) as unknown as Record<string, unknown>, true);
4306
+ }
4307
+ }
3739
4308
  if (operation === 'controller_release') {
3740
4309
  try {
3741
4310
  const workId = String(args.work_id ?? '').trim();
3742
4311
  const identity = authenticatedFacadeControllerIdentity(ctx, args);
3743
4312
  const owner = getControllerSession(store, workId);
3744
- if (owner && owner.controllerId !== identity.controllerId) {
3745
- throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId} is owned by ${owner.controllerId}`);
4313
+ if (owner) {
4314
+ const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
4315
+ const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
4316
+ if (owner.controllerId !== identity.controllerId) {
4317
+ throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId} is owned by ${owner.controllerId}`);
4318
+ }
4319
+ if (ownerPrincipal !== identity.principalId) {
4320
+ throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
4321
+ }
4322
+ if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) {
4323
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
4324
+ }
4325
+ if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
4326
+ throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
4327
+ }
4328
+ const released = releaseControllerSessionWithAuthority(store, {
4329
+ workId,
4330
+ actor: `controller-release:${identity.controllerId}:${identity.controllerInstanceId}`,
4331
+ authority: {
4332
+ controllerId: owner.controllerId,
4333
+ controllerType: owner.controllerType,
4334
+ principalId: ownerPrincipal,
4335
+ controllerInstanceId: ownerInstanceId,
4336
+ claimGeneration: owner.claimGeneration,
4337
+ },
4338
+ });
4339
+ if (!released.allowed) {
4340
+ throw new Error(`WORK_CONTROLLER_RELEASE_FENCED: ${workId}:${released.reason}`);
4341
+ }
3746
4342
  }
3747
- releaseControllerSession(store, workId, identity.controllerId);
3748
4343
  const executionSession = readExecutionSession(ctx.controllerHome, identity);
3749
4344
  if (executionSession?.activeWorkId === workId) {
3750
4345
  updateExecutionSession(ctx.controllerHome, identity, { activeWorkId: undefined, lastValidatedAt: new Date().toISOString() });
3751
4346
  }
3752
- return result(buildFacadeResult({ summary: 'Controller lease released.', data: {} }) as unknown as Record<string, unknown>);
4347
+ const relayStore = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
4348
+ const relay = owner ? beginControllerRoundRelayAfterRelease(
4349
+ relayStore,
4350
+ { workId, releasedSession: owner },
4351
+ ) : undefined;
4352
+ if (relay?.status === 'dispatching') {
4353
+ try {
4354
+ assertAutomatedOperationAllowed('external_controller_wake', {
4355
+ controller_type: 'chatgpt',
4356
+ relay_scope_id: relay.relayScopeId,
4357
+ requirement_id: relay.requirementId,
4358
+ });
4359
+ const prompt = buildControllerRoundRelayPrompt(relayStore, relay);
4360
+ const dispatched = await runWorkChatgptContinuation({
4361
+ controllerHome: ctx.controllerHome,
4362
+ repoId: repository.repoId,
4363
+ repoRoot: repository.canonicalRoot,
4364
+ workId,
4365
+ prompt,
4366
+ browserSessionId: relay.browserSessionId,
4367
+ conversationUrl: relay.conversationUrl,
4368
+ tabPolicy: 'reuse',
4369
+ });
4370
+ if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CONTROLLER_RELAY_DISPATCH_FAILED');
4371
+ // The next prompt is externally committed before this transition is
4372
+ // recorded. Contiguous immediate rounds intentionally retain one
4373
+ // Forge-owned tab, avoiding a close/reopen race and duplicate prompt.
4374
+ recordControllerRoundTabSettlement(relayStore, {
4375
+ workId,
4376
+ status: 'retained_for_immediate_continuation',
4377
+ });
4378
+ const completed = finishControllerRoundRelayDispatch(
4379
+ relayStore,
4380
+ { workId, ok: true, browserSessionId: dispatched.browserSessionId, conversationUrl: dispatched.conversationUrl },
4381
+ );
4382
+ return result(buildFacadeResult({
4383
+ summary: `Controller lease released and immediate relay ${relay.relayScopeId} dispatched through the canonical ChatGPT launcher.`,
4384
+ data: { relay: completed, dispatch: dispatched },
4385
+ }) as unknown as Record<string, unknown>);
4386
+ } catch (relayError) {
4387
+ const failed = finishControllerRoundRelayDispatch(
4388
+ relayStore,
4389
+ { workId, ok: false, error: relayError instanceof Error ? relayError.message : String(relayError) },
4390
+ );
4391
+ return result(buildFacadeResult({
4392
+ status: 'blocked',
4393
+ summary: `Controller lease released, but immediate relay dispatch failed: ${relayError instanceof Error ? relayError.message : String(relayError)}`,
4394
+ data: { relay: failed },
4395
+ }) as unknown as Record<string, unknown>, true);
4396
+ }
4397
+ }
4398
+ const settledRelay = getControllerRoundRelay(relayStore, workId);
4399
+ let tabSettlement;
4400
+ if (
4401
+ owner?.controllerType === 'chatgpt'
4402
+ && settledRelay
4403
+ && ['waiting', 'waiting_for_user', 'goal_complete', 'blocked', 'failed'].includes(settledRelay.status)
4404
+ ) {
4405
+ const binding = getChatgptWorkConversationBinding(store, workId);
4406
+ const browserSessionId = settledRelay.browserSessionId ?? binding?.latestBrowserSessionId;
4407
+ if (browserSessionId) {
4408
+ tabSettlement = await settleWorkChatgptAutomationTab({
4409
+ controllerHome: ctx.controllerHome,
4410
+ workId,
4411
+ browserSessionId,
4412
+ });
4413
+ recordControllerRoundTabSettlement(relayStore, {
4414
+ workId,
4415
+ status: tabSettlement.status,
4416
+ error: tabSettlement.error?.message,
4417
+ });
4418
+ }
4419
+ }
4420
+ return result(buildFacadeResult({
4421
+ summary: 'Controller lease released.',
4422
+ data: { relay: getControllerRoundRelay(relayStore, workId) ?? relay, tabSettlement },
4423
+ }) as unknown as Record<string, unknown>);
3753
4424
  } catch (error) {
3754
4425
  return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller release failed.', data: {} }) as unknown as Record<string, unknown>, true);
3755
4426
  }
@@ -3758,11 +4429,101 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3758
4429
  try {
3759
4430
  const controllerType = String(args.controller_type ?? 'codex');
3760
4431
  if (!['chatgpt', 'codex', 'grok', 'claude'].includes(controllerType)) throw new Error('CONTROLLER_TYPE_INVALID');
3761
- const launched = launchSuperController({ work: store, handoff: store }, {
3762
- controllerType: controllerType as 'chatgpt' | 'codex' | 'grok' | 'claude',
4432
+ const workId = String(args.work_id ?? '').trim();
4433
+ const launchArgs = Array.isArray(args.launch_args) ? args.launch_args.map(String) : [];
4434
+ if (controllerType === 'chatgpt') {
4435
+ const work = getWorkContract(store, workId);
4436
+ if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
4437
+ const handoffId = typeof args.handoff_id === 'string' ? args.handoff_id.trim() : '';
4438
+ const handoff = handoffId ? getHandoffItem(store, handoffId) : undefined;
4439
+ const valueForFlag = (flag: string): string | undefined => {
4440
+ const index = launchArgs.indexOf(flag);
4441
+ if (index < 0) return undefined;
4442
+ const value = launchArgs[index + 1];
4443
+ if (!value || value.startsWith('--')) throw new Error(`CHATGPT_LAUNCH_ARG_VALUE_REQUIRED: ${flag}`);
4444
+ return value;
4445
+ };
4446
+ const supportedFlags = new Set(['--model', '--reasoning', '--tab-policy', '--timeout-ms']);
4447
+ for (let index = 0; index < launchArgs.length; index += 2) {
4448
+ const flag = launchArgs[index];
4449
+ if (!flag || !supportedFlags.has(flag)) throw new Error(`CHATGPT_LAUNCH_ARG_UNSUPPORTED: ${flag ?? ''}`);
4450
+ if (!launchArgs[index + 1] || launchArgs[index + 1]!.startsWith('--')) throw new Error(`CHATGPT_LAUNCH_ARG_VALUE_REQUIRED: ${flag}`);
4451
+ }
4452
+ const reasoning = valueForFlag('--reasoning') ?? 'high';
4453
+ if (!['medium', 'high', 'xhigh'].includes(reasoning)) throw new Error(`CHATGPT_LAUNCH_REASONING_INVALID: ${reasoning}`);
4454
+ const tabPolicy = valueForFlag('--tab-policy') ?? 'auto';
4455
+ if (!['auto', 'reuse', 'new'].includes(tabPolicy)) throw new Error(`CHATGPT_LAUNCH_TAB_POLICY_INVALID: ${tabPolicy}`);
4456
+ const timeoutValue = valueForFlag('--timeout-ms');
4457
+ const timeoutMs = timeoutValue === undefined ? undefined : Number(timeoutValue);
4458
+ if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) throw new Error(`CHATGPT_LAUNCH_TIMEOUT_INVALID: ${timeoutValue}`);
4459
+ const continuationPrompt = typeof args.continuation_prompt === 'string' ? args.continuation_prompt.trim() : '';
4460
+ const relay = beginInitialControllerRoundDispatch(
4461
+ { controllerHome: ctx.controllerHome, repoId: repository.repoId },
4462
+ {
4463
+ workId,
4464
+ identity: authenticatedFacadeControllerIdentity(ctx, args),
4465
+ requirementId: work.requirementId,
4466
+ browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
4467
+ conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
4468
+ },
4469
+ );
4470
+ if (relay.status === 'blocked') {
4471
+ throw new Error(`CONTROLLER_RELAY_LAUNCH_BLOCKED: ${relay.blockedReason ?? relay.relayScopeId}`);
4472
+ }
4473
+ const prompt = [
4474
+ `Continue Forge Work ${work.workId} in repo ${work.repoId}.`,
4475
+ `Objective: ${work.objective}`,
4476
+ `Acceptance: ${work.acceptanceCriteria.join('; ') || 'none declared'}`,
4477
+ `Current status: ${work.status}`,
4478
+ `Controller round: ${relay.relayScopeId}. launcher_start opened this durable round; dispatch success is not semantic completion.`,
4479
+ handoff ? `Handoff: ${handoff.summary}\nNext: ${handoff.recommendedContinuationPrompt ?? handoff.recommendedPrompt}` : '',
4480
+ continuationPrompt ? `Continuation: ${continuationPrompt}` : '',
4481
+ 'Treat Forge Work/Plan/evidence as source of truth. Claim the exact Work before any mutation; if ownership cannot be established, do not mutate.',
4482
+ `Before this ChatGPT round ends, submit exactly one rh_work controller_disposition for Work ${work.workId} and relay_scope_id=${relay.relayScopeId}: continue_immediately, wait, wait_for_user (with an active Handoff), or goal_complete. If this client schema is frozen and omits controller_disposition, use rh_work operation=repair with capability_id=controller.disposition:<disposition>:${relay.relayScopeId}; it is mapped to the same canonical disposition path. Ending the response without a disposition does not close the round and may trigger bounded mechanical recovery.`,
4483
+ ].filter(Boolean).join('\n');
4484
+ let dispatched: Awaited<ReturnType<typeof runWorkChatgptContinuation>>;
4485
+ try {
4486
+ dispatched = await runWorkChatgptContinuation({
4487
+ controllerHome: ctx.controllerHome,
4488
+ repoId: repository.repoId,
4489
+ repoRoot: repository.canonicalRoot,
4490
+ workId,
4491
+ prompt,
4492
+ browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
4493
+ conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
4494
+ model: valueForFlag('--model') ?? 'gpt-5.6',
4495
+ reasoning: reasoning as 'medium' | 'high' | 'xhigh',
4496
+ tabPolicy: tabPolicy as 'auto' | 'reuse' | 'new',
4497
+ timeoutMs,
4498
+ });
4499
+ if (dispatched.status === 'failed') throw new Error(dispatched.error?.message ?? 'CHATGPT_WORK_CONTINUATION_FAILED');
4500
+ } catch (launchError) {
4501
+ finishControllerRoundRelayDispatch(
4502
+ { controllerHome: ctx.controllerHome, repoId: repository.repoId },
4503
+ { workId, ok: false, error: launchError instanceof Error ? launchError.message : String(launchError) },
4504
+ );
4505
+ throw launchError;
4506
+ }
4507
+ const completedRelay = finishControllerRoundRelayDispatch(
4508
+ { controllerHome: ctx.controllerHome, repoId: repository.repoId },
4509
+ { workId, ok: true, browserSessionId: dispatched.browserSessionId, conversationUrl: dispatched.conversationUrl },
4510
+ );
4511
+ return result(buildFacadeResult({
4512
+ summary: 'ChatGPT continuation dispatched; wake completion remains pending until the new ChatGPT Controller claims the Work, and semantic closure still requires an explicit disposition.',
4513
+ data: {
4514
+ workId,
4515
+ relay: completedRelay,
4516
+ browserSessionId: dispatched.browserSessionId,
4517
+ conversationUrl: dispatched.conversationUrl,
4518
+ executionPreferenceVerified: dispatched.executionPreferenceVerified,
4519
+ },
4520
+ }) as unknown as Record<string, unknown>);
4521
+ }
4522
+ const launched = await launchSuperController({ work: store, handoff: store }, {
4523
+ controllerType: controllerType as 'codex' | 'grok' | 'claude',
3763
4524
  executable: typeof args.executable === 'string' && args.executable.trim() ? args.executable.trim() : undefined,
3764
- args: Array.isArray(args.launch_args) ? args.launch_args.map(String) : [],
3765
- workId: String(args.work_id ?? '').trim(),
4525
+ args: launchArgs,
4526
+ workId,
3766
4527
  launchReservationMs: typeof args.launch_reservation_ms === 'number' ? args.launch_reservation_ms : typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
3767
4528
  handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
3768
4529
  browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
@@ -3770,11 +4531,27 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3770
4531
  continuationPrompt: typeof args.continuation_prompt === 'string' ? args.continuation_prompt : undefined,
3771
4532
  cwd: repository.canonicalRoot,
3772
4533
  });
3773
- return result(buildFacadeResult({ summary: `Thin Launcher started ${launched.controllerType}.`, data: { pid: launched.pid, executable: launched.executable, workId: String(args.work_id ?? ''), reservationId: launched.reservationId } }) as unknown as Record<string, unknown>);
4534
+ 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>);
3774
4535
  } catch (error) {
3775
4536
  return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Launcher failed.', data: {} }) as unknown as Record<string, unknown>, true);
3776
4537
  }
3777
4538
  }
4539
+ if (operation === 'plan_list') {
4540
+ const plans = listPlanContracts({ ...store, status: 'active', limit: typeof args.limit === 'number' ? args.limit : 20 });
4541
+ const facade = buildFacadeResult({
4542
+ summary: `${plans.length} active PlanContract(s) in this repository.`,
4543
+ data: { plans: plans.map(summarizePlanContract), bounded: true },
4544
+ });
4545
+ return result(facade as unknown as Record<string, unknown>);
4546
+ }
4547
+ if (operation === 'plan_get') {
4548
+ const plan = getPlanContract(store, String(args.plan_id ?? ''));
4549
+ const facade = plan
4550
+ ? buildFacadeResult({ summary: `PlanContract ${plan.planId} retrieved.`, data: { plan: args.detail_level === 'detail' ? plan : summarizePlanContract(plan) }, detailLevel: args.detail_level === 'detail' ? 'detail' : 'summary' })
4551
+ : buildFacadeResult({ status: 'not_found', summary: `PlanContract ${String(args.plan_id ?? '')} not found.`, data: { planId: String(args.plan_id ?? '') } });
4552
+ return result(facade as unknown as Record<string, unknown>, !plan);
4553
+ }
4554
+
3778
4555
  const checks = listControllerChecks(repository.canonicalRoot);
3779
4556
  const workloopCtx = {
3780
4557
  workStore: store,
@@ -3786,82 +4563,173 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3786
4563
  controllerInstanceId: ctx.controllerInstanceId,
3787
4564
  availableChecks: checks,
3788
4565
  sourceRevision: gitSnapshot(repository.canonicalRoot).head ?? undefined,
3789
- materializeIsolatedWorkspace: ({ workId, title, baseRef }: { workId: string; title: string; baseRef?: string }) => {
3790
- const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, { requestId: workId, title, baseRef });
4566
+ materializeIsolatedWorkspace: ({ workId, title, baseRef, needsDependencies }: { workId: string; title: string; baseRef?: string; needsDependencies?: boolean }) => {
4567
+ const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
4568
+ requestId: workId,
4569
+ title,
4570
+ baseRef,
4571
+ prepareDependencies: needsDependencies === true,
4572
+ });
3791
4573
  if (!workspace.managed || !workspace.checkoutId || !workspace.root) throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
3792
4574
  return { checkoutId: workspace.checkoutId, root: workspace.root, baseRevision: workspace.baseRevision, managed: true as const };
3793
4575
  },
3794
4576
  };
3795
4577
 
4578
+ if (operation === 'requirement_create') {
4579
+ const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id.trim() : '';
4580
+ const title = typeof args.requirement_title === 'string' ? args.requirement_title.trim().slice(0, 500) : '';
4581
+ const outcomeStatement = typeof args.requirement_outcome === 'string' ? args.requirement_outcome.trim().slice(0, 2_000) : '';
4582
+ const acceptanceCriteria = Array.isArray(args.requirement_acceptance_criteria)
4583
+ ? args.requirement_acceptance_criteria.map((value) => String(value).trim()).filter(Boolean).slice(0, 50).map((value) => value.slice(0, 500))
4584
+ : [];
4585
+ const requiredDeliveryReferences = Array.isArray(args.requirement_delivery_references)
4586
+ ? args.requirement_delivery_references.map((value) => String(value).trim()).filter(Boolean).slice(0, 50).map((value) => value.slice(0, 500))
4587
+ : [];
4588
+ const legacyAliases = Array.isArray(args.requirement_legacy_aliases)
4589
+ ? args.requirement_legacy_aliases.map((value) => String(value).trim()).filter(Boolean).slice(0, 20).map((value) => value.slice(0, 160))
4590
+ : [];
4591
+ if (!requirementId || !title || !outcomeStatement) {
4592
+ return result(buildFacadeResult({
4593
+ status: 'blocked',
4594
+ summary: 'REQUIREMENT_CREATE_INPUT_REQUIRED: requirement_id, requirement_title, and requirement_outcome are required.',
4595
+ data: { requirementCreated: false },
4596
+ }) as unknown as Record<string, unknown>, true);
4597
+ }
4598
+ const existing = readRequirement({ controllerHome: ctx.controllerHome }, requirementId)?.value;
4599
+ if (existing) {
4600
+ const identical = existing.title === title
4601
+ && existing.outcomeStatement === outcomeStatement
4602
+ && JSON.stringify(existing.acceptanceCriteria) === JSON.stringify(acceptanceCriteria)
4603
+ && JSON.stringify(existing.requiredDeliveryReferences) === JSON.stringify(requiredDeliveryReferences)
4604
+ && JSON.stringify(existing.legacyAliases) === JSON.stringify(legacyAliases);
4605
+ if (!identical) {
4606
+ return result(buildFacadeResult({
4607
+ status: 'blocked',
4608
+ summary: `REQUIREMENT_ALREADY_EXISTS_CONFLICT: ${requirementId}. Existing Requirement authority was not changed.`,
4609
+ data: { requirement: existing, requirementCreated: false, admissionDecision: 'existing_conflict' },
4610
+ suggestedNextActions: [],
4611
+ }) as unknown as Record<string, unknown>, true);
4612
+ }
4613
+ return result(buildFacadeResult({
4614
+ summary: `REQUIREMENT_AUTHORITY_REUSED: ${requirementId}. Requirement authority does not imply a Plan; Controller chooses the next action.`,
4615
+ data: { requirement: existing, requirementCreated: false, admissionDecision: 'reuse_existing' },
4616
+ suggestedNextActions: [],
4617
+ }) as unknown as Record<string, unknown>);
4618
+ }
4619
+ const requirement = createRequirement({ controllerHome: ctx.controllerHome }, {
4620
+ requirementId,
4621
+ title,
4622
+ outcomeStatement,
4623
+ acceptanceCriteria,
4624
+ requiredDeliveryReferences,
4625
+ legacyAliases,
4626
+ });
4627
+ return result(buildFacadeResult({
4628
+ summary: `Requirement ${requirementId} created. Requirement authority does not imply a Plan; Controller chooses the next action.`,
4629
+ data: { requirement, requirementCreated: true, admissionDecision: 'created' },
4630
+ suggestedNextActions: [],
4631
+ }) as unknown as Record<string, unknown>);
4632
+ }
4633
+
3796
4634
  if (operation.startsWith('plan_')) {
3797
4635
  try {
3798
4636
  if (operation === 'plan_create') {
3799
4637
  const rawSteps = Array.isArray(args.plan_steps) ? args.plan_steps : [];
3800
4638
  const requestedRequirementId = typeof args.requirement_id === 'string' && args.requirement_id.trim() ? args.requirement_id.trim() : undefined;
3801
- const requestedScopeKey = String(args.scope_key ?? '').trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 160);
3802
- const activePlans = listPlanContracts({ ...store, status: 'active', limit: 100 });
3803
- const exactScopeAuthority = requestedScopeKey
3804
- ? activePlans.find((candidate) => candidate.scopeKey === requestedScopeKey)
3805
- : undefined;
3806
- if (exactScopeAuthority) {
3807
- const facade = buildFacadeResult({
3808
- summary: `PLAN_AUTHORITY_REUSED: active Plan ${exactScopeAuthority.planId} already owns scope ${requestedScopeKey}; no duplicate draft was created.`,
3809
- data: {
3810
- plan: summarizePlanContract(exactScopeAuthority),
3811
- executionStarted: false,
3812
- planContractCreated: false,
3813
- admissionDecision: 'reuse_existing',
3814
- resolutionRequired: false,
3815
- },
3816
- suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: exactScopeAuthority.planId }, risk: 'readonly', confidence: 'high' }],
3817
- });
3818
- return result(facade as unknown as Record<string, unknown>);
3819
- }
3820
- const requirementPlans = requestedRequirementId
3821
- ? activePlans.filter((candidate) => candidate.requirementId === requestedRequirementId)
3822
- : [];
3823
- const requestedPlanRelation = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
4639
+ const requestedPlanRelation: 'extend' | 'parallel' | undefined = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
3824
4640
  ? args.plan_relation
3825
4641
  : undefined;
3826
4642
  const relatedPlanId = typeof args.related_plan_id === 'string' && args.related_plan_id.trim() ? args.related_plan_id.trim() : undefined;
3827
- const relatedPlan = relatedPlanId
3828
- ? requirementPlans.find((candidate) => candidate.planId === relatedPlanId)
3829
- : requirementPlans.length === 1 ? requirementPlans[0] : undefined;
3830
- if (requirementPlans.length > 0 && !requestedPlanRelation) {
4643
+ if (requestedRequirementId && !readRequirement({ controllerHome: ctx.controllerHome }, requestedRequirementId)) {
3831
4644
  const facade = buildFacadeResult({
3832
- summary: `PLAN_RELATION_RESOLUTION_REQUIRED: Requirement ${requestedRequirementId} already has ${requirementPlans.length} active Plan slice(s). Decide whether this scope extends one of them or is an intentional parallel slice before creating a draft.`,
3833
- data: {
3834
- executionStarted: false,
3835
- planContractCreated: false,
3836
- admissionDecision: 'resolution_required',
3837
- resolutionRequired: true,
3838
- candidates: requirementPlans.slice(0, 8).map(summarizePlanContract),
3839
- allowedPlanRelations: ['extend', 'parallel'],
3840
- },
3841
- suggestedNextActions: requirementPlans.slice(0, 3).map((candidate) => ({ label: `Read ${candidate.planId}`, tool: 'rh_work', operation: 'plan_get', payload: { plan_id: candidate.planId }, risk: 'readonly' as const, confidence: 'high' as const })),
4645
+ status: 'failed',
4646
+ summary: `PLAN_REQUIREMENT_NOT_FOUND: ${requestedRequirementId}. Plan was not persisted; create or reconcile the Requirement authority first.`,
4647
+ data: { executionStarted: false, planContractCreated: false, admissionDecision: 'missing_requirement', requirementId: requestedRequirementId },
3842
4648
  });
3843
- return result(facade as unknown as Record<string, unknown>);
4649
+ return result(facade as unknown as Record<string, unknown>, true);
3844
4650
  }
3845
- if (requirementPlans.length > 0 && requestedPlanRelation === 'extend') {
3846
- if (!relatedPlan) {
4651
+ const admissionInput = {
4652
+ requirementId: requestedRequirementId,
4653
+ scopeKey: String(args.scope_key ?? ''),
4654
+ planRelation: requestedPlanRelation,
4655
+ relatedPlanId,
4656
+ };
4657
+ const renderPlanAdmission = (admission: ReturnType<typeof resolvePlanAdmission>): CallToolResult | undefined => {
4658
+ if (admission.admissionDecision === 'create_new') return undefined;
4659
+ if (admission.reason === 'exact_scope_authority' && admission.plan) {
4660
+ const facade = buildFacadeResult({
4661
+ summary: `PLAN_AUTHORITY_REUSED: active Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; no duplicate draft was created.`,
4662
+ data: {
4663
+ plan: summarizePlanContract(admission.plan),
4664
+ executionStarted: false,
4665
+ planContractCreated: false,
4666
+ admissionDecision: 'reuse_existing',
4667
+ resolutionRequired: false,
4668
+ },
4669
+ suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: admission.plan.planId }, risk: 'readonly', confidence: 'high' }],
4670
+ });
4671
+ return result(facade as unknown as Record<string, unknown>);
4672
+ }
4673
+ if (admission.reason === 'requirement_relation_required') {
4674
+ const facade = buildFacadeResult({
4675
+ summary: `PLAN_RELATION_RESOLUTION_REQUIRED: Requirement ${requestedRequirementId} already has ${admission.candidates.length} active Plan slice(s). Decide whether this scope extends one of them or is an intentional parallel slice before creating a draft.`,
4676
+ data: {
4677
+ executionStarted: false,
4678
+ planContractCreated: false,
4679
+ admissionDecision: 'resolution_required',
4680
+ resolutionRequired: true,
4681
+ candidates: admission.candidates.map(summarizePlanContract),
4682
+ allowedPlanRelations: admission.allowedPlanRelations ?? ['extend', 'parallel'],
4683
+ },
4684
+ suggestedNextActions: admission.candidates.slice(0, 3).map((candidate) => ({ label: `Read ${candidate.planId}`, tool: 'rh_work', operation: 'plan_get', payload: { plan_id: candidate.planId }, risk: 'readonly' as const, confidence: 'high' as const })),
4685
+ });
4686
+ return result(facade as unknown as Record<string, unknown>);
4687
+ }
4688
+ if (admission.reason === 'extension_target_required') {
3847
4689
  const facade = buildFacadeResult({
3848
4690
  summary: `PLAN_EXTENSION_TARGET_REQUIRED: select related_plan_id from the active Plan slices for Requirement ${requestedRequirementId}.`,
3849
- data: { executionStarted: false, planContractCreated: false, admissionDecision: 'resolution_required', resolutionRequired: true, candidates: requirementPlans.slice(0, 8).map(summarizePlanContract) },
4691
+ data: { executionStarted: false, planContractCreated: false, admissionDecision: 'resolution_required', resolutionRequired: true, candidates: admission.candidates.map(summarizePlanContract) },
3850
4692
  });
3851
4693
  return result(facade as unknown as Record<string, unknown>);
3852
4694
  }
4695
+ if (admission.reason === 'extend_existing' && admission.plan) {
4696
+ // plan_create + plan_relation=extend is the atomic serial-replan
4697
+ // path. Admission continues under the same lock so successor
4698
+ // creation and predecessor supersession are persisted together.
4699
+ return undefined;
4700
+ }
4701
+ throw new Error(`PLAN_ADMISSION_RESULT_INVALID: ${admission.admissionDecision}:${admission.reason}`);
4702
+ };
4703
+ const activePlans = listPlanContracts({ ...store, status: 'active', limit: 100 });
4704
+ const preflightAdmission = resolvePlanAdmission(activePlans, admissionInput);
4705
+ const preflightResult = renderPlanAdmission(preflightAdmission);
4706
+ if (preflightResult) return preflightResult;
4707
+ const requestedPlanCheckIds = rawSteps
4708
+ .filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step))
4709
+ .flatMap((step) => Array.isArray(step.check_ids) ? step.check_ids.map(String) : []);
4710
+ const normalizedPlanChecks = normalizeCheckIds(requestedPlanCheckIds, checks);
4711
+ if (normalizedPlanChecks.invalidCheckIds.length > 0) {
3853
4712
  const facade = buildFacadeResult({
3854
- summary: `PLAN_EXTENSION_REQUIRES_REPLAN: ${relatedPlan.planId} remains the serial authority. Extend/replan and supersede that authority rather than creating a second active Plan draft.`,
3855
- data: { plan: summarizePlanContract(relatedPlan), executionStarted: false, planContractCreated: false, admissionDecision: 'extend_existing', resolutionRequired: false },
3856
- suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: relatedPlan.planId }, risk: 'readonly', confidence: 'high' }],
4713
+ status: 'failed',
4714
+ 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.`,
4715
+ data: {
4716
+ executionStarted: false,
4717
+ planContractCreated: false,
4718
+ admissionDecision: 'invalid_checks',
4719
+ normalizedChecks: normalizedPlanChecks,
4720
+ registeredCheckIds: checks.map((check) => check.id).slice(0, 80),
4721
+ },
4722
+ suggestedNextActions: [],
3857
4723
  });
3858
- return result(facade as unknown as Record<string, unknown>);
4724
+ return result(facade as unknown as Record<string, unknown>, true);
3859
4725
  }
3860
- const plan = createPlanContract(store, {
4726
+ const admitted = await admitPlanContractAsync(store, {
3861
4727
  planId: String(args.plan_id ?? ''),
3862
4728
  repoId: repository.repoId,
3863
4729
  requirementId: requestedRequirementId,
3864
4730
  scopeKey: String(args.scope_key ?? ''),
4731
+ planRelation: requestedPlanRelation,
4732
+ relatedPlanId,
3865
4733
  sourceRevision: String(args.source_revision ?? ''),
3866
4734
  goal: String(args.objective ?? ''),
3867
4735
  nonGoals: Array.isArray(args.non_goals) ? args.non_goals.map(String) : undefined,
@@ -3881,33 +4749,43 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3881
4749
  acceptanceCriteria: Array.isArray(step.acceptance_criteria) ? step.acceptance_criteria.map(String) : [],
3882
4750
  })),
3883
4751
  });
4752
+ const racedAdmissionResult = renderPlanAdmission(admitted);
4753
+ if (racedAdmissionResult) return racedAdmissionResult;
4754
+ if (!admitted.plan) throw new Error('PLAN_ADMISSION_CREATE_MISSING_PLAN');
4755
+ const plan = admitted.plan;
3884
4756
  const facade = buildFacadeResult({
3885
- summary: `PlanContract ${plan.planId} created as draft after authority preflight; no execution was started.`,
4757
+ summary: `PlanContract ${plan.planId} created as draft after atomic authority admission; no execution was started.`,
3886
4758
  data: { plan: summarizePlanContract(plan), executionStarted: false, planContractCreated: true, admissionDecision: 'create_new' },
3887
4759
  suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: plan.planId }, risk: 'workspace_write', confidence: 'medium' }],
3888
4760
  });
3889
4761
  return result(facade as unknown as Record<string, unknown>);
3890
4762
  }
3891
- if (operation === 'plan_list') {
3892
- const plans = listPlanContracts({ ...store, status: 'active', limit: typeof args.limit === 'number' ? args.limit : 20 });
4763
+ if (operation === 'plan_approve') {
4764
+ const plan = await approvePlanContractAsync(store, String(args.plan_id ?? ''));
3893
4765
  const facade = buildFacadeResult({
3894
- summary: `${plans.length} active PlanContract(s) in this repository.`,
3895
- data: { plans: plans.map(summarizePlanContract), bounded: true },
4766
+ summary: `PlanContract ${plan.planId} approved at source revision ${plan.sourceRevision}; execution remains explicit.`,
4767
+ data: { plan: summarizePlanContract(plan), executionStarted: false },
3896
4768
  });
3897
4769
  return result(facade as unknown as Record<string, unknown>);
3898
4770
  }
3899
- if (operation === 'plan_get') {
3900
- const plan = getPlanContract(store, String(args.plan_id ?? ''));
3901
- const facade = plan
3902
- ? buildFacadeResult({ summary: `PlanContract ${plan.planId} retrieved.`, data: { plan: args.detail_level === 'detail' ? plan : summarizePlanContract(plan) }, detailLevel: args.detail_level === 'detail' ? 'detail' : 'summary' })
3903
- : buildFacadeResult({ status: 'not_found', summary: `PlanContract ${String(args.plan_id ?? '')} not found.`, data: { planId: String(args.plan_id ?? '') } });
3904
- return result(facade as unknown as Record<string, unknown>, !plan);
3905
- }
3906
- if (operation === 'plan_approve') {
3907
- const plan = approvePlanContract(store, String(args.plan_id ?? ''));
4771
+ if (operation === 'plan_accept_step') {
4772
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
4773
+ const planId = String(args.plan_id ?? '').trim();
4774
+ const stepId = String(args.plan_step_id ?? '').trim();
4775
+ const rationale = String(args.acceptance_rationale ?? '').trim();
4776
+ const plan = acceptPlanStepEvidence(store, {
4777
+ planId,
4778
+ stepId,
4779
+ reviewer: identity.principalId,
4780
+ rationale,
4781
+ acceptedSourceRevision: workloopCtx.sourceRevision,
4782
+ });
3908
4783
  const facade = buildFacadeResult({
3909
- summary: `PlanContract ${plan.planId} approved at source revision ${plan.sourceRevision}; execution remains explicit.`,
3910
- data: { plan: summarizePlanContract(plan), executionStarted: false },
4784
+ summary: `Plan step ${stepId} semantically accepted by the current Controller.`,
4785
+ data: { plan: summarizePlanContract(plan), semanticAcceptanceRecorded: true, reviewer: identity.principalId },
4786
+ suggestedNextActions: plan.status === 'finalized'
4787
+ ? []
4788
+ : [{ label: 'Continue the next approved Plan step', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: plan.planId }, risk: 'readonly', confidence: 'high' }],
3911
4789
  });
3912
4790
  return result(facade as unknown as Record<string, unknown>);
3913
4791
  }
@@ -3949,7 +4827,69 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3949
4827
  }
3950
4828
 
3951
4829
  if (operation === 'stop') {
3952
- const facade = runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'stop', args);
4830
+ const workId = String(args.work_id ?? '').trim();
4831
+ const currentOwner = workId ? getControllerSession(store, workId) : undefined;
4832
+ let terminalizationAuthority: ControllerTerminalizationAuthority | undefined;
4833
+ if (currentOwner) {
4834
+ try {
4835
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
4836
+ const ownerPrincipal = currentOwner.principalId?.trim() || currentOwner.controllerId;
4837
+ const ownerInstanceId = currentOwner.controllerInstanceId?.trim() || '';
4838
+ if (currentOwner.controllerId !== identity.controllerId || ownerPrincipal !== identity.principalId) {
4839
+ throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
4840
+ }
4841
+ if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) {
4842
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
4843
+ }
4844
+ if (typeof currentOwner.claimGeneration !== 'number' || currentOwner.claimGeneration < 1) {
4845
+ throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
4846
+ }
4847
+ terminalizationAuthority = {
4848
+ controllerId: currentOwner.controllerId,
4849
+ controllerType: currentOwner.controllerType,
4850
+ principalId: ownerPrincipal,
4851
+ controllerInstanceId: ownerInstanceId,
4852
+ claimGeneration: currentOwner.claimGeneration,
4853
+ };
4854
+ } catch (error) {
4855
+ const blocked = buildFacadeResult({
4856
+ status: 'blocked',
4857
+ summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`,
4858
+ data: { workId, terminalizationApplied: false },
4859
+ });
4860
+ return result(blocked as unknown as Record<string, unknown>, true);
4861
+ }
4862
+ } else if (!['user', 'system'].includes(String(args.requested_by ?? ''))) {
4863
+ const blocked = buildFacadeResult({
4864
+ status: 'blocked',
4865
+ summary: `WORK_CONTROLLER_OWNER_REQUIRED: ${workId}; unclaimed stop requires explicit requested_by=user or requested_by=system authority.`,
4866
+ data: { workId, terminalizationApplied: false },
4867
+ });
4868
+ return result(blocked as unknown as Record<string, unknown>, true);
4869
+ }
4870
+
4871
+ const fenced = withControllerSessionTerminalizationFence(
4872
+ store,
4873
+ {
4874
+ workId,
4875
+ actor: `rh-work-stop:${terminalizationAuthority?.controllerId ?? String(args.requested_by ?? 'explicit')}`,
4876
+ authority: terminalizationAuthority,
4877
+ },
4878
+ () => runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'stop', args),
4879
+ );
4880
+ if (!fenced.allowed) {
4881
+ const blocked = buildFacadeResult({
4882
+ status: 'blocked',
4883
+ summary: `WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`,
4884
+ data: {
4885
+ workId,
4886
+ terminalizationApplied: false,
4887
+ currentClaimGeneration: fenced.owner?.claimGeneration,
4888
+ },
4889
+ });
4890
+ return result(blocked as unknown as Record<string, unknown>, true);
4891
+ }
4892
+ const facade = fenced.value;
3953
4893
  if (facade.status !== 'ok') {
3954
4894
  return result(facade as unknown as Record<string, unknown>, true);
3955
4895
  }
@@ -3958,20 +4898,25 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3958
4898
  if (!physical) return result(facade as unknown as Record<string, unknown>);
3959
4899
  const cleanup = contextRecord(physical.structuredContent);
3960
4900
  const cleanupCompleted = cleanup.cleanupCompleted === true || contextRecord(cleanup.work).state === 'cleaned';
4901
+ const cleanupRetained = cleanup.cleanupRetained === true;
4902
+ const cleanupSettled = cleanupCompleted || cleanupRetained;
3961
4903
  const response = {
3962
4904
  ...facade,
3963
- status: cleanupCompleted ? 'ok' : 'blocked',
4905
+ status: cleanupSettled ? 'ok' : 'blocked',
3964
4906
  summary: cleanupCompleted
3965
4907
  ? `${facade.summary} Managed worktree and branch cleanup completed automatically.`
3966
- : `${facade.summary} Automatic managed-resource cleanup is incomplete and remains visible for retry.`,
4908
+ : cleanupRetained
4909
+ ? `${facade.summary} Managed worktree and branch retention was recorded durably; automatic cleanup is disabled for this terminal Work.`
4910
+ : `${facade.summary} Automatic managed-resource cleanup is incomplete and remains visible for retry.`,
3967
4911
  data: {
3968
4912
  ...(facade.data && typeof facade.data === 'object' ? facade.data : {}),
3969
4913
  worktreeDeleted: cleanupCompleted,
3970
- cleanupPending: !cleanupCompleted,
4914
+ cleanupPending: !cleanupSettled,
4915
+ cleanupRetained,
3971
4916
  lifecycleCleanup: cleanup,
3972
4917
  },
3973
4918
  };
3974
- return result(response as unknown as Record<string, unknown>, !cleanupCompleted || physical.isError === true);
4919
+ return result(response as unknown as Record<string, unknown>, !cleanupSettled || physical.isError === true);
3975
4920
  } catch (error) {
3976
4921
  const response = {
3977
4922
  ...facade,
@@ -3989,6 +4934,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3989
4934
 
3990
4935
  if (operation === 'finalize') {
3991
4936
  const workId = String(args.work_id ?? '').trim();
4937
+ if (workId) reconcileTerminalFacadeWorkVerifications(ctx, repository, workId);
3992
4938
  let before = workId ? getWorkContract(store, workId) : undefined;
3993
4939
  if (before && !before.completionReceipt && args.reconcile_historical_delivery === true) {
3994
4940
  try {
@@ -3998,7 +4944,13 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3998
4944
  throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
3999
4945
  }
4000
4946
  const historicalHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
4001
- if (historicalHandle?.managedWorktree) throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_MANAGED_CLEANUP_REQUIRED: ${workId}`);
4947
+ if (historicalHandle?.managedWorktree) {
4948
+ const managedCleanupComplete = historicalHandle.finalization.merge === 'done'
4949
+ && historicalHandle.finalization.branchCleanup === 'done'
4950
+ && historicalHandle.finalization.worktreeCleanup === 'done'
4951
+ && !existsSync(historicalHandle.worktreePath);
4952
+ if (!managedCleanupComplete) throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_MANAGED_CLEANUP_REQUIRED: ${workId}`);
4953
+ }
4002
4954
  const reconciliation = acceptReviewedDirectEditWorkReconciliation({
4003
4955
  controllerHome: ctx.controllerHome,
4004
4956
  repoId: repository.repoId,
@@ -4013,7 +4965,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4013
4965
  cleanupOwnershipProof: String(args.reconcile_cleanup_proof ?? ''),
4014
4966
  });
4015
4967
  if (historicalHandle && historicalHandle.state !== 'cleaned') {
4016
- const finalization: WorkHandleState['finalization'] = { validation: 'done', commit: 'done', merge: 'skipped', branchCleanup: 'skipped', worktreeCleanup: 'skipped' };
4968
+ const finalization: WorkHandleState['finalization'] = historicalHandle.managedWorktree
4969
+ ? historicalHandle.finalization
4970
+ : { validation: 'done', commit: 'done', merge: 'skipped', branchCleanup: 'skipped', worktreeCleanup: 'skipped' };
4017
4971
  const delivered = historicalHandle.state === 'committed' || historicalHandle.state === 'merged' || historicalHandle.state === 'failed_terminal_cleanup'
4018
4972
  ? historicalHandle
4019
4973
  : transitionWorkHandle(ctx.controllerHome, historicalHandle, 'committed', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
@@ -4026,7 +4980,33 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4026
4980
  return result(blocked as unknown as Record<string, unknown>, true);
4027
4981
  }
4028
4982
  }
4029
- if (before && !before.completionReceipt) {
4983
+ // Legacy callers could classify repository mutation + Git remote delivery
4984
+ // as remote_effect. A physical WorkHandle proves this is repository delivery,
4985
+ // so keep it on work_finalize instead of demanding a plugin action receipt.
4986
+ // Pure plugin remote effects have no repository WorkHandle and retain the
4987
+ // action-receipt finalization contract.
4988
+ const repositoryDeliveryHandle = workId
4989
+ ? readWorkHandle(ctx.controllerHome, repository.repoId, workId)
4990
+ : undefined;
4991
+ if (before?.workKind === 'remote_effect' && !before.completionReceipt && !repositoryDeliveryHandle) {
4992
+ try {
4993
+ before = finalizeRemoteEffectWorkFromActionReceipt(ctx.controllerHome, repository.repoId, workId);
4994
+ } catch (error) {
4995
+ const blocked = buildFacadeResult({
4996
+ status: 'blocked',
4997
+ summary: error instanceof Error ? error.message : 'Remote-effect semantic finalization failed.',
4998
+ data: { workId, lifecycleClosed: false },
4999
+ });
5000
+ return result(blocked as unknown as Record<string, unknown>, true);
5001
+ }
5002
+ }
5003
+ const completedCleanupPending = Boolean(
5004
+ before?.completionReceipt
5005
+ && args.cleanup !== false
5006
+ && before.worktreeRef?.trim()
5007
+ && existsSync(before.worktreeRef),
5008
+ );
5009
+ if (before && before.workKind !== 'local_effect' && (!before.completionReceipt || completedCleanupPending)) {
4030
5010
  try {
4031
5011
  const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'finalize');
4032
5012
  if (physical?.isError === true) return physical;
@@ -4041,8 +5021,66 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4041
5021
  return result(blocked as unknown as Record<string, unknown>, true);
4042
5022
  }
4043
5023
  }
4044
- const facade = runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'finalize', args);
5024
+ const semanticWork = getWorkContract(store, workId);
5025
+ let facade;
5026
+ if (semanticWork && !['completed', 'failed', 'cancelled'].includes(semanticWork.status)) {
5027
+ try {
5028
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
5029
+ let owner = getControllerSession(store, workId);
5030
+ if (!owner) {
5031
+ owner = claimControllerSession(store, {
5032
+ workId,
5033
+ controllerId: identity.controllerId,
5034
+ controllerType: identity.controllerType,
5035
+ sessionId: identity.sessionId,
5036
+ principalId: identity.principalId,
5037
+ controllerInstanceId: identity.controllerInstanceId,
5038
+ expectedClaimGeneration: 0,
5039
+ leaseMs: 3_600_000,
5040
+ });
5041
+ }
5042
+ const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
5043
+ const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
5044
+ if (owner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
5045
+ if (ownerPrincipal !== identity.principalId) throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
5046
+ if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
5047
+ if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
5048
+ const fenced = withControllerSessionTerminalizationFence(
5049
+ store,
5050
+ {
5051
+ workId,
5052
+ actor: `rh-work-finalize:${identity.controllerId}:${identity.controllerInstanceId}`,
5053
+ authority: {
5054
+ controllerId: owner.controllerId,
5055
+ controllerType: owner.controllerType,
5056
+ principalId: ownerPrincipal,
5057
+ controllerInstanceId: ownerInstanceId,
5058
+ claimGeneration: owner.claimGeneration,
5059
+ },
5060
+ },
5061
+ () => runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'finalize', args),
5062
+ );
5063
+ if (!fenced.allowed) {
5064
+ throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`);
5065
+ }
5066
+ facade = fenced.value;
5067
+ } catch (error) {
5068
+ const blocked = buildFacadeResult({
5069
+ status: 'blocked',
5070
+ summary: error instanceof Error ? error.message : `Work ${workId} semantic finalization authority check failed.`,
5071
+ data: { workId, terminalizationApplied: false, lifecycleClosed: false },
5072
+ });
5073
+ return result(blocked as unknown as Record<string, unknown>, true);
5074
+ }
5075
+ } else {
5076
+ facade = runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'finalize', args);
5077
+ }
4045
5078
  const completed = getWorkContract(store, workId);
5079
+ // Finalizing a Work proves the Work lifecycle only. A Plan step may aggregate
5080
+ // acceptance criteria that are broader than this Work (for example, a canary
5081
+ // plus a later stabilization soak), so finalize must never synthesize semantic
5082
+ // Plan acceptance. Only the explicit plan_accept_step operation may promote a
5083
+ // validating step to completed after the Controller reviews all criteria.
4046
5084
  const lifecycleClosed = Boolean(completed?.completionReceipt)
4047
5085
  && (!readWorkHandle(ctx.controllerHome, repository.repoId, workId)
4048
5086
  || readWorkHandle(ctx.controllerHome, repository.repoId, workId)?.finalization.worktreeCleanup !== 'pending');
@@ -4057,13 +5095,23 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4057
5095
  }
4058
5096
 
4059
5097
  let resumedControllerSession: ReturnType<typeof resumeControllerSession> | undefined;
5098
+ let cancelledWorkReauthorized = false;
5099
+ let reconstructedCancelledCheckout = false;
5100
+ let continuationSourceRevision = workloopCtx.sourceRevision;
5101
+ let continuationWorkspaceFingerprint: string | undefined;
5102
+ let continuationWorkspaceChangedPaths: string[] | undefined;
4060
5103
  if (operation === 'continue') {
4061
5104
  try {
4062
5105
  const workId = String(args.work_id ?? '').trim();
4063
- const work = getWorkContract(store, workId);
5106
+ let work = getWorkContract(store, workId);
5107
+ if (work?.status === 'cancelled') {
5108
+ const resumed = reauthorizeCancelledFacadeWork(ctx, repository, workId, args);
5109
+ cancelledWorkReauthorized = true;
5110
+ reconstructedCancelledCheckout = resumed.reconstructedCheckout;
5111
+ work = getWorkContract(store, workId);
5112
+ }
4064
5113
  if (work && !['cancelled', 'completed', 'failed'].includes(work.status)) {
4065
5114
  const identity = authenticatedFacadeControllerIdentity(ctx, args);
4066
- const currentOwner = getControllerSession(store, workId);
4067
5115
  resumedControllerSession = resumeControllerSession(store, {
4068
5116
  workId,
4069
5117
  controllerId: identity.controllerId,
@@ -4073,6 +5121,23 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4073
5121
  controllerInstanceId: identity.controllerInstanceId,
4074
5122
  leaseMs: 3_600_000,
4075
5123
  });
5124
+ const existingHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
5125
+ if (existingHandle && (existingHandle.principalId !== identity.principalId || existingHandle.sessionId !== identity.sessionId)) {
5126
+ writeWorkHandle(ctx.controllerHome, {
5127
+ ...existingHandle,
5128
+ principalId: identity.principalId,
5129
+ sessionId: identity.sessionId,
5130
+ });
5131
+ }
5132
+ if (work.worktreePolicy.required === true && !work.worktreeRef) {
5133
+ materializeFacadeWorkPlacement(ctx, repository, workId, args);
5134
+ }
5135
+ }
5136
+ if (workId) {
5137
+ const reconciled = reconcileTerminalFacadeWorkVerifications(ctx, repository, workId);
5138
+ continuationSourceRevision = reconciled.sourceRevision ?? continuationSourceRevision;
5139
+ continuationWorkspaceFingerprint = reconciled.workspaceFingerprint ?? continuationWorkspaceFingerprint;
5140
+ continuationWorkspaceChangedPaths = reconciled.workspaceChangedPaths ?? continuationWorkspaceChangedPaths;
4076
5141
  }
4077
5142
  } catch (error) {
4078
5143
  const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller resume failed.', data: { operation, executionStarted: false, ownershipResumed: false } });
@@ -4080,11 +5145,30 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4080
5145
  }
4081
5146
  }
4082
5147
 
4083
- const facade = runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, operation as 'start' | 'continue', args);
5148
+ const semanticAdmissionRequired = operation === 'start' && Boolean(
5149
+ (typeof args.plan_id === 'string' && args.plan_id.trim())
5150
+ || (typeof args.plan_step_id === 'string' && args.plan_step_id.trim())
5151
+ || (typeof args.requirement_id === 'string' && args.requirement_id.trim())
5152
+ || (typeof args.related_work_id === 'string' && args.related_work_id.trim())
5153
+ || (typeof args.work_relation === 'string' && args.work_relation.trim()),
5154
+ );
5155
+ const startContext = {
5156
+ ...workloopCtx,
5157
+ sourceRevision: continuationSourceRevision ?? undefined,
5158
+ workspaceFingerprint: continuationWorkspaceFingerprint,
5159
+ workspaceChangedPaths: continuationWorkspaceChangedPaths,
5160
+ semanticAdmissionLocked: semanticAdmissionRequired,
5161
+ };
5162
+ const facade = semanticAdmissionRequired
5163
+ ? await withPrimaryWorkAdmissionLockAsync(store, () => runGoalWorkloop(startContext, 'start', args))
5164
+ : runGoalWorkloop(startContext, operation as 'start' | 'continue', args);
4084
5165
  const facadeData = facade.data && typeof facade.data === 'object' ? facade.data as Record<string, unknown> : {};
4085
5166
  const facadeWorkId = contextText(contextRecord(facadeData.work).workId, 200);
4086
5167
  if (facade.status === 'ok' && facadeWorkId) {
4087
5168
  try {
5169
+ if (facadeData.workContractCreated === true) {
5170
+ materializeFacadeWorkPlacement(ctx, repository, facadeWorkId, args);
5171
+ }
4088
5172
  const handle = ensureFacadeWorkHandle(ctx, repository, facadeWorkId, args);
4089
5173
  if (handle) facadeData.executionHandle = { workId: handle.workId, checkoutId: handle.checkoutId, managedWorktree: handle.managedWorktree, state: handle.state };
4090
5174
  if (facadeData.workContractCreated === true) {
@@ -4093,112 +5177,33 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4093
5177
  facadeData.ownershipClaimed = true;
4094
5178
  }
4095
5179
  } catch (error) {
4096
- const blocked = buildFacadeResult({ status: 'blocked', summary: `WORK_HANDLE_MATERIALIZATION_FAILED: ${error instanceof Error ? error.message : String(error)}`, data: { ...facadeData, workId: facadeWorkId, executionStarted: false } });
5180
+ 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 } });
4097
5181
  return result(blocked as unknown as Record<string, unknown>, true);
4098
5182
  }
4099
5183
  }
4100
5184
  const response = resumedControllerSession
4101
5185
  ? {
4102
5186
  ...facade,
4103
- summary: `Controller ownership resumed for ${resumedControllerSession.workId}. ${facade.summary}`,
5187
+ ...(cancelledWorkReauthorized
5188
+ ? {
5189
+ status: 'ok' as const,
5190
+ summary: `Explicit current-user reauthorization resumed ${resumedControllerSession.workId}; implementation may continue on the exact Work identity.`,
5191
+ }
5192
+ : { summary: `Controller ownership resumed for ${resumedControllerSession.workId}. ${facade.summary}` }),
4104
5193
  data: {
4105
5194
  ...facadeData,
4106
5195
  ownershipResumed: true,
4107
5196
  controllerSession: resumedControllerSession,
5197
+ ...(cancelledWorkReauthorized ? {
5198
+ cancelledWorkReauthorized: true,
5199
+ reconstructedCancelledCheckout,
5200
+ nextStep: 'execute',
5201
+ } : {}),
4108
5202
  },
4109
5203
  }
4110
5204
  : facade;
4111
5205
  return result(response as unknown as Record<string, unknown>, response.status === 'blocked' || response.status === 'failed' || response.status === 'not_found');
4112
5206
  }
4113
- case 'work_submit': {
4114
- const operationArgs = args.arguments && typeof args.arguments === 'object' && !Array.isArray(args.arguments)
4115
- ? { ...(args.arguments as Record<string, unknown>) }
4116
- : {};
4117
- const requestedCheckoutId = typeof operationArgs.checkout_id === 'string' && operationArgs.checkout_id.trim()
4118
- ? operationArgs.checkout_id.trim()
4119
- : undefined;
4120
- const repository = selected(ctx, requestedCheckoutId ? { ...args, checkout_id: requestedCheckoutId } : args);
4121
- const requestId = String(args.request_id ?? '').trim();
4122
- const operation = String(args.operation ?? '').trim();
4123
- if (!requestId) throw new Error('INVALID_ARGUMENT: work_submit is missing required argument(s): request_id');
4124
- if (!operation) throw new Error('INVALID_ARGUMENT: work_submit is missing required argument(s): operation');
4125
- if (operation.startsWith('work_')) {
4126
- throw new Error('WORK_OPERATION_INVALID: choose an existing durable controller operation');
4127
- }
4128
- if (RETIRED_AGENT_OPERATIONS.has(operation)) {
4129
- throw new Error('AGENT_RUN_RETIRED: Kernel-managed Agent Runs are retired. Accept a WorkContract and launch an external SuperController instead.');
4130
- }
4131
- const definition = getMcpToolDefinition(ctx, operation);
4132
- if (!definition) {
4133
- throw new Error(`WORK_OPERATION_INVALID: ${operation} is unknown or not eligible for durable execution`);
4134
- }
4135
-
4136
- const isRepositoryTool = operation.startsWith('repository_');
4137
- const workerArgs = schemaAwareWorkSubmitArguments(
4138
- operation,
4139
- operationArgs,
4140
- repository,
4141
- definition,
4142
- args.timeout_ms,
4143
- );
4144
- // Validate the target operation before any WorkContract / index write.
4145
- validateMcpToolArguments(operation, injectDurableCommandFields(definition), workerArgs);
4146
- delete workerArgs.request_id;
4147
- delete workerArgs.apply_mode;
4148
- delete workerArgs.wait;
4149
- delete workerArgs.wait_ms;
4150
- delete workerArgs.await_result;
4151
- delete workerArgs.wait_for_result;
4152
-
4153
- const existingRequest = getWorkContractByRequestId(ctx.controllerHome, requestId);
4154
- if (existingRequest && existingRequest.repoId !== repository.repoId) {
4155
- throw new Error(`REQUEST_ID_REPO_CONFLICT: ${requestId} already belongs to repository ${existingRequest.repoId}`);
4156
- }
4157
-
4158
- const argumentHash = hashMcpToolArguments(workerArgs);
4159
- const semanticKey = `${isRepositoryTool ? 'repository-tool' : 'mcp-tool'}:${operation}:${repository.repoId}:${argumentHash}`;
4160
- const claims = claimsForMcpOperation(operation, workerArgs, repository.repoId, repository.activeCheckoutId);
4161
- const operationMetadata = operationMetadataForTool(
4162
- operation,
4163
- definition,
4164
- claims,
4165
- typeof args.timeout_ms === 'number' ? args.timeout_ms : 30_000,
4166
- workerArgs,
4167
- repository.defaultBranch,
4168
- );
4169
-
4170
- const accepted = acceptSubmittedWorkContract(ctx.controllerHome, {
4171
- requestId,
4172
- repoId: repository.repoId,
4173
- parentWorkId: typeof args.parent_work_id === 'string' && args.parent_work_id.trim() ? args.parent_work_id.trim() : undefined,
4174
- semanticKey,
4175
- operation: {
4176
- name: operation,
4177
- semanticKey,
4178
- argumentHash,
4179
- mode: operationMetadata.mode,
4180
- idempotent: operationMetadata.idempotent,
4181
- replayable: operationMetadata.replayable,
4182
- resourceClaims: operationMetadata.resourceClaims.map((claim) => ({
4183
- resourceKey: claim.resourceKey,
4184
- mode: claim.mode,
4185
- ...(claim.quantity !== undefined ? { quantity: claim.quantity } : {}),
4186
- })),
4187
- },
4188
- objective: `Accepted operation ${operation}`,
4189
- mode: 'direct_control',
4190
- });
4191
-
4192
- const work = summarizeSubmittedWorkContract(accepted.contract);
4193
- return result({
4194
- accepted: true,
4195
- deduplicated: accepted.deduplicated,
4196
- operation,
4197
- nextAction: work.nextAction,
4198
- work,
4199
- workContract: accepted.contract,
4200
- });
4201
- }
4202
5207
  case 'work_get': {
4203
5208
  const repository = selected(ctx, args);
4204
5209
  const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
@@ -4318,9 +5323,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4318
5323
  const job = resolveWorkJob(ctx, repository.repoId, args);
4319
5324
  if (!job) {
4320
5325
  const processRef = String(args.work_id ?? args.request_id ?? '').trim();
4321
- const process = getProcessHandle(ctx.controllerHome, repository.repoId, processRef);
5326
+ const process = getRepositoryCommandProcess(ctx.controllerHome, repository.repoId, processRef);
4322
5327
  if (!process) return result({ error: { code: 'WORK_NOT_FOUND', message: 'No Work or managed process matched this repository and identifier.', errorClass: 'not_found', summary: '未找到对应任务。' } }, true);
4323
- const waitedProcess = await waitForProcess(ctx.controllerHome, repository.repoId, processRef, { timeoutMs: waitMs });
5328
+ const waitedProcess = await waitRepositoryCommandProcess(ctx.controllerHome, repository.repoId, processRef, { timeoutMs: waitMs });
4324
5329
  const digest = managedProcessOperationDigest(waitedProcess);
4325
5330
  return result({
4326
5331
  work: { kind: 'managed_process', processId: processRef },
@@ -4642,27 +5647,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4642
5647
  nonBlocking: true,
4643
5648
  });
4644
5649
  }
4645
- case 'controller_context_pack': {
4646
- const repository = selected(ctx, args);
4647
- const pack = buildControllerContextPack(repository.canonicalRoot, ctx.policy, {
4648
- description: typeof args.description === 'string' ? args.description : undefined,
4649
- issueId: typeof args.issue_id === 'string' ? args.issue_id : undefined,
4650
- taskId: typeof args.task_id === 'string' ? args.task_id : undefined,
4651
- knownPaths: stringList(args.known_paths),
4652
- includeGlobs: stringList(args.include_globs),
4653
- excludeGlobs: stringList(args.exclude_globs),
4654
- searchTerms: stringList(args.search_terms),
4655
- maxFiles: typeof args.max_files === 'number' ? args.max_files : undefined,
4656
- maxSnippets: typeof args.max_snippets === 'number' ? args.max_snippets : undefined,
4657
- maxCharsPerSnippet: typeof args.max_chars_per_snippet === 'number' ? args.max_chars_per_snippet : undefined,
4658
- structuralContext: args.structural_context === 'auto' || args.structural_context === 'required' ? args.structural_context : 'off',
4659
- });
4660
- return result({
4661
- repoId: repository.repoId,
4662
- repository: repositorySummary(repository),
4663
- contextPack: pack,
4664
- });
4665
- }
4666
5650
  case 'controller_context': {
4667
5651
  const responseStartedAt = performance.now();
4668
5652
  const phaseTimingsMs: Record<string, number> = {};
@@ -4679,6 +5663,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4679
5663
  structuralContext: 'required',
4680
5664
  maxFiles: 8,
4681
5665
  maxSnippets: 20,
5666
+ session: ctx.sessionId?.trim()
5667
+ ? { sessionId: ctx.sessionId.trim(), repoId: repository.repoId, checkoutId: repository.activeCheckoutId }
5668
+ : undefined,
4682
5669
  })
4683
5670
  : undefined;
4684
5671
  markPhase('repositoryRouting', repositoryStartedAt);
@@ -5041,7 +6028,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5041
6028
  ? 'Raw job state is intentionally not returned through MCP. Use the bounded job summary, events, and get_artifact with artifactId (ART-...), not evidenceId (EVD-...).'
5042
6029
  : jobSummary.terminal
5043
6030
  ? String(jobSummary.summary ?? '')
5044
- : 'Job is still active. Poll get_job without waiting, or use work_wait only when blocking is explicitly required.',
6031
+ : 'Historical Job is still active. Continue independent work; read it only if an observation can change the next decision, and use work_wait only when this exact result becomes a dependency. Do not periodically poll.',
5045
6032
  }, jobSummary.phase === 'failed' || jobSummary.phase === 'timed_out');
5046
6033
  }
5047
6034
  case 'repository_change_verify': {
@@ -5289,154 +6276,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5289
6276
  cancelPendingApprovals: args.cancel_pending_approvals === true,
5290
6277
  }) as unknown as Record<string, unknown>);
5291
6278
  }
5292
- case 'goal_create':
5293
- case 'goal_list':
5294
- case 'goal_get':
5295
- case 'goal_start':
5296
- case 'goal_continue':
5297
- case 'goal_stop':
5298
- case 'goal_finalize':
5299
- case 'goal_status':
5300
- case 'goal_tick_once':
5301
- case 'goal_handoff_packet_create':
5302
- case 'goal_handoff_packet_get':
5303
- case 'provider_list':
5304
- case 'provider_health':
5305
- case 'provider_config_status':
5306
- case 'executor_route_preview':
5307
- case 'executor_dispatch':
5308
- case 'repair_plan':
5309
- case 'repair_continue': {
5310
- const repository = selected(ctx, args);
5311
- if (['goal_create', 'goal_start', 'goal_continue', 'goal_tick_once', 'executor_dispatch', 'repair_continue'].includes(name)) {
5312
- return result({
5313
- error: {
5314
- code: 'GOAL_LOOP_DEPRECATED',
5315
- message: 'Goal provider dispatch is retired. Use rh_work.plan_create/plan_approve, rh_work.controller_claim, rh_work.launcher_start, and rh_inbox for continuation.',
5316
- },
5317
- deprecated: true,
5318
- migration: ['rh_work.plan_create', 'rh_work.controller_claim', 'rh_work.launcher_start', 'rh_inbox.create'],
5319
- }, true);
5320
- }
5321
- const goalCtx: GoalLoopContext = {
5322
- goalStore: { controllerHome: ctx.controllerHome, repoId: repository.repoId },
5323
- packetStore: { controllerHome: ctx.controllerHome, repoId: repository.repoId },
5324
- repoId: repository.repoId,
5325
- };
5326
- const goalId = typeof args.goal_id === 'string' ? args.goal_id : '';
5327
- const taskIntent = typeof args.task_intent === 'string' ? args.task_intent as TaskIntent : undefined;
5328
- switch (name) {
5329
- case 'goal_create': {
5330
- const goal = goalCreate(goalCtx, {
5331
- title: String(args.title ?? ''),
5332
- objective: String(args.objective ?? ''),
5333
- mode: args.mode === 'manual' || args.mode === 'supervised' || args.mode === 'autonomous' ? args.mode : 'autonomous',
5334
- issueId: typeof args.issue_id === 'string' ? args.issue_id : undefined,
5335
- taskIds: Array.isArray(args.task_ids) ? args.task_ids.map(String) : undefined,
5336
- acceptanceCriteria: Array.isArray(args.acceptance_criteria) ? args.acceptance_criteria.map(String) : undefined,
5337
- checkIds: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
5338
- allowedExecutors: Array.isArray(args.allowed_executors) ? args.allowed_executors.map(String) : undefined,
5339
- forbiddenExecutors: Array.isArray(args.forbidden_executors) ? args.forbidden_executors.map(String) : undefined,
5340
- retryBudget: typeof args.retry_budget === 'number' ? args.retry_budget : undefined,
5341
- });
5342
- return result({ goal, summary: summarizeGoalPublic(goal) });
5343
- }
5344
- case 'goal_list': {
5345
- const status = typeof args.status === 'string' ? args.status as GoalStatus | 'active' | 'all' : 'active';
5346
- const goals = goalList(goalCtx, status, typeof args.limit === 'number' ? args.limit : 50);
5347
- return result({ goals: goals.map(summarizeGoalPublic), count: goals.length });
5348
- }
5349
- case 'goal_get': {
5350
- const goal = goalGet(goalCtx, goalId);
5351
- if (!goal) return result({ error: { code: 'GOAL_NOT_FOUND', message: `Goal not found: ${goalId}` } }, true);
5352
- return result({ goal, summary: summarizeGoalPublic(goal) });
5353
- }
5354
- case 'goal_start':
5355
- return result({ tick: goalStart(goalCtx, goalId) });
5356
- case 'goal_continue':
5357
- return result({ tick: goalContinue(goalCtx, goalId) });
5358
- case 'goal_stop':
5359
- return result({ goal: summarizeGoalPublic(goalStop(goalCtx, goalId, typeof args.reason === 'string' ? args.reason : undefined)) });
5360
- case 'goal_finalize': {
5361
- const finalized = goalFinalize(goalCtx, goalId, { force: args.force === true });
5362
- return result({ ok: finalized.ok, reason: finalized.reason, goal: summarizeGoalPublic(finalized.goal) }, !finalized.ok);
5363
- }
5364
- case 'goal_status':
5365
- return result(goalStatus(goalCtx, goalId || undefined));
5366
- case 'goal_tick_once': {
5367
- if (goalId) {
5368
- return result({
5369
- tick: goalTickOnce(goalCtx, goalId, {
5370
- taskIntent,
5371
- providerFailure: args.provider_failure === true,
5372
- externalWrite: args.external_write === true,
5373
- approvalConfirmed: args.approval_confirmed === true,
5374
- verificationResult: typeof args.verification_check_id === 'string'
5375
- ? {
5376
- checkId: args.verification_check_id,
5377
- ok: args.verification_ok === true,
5378
- }
5379
- : undefined,
5380
- }),
5381
- });
5382
- }
5383
- return result({ ticks: tickActiveGoals(goalCtx) });
5384
- }
5385
- case 'goal_handoff_packet_create':
5386
- return result({
5387
- packet: goalHandoffPacketCreate(goalCtx, goalId, {
5388
- blockers: Array.isArray(args.blockers) ? args.blockers.map(String) : undefined,
5389
- requiredUserDecision: typeof args.required_user_decision === 'string' ? args.required_user_decision : undefined,
5390
- recommendedProvider: typeof args.recommended_provider === 'string' ? args.recommended_provider : undefined,
5391
- }),
5392
- });
5393
- case 'goal_handoff_packet_get': {
5394
- const packet = goalHandoffPacketGet(goalCtx, String(args.packet_id ?? ''));
5395
- if (!packet) return result({ error: { code: 'PACKET_NOT_FOUND', message: 'Handoff packet not found.' } }, true);
5396
- return result({ packet });
5397
- }
5398
- case 'provider_list':
5399
- return result({ providers: providerListAction(goalCtx), policyOwner: 'forge' });
5400
- case 'provider_health':
5401
- return result({
5402
- health: providerHealthAction(goalCtx, typeof args.provider_id === 'string' ? args.provider_id : undefined),
5403
- redacted: true,
5404
- });
5405
- case 'provider_config_status':
5406
- return result(providerConfigStatusAction(goalCtx));
5407
- case 'executor_route_preview':
5408
- return result({
5409
- route: executorRoutePreview(goalCtx, {
5410
- goalId: goalId || undefined,
5411
- taskIntent,
5412
- risk: typeof args.risk === 'string' ? args.risk as 'readonly' | 'local_repo_write' | 'workspace_write' | 'remote_write' | 'destructive' | 'raw_secret_config' : undefined,
5413
- objective: typeof args.objective === 'string' ? args.objective : undefined,
5414
- }),
5415
- });
5416
- case 'executor_dispatch':
5417
- return result(executorDispatch(goalCtx, {
5418
- goalId,
5419
- providerId: typeof args.provider_id === 'string' ? args.provider_id : undefined,
5420
- taskIntent,
5421
- risk: typeof args.risk === 'string' ? args.risk as 'readonly' | 'local_repo_write' | 'workspace_write' | 'remote_write' | 'destructive' | 'raw_secret_config' : undefined,
5422
- approvalConfirmed: args.approval_confirmed === true,
5423
- externalWrite: args.external_write === true,
5424
- strongConfirmationText: typeof args.strong_confirmation_text === 'string' ? args.strong_confirmation_text : undefined,
5425
- }));
5426
- case 'repair_plan':
5427
- return result(repairPlan(goalCtx, goalId));
5428
- case 'repair_continue':
5429
- return result({
5430
- tick: repairContinue(goalCtx, goalId, {
5431
- forceFailureClass: typeof args.force_failure_class === 'string'
5432
- ? args.force_failure_class as import('../../control-plane/goal-loop').FailureClass
5433
- : undefined,
5434
- }),
5435
- });
5436
- default:
5437
- return result({ error: { code: 'GOAL_LOOP_UNKNOWN', message: name } }, true);
5438
- }
5439
- }
5440
6279
  case 'workspace_auth_status': {
5441
6280
  const repository = selected(ctx, args);
5442
6281
  return result(buildWorkspaceAuthStatus(listAssistantPluginManifests(ctx.controllerHome, repository)));
@@ -5449,66 +6288,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5449
6288
  redirectUri: typeof args.redirect_uri === 'string' ? args.redirect_uri : undefined,
5450
6289
  }));
5451
6290
  }
5452
- case 'assistant_model_readiness': {
5453
- return result(assistantModelReadiness());
5454
- }
5455
- case 'assistant_standing_grants': {
5456
- const repository = selected(ctx, args);
5457
- return result(listAssistantStandingGrants(ctx.controllerHome, repository, {
5458
- status: typeof args.status === 'string' ? args.status as any : undefined,
5459
- limit: typeof args.limit === 'number' ? args.limit : undefined,
5460
- }));
5461
- }
5462
- case 'assistant_standing_grant_create': {
5463
- const repository = selected(ctx, args);
5464
- return result({ grant: createAssistantStandingGrant(ctx.controllerHome, repository, {
5465
- name: typeof args.name === 'string' ? args.name : undefined,
5466
- pluginId: String(args.plugin_id ?? '').trim(),
5467
- actionId: String(args.action_id ?? '').trim(),
5468
- routineIds: Array.isArray(args.routine_ids) ? args.routine_ids.map(String) : undefined,
5469
- senderAllowlist: Array.isArray(args.sender_allowlist) ? args.sender_allowlist.map(String) : undefined,
5470
- subjectContains: Array.isArray(args.subject_contains) ? args.subject_contains.map(String) : undefined,
5471
- minConfidence: typeof args.min_confidence === 'number' ? args.min_confidence : undefined,
5472
- maxPerRun: typeof args.max_per_run === 'number' ? args.max_per_run : undefined,
5473
- expiresInDays: typeof args.expires_in_days === 'number' ? args.expires_in_days : undefined,
5474
- confirmAuthorization: args.confirm_authorization === true,
5475
- origin: { surface: 'mcp', actor: 'assistant_standing_grant_create' },
5476
- }) });
5477
- }
5478
- case 'assistant_standing_grant_revoke': {
5479
- const repository = selected(ctx, args);
5480
- return result({ grant: revokeAssistantStandingGrant(ctx.controllerHome, repository, {
5481
- grantId: String(args.grant_id ?? '').trim(),
5482
- reason: typeof args.reason === 'string' ? args.reason : undefined,
5483
- confirmAuthorization: args.confirm_authorization === true,
5484
- origin: { surface: 'mcp', actor: 'assistant_standing_grant_revoke' },
5485
- }) });
5486
- }
5487
- case 'assistant_action_proposals': {
5488
- const repository = selected(ctx, args);
5489
- const proposalId = typeof args.proposal_id === 'string' ? args.proposal_id.trim() : '';
5490
- return result(proposalId
5491
- ? { proposal: getAssistantActionProposal(ctx.controllerHome, repository, proposalId) }
5492
- : listAssistantActionProposals(ctx.controllerHome, repository, {
5493
- status: typeof args.status === 'string' ? args.status as any : undefined,
5494
- limit: typeof args.limit === 'number' ? args.limit : undefined,
5495
- }));
5496
- }
5497
- case 'assistant_action_proposal_resolve': {
5498
- const repository = selected(ctx, args);
5499
- const proposalId = String(args.proposal_id ?? '').trim();
5500
- if (args.decision === 'reject') {
5501
- return result({ proposal: rejectAssistantActionProposal(ctx.controllerHome, repository, proposalId, typeof args.reason === 'string' ? args.reason : undefined) });
5502
- }
5503
- if (args.confirm_authorization !== true) throw new Error('ASSISTANT_ACTION_APPROVAL_REQUIRED: confirm_authorization=true');
5504
- const requestId = String(args.request_id ?? `assistant-proposal:${proposalId}`).trim();
5505
- return result({ proposal: await approveAssistantActionProposal(ctx.controllerHome, repository, {
5506
- proposalId,
5507
- requestId,
5508
- confirmationText: typeof args.confirmation_text === 'string' ? args.confirmation_text : undefined,
5509
- origin: { surface: 'mcp', actor: 'assistant_action_proposal_resolve' },
5510
- }) });
5511
- }
5512
6291
  case 'external_filesystem_targets_list': {
5513
6292
  const repository = selected(ctx, args);
5514
6293
  return result(listExternalFilesystemTargets(repository.canonicalRoot));
@@ -5711,27 +6490,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5711
6490
  plugin: summarizePlugin(getAssistantPluginManifest(ctx.controllerHome, repository, pluginId)),
5712
6491
  });
5713
6492
  }
5714
- case 'assistant_readiness': {
5715
- const repository = selected(ctx, args);
5716
- const readiness = buildAssistantReadinessReport(ctx.controllerHome, repository);
5717
- return result({ ...readiness });
5718
- }
5719
-
5720
- case 'gmail_triage_rules': {
5721
- const repository = selected(ctx, args);
5722
- return result({ repoId: repository.repoId, checkoutId: repository.activeCheckoutId, path: '.forge/assistant/gmail-triage-rules.json', ...readGmailTriageRules(repository) });
5723
- }
5724
- case 'gmail_triage_rule_upsert': {
5725
- const repository = selected(ctx, args);
5726
- const upserted = upsertGmailTriageRule(repository, args);
5727
- return result({ repoId: repository.repoId, checkoutId: repository.activeCheckoutId, ...upserted });
5728
- }
5729
- case 'gmail_triage_plan': {
5730
- const repository = selected(ctx, args);
5731
- let manifest;
5732
- try { manifest = getAssistantPluginManifest(ctx.controllerHome, repository, 'gmail'); } catch (_error) { manifest = undefined; }
5733
- return result(buildGmailTriagePlan(repository, { manifest, items: args.items, query: args.query }) as unknown as Record<string, unknown>);
5734
- }
5735
6493
  case 'review_artifacts_prepare': {
5736
6494
  const repository = selected(ctx, args);
5737
6495
  return result(ensureReviewArtifactRoots(repository));
@@ -5752,93 +6510,64 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5752
6510
  const repository = selected(ctx, args);
5753
6511
  return result(buildWorkflowWatchdogReport(ctx.controllerHome, repository, { staleMinutes: args.stale_minutes, includeProcesses: args.include_processes }) as unknown as Record<string, unknown>);
5754
6512
  }
5755
- case 'ios_xcode_status': {
5756
- return result({ ...iosXcodeStatus() });
5757
- }
5758
- case 'ios_simulators_list': {
5759
- return result({ ...iosSimulatorsList({
6513
+ case 'ios_xcode_status':
6514
+ return legacyIosPluginAction(ctx, name, args, 'xcode_status', {});
6515
+ case 'ios_simulators_list':
6516
+ return legacyIosPluginAction(ctx, name, args, 'list_simulators', {
5760
6517
  runtime: typeof args.runtime === 'string' ? args.runtime : undefined,
5761
6518
  name: typeof args.name === 'string' ? args.name : undefined,
5762
- }) });
5763
- }
5764
- case 'ios_project_discover': {
5765
- const repository = selected(ctx, args);
5766
- return result({ repoId: repository.repoId, ...iosProjectDiscover(repository) });
5767
- }
5768
- case 'ios_schemes_list': {
5769
- const repository = selected(ctx, args);
5770
- return result({ repoId: repository.repoId, ...iosSchemesList(repository, {
6519
+ });
6520
+ case 'ios_project_discover':
6521
+ return legacyIosPluginAction(ctx, name, args, 'discover_project', {});
6522
+ case 'ios_schemes_list':
6523
+ return legacyIosPluginAction(ctx, name, args, 'list_schemes', {
5771
6524
  workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
5772
6525
  project: typeof args.project === 'string' ? args.project : undefined,
5773
- }) });
5774
- }
5775
- case 'ios_simulator_boot': {
5776
- if (args.confirm_authorization !== true) throw new Error('IOS_AUTHORIZATION_REQUIRED: confirm_authorization must be true');
5777
- return result({ ...iosSimulatorBoot({
6526
+ });
6527
+ case 'ios_simulator_boot':
6528
+ return legacyIosPluginAction(ctx, name, args, 'launch_simulator', {
5778
6529
  udid: String(args.udid ?? '').trim(),
5779
- openSimulator: args.open_simulator !== false,
5780
- timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
5781
- }) });
5782
- }
5783
- case 'ios_app_build': {
5784
- const repository = selected(ctx, args);
5785
- return result({ repoId: repository.repoId, ...iosAppBuild(repository, {
6530
+ open_simulator: args.open_simulator !== false,
6531
+ timeout_ms: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
6532
+ });
6533
+ case 'ios_app_build':
6534
+ return legacyIosPluginAction(ctx, name, args, 'build', {
5786
6535
  scheme: String(args.scheme ?? '').trim(),
5787
6536
  udid: typeof args.udid === 'string' ? args.udid : undefined,
5788
- simulatorName: typeof args.simulator_name === 'string' ? args.simulator_name : undefined,
6537
+ simulator_name: typeof args.simulator_name === 'string' ? args.simulator_name : undefined,
5789
6538
  workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
5790
6539
  project: typeof args.project === 'string' ? args.project : undefined,
5791
6540
  configuration: typeof args.configuration === 'string' ? args.configuration : undefined,
5792
- timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
5793
- }) });
5794
- }
5795
- case 'ios_app_install': {
5796
- if (args.confirm_authorization !== true) throw new Error('IOS_AUTHORIZATION_REQUIRED: confirm_authorization must be true');
5797
- const repository = selected(ctx, args);
5798
- return result({ repoId: repository.repoId, ...iosAppInstall(repository, {
5799
- udid: String(args.udid ?? '').trim(),
5800
- appPath: String(args.app_path ?? '').trim(),
5801
- }) });
5802
- }
5803
- case 'ios_app_launch': {
5804
- if (args.confirm_authorization !== true) throw new Error('IOS_AUTHORIZATION_REQUIRED: confirm_authorization must be true');
5805
- return result({ ...iosAppLaunch({
5806
- udid: String(args.udid ?? '').trim(),
5807
- bundleId: String(args.bundle_id ?? '').trim(),
5808
- arguments: Array.isArray(args.arguments) ? args.arguments.map(String) : undefined,
5809
- }) });
5810
- }
5811
- case 'ios_simulator_screenshot': {
5812
- const repository = selected(ctx, args);
5813
- return result({ repoId: repository.repoId, ...iosSimulatorScreenshot(repository, {
6541
+ timeout_ms: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
6542
+ });
6543
+ case 'ios_simulator_screenshot':
6544
+ return legacyIosPluginAction(ctx, name, args, 'capture_screenshot', {
5814
6545
  udid: String(args.udid ?? '').trim(),
5815
6546
  label: typeof args.label === 'string' ? args.label : undefined,
5816
- }) });
5817
- }
5818
- case 'ios_simulator_log_tail': {
5819
- const repository = selected(ctx, args);
5820
- return result({ repoId: repository.repoId, ...iosSimulatorLogTail(repository, {
5821
- udid: String(args.udid ?? '').trim(),
5822
- process: typeof args.process === 'string' ? args.process : undefined,
5823
- last: typeof args.last === 'string' ? args.last : undefined,
5824
- maxBytes: typeof args.max_bytes === 'number' ? args.max_bytes : undefined,
5825
- }) });
5826
- }
5827
- case 'ios_ui_smoke_test': {
5828
- if (args.confirm_authorization !== true) throw new Error('IOS_AUTHORIZATION_REQUIRED: confirm_authorization must be true');
5829
- const repository = selected(ctx, args);
5830
- return result({ repoId: repository.repoId, ...iosUiSmokeTest(repository, {
5831
- udid: typeof args.udid === 'string' ? args.udid : undefined,
5832
- simulatorName: typeof args.simulator_name === 'string' ? args.simulator_name : undefined,
6547
+ });
6548
+ case 'ios_ui_smoke_test':
6549
+ return legacyIosPluginAction(ctx, name, args, 'smoke_review', {
5833
6550
  scheme: typeof args.scheme === 'string' ? args.scheme : undefined,
5834
- bundleId: typeof args.bundle_id === 'string' ? args.bundle_id : undefined,
6551
+ bundle_id: typeof args.bundle_id === 'string' ? args.bundle_id : undefined,
6552
+ udid: typeof args.udid === 'string' ? args.udid : undefined,
6553
+ simulator_name: typeof args.simulator_name === 'string' ? args.simulator_name : undefined,
5835
6554
  workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
5836
6555
  project: typeof args.project === 'string' ? args.project : undefined,
5837
6556
  configuration: typeof args.configuration === 'string' ? args.configuration : undefined,
5838
- appPath: typeof args.app_path === 'string' ? args.app_path : undefined,
5839
- screenshotLabel: typeof args.screenshot_label === 'string' ? args.screenshot_label : undefined,
5840
- }) });
5841
- }
6557
+ app_path: typeof args.app_path === 'string' ? args.app_path : undefined,
6558
+ screenshot_label: typeof args.screenshot_label === 'string' ? args.screenshot_label : undefined,
6559
+ });
6560
+ case 'ios_app_install':
6561
+ case 'ios_app_launch':
6562
+ case 'ios_simulator_log_tail':
6563
+ return result({
6564
+ accepted: false,
6565
+ mode: 'compatibility_migration',
6566
+ path: 'plugin_action_execute',
6567
+ rejectCode: 'LEGACY_IOS_ATOMIC_RETIRED',
6568
+ message: `${name} no longer owns an independent iOS execution path. Use plugin_action_execute with plugin_id=ios and action_id=smoke_review for staged simulator validation.`,
6569
+ migration: { tool: 'plugin_action_execute', plugin_id: 'ios', action_id: 'smoke_review' },
6570
+ }, true);
5842
6571
  case 'runtime_cleanup_preview': {
5843
6572
  const repository = selected(ctx, args);
5844
6573
  const preview = previewRuntimeCleanup(repository.canonicalRoot, {
@@ -5912,7 +6641,63 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5912
6641
  };
5913
6642
  return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId, direct.result);
5914
6643
  }
6644
+ if (repository.repoId !== '__controller__' && action?.executionMode === 'lightweight_process') {
6645
+ const timeoutMs = Math.max(1_000, request.timeoutMs ?? action?.defaultTimeoutMs ?? 10 * 60_000);
6646
+ let { handle } = await startLightweightPluginAction({
6647
+ controllerHome: ctx.controllerHome,
6648
+ repository,
6649
+ request,
6650
+ interactiveWaitMs: typeof args.interactive_wait_ms === 'number' ? args.interactive_wait_ms : 750,
6651
+ timeoutMs,
6652
+ });
6653
+ if (!handle.completed && args.wait === true) {
6654
+ handle = await waitLightweightPluginAction(
6655
+ ctx.controllerHome,
6656
+ repository.repoId,
6657
+ handle.processId,
6658
+ typeof args.wait_ms === 'number' ? Math.max(1, args.wait_ms) : 15_000,
6659
+ ctx.signal,
6660
+ );
6661
+ }
6662
+ if (!handle.completed) {
6663
+ return result({
6664
+ accepted: true,
6665
+ direct: false,
6666
+ durable: false,
6667
+ mode: 'lightweight_process',
6668
+ plugin: summarizePluginActionReceipt(manifest),
6669
+ action: action ? {
6670
+ actionId: action.actionId,
6671
+ risk: action.risk,
6672
+ confirmation: action.confirmation,
6673
+ requiredConfirmationText: action.requiredConfirmationText,
6674
+ } : { actionId },
6675
+ scope: 'repository',
6676
+ requestId,
6677
+ process: handle,
6678
+ resultRef: { kind: 'process_logs', processId: handle.processId },
6679
+ next: 'The typed plugin action is isolated from the Canonical Runtime. Use process_wait on processId; after completion, call plugin_action_execute again with the same request_id to retrieve the deduplicated structured receipt.',
6680
+ });
6681
+ }
6682
+ if (!handle.ok) {
6683
+ return result({
6684
+ accepted: true,
6685
+ direct: false,
6686
+ durable: false,
6687
+ mode: 'lightweight_process',
6688
+ requestId,
6689
+ process: handle,
6690
+ error: {
6691
+ code: handle.timedOut ? 'PLUGIN_ACTION_TIMEOUT' : handle.cancelled ? 'PLUGIN_ACTION_CANCELLED' : 'PLUGIN_ACTION_FAILED',
6692
+ message: handle.stderrTail || handle.stdoutTail || `Plugin action process exited with code ${String(handle.exitCode)}`,
6693
+ },
6694
+ }, true);
6695
+ }
6696
+ }
6697
+ // The sidecar writes the authoritative receipt. Re-entering the store
6698
+ // with the same request id is a bounded deduplicated read of that result.
5915
6699
  const submitted = await submitAssistantPluginAction(ctx.controllerHome, repository, request);
6700
+ const compactResult = compactSubmittedPluginActionResult(submitted.result);
5916
6701
  const value = {
5917
6702
  accepted: true,
5918
6703
  deduplicated: submitted.deduplicated,
@@ -5929,7 +6714,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5929
6714
  receiptId: submitted.receipt.receiptId,
5930
6715
  requestId: submitted.receipt.requestId,
5931
6716
  ...(submitted.receipt.workId ? { workId: submitted.receipt.workId } : {}),
5932
- result: submitted.result,
6717
+ authorization: submitted.authorization,
6718
+ result: compactResult,
5933
6719
  detail: {
5934
6720
  tool: 'rh_context',
5935
6721
  arguments: {
@@ -5940,7 +6726,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5940
6726
  },
5941
6727
  next: 'Continue with the returned bounded plugin result; use rh_context capability detail only when the typed action schema/policy is needed.',
5942
6728
  };
5943
- return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId, submitted.result);
6729
+ return resultWithPluginArtifactImages(value, ctx.controllerHome, repository.repoId, compactResult);
5944
6730
  }
5945
6731
  case 'toolchain_plugin_summary': {
5946
6732
  const pluginId = String(args.plugin_id ?? '').trim();
@@ -5990,7 +6776,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5990
6776
  next: continuation.nextSafeAction,
5991
6777
  }, contract.status === 'failed' || continuation.reconciliationRequired);
5992
6778
  }
5993
- const process = getProcessHandle(ctx.controllerHome, repository.repoId, workRef);
6779
+ const process = getRepositoryCommandProcess(ctx.controllerHome, repository.repoId, workRef);
5994
6780
  if (!process) return result({ error: { code: 'WORK_NOT_FOUND', message: 'No Work or managed process matched work_ref.', errorClass: 'not_found', summary: '未找到对应任务。' } }, true);
5995
6781
  const digest = managedProcessOperationDigest(process);
5996
6782
  return result({
@@ -5999,7 +6785,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5999
6785
  taskLedgerStatus: taskLedger.status,
6000
6786
  next: process.completed === true
6001
6787
  ? 'Managed process is terminal; inspect the bounded digest above.'
6002
- : `Poll process_get or process_wait with process_id=${workRef}; do not re-run the original operation.`,
6788
+ : `Continue independent work. Use process_get only if an observation can change the next decision; join once with process_wait when this exact result becomes a dependency. Do not re-run the original operation.`,
6003
6789
  }, digest.phase === 'failed' || digest.phase === 'timed_out');
6004
6790
  }
6005
6791
  case 'model_clients_summary': {
@@ -6044,70 +6830,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
6044
6830
  model: typeof args.model === 'string' ? args.model : undefined,
6045
6831
  }) });
6046
6832
  }
6047
- case 'create_schedule': {
6048
- const repository = selected(ctx, args);
6049
- const operation = String(args.operation ?? '').trim();
6050
- const operationArgs = args.arguments && typeof args.arguments === 'object' ? args.arguments as Record<string, unknown> : {};
6051
- assertAutomatedOperationAllowed(operation, operationArgs);
6052
- const scheduleRequestId = typeof args.request_id === 'string' && args.request_id.trim()
6053
- ? args.request_id.trim()
6054
- : `schedule:auto:${repository.repoId}:${createHash('sha256').update(JSON.stringify({ name: args.name, operation, arguments: operationArgs, everyMinutes: args.every_minutes })).digest('hex').slice(0, 20)}:${Math.floor(Date.now() / (5 * 60_000))}`;
6055
- const schedule = createSchedule(ctx.controllerHome, {
6056
- requestId: scheduleRequestId,
6057
- repoId: repository.repoId,
6058
- name: String(args.name ?? '').trim(),
6059
- enabled: true,
6060
- trigger: {
6061
- type: ['interval', 'cron', 'calendar', 'condition', 'repository-event', 'dependency-checkpoint', 'manual'].includes(String(args.trigger_type))
6062
- ? String(args.trigger_type) as 'interval' | 'cron' | 'calendar' | 'condition' | 'repository-event' | 'dependency-checkpoint' | 'manual'
6063
- : typeof args.every_minutes === 'number' ? 'interval' : 'manual',
6064
- everyMinutes: typeof args.every_minutes === 'number' ? Math.max(1, args.every_minutes) : undefined,
6065
- cronExpression: typeof args.cron_expression === 'string' ? args.cron_expression : undefined,
6066
- calendarAt: typeof args.calendar_at === 'string' ? args.calendar_at : undefined,
6067
- condition: args.condition && typeof args.condition === 'object' ? args.condition as never : undefined,
6068
- eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
6069
- dependencyJobIds: Array.isArray(args.dependency_job_ids) ? args.dependency_job_ids.map(String) : undefined,
6070
- },
6071
- policy: {
6072
- maxActiveOccurrences: 1,
6073
- maxFailures: typeof args.max_failures === 'number' ? Math.max(1, args.max_failures) : 3,
6074
- cooldownMinutes: typeof args.cooldown_minutes === 'number' ? Math.max(0, args.cooldown_minutes) : 120,
6075
- dailyBudgetMinutes: typeof args.daily_budget_minutes === 'number' ? Math.max(1, args.daily_budget_minutes) : 180,
6076
- shadowMode: args.shadow_mode !== false,
6077
- backoffBaseMinutes: typeof args.backoff_base_minutes === 'number' ? Math.max(1, args.backoff_base_minutes) : 5,
6078
- backoffMaxMinutes: typeof args.backoff_max_minutes === 'number' ? Math.max(1, args.backoff_max_minutes) : 24 * 60,
6079
- },
6080
- action: { operation, arguments: operationArgs, resourceClaims: claimsForMcpOperation(operation, operationArgs, repository.repoId, repository.activeCheckoutId) },
6081
- stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : ['release_ready', 'external_blocker', 'human_review_required'],
6082
- });
6083
- return result({ schedule });
6084
- }
6085
- case 'list_schedules': {
6086
- const repository = selected(ctx, args);
6087
- const schedules = listSchedules(ctx.controllerHome, repository.repoId);
6088
- if (args.include_occurrences !== true) return result({ schedules });
6089
- const occurrences = listOccurrences(ctx.controllerHome, repository.repoId, undefined, 100);
6090
- const decisions = occurrences.flatMap((occurrence) => occurrence.decisionId
6091
- ? [getScheduleDecision(ctx.controllerHome, repository.repoId, occurrence.decisionId)].filter(Boolean)
6092
- : []);
6093
- return result({ schedules, occurrences, decisions });
6094
- }
6095
- case 'pause_schedule': {
6096
- const repository = selected(ctx, args);
6097
- const schedule = getSchedule(ctx.controllerHome, repository.repoId, String(args.schedule_id ?? ''));
6098
- return result({ schedule: saveSchedule(ctx.controllerHome, { ...schedule, enabled: false, pausedReason: typeof args.reason === 'string' ? args.reason : 'Paused by user.' }) });
6099
- }
6100
- case 'trigger_schedule': {
6101
- const repository = selected(ctx, args);
6102
- const schedule = getSchedule(ctx.controllerHome, repository.repoId, String(args.schedule_id ?? ''));
6103
- const occurrence = await evaluateSchedule(ctx.controllerHome, schedule, true, {
6104
- source: typeof args.event_name === 'string' ? 'repository-event' : 'manual',
6105
- eventName: typeof args.event_name === 'string' ? args.event_name : undefined,
6106
- eventId: typeof args.event_id === 'string' ? args.event_id : undefined,
6107
- data: args.event_data && typeof args.event_data === 'object' ? args.event_data as Record<string, unknown> : undefined,
6108
- });
6109
- return result({ occurrence });
6110
- }
6111
6833
  case 'request_release_gate': {
6112
6834
  const repository = selected(ctx, args);
6113
6835
  const requestId = typeof args.request_id === 'string' && args.request_id.trim()
@@ -6123,93 +6845,6 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
6123
6845
  suggestedOperation: 'rh_work.controller_claim followed by Process Runtime checks and explicit release authorization.',
6124
6846
  });
6125
6847
  }
6126
- case 'create_portfolio_workflow': {
6127
- const rawSteps = Array.isArray(args.steps) ? args.steps : [];
6128
- const workflow = createPortfolioWorkflow(ctx.controllerHome, {
6129
- name: String(args.name ?? '').trim(),
6130
- requestId: String(args.request_id ?? '').trim(),
6131
- failurePolicy: args.failure_policy === 'compensate' ? 'compensate' : 'stop',
6132
- steps: rawSteps.map((raw, index) => {
6133
- if (!raw || typeof raw !== 'object') throw new Error(`PORTFOLIO_STEP_INVALID: step ${index + 1}`);
6134
- const step = raw as Record<string, unknown>;
6135
- const stepRepoId = String(step.repo_id ?? '').trim();
6136
- const operation = String(step.operation ?? '').trim();
6137
- const operationArgs = step.arguments && typeof step.arguments === 'object' ? step.arguments as Record<string, unknown> : {};
6138
- if (!stepRepoId || !operation) throw new Error(`PORTFOLIO_STEP_INVALID: step ${index + 1} requires repo_id and operation`);
6139
- assertAutomatedOperationAllowed(operation, operationArgs);
6140
- const checkoutId = resolveRepositorySelection({ repoId: stepRepoId, controllerHome: ctx.controllerHome, allowSoleRepository: false }).activeCheckoutId;
6141
- const compensation = step.compensation && typeof step.compensation === 'object'
6142
- ? step.compensation as Record<string, unknown>
6143
- : undefined;
6144
- if (compensation) {
6145
- const compensationOperation = String(compensation.operation ?? '').trim();
6146
- const compensationArguments = compensation.arguments && typeof compensation.arguments === 'object'
6147
- ? compensation.arguments as Record<string, unknown>
6148
- : {};
6149
- assertAutomatedOperationAllowed(compensationOperation, compensationArguments);
6150
- }
6151
- return {
6152
- stepId: String(step.step_id ?? `step-${index + 1}`),
6153
- repoId: stepRepoId,
6154
- operation,
6155
- arguments: operationArgs,
6156
- dependsOn: Array.isArray(step.depends_on) ? step.depends_on.map(String) : [],
6157
- priority: ['P0', 'P1', 'P2', 'P3', 'P4'].includes(String(step.priority)) ? String(step.priority) as 'P0' | 'P1' | 'P2' | 'P3' | 'P4' : 'P2',
6158
- resourceClaims: claimsForMcpOperation(operation, operationArgs, stepRepoId, checkoutId),
6159
- compensation: compensation ? { operation: String(compensation.operation ?? ''), arguments: compensation.arguments && typeof compensation.arguments === 'object' ? compensation.arguments as Record<string, unknown> : undefined } : undefined,
6160
- status: 'pending' as const,
6161
- };
6162
- }),
6163
- });
6164
- return result({ workflow });
6165
- }
6166
- case 'list_portfolio_workflows':
6167
- return result({ workflows: listPortfolioWorkflows(ctx.controllerHome, typeof args.limit === 'number' ? args.limit : 100) });
6168
- case 'get_portfolio_workflow':
6169
- return result({ workflow: getPortfolioWorkflow(ctx.controllerHome, String(args.workflow_id ?? '')) });
6170
- case 'record_candidate_finding': {
6171
- const repository = selected(ctx, args);
6172
- const semanticKey = String(args.semantic_key ?? '').trim();
6173
- const requestId = typeof args.request_id === 'string' && args.request_id.trim()
6174
- ? args.request_id.trim()
6175
- : `candidate:${repository.repoId}:${createHash('sha256').update(semanticKey).digest('hex').slice(0, 20)}:${Math.floor(Date.now() / (5 * 60_000))}`;
6176
- const finding = recordCandidateFinding(ctx.controllerHome, {
6177
- repoId: repository.repoId,
6178
- requestId,
6179
- semanticKey,
6180
- title: String(args.title ?? '').trim(),
6181
- summary: typeof args.summary === 'string' ? args.summary : undefined,
6182
- severity: ['low', 'medium', 'high', 'critical'].includes(String(args.severity))
6183
- ? String(args.severity) as 'low' | 'medium' | 'high' | 'critical'
6184
- : 'medium',
6185
- evidence: {
6186
- source: 'mcp',
6187
- reference: typeof args.reference === 'string' ? args.reference : undefined,
6188
- details: args.evidence && typeof args.evidence === 'object' ? args.evidence as Record<string, unknown> : undefined,
6189
- },
6190
- });
6191
- return result({ finding });
6192
- }
6193
- case 'list_candidate_findings': {
6194
- const repository = selected(ctx, args);
6195
- return result({ findings: listCandidateFindings(ctx.controllerHome, repository.repoId, {
6196
- includeTerminal: args.include_terminal === true,
6197
- limit: typeof args.limit === 'number' ? args.limit : 100,
6198
- }) });
6199
- }
6200
- case 'promote_candidate_finding': {
6201
- const repository = selected(ctx, args);
6202
- const finding = getCandidateFinding(ctx.controllerHome, repository.repoId, String(args.finding_id ?? ''));
6203
- return result({
6204
- accepted: false,
6205
- mode: 'external_controller_required',
6206
- repoId: repository.repoId,
6207
- finding,
6208
- rejectCode: 'EXECUTION_JOB_RETIRED',
6209
- message: 'Candidate promotion no longer creates an ExecutionJob. An external Controller must create the Issue after claiming the related Work.',
6210
- suggestedOperation: 'Create or claim WorkContract, then use the explicit issue-creation tool from the external Controller session.',
6211
- });
6212
- }
6213
6848
  default: return undefined;
6214
6849
  }
6215
6850
  } catch (error) {