@moretea-labs/forge 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +125 -101
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +69 -29
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +150 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +815 -1015
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +3 -2
  166. package/src/runtime/root/package-runtime-service.ts +48 -3
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. package/src/runtime/workflow/schedules/store.ts +79 -10
@@ -6,8 +6,10 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
6
6
  import type { CallToolResult as SdkCallToolResult } from '@modelcontextprotocol/sdk/types.js';
7
7
  import { collectRuntimePerformanceDiagnostics, inferLocalControllerProcess } from '../../diagnostics/performance';
8
8
  import { defaultSemanticProviderRegistry, type SemanticNavigationKind, type SemanticNavigationRequest } from '../../context/semantic-navigation';
9
- import type { McpToolDefinition, CallToolResult } from '../../../cli/mcp/tools';
9
+ import type { McpToolDefinition, CallToolResult } from '../../../cli/mcp/tool-contract';
10
10
  import type { MultiRepositoryMcpToolContext } from '../../../cli/mcp/multi-repository';
11
+ import { allControllerToolDefinitions, controllerExposureSnapshot, controllerToolSurfaceStatus } from '../../../cli/mcp/toolset';
12
+ import { legacyIosPluginInvocation } from './legacy-ios-tool-adapter';
11
13
  import { repositoryScopedToolArgs } from '../../../cli/mcp/multi-repository';
12
14
  import { resolveMcpPath } from '../../../cli/mcp/paths';
13
15
  import { freshGitIdentity } from '../../../cli/repository/inspector';
@@ -18,14 +20,19 @@ import { repositoryControllerRoot } from '../../../cli/repositories/controller-h
18
20
  import { cancelExecutionJob, findExecutionJob, getExecutionJob, getExecutionJobByRequestId, listExecutionJobs } from '../../execution/jobs/store';
19
21
  import { waitForExecutionJob } from '../../execution/jobs/wait';
20
22
  import type { ExecutionJob } from '../../execution/jobs/types';
21
- import { DEFAULT_WORK_CHECK_LEASE_WAIT_MS, checkRequiresDurableWorkflow, getProcessHandle, getProcessRecord, isManagedProcessActive, listProcessRecords, listRecoverableProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics, readPersistedCheckResultReceipt, runPersistedCheckViaProcessRuntime, waitForProcess } from '../../execution/process-runtime';
23
+ import { DEFAULT_WORK_CHECK_LEASE_WAIT_MS, getProcessHandle, getProcessRecord, isManagedProcessActive, listProcessRecords, listRecoverableProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics } from '../../execution/process-runtime';
24
+ import { classifyPersistedCheckTerminalEvidence } from '../../execution/process-runtime/check-result';
25
+ export { classifyTerminalCheckEvidence } from '../../execution/process-runtime/check-result';
26
+ import { listWorkBoundRepositoryProcessEvidence, listWorkBoundRepositoryRemoteEffectProcessEvidence } from '../../control-plane/execution/work-process-evidence';
27
+ import { appendWorkEvidence, recordWorkCompletionReceipt } from '../../control-plane/facade/work-contract-store';
22
28
  import { getRepositoryCommandProcess, waitRepositoryCommandProcess } from '../../execution/process-runtime/command-facade';
23
29
  import { buildJobOperationDigest } from '../../control-plane/facade/operation-digest';
24
- import { readWorkHandle, transitionWorkHandle, workDeliveryBaseRevision, writeWorkHandle, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
30
+ import { readWorkHandle, transitionWorkHandle, workDeliveryBaseRevision, type WorkHandleState } from '../../control-plane/execution/work-handle-store';
31
+ import { ensureRepositoryWorkHandle, rebindRepositoryWorkHandleControllerIdentity } from '../../control-plane/execution/work-handle-authority';
25
32
  import { recoverTerminalWorkHandle } from '../../control-plane/execution/work-terminal-cleanup';
26
- import { gitCommitAtRef, gitWorktreeSnapshot } from '../../control-plane/execution/work-lifecycle-audit';
27
- import { executionIdentityForRepository } from '../../control-plane/execution/execution-identity';
28
33
  import { commandFingerprint, verificationInputFingerprint, workspaceValidationFingerprint } from '../../control-plane/execution/verification-evidence';
34
+ import { resolveWorkVerificationContext } from '../../control-plane/execution/work-verification-context';
35
+ import { executeWorkVerification } from '../../control-plane/execution/work-verification-service';
29
36
  import { acceptReviewedDirectEditWorkReconciliation, reconcileFinalizedDirectEditWorksAfterCommit } from '../../control-plane/execution/direct-edit-work-completion';
30
37
  import { readJobEvents } from '../../evidence/event-ledger';
31
38
  import { readExecutionArtifact } from '../../evidence/artifact-store';
@@ -58,12 +65,6 @@ import {
58
65
  triggerWorkContinuationSchedule,
59
66
  type ContinuationControllerType,
60
67
  } from '../../workflow/schedules/work-continuation';
61
- import {
62
- gatewayRouteBehaviorSnapshot,
63
- getMcpToolDefinition,
64
- RETIRED_AGENT_OPERATIONS,
65
- routeDurableMcpCall,
66
- } from './router';
67
68
  import { assertAutomatedOperationAllowed } from '../../control-plane/governance/external-effects';
68
69
  import { ensureRepositoryRuntimeStorage } from '../../../cli/repositories/runtime-storage';
69
70
  import { assessWorkMode, parseExplicitTaskMode } from '../../../cli/controller/work-mode';
@@ -174,8 +175,6 @@ import {
174
175
  listWorkContracts,
175
176
  getWorkContract,
176
177
  getWorkContractByRequestId,
177
- updateWorkContract,
178
- resumeRetainedCancelledWorkContract,
179
178
  buildWorkContinuationSnapshot,
180
179
  acceptPlanStepEvidence,
181
180
  admitPlanContractAsync,
@@ -185,40 +184,48 @@ import {
185
184
  resolvePlanAdmission,
186
185
  withPrimaryWorkAdmissionLockAsync,
187
186
  repairDanglingPlanStepWorkBinding,
187
+ repairDraftPlanContractAsync,
188
188
  completePlanStepForWork,
189
189
  summarizePlanContract,
190
190
  supersedePlanContract,
191
191
  verifyGoalWorkloop,
192
192
  type FacadeTool,
193
+ bindControllerSessionToCurrentRuntime,
193
194
  claimControllerSession,
195
+ controllerSessionPrincipalId,
194
196
  getControllerSession,
195
- releaseControllerSession,
196
197
  releaseControllerSessionWithAuthority,
198
+ releaseObservedControllerSession,
197
199
  resumeControllerSession,
198
200
  withControllerSessionTerminalizationFence,
199
201
  type ControllerTerminalizationAuthority,
200
202
  } from '../../control-plane/facade';
201
203
  import { currentControllerInstanceId, readExecutionSession, startExecutionSession, updateExecutionSession } from '../../control-plane/execution/session-store';
202
204
  import { changedPaths as workChangedPaths } from '../../control-plane/execution/work-task-receipt';
203
- import { createRequirement, readRequirement } from '../../control-plane/persistence/requirement-store';
205
+ import { readRequirement } from '../../control-plane/persistence/requirement-store';
206
+ import { admitRequirement } from '../../control-plane/facade/requirement-authority';
204
207
  import { ensureManagedWorkspace } from '../../execution/managed-workspace';
208
+ import { materializeRepositoryWorkPlacement } from '../../control-plane/facade/repository-work-admission';
209
+ import { reauthorizeRetainedCancelledRepositoryWork } from '../../control-plane/execution/retained-work-resume';
205
210
  import { currentPermissionSnapshotVersion } from '../../control-plane/execution/validation';
206
211
  import { observeRuntimeStatus } from '../../root/status';
207
212
  import { reconcileWorkValidation } from './work-validation-reconciler';
208
213
  import { callExecutionTool } from './execution-tools';
209
214
  import { launchSuperController } from '../../control-plane/launcher/thin-launcher';
210
215
  import { runStandaloneChatgptPrompt, runWorkChatgptContinuation, settleWorkChatgptAutomationTab } from '../../control-plane/launcher/chatgpt-work-continuation';
211
- import { getChatgptWorkConversationBinding } from '../../control-plane/launcher/chatgpt-work-binding-store';
216
+ import { getChatgptWorkConversationBinding, hasChatgptConversationIdentity } from '../../control-plane/launcher/chatgpt-work-binding-store';
212
217
  import {
213
218
  acknowledgeControllerRoundClaim,
214
219
  beginControllerRoundRelayAfterRelease,
215
220
  beginInitialControllerRoundDispatch,
221
+ reconcileControllerRoundAfterAbandonedRelease,
216
222
  buildControllerRoundRelayPrompt,
217
223
  finishControllerRoundRelayDispatch,
218
224
  getControllerRoundRelay,
219
225
  parseControllerDispositionCompatibilityCapability,
220
226
  recordControllerRoundTabSettlement,
221
227
  submitControllerRoundDisposition,
228
+ type ControllerRoundRelayRecord,
222
229
  type ControllerRoundDisposition,
223
230
  } from '../../control-plane/facade/controller-round-relay';
224
231
 
@@ -617,30 +624,6 @@ async function callStandaloneRecoveryTool(
617
624
 
618
625
  export const RH_WORK_VERIFY_LEASE_WAIT_MS = DEFAULT_WORK_CHECK_LEASE_WAIT_MS;
619
626
 
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
-
644
627
  function result(value: Record<string, unknown>, isError = false): CallToolResult {
645
628
  // Compact text channel by default (no pretty-print bloat).
646
629
  return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value, ...(isError ? { isError: true } : {}) };
@@ -849,6 +832,7 @@ export interface RuntimeIdentitySnapshot {
849
832
  runtimeInstanceId?: string;
850
833
  controllerInstanceId?: string;
851
834
  endpoint?: string;
835
+ running?: boolean;
852
836
  ready?: boolean;
853
837
  reasonCodes?: string[];
854
838
  toolset?: string;
@@ -869,6 +853,7 @@ export function runtimeIdentitySnapshot(ctx: MultiRepositoryMcpToolContext): Run
869
853
  runtimeInstanceId: snapshot?.runtimeInstanceId,
870
854
  controllerInstanceId: snapshot?.runtimeInstanceId,
871
855
  endpoint: snapshot?.endpoint,
856
+ running: observation.running,
872
857
  ready: observation.ready,
873
858
  reasonCodes: observation.reasonCodes,
874
859
  toolset: ctx.toolset,
@@ -1118,20 +1103,25 @@ function compactControllerContextSummaryPayload(payload: Record<string, unknown>
1118
1103
  function authenticatedFacadeControllerIdentity(
1119
1104
  ctx: MultiRepositoryMcpToolContext,
1120
1105
  args: Record<string, unknown>,
1106
+ options: { allowTransportSessionRollover?: boolean } = {},
1121
1107
  ): { controllerId: string; principalId: string; sessionId: string; controllerInstanceId: string; controllerType: 'chatgpt' | 'codex' | 'claude' | 'grok' | 'human' } {
1122
1108
  const principalId = ctx.principalId?.trim();
1123
- const sessionId = ctx.sessionId?.trim();
1124
- if (!principalId || !sessionId) {
1125
- throw new Error('CONTROLLER_AUTHENTICATED_SESSION_REQUIRED: reconnect or call session_start through the authenticated MCP transport');
1126
- }
1109
+ const transportSessionId = ctx.sessionId?.trim();
1127
1110
  const requestedControllerId = typeof args.controller_id === 'string' ? args.controller_id.trim() : '';
1128
1111
  const requestedSessionId = typeof args.session_id === 'string' ? args.session_id.trim() : '';
1112
+ if (!principalId || (!transportSessionId && !requestedSessionId)) {
1113
+ throw new Error('CONTROLLER_AUTHENTICATED_SESSION_REQUIRED: reconnect or provide session_id through the authenticated MCP transport');
1114
+ }
1115
+ // The authenticated principal proves who is calling; session_id is the
1116
+ // Controller-owned opaque lease capability that keeps one conversation/round
1117
+ // stable across replaceable MCP transport sessions. Prefer that explicit
1118
+ // capability whenever supplied. A foreign same-principal conversation still
1119
+ // cannot rebind an existing Work unless it possesses the exact opaque session
1120
+ // (or the stronger relay-round capability checked by the caller).
1121
+ const sessionId = requestedSessionId || transportSessionId!;
1129
1122
  if (requestedControllerId && requestedControllerId !== principalId) {
1130
1123
  throw new Error('CONTROLLER_ID_CONTEXT_MISMATCH: controller_id must match the authenticated principal');
1131
1124
  }
1132
- if (requestedSessionId && requestedSessionId !== sessionId) {
1133
- throw new Error('CONTROLLER_SESSION_CONTEXT_MISMATCH: session_id must match the authenticated MCP session');
1134
- }
1135
1125
  const requestedControllerType = typeof args.controller_type === 'string' && ['chatgpt', 'codex', 'claude', 'grok', 'human'].includes(args.controller_type)
1136
1126
  ? args.controller_type as 'chatgpt' | 'codex' | 'claude' | 'grok' | 'human'
1137
1127
  : undefined;
@@ -1148,245 +1138,174 @@ function authenticatedFacadeControllerIdentity(
1148
1138
  };
1149
1139
  }
1150
1140
 
1151
- function ensureFacadeWorkHandle(
1141
+ export function dispatchedChatgptRelayAuthorizesStaleControllerRecovery(
1142
+ relay: ControllerRoundRelayRecord | undefined,
1143
+ controllerType: 'chatgpt' | 'codex' | 'claude' | 'grok' | 'human',
1144
+ ): boolean {
1145
+ if (controllerType !== 'chatgpt' || relay?.status !== 'dispatched' || !relay.browserSessionId || !relay.conversationUrl) {
1146
+ return false;
1147
+ }
1148
+ try {
1149
+ return hasChatgptConversationIdentity(relay.conversationUrl);
1150
+ } catch {
1151
+ return false;
1152
+ }
1153
+ }
1154
+
1155
+ function assertFacadeControllerRoundAuthority(
1152
1156
  ctx: MultiRepositoryMcpToolContext,
1153
- repository: ReturnType<typeof selected>,
1157
+ store: { controllerHome: string; repoId: string },
1154
1158
  workId: string,
1155
1159
  args: Record<string, unknown>,
1156
- ): WorkHandleState | undefined {
1157
- const existing = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
1158
- if (existing) return existing;
1159
- const contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
1160
- if (!contract || contract.workKind !== 'repository_change' || contract.mode !== 'goal_workloop' || !contract.checkoutId) return undefined;
1161
- const executionRepository = resolveRepositorySelection({ repoId: repository.repoId, checkoutId: contract.checkoutId, controllerHome: ctx.controllerHome, allowSoleRepository: false });
1162
- const checkout = selectRepositoryCheckout(executionRepository, contract.checkoutId, { allowArchived: true });
1163
- const status = repositoryGitStatus(checkout);
1164
- if (!status.branch) throw new Error(`WORKTREE_DETACHED: ${contract.checkoutId} has no branch`);
1165
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
1166
- const at = new Date().toISOString();
1167
- const managedWorktree = Boolean(contract.worktreeRef) && resolve(contract.worktreeRef!) === resolve(checkout.canonicalRoot) && resolve(checkout.canonicalRoot) !== resolve(repository.canonicalRoot);
1168
- return writeWorkHandle(ctx.controllerHome, {
1169
- schemaVersion: 1,
1170
- workId,
1171
- sessionId: identity.sessionId,
1172
- principalId: identity.principalId,
1173
- repositoryId: repository.repoId,
1174
- checkoutId: contract.checkoutId,
1175
- worktreePath: checkout.canonicalRoot,
1176
- branch: status.branch,
1177
- sourceCheckoutId: repository.activeCheckoutId,
1178
- managedWorktree,
1179
- workContractId: contract.workId,
1180
- baseCommit: contract.baseRevision ?? status.head ?? undefined,
1181
- deliveryBaseCommit: contract.baseRevision ?? status.head ?? undefined,
1182
- expectedHead: status.head ?? contract.baseRevision,
1183
- permissionSnapshotVersion: currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId),
1184
- state: 'prepared',
1185
- createdAt: at,
1186
- updatedAt: at,
1187
- finalization: { validation: 'pending', commit: 'pending', merge: 'pending', branchCleanup: 'pending', worktreeCleanup: 'pending' },
1188
- cleanupResponsibility: { owner: 'work_finalizer', registeredAt: at },
1189
- });
1160
+ ): ControllerRoundRelayRecord | undefined {
1161
+ const relay = getControllerRoundRelay(store, workId);
1162
+ if (!relay) return undefined;
1163
+
1164
+ const expectedAuthorityId = relay.authorityId?.trim() || '';
1165
+ if (expectedAuthorityId) {
1166
+ const requestedScopeId = typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : '';
1167
+ const requestedAuthorityId = typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '';
1168
+ if (!requestedScopeId || requestedScopeId !== relay.relayScopeId) {
1169
+ throw new Error(`WORK_CONTROLLER_RELAY_SCOPE_MISMATCH: ${workId}:expected=${relay.relayScopeId}`);
1170
+ }
1171
+ if (!requestedAuthorityId) {
1172
+ throw new Error(`WORK_CONTROLLER_ROUND_AUTHORITY_REQUIRED: ${workId}`);
1173
+ }
1174
+ if (requestedAuthorityId !== expectedAuthorityId) {
1175
+ throw new Error(`WORK_CONTROLLER_ROUND_AUTHORITY_MISMATCH: ${workId}`);
1176
+ }
1177
+ return relay;
1178
+ }
1179
+
1180
+ // Legacy relay records predate the per-round capability. Preserve only the
1181
+ // exact already-claimed durable epoch; never rebind a replacement transport by
1182
+ // shared OAuth principal alone. A fresh launcher/recovery round upgrades the
1183
+ // record and receives an opaque authority capability.
1184
+ const owner = getControllerSession(store, workId);
1185
+ const principalId = ctx.principalId?.trim() || '';
1186
+ const sessionId = ctx.sessionId?.trim() || '';
1187
+ const controllerInstanceId = ctx.controllerInstanceId?.trim() || '';
1188
+ if (
1189
+ owner
1190
+ && owner.sessionId === sessionId
1191
+ && controllerSessionPrincipalId(owner) === principalId
1192
+ && (owner.controllerInstanceId?.trim() || '') === controllerInstanceId
1193
+ ) return relay;
1194
+ throw new Error(`WORK_CONTROLLER_ROUND_AUTHORITY_UPGRADE_REQUIRED: ${workId}`);
1190
1195
  }
1191
1196
 
1192
- function materializeFacadeWorkPlacement(
1197
+ function bindFacadeControllerOwnership(
1193
1198
  ctx: MultiRepositoryMcpToolContext,
1194
- repository: ReturnType<typeof selected>,
1199
+ store: { controllerHome: string; repoId: string },
1195
1200
  workId: string,
1196
- args: Record<string, unknown>,
1201
+ identity: ReturnType<typeof authenticatedFacadeControllerIdentity>,
1202
+ options: { allowClaimIfMissing?: boolean; leaseMs?: number } = {},
1197
1203
  ) {
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 },
1204
+ const runtime = runtimeIdentitySnapshot(ctx);
1205
+ return bindControllerSessionToCurrentRuntime(store, {
1206
+ workId,
1207
+ controllerId: identity.controllerId,
1208
+ controllerType: identity.controllerType,
1209
+ sessionId: identity.sessionId,
1210
+ principalId: identity.principalId,
1211
+ controllerInstanceId: identity.controllerInstanceId,
1212
+ currentRuntimeInstanceId: runtime.running ? runtime.runtimeInstanceId : undefined,
1213
+ allowClaimIfMissing: options.allowClaimIfMissing,
1214
+ leaseMs: options.leaseMs ?? 3_600_000,
1215
1215
  });
1216
1216
  }
1217
1217
 
1218
- function reauthorizeCancelledFacadeWork(
1218
+ function currentFacadeTerminalizationAuthority(
1219
1219
  ctx: MultiRepositoryMcpToolContext,
1220
- repository: ReturnType<typeof selected>,
1220
+ store: { controllerHome: string; repoId: string },
1221
1221
  workId: string,
1222
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`);
1223
+ ): ControllerTerminalizationAuthority {
1224
+ let owner = getControllerSession(store, workId);
1225
+ if (!owner) {
1226
+ throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${workId}; terminalization requires an explicit controller_claim for this exact Work.`);
1229
1227
  }
1230
1228
  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 };
1229
+ const ownerPrincipal = controllerSessionPrincipalId(owner);
1230
+ if (owner.controllerId !== identity.controllerId) {
1231
+ throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId} is owned by ${owner.controllerId}`);
1295
1232
  }
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}`);
1233
+ if (owner.controllerType !== identity.controllerType) {
1234
+ throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${workId} is owned by ${owner.controllerType}`);
1306
1235
  }
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}`);
1236
+ if (ownerPrincipal !== identity.principalId) {
1237
+ throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
1315
1238
  }
1316
- if (handle.checkoutId !== recordedCheckoutId || resolve(handle.worktreePath) !== resolve(recordedWorktree)) {
1317
- throw new Error(`WORK_CANCELLED_RESUME_CHECKOUT_OWNERSHIP_MISMATCH: ${workId}`);
1239
+ const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
1240
+ if (!ownerInstanceId) {
1241
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
1318
1242
  }
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}`);
1243
+ if (ownerInstanceId !== identity.controllerInstanceId || owner.sessionId !== identity.sessionId) {
1244
+ const relay = assertFacadeControllerRoundAuthority(ctx, store, workId, args);
1245
+ if (!relay?.authorityId?.trim()) {
1246
+ if (ownerInstanceId !== identity.controllerInstanceId) {
1247
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
1248
+ }
1249
+ throw new Error(`WORK_CONTROLLER_SCOPE_MISMATCH: ${workId}; terminalization cannot rebind ownership implicitly. Call controller_claim for this exact Work in the current controller scope, then retry.`);
1323
1250
  }
1251
+ // The opaque per-round relay capability is the durable conversation-round
1252
+ // authority. MCP transport sessions are replaceable execution bindings, so
1253
+ // an exact upgraded relay may reconcile the same authenticated owner onto
1254
+ // the current canonical Runtime/session before the claim-generation fence is
1255
+ // captured for terminalization. Foreign or legacy rounds fail above.
1256
+ owner = bindFacadeControllerOwnership(ctx, store, workId, identity);
1324
1257
  }
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
- }
1258
+ const reboundPrincipal = controllerSessionPrincipalId(owner);
1259
+ const reboundInstanceId = owner.controllerInstanceId?.trim() || '';
1260
+ if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
1261
+ throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
1330
1262
  }
1263
+ return {
1264
+ controllerId: owner.controllerId,
1265
+ controllerType: owner.controllerType,
1266
+ principalId: reboundPrincipal,
1267
+ controllerInstanceId: reboundInstanceId,
1268
+ claimGeneration: owner.claimGeneration,
1269
+ };
1270
+ }
1331
1271
 
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,
1272
+ function ensureFacadeWorkHandle(
1273
+ ctx: MultiRepositoryMcpToolContext,
1274
+ repository: ReturnType<typeof selected>,
1275
+ workId: string,
1276
+ args: Record<string, unknown>,
1277
+ ): WorkHandleState | undefined {
1278
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
1279
+ return ensureRepositoryWorkHandle({
1280
+ controllerHome: ctx.controllerHome,
1281
+ repository,
1282
+ workId,
1283
+ identity: { sessionId: identity.sessionId, principalId: identity.principalId },
1343
1284
  });
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
- }
1285
+ }
1355
1286
 
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 };
1287
+ function materializeFacadeWorkPlacement(
1288
+ ctx: MultiRepositoryMcpToolContext,
1289
+ repository: ReturnType<typeof selected>,
1290
+ workId: string,
1291
+ args: Record<string, unknown>,
1292
+ ) {
1293
+ return materializeRepositoryWorkPlacement(
1294
+ { controllerHome: ctx.controllerHome, repoId: repository.repoId },
1295
+ workId,
1296
+ (contract) => {
1297
+ const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
1298
+ requestId: workId,
1299
+ title: contract.objective,
1300
+ baseRef: contract.baseRevision,
1301
+ prepareDependencies: args.needs_dependencies === true,
1302
+ });
1303
+ if (workspace.checkoutId === repository.activeCheckoutId) {
1304
+ throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
1305
+ }
1306
+ return workspace;
1307
+ },
1308
+ );
1390
1309
  }
1391
1310
 
1392
1311
  function claimNewFacadeWork(
@@ -1465,11 +1384,42 @@ async function finalizeFacadeWorkHandle(
1465
1384
  });
1466
1385
  }
1467
1386
 
1468
- const worktree = selectRepositoryCheckout(repository, handle.checkoutId, { allowArchived: true });
1387
+ let worktree: ReturnType<typeof selectRepositoryCheckout>;
1388
+ try {
1389
+ worktree = selectRepositoryCheckout(repository, handle.checkoutId, { allowArchived: true });
1390
+ } catch (error) {
1391
+ const message = error instanceof Error ? error.message : String(error);
1392
+ const exactChangedHead = Boolean(handle.managedWorktree
1393
+ && handle.expectedHead
1394
+ && handle.baseCommit
1395
+ && handle.expectedHead !== handle.baseCommit);
1396
+ if (
1397
+ !exactChangedHead
1398
+ || args.cleanup === false
1399
+ || (!message.startsWith('CHECKOUT_NOT_ACTIVE:') && !message.startsWith('checkout not found for '))
1400
+ ) throw error;
1401
+ // Physical cleanup can succeed before the Work completion receipt is
1402
+ // persisted. Retry through the lower finalizer's exact target-containment
1403
+ // reconciliation instead of requiring a checkout that is already gone.
1404
+ // commit/merge are intentionally disabled here: the missing checkout may
1405
+ // never become permission to replay mutation. If expectedHead is not
1406
+ // already contained by targetBranch, work_finalize fails closed.
1407
+ return callExecutionTool(ctx, 'work_finalize', {
1408
+ ...common,
1409
+ commit: false,
1410
+ merge: false,
1411
+ completion_outcome: 'completed_changed',
1412
+ });
1413
+ }
1469
1414
  const status = repositoryGitStatus(worktree);
1470
1415
  const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
1471
1416
  const committedDelta = Boolean(head && handle.baseCommit && head !== handle.baseCommit);
1472
- const commit = typeof args.commit === 'boolean' ? args.commit : !status.clean;
1417
+ const requestedCommit = typeof args.commit === 'boolean' ? args.commit : !status.clean;
1418
+ // A clean checkout whose HEAD already differs from base is already committed.
1419
+ // Treat commit=true as satisfied by that exact candidate instead of trying to
1420
+ // manufacture an empty commit and recording GIT_NOTHING_STAGED as a delivery
1421
+ // failure. The candidate is still adopted/revalidated and merge-fenced below.
1422
+ const commit = status.clean && committedDelta ? false : requestedCommit;
1473
1423
  const workContract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
1474
1424
  if (
1475
1425
  status.clean
@@ -1585,29 +1535,22 @@ function pluginRepository(
1585
1535
  : selected(ctx, args);
1586
1536
  }
1587
1537
 
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(
1538
+ async function legacyIosPluginAction(
1593
1539
  ctx: MultiRepositoryMcpToolContext,
1594
1540
  legacyTool: string,
1595
1541
  args: Record<string, unknown>,
1596
- actionId: string,
1597
- actionArgs: Record<string, unknown>,
1598
1542
  ): Promise<CallToolResult | undefined> {
1599
1543
  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()}`;
1544
+ const invocation = legacyIosPluginInvocation(legacyTool, args);
1545
+ if (!invocation) return undefined;
1603
1546
  return callRuntimeTool(ctx, 'plugin_action_execute', {
1604
1547
  repo_id: repository.repoId,
1605
1548
  checkout_id: repository.activeCheckoutId,
1606
1549
  plugin_id: 'ios',
1607
- action_id: actionId,
1608
- request_id: requestId,
1609
- arguments: definedArguments(actionArgs),
1610
- ...(args.confirm_authorization === true ? { confirm_authorization: true } : {}),
1550
+ action_id: invocation.actionId,
1551
+ request_id: invocation.requestId,
1552
+ arguments: invocation.arguments,
1553
+ ...(invocation.confirmAuthorization ? { confirm_authorization: true } : {}),
1611
1554
  });
1612
1555
  }
1613
1556
 
@@ -1817,6 +1760,111 @@ export async function controllerReadinessEvidence(
1817
1760
  };
1818
1761
  }
1819
1762
 
1763
+ /**
1764
+ * Runtime Source drift for MCP readiness.
1765
+ *
1766
+ * `currentRuntimeRoot` is only for tests that pin a Controller Runtime Source
1767
+ * fixture. Callers must never pass an execution repository canonicalRoot here.
1768
+ */
1769
+ export function runtimeSourceSnapshotStatus(
1770
+ active: RuntimeSourceIdentity | undefined,
1771
+ currentRuntimeRoot?: string,
1772
+ ) {
1773
+ const drift = evaluateActiveRuntimeSourceDrift(active, {
1774
+ currentRuntimeRoot,
1775
+ });
1776
+ return {
1777
+ current: drift.current,
1778
+ restartRequired: drift.restartRequired,
1779
+ reasons: drift.reasons,
1780
+ code: drift.code,
1781
+ };
1782
+ }
1783
+
1784
+ export function repositoryExecutionReadiness(
1785
+ repoRoot: string,
1786
+ availableChecks: ReturnType<typeof listControllerChecks>,
1787
+ requestedCheckIds: string[] = [],
1788
+ schedulingScope: { repoId?: string; checkoutId?: string } = {},
1789
+ ): Record<string, unknown> {
1790
+ const git = gitSnapshot(repoRoot);
1791
+ const registeredCheckIds = availableChecks.map((check) => check.id);
1792
+ const normalizedChecks = normalizeCheckIds(requestedCheckIds, availableChecks);
1793
+ const hasPackageJson = existsSync(join(repoRoot, 'package.json'));
1794
+ const nodeModulesReady = !hasPackageJson || existsSync(join(repoRoot, 'node_modules'));
1795
+ const lockCandidates = [
1796
+ ['bun', 'bun.lock'],
1797
+ ['bun', 'bun.lockb'],
1798
+ ['pnpm', 'pnpm-lock.yaml'],
1799
+ ['npm', 'package-lock.json'],
1800
+ ['yarn', 'yarn.lock'],
1801
+ ] as const;
1802
+ const detectedLock = lockCandidates.find(([, path]) => existsSync(join(repoRoot, path)));
1803
+ const packageManager = detectedLock?.[0];
1804
+ const bootstrapCommand = hasPackageJson && !nodeModulesReady
1805
+ ? packageManager === 'bun' ? ['bun', 'install', '--frozen-lockfile']
1806
+ : packageManager === 'pnpm' ? ['pnpm', 'install', '--frozen-lockfile']
1807
+ : packageManager === 'npm' ? ['npm', 'ci']
1808
+ : packageManager === 'yarn' ? ['yarn', 'install', '--frozen-lockfile']
1809
+ : undefined
1810
+ : undefined;
1811
+ const hasPythonManifest = existsSync(join(repoRoot, 'pyproject.toml'))
1812
+ || existsSync(join(repoRoot, 'requirements.txt'))
1813
+ || existsSync(join(repoRoot, 'requirements-dev.txt'));
1814
+ const localPythonReady = !hasPythonManifest
1815
+ || existsSync(join(repoRoot, '.venv', 'bin', 'python'))
1816
+ || existsSync(join(repoRoot, '.venv', 'Scripts', 'python.exe'));
1817
+ const checkScheduling = buildCheckExecutionSchedule({
1818
+ checks: availableChecks,
1819
+ requestedCheckIds,
1820
+ repoId: schedulingScope.repoId?.trim() || 'selected-repository',
1821
+ checkoutId: schedulingScope.checkoutId?.trim() || 'active',
1822
+ });
1823
+ const blockers = [
1824
+ ...(!nodeModulesReady ? [{ code: 'NODE_DEPENDENCIES_MISSING', message: 'package.json is present but node_modules is not materialized in this checkout.' }] : []),
1825
+ ...normalizedChecks.invalidCheckIds.map((checkId) => ({ code: 'CHECK_NOT_REGISTERED', message: `Requested check is not registered: ${checkId}`, checkId })),
1826
+ ];
1827
+ return {
1828
+ readyForFocusedExecution: blockers.length === 0,
1829
+ git: { head: git.head, branch: git.branch, dirty: git.dirty },
1830
+ checks: {
1831
+ registeredCount: registeredCheckIds.length,
1832
+ registeredCheckIds: registeredCheckIds.slice(0, 80),
1833
+ requestedCheckIds: requestedCheckIds.slice(0, 40),
1834
+ normalized: normalizedChecks,
1835
+ },
1836
+ checkScheduling: {
1837
+ waveCount: checkScheduling.waves.length,
1838
+ maxParallel: checkScheduling.maxParallel,
1839
+ waveSummaries: checkScheduling.waves.map((wave) => `wave ${wave.wave}: ${wave.checkIds.join(', ')}`),
1840
+ conflictSummaries: checkScheduling.conflicts.map((conflict) => {
1841
+ const resources = [...new Set(conflict.resources.flatMap(({ left, right }) => [left.resourceKey, right.resourceKey]))];
1842
+ return `${conflict.leftCheckId} <> ${conflict.rightCheckId}: ${resources.join(', ')}`;
1843
+ }),
1844
+ invalidCheckIds: checkScheduling.invalidCheckIds,
1845
+ guidance: checkScheduling.guidance,
1846
+ },
1847
+ dependencies: {
1848
+ node: {
1849
+ applicable: hasPackageJson,
1850
+ ready: nodeModulesReady,
1851
+ packageManager: packageManager ?? null,
1852
+ lockfile: detectedLock?.[1] ?? null,
1853
+ ...(bootstrapCommand ? { bootstrapCommand } : {}),
1854
+ },
1855
+ python: {
1856
+ applicable: hasPythonManifest,
1857
+ localVirtualEnvReady: localPythonReady,
1858
+ advisoryOnly: true,
1859
+ },
1860
+ },
1861
+ blockers,
1862
+ guidance: bootstrapCommand
1863
+ ? [`Materialize checkout dependencies before tests/builds: ${bootstrapCommand.join(' ')}`]
1864
+ : [],
1865
+ };
1866
+ }
1867
+
1820
1868
  export async function controllerReadiness(
1821
1869
  ctx: MultiRepositoryMcpToolContext,
1822
1870
  repository = ctx.explicitRepository,
@@ -2308,11 +2356,85 @@ async function runFacadeRepair(
2308
2356
  const planId = typeof args.plan_id === 'string' ? args.plan_id.trim() : '';
2309
2357
  const planStepId = typeof args.plan_step_id === 'string' ? args.plan_step_id.trim() : '';
2310
2358
 
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 } });
2359
+ if (planId && !planStepId) {
2360
+ const plan = getPlanContract(store, planId);
2361
+ if (!plan) {
2362
+ const facade = buildFacadeResult({ status: 'not_found', summary: `PlanContract ${planId} not found.`, data: { operation: repairOperation, dryRun, planId, repaired: false } });
2363
+ return result(facade as unknown as Record<string, unknown>, true);
2364
+ }
2365
+ if (plan.status !== 'draft') {
2366
+ const facade = buildFacadeResult({ status: 'blocked', summary: `PLAN_DRAFT_REPAIR_STATUS_INVALID: ${plan.planId}:${plan.status}`, data: { operation: repairOperation, dryRun, planId, repaired: false } });
2367
+ return result(facade as unknown as Record<string, unknown>, true);
2368
+ }
2369
+ if (repairOperation !== 'repair' || dryRun) {
2370
+ const facade = buildFacadeResult({
2371
+ summary: `PlanContract ${plan.planId} is a draft. Exact in-place repair is available; the Plan identity and Requirement authority are preserved and only a fully valid draft may be persisted.`,
2372
+ data: { operation: repairOperation, dryRun, plan: summarizePlanContract(plan), repaired: false, repairRequired: true },
2373
+ suggestedNextActions: [{ label: 'Repair this exact draft Plan', tool: 'rh_work', operation: 'repair', payload: { plan_id: plan.planId, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
2374
+ });
2375
+ return result(facade as unknown as Record<string, unknown>);
2376
+ }
2377
+ const rawSteps = Array.isArray(args.plan_steps)
2378
+ ? args.plan_steps
2379
+ : plan.steps.map((step) => ({
2380
+ id: step.id, objective: step.objective, dependencies: step.dependencies, authoritative_files: step.authoritativeFiles,
2381
+ allowed_paths: step.allowedPaths, forbidden_paths: step.forbiddenPaths, check_ids: step.checks, acceptance_criteria: step.acceptanceCriteria,
2382
+ }));
2383
+ const steps = rawSteps
2384
+ .filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step))
2385
+ .map((step) => ({
2386
+ id: String(step.id ?? ''),
2387
+ objective: String(step.objective ?? ''),
2388
+ dependencies: Array.isArray(step.dependencies) ? step.dependencies.map(String) : [],
2389
+ authoritativeFiles: Array.isArray(step.authoritative_files) ? step.authoritative_files.map(String) : [],
2390
+ allowedPaths: Array.isArray(step.allowed_paths) ? step.allowed_paths.map(String) : [],
2391
+ forbiddenPaths: Array.isArray(step.forbidden_paths) ? step.forbidden_paths.map(String) : [],
2392
+ checks: Array.isArray(step.check_ids) ? step.check_ids.map(String) : [],
2393
+ acceptanceCriteria: Array.isArray(step.acceptance_criteria) ? step.acceptance_criteria.map(String) : [],
2394
+ }));
2395
+ const availableChecks = listControllerChecks(repository.canonicalRoot);
2396
+ const normalizedPlanChecks = normalizeCheckIds(steps.flatMap((step) => step.checks), availableChecks);
2397
+ if (normalizedPlanChecks.invalidCheckIds.length > 0) {
2398
+ const facade = buildFacadeResult({
2399
+ status: 'failed',
2400
+ summary: `PLAN_CHECKS_INVALID: ${normalizedPlanChecks.invalidCheckIds.join(', ')}. Draft repair was not persisted.`,
2401
+ data: { operation: repairOperation, dryRun: false, planId, repaired: false, normalizedChecks: normalizedPlanChecks, registeredCheckIds: availableChecks.map((check) => check.id).slice(0, 80) },
2402
+ suggestedNextActions: [],
2403
+ });
2404
+ return result(facade as unknown as Record<string, unknown>, true);
2405
+ }
2406
+ try {
2407
+ const repaired = await repairDraftPlanContractAsync(store, planId, {
2408
+ expectedSourceRevision: plan.sourceRevision,
2409
+ scopeKey: typeof args.scope_key === 'string' ? args.scope_key : plan.scopeKey,
2410
+ sourceRevision: typeof args.source_revision === 'string' ? args.source_revision : plan.sourceRevision,
2411
+ goal: typeof args.objective === 'string' ? args.objective : plan.goal,
2412
+ nonGoals: Array.isArray(args.non_goals) ? args.non_goals.map(String) : plan.nonGoals,
2413
+ assumptions: Array.isArray(args.assumptions) ? args.assumptions.map(String) : plan.assumptions,
2414
+ resolvedDecisions: Array.isArray(args.resolved_decisions) ? args.resolved_decisions.map(String) : plan.resolvedDecisions,
2415
+ stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : plan.stopConditions,
2416
+ replanConditions: Array.isArray(args.replan_conditions) ? args.replan_conditions.map(String) : plan.replanConditions,
2417
+ integrationStrategy: typeof args.integration_strategy === 'string' ? args.integration_strategy : plan.integrationStrategy,
2418
+ steps,
2419
+ });
2420
+ const facade = buildFacadeResult({
2421
+ summary: `PlanContract ${repaired.planId} draft repaired in place; identity and Requirement authority were preserved.`,
2422
+ data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(repaired), repaired: true, replacementPlanCreated: false },
2423
+ suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: repaired.planId }, risk: 'workspace_write', confidence: 'medium' }],
2424
+ });
2425
+ return result(facade as unknown as Record<string, unknown>);
2426
+ } catch (error) {
2427
+ const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_DRAFT_REPAIR_FAILED', data: { operation: repairOperation, dryRun: false, planId, repaired: false } });
2314
2428
  return result(facade as unknown as Record<string, unknown>, true);
2315
2429
  }
2430
+ }
2431
+
2432
+ if (planStepId && !planId) {
2433
+ 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 } });
2434
+ return result(facade as unknown as Record<string, unknown>, true);
2435
+ }
2436
+
2437
+ if (planId && planStepId) {
2316
2438
  const plan = getPlanContract(store, planId);
2317
2439
  const step = plan?.steps.find((candidate) => candidate.id === planStepId);
2318
2440
  if (!plan || !step) {
@@ -2496,26 +2618,85 @@ async function runFacadeRepair(
2496
2618
  return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked' || facade.status === 'approval_required' || facade.status === 'failed');
2497
2619
  }
2498
2620
 
2499
- function reconcileTerminalFacadeWorkVerifications(
2621
+ function repositoryWorkHandleHasSourceDelta(
2622
+ repository: ReturnType<typeof selected>,
2623
+ handle: WorkHandleState,
2624
+ ): boolean {
2625
+ try {
2626
+ const checkout = selectRepositoryCheckout(repository, handle.checkoutId, { allowArchived: true });
2627
+ const status = repositoryGitStatus(checkout);
2628
+ const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
2629
+ return !status.clean || Boolean(head && handle.baseCommit && head !== handle.baseCommit);
2630
+ } catch (_error) {
2631
+ // A checkout may already have been removed by an earlier bounded cleanup
2632
+ // attempt. The WorkHandle's fenced expectedHead remains authoritative for
2633
+ // whether repository delivery existed and must not be downgraded to an
2634
+ // effect-only semantic completion.
2635
+ return Boolean(handle.expectedHead && handle.baseCommit && handle.expectedHead !== handle.baseCommit);
2636
+ }
2637
+ }
2638
+
2639
+ function finalizeRemoteEffectWorkFromRepositoryProcessReceipt(
2500
2640
  ctx: MultiRepositoryMcpToolContext,
2501
2641
  repository: ReturnType<typeof selected>,
2502
2642
  workId: string,
2503
- ): { sourceRevision?: string; workspaceFingerprint?: string; workspaceChangedPaths?: string[]; reconciledProcessIds: string[] } {
2643
+ checkoutId: string,
2644
+ ) {
2504
2645
  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: [] };
2646
+ const work = getWorkContract(store, workId);
2647
+ if (!work) throw new Error(`WORK_REMOTE_EFFECT_PROCESS_BINDING_NOT_FOUND: ${workId}`);
2648
+ if (work.workKind !== 'remote_effect') {
2649
+ throw new Error(`WORK_REMOTE_EFFECT_PROCESS_KIND_MISMATCH: ${workId} is ${work.workKind}, expected remote_effect`);
2650
+ }
2651
+ if (work.status === 'completed' && work.completionReceipt?.source === 'remote_effect') return work;
2652
+ const evidence = listWorkBoundRepositoryRemoteEffectProcessEvidence({
2653
+ controllerHome: ctx.controllerHome,
2654
+ repoId: repository.repoId,
2655
+ checkoutId,
2656
+ workId,
2657
+ })[0];
2658
+ if (!evidence) return undefined;
2659
+ if (!work.evidenceRefs.some((candidate) => candidate.evidenceId === evidence.processId)) {
2660
+ appendWorkEvidence(store, workId, {
2661
+ evidenceId: evidence.processId,
2662
+ title: 'trusted repository remote effect completed',
2663
+ summary: `Trusted Work-attributed git push completed with durable Process ${evidence.processId}.`,
2664
+ detailLevel: 'summary',
2665
+ });
2515
2666
  }
2667
+ return recordWorkCompletionReceipt(
2668
+ store,
2669
+ workId,
2670
+ {
2671
+ schemaVersion: 1,
2672
+ receiptId: evidence.processId,
2673
+ source: 'remote_effect',
2674
+ workId,
2675
+ authority: 'repository_process',
2676
+ actionId: evidence.actionId,
2677
+ requestId: evidence.requestId,
2678
+ semanticKey: evidence.semanticKey,
2679
+ resultDigest: evidence.resultDigest,
2680
+ processId: evidence.processId,
2681
+ recordedAt: evidence.finishedAt ?? new Date().toISOString(),
2682
+ },
2683
+ 'completed_remote',
2684
+ 'remote_effect',
2685
+ );
2686
+ }
2687
+
2688
+ function reconcileTerminalFacadeWorkVerifications(
2689
+ ctx: MultiRepositoryMcpToolContext,
2690
+ repository: ReturnType<typeof selected>,
2691
+ workId: string,
2692
+ ): { sourceRevision?: string; workspaceFingerprint?: string; workspaceChangedPaths?: string[]; reconciledProcessIds: string[]; workBoundProcessEvidenceIds: string[] } {
2693
+ const resolvedVerification = resolveWorkVerificationContext({ controllerHome: ctx.controllerHome, repository, workId });
2694
+ if (!resolvedVerification.ok) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
2695
+ const { store, workContract, repository: verificationRepository, checks: availableChecks } = resolvedVerification.context;
2696
+ if (!workContract || workContract.completionReceipt) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
2516
2697
  const verificationStatus = repositoryGitStatus(verificationRepository);
2517
2698
  const sourceRevision = verificationStatus.head ?? undefined;
2518
- if (!sourceRevision) return { reconciledProcessIds: [] };
2699
+ if (!sourceRevision) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
2519
2700
  const verificationHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
2520
2701
  const deliveryBaseRevision = verificationHandle
2521
2702
  ? workDeliveryBaseRevision(verificationHandle)
@@ -2530,7 +2711,22 @@ function reconcileTerminalFacadeWorkVerifications(
2530
2711
  ...verificationStatus.untracked,
2531
2712
  ])].sort();
2532
2713
  const workspaceFingerprint = workspaceValidationFingerprint(verificationRepository.canonicalRoot, verificationStatus);
2533
- const availableChecks = listControllerChecks(repository.canonicalRoot);
2714
+ // Work-bound repository Process evidence remains semantic/result evidence,
2715
+ // never a typed check receipt. repository_change may use it only when no
2716
+ // checks are declared. local_effect may bind the same exact durable Process
2717
+ // ids for Controller semantic review even when checks are declared; the
2718
+ // normal missing-check gate below still requires typed verification receipts.
2719
+ const workBoundProcessEvidenceIds = (
2720
+ workContract.workKind === 'local_effect'
2721
+ || (workContract.workKind === 'repository_change' && workContract.checks.length === 0)
2722
+ )
2723
+ ? listWorkBoundRepositoryProcessEvidence({
2724
+ controllerHome: ctx.controllerHome,
2725
+ repoId: repository.repoId,
2726
+ checkoutId: verificationRepository.activeCheckoutId,
2727
+ workId,
2728
+ }).map((evidence) => evidence.processId)
2729
+ : [];
2534
2730
  const workloopCtx = {
2535
2731
  workStore: store,
2536
2732
  handoffStore: store,
@@ -2585,29 +2781,11 @@ function reconcileTerminalFacadeWorkVerifications(
2585
2781
  const latestContract = getWorkContract(store, workId);
2586
2782
  if (latestContract?.checkRefs.some((entry) => entry.receipt?.receiptId === receipt.receiptId)) continue;
2587
2783
 
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
2784
  const legacyEvidence = record.origin?.checkResultReceiptPath
2595
2785
  ? undefined
2596
2786
  : 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;
2787
+ const evidenceState = classifyPersistedCheckTerminalEvidence(record, normalizedCheckId, { legacyEvidence });
2788
+ const failureClass = evidenceState.failureClass;
2611
2789
  const infrastructureFailed = receipt.timedOut
2612
2790
  || receipt.cancelled
2613
2791
  || evidenceState.state !== 'matched'
@@ -2619,500 +2797,59 @@ function reconcileTerminalFacadeWorkVerifications(
2619
2797
  sourceRevision,
2620
2798
  workspaceFingerprint,
2621
2799
  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
-
2637
- async function runFacadeVerify(
2638
- ctx: MultiRepositoryMcpToolContext,
2639
- repository: ReturnType<typeof selected>,
2640
- args: Record<string, unknown>,
2641
- ): Promise<CallToolResult> {
2642
- const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
2643
- const checks = listControllerChecks(repository.canonicalRoot);
2644
- const workloopCtx = {
2645
- workStore: store,
2646
- handoffStore: store,
2647
- repoId: repository.repoId,
2648
- availableChecks: checks,
2649
- };
2650
- const workId = typeof args.work_id === 'string' ? args.work_id : '';
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
-
2675
- const classified = classifyVerificationOutcome({
2676
- checkId,
2677
- available: checks,
2678
- });
2679
-
2680
- if (classified.outcome === 'invalid_check_id') {
2681
- if (workId) {
2682
- const facade = verifyGoalWorkloop(workloopCtx, { workId, checkId });
2683
- return result(facade as unknown as Record<string, unknown>);
2684
- }
2685
- return result(buildFacadeResult({
2686
- status: 'ok',
2687
- summary: classified.summary,
2688
- data: {
2689
- verification: {
2690
- checkId,
2691
- outcome: 'invalid_check_id',
2692
- isAcceptanceFailure: false,
2693
- isInfrastructureIssue: true,
2694
- doesNotRequestTaskChanges: true,
2695
- },
2696
- registeredCheckCount: checks.length,
2697
- },
2698
- warnings: classified.warnings,
2699
- suggestedNextActions: normalizeCheckIds(checks.slice(0, 3).map((check) => check.id), checks).suggestedNextActions,
2700
- }) as unknown as Record<string, unknown>);
2701
- }
2702
-
2703
- // Simulation path for unit tests / explicit dry verification without process execution.
2704
- if (args.simulate_check === true || args.infrastructure_failed === true || args.check_failed === true || args.skipped === true) {
2705
- if (!workId) {
2706
- return result(buildFacadeResult({
2707
- status: args.check_failed === true ? 'failed' : 'ok',
2708
- summary: 'Simulated verification without WorkContract.',
2709
- data: {
2710
- verification: {
2711
- checkId: classified.normalizedCheckId,
2712
- outcome: args.skipped ? 'skipped' : args.infrastructure_failed ? 'infrastructure_failure' : args.check_failed ? 'valid_fail' : 'valid_pass',
2713
- isAcceptanceFailure: args.check_failed === true,
2714
- simulated: true,
2715
- },
2716
- },
2717
- }) as unknown as Record<string, unknown>, args.check_failed === true);
2718
- }
2719
- const facade = verifyGoalWorkloop(workloopCtx, {
2720
- workId,
2721
- checkId: classified.normalizedCheckId ?? checkId,
2722
- infrastructureFailed: args.infrastructure_failed === true,
2723
- checkFailed: args.check_failed === true,
2724
- skipped: args.skipped === true,
2725
- });
2726
- return result(facade as unknown as Record<string, unknown>, facade.status === 'failed');
2727
- }
2728
-
2729
- // Real checks share the same persisted Process Runtime path as run_check and
2730
- // work_validate. Never synchronously block the MCP facade on long native tests.
2731
- try {
2732
- const normalizedCheckId = classified.normalizedCheckId!;
2733
- const requestId = typeof args.request_id === 'string' && args.request_id.trim()
2734
- ? args.request_id.trim()
2735
- : undefined;
2736
- const verificationRepository = workContract?.checkoutId
2737
- ? selectRepositoryCheckout(repository, workContract.checkoutId, { allowArchived: true })
2738
- : repository;
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
- }
2769
- const executed = await runPersistedCheckViaProcessRuntime({
2770
- controllerHome: ctx.controllerHome,
2771
- repoId: verificationRepository.repoId,
2772
- checkoutId: verificationRepository.activeCheckoutId,
2773
- repoRoot: verificationRepository.canonicalRoot,
2774
- executionIdentity: executionIdentityForRepository(verificationRepository, workId ? { workId } : {}),
2775
- checkId: normalizedCheckId,
2776
- timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
2777
- // rh_work.verify is a control-plane continuation primitive: callers should
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.
2782
- interactiveWaitMs: 0,
2783
- leaseWaitMs: RH_WORK_VERIFY_LEASE_WAIT_MS,
2784
- requestId,
2785
- requestSemanticFingerprint: verificationRequestFingerprint,
2786
- workId: workId || undefined,
2787
- commandId: requestId,
2788
- verificationSnapshot: workContract ? {
2789
- workId: workContract.workId,
2790
- allowedPaths: workContract.allowedPaths,
2791
- forbiddenPaths: workContract.forbiddenPaths,
2792
- } : undefined,
2793
- allowDurableCheckExecution,
2794
- });
2795
-
2796
- if (executed.mode === 'durable') {
2797
- return result(buildFacadeResult({
2798
- status: 'blocked',
2799
- summary: `Check ${normalizedCheckId} requires an explicit durable workflow; no acceptance result was recorded.`,
2800
- data: {
2801
- verification: {
2802
- checkId: normalizedCheckId,
2803
- outcome: 'deferred',
2804
- isAcceptanceFailure: false,
2805
- isInfrastructureIssue: false,
2806
- durable: executed.durable,
2807
- observedGitHead,
2808
- },
2809
- },
2810
- suggestedNextActions: [{
2811
- label: 'Continue Work with the durable check requirement',
2812
- tool: 'rh_work',
2813
- operation: 'continue',
2814
- payload: { work_id: workId || undefined },
2815
- risk: 'workspace_write',
2816
- confidence: 'high',
2817
- }],
2818
- }) as unknown as Record<string, unknown>, true);
2819
- }
2820
-
2821
- const handle = executed.process;
2822
- if (!handle) throw new Error(`PROCESS_CHECK_HANDLE_MISSING: ${normalizedCheckId}`);
2823
- const record = getProcessRecord(ctx.controllerHome, verificationRepository.repoId, handle.processId);
2824
- const checkContentRevision = record?.checkExecution?.revision;
2825
-
2826
- if (!handle.completed) {
2827
- return result(buildFacadeResult({
2828
- status: 'ok',
2829
- summary: `Check ${normalizedCheckId} is running through Process Runtime; continue other work and reattach to ${handle.processId}.`,
2830
- data: {
2831
- verification: {
2832
- checkId: normalizedCheckId,
2833
- outcome: 'running',
2834
- isAcceptanceFailure: false,
2835
- isInfrastructureIssue: false,
2836
- executed: true,
2837
- completed: false,
2838
- processId: handle.processId,
2839
- processStatus: handle.status,
2840
- deduplicated: handle.deduplicated === true,
2841
- semanticDeduplicated: handle.semanticDeduplicated === true,
2842
- checkContentRevision,
2843
- observedGitHead,
2844
- verificationIsolation: workContract ? 'work_snapshot' : 'shared_checkout',
2845
- revisionSemantics: 'checkContentRevision is a content-bound Check identity; observedGitHead is Git HEAD and is not interchangeable.',
2846
- },
2847
- },
2848
- rawAvailable: false,
2849
- }) as unknown as Record<string, unknown>);
2850
- }
2851
-
2852
- if (!record) throw new Error(`PROCESS_CHECK_RECORD_MISSING: ${handle.processId}`);
2853
- const receipt = processCheckCompletionReceipt(record, {
2854
- repoId: verificationRepository.repoId,
2855
- checkId: normalizedCheckId,
2856
- processId: handle.processId,
2857
- ...(record.checkExecution ? {
2858
- checkoutId: verificationRepository.activeCheckoutId,
2859
- workId: workId || undefined,
2860
- requestId,
2861
- checkExecution: {
2862
- cacheKey: record.checkExecution.cacheKey,
2863
- revision: record.checkExecution.revision,
2864
- definitionDigest: record.checkExecution.definitionDigest,
2865
- environmentFingerprint: record.checkExecution.environmentFingerprint,
2866
- timeoutMs: record.checkExecution.timeoutMs,
2867
- scopeKey: record.checkExecution.scopeKey,
2868
- },
2869
- } : {}),
2870
- });
2871
- const structuredCheckResult = readPersistedCheckResultReceipt(record.origin?.checkResultReceiptPath);
2872
- const structuredResultMatchesProcess = Boolean(
2873
- structuredCheckResult
2874
- && record.checkExecution?.cacheKey
2875
- && structuredCheckResult.checkId === normalizedCheckId
2876
- && structuredCheckResult.cacheKey === record.checkExecution.cacheKey,
2877
- );
2878
- const legacyEvidence = record.origin?.checkResultReceiptPath
2879
- ? undefined
2880
- : readLatestControllerCheckEvidence(verificationRepository.canonicalRoot, normalizedCheckId);
2881
- const legacyEvidenceMatchesProcess = Boolean(
2882
- legacyEvidence?.cacheKey
2883
- && record.checkExecution?.cacheKey
2884
- && legacyEvidence.cacheKey === record.checkExecution.cacheKey,
2885
- );
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';
2894
- const failureClass = structuredResultMatchesProcess
2895
- ? structuredCheckResult?.failureClass
2896
- : legacyEvidenceMatchesProcess ? legacyEvidence?.failureClass : undefined;
2897
- const infrastructureFailed = receipt.timedOut
2898
- || receipt.cancelled
2899
- || !resultMatchesProcess
2900
- || (!receipt.ok && failureClass !== 'acceptance_failure');
2901
- const checkFailed = !receipt.ok && !infrastructureFailed;
2902
- const boundedStatus = receipt.ok ? 'pass' : infrastructureFailed ? 'infrastructure_failure' : 'fail';
2903
- const commonVerification = {
2904
- checkId: normalizedCheckId,
2905
- outcome: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'valid_pass' : 'valid_fail',
2906
- isAcceptanceFailure: checkFailed,
2907
- isInfrastructureIssue: infrastructureFailed,
2908
- executed: true,
2909
- completed: true,
2910
- processId: receipt.processId,
2911
- processStatus: receipt.runtimeStatus,
2912
- ok: receipt.ok,
2913
- timedOut: receipt.timedOut,
2914
- cancelled: receipt.cancelled,
2915
- failureClass: infrastructureFailed ? 'infrastructure_failure' : failureClass,
2916
- deduplicated: handle.deduplicated === true,
2917
- semanticDeduplicated: handle.semanticDeduplicated === true,
2918
- checkContentRevision: receipt.checkRevision,
2919
- observedGitHead,
2920
- revisionSemantics: 'checkContentRevision is a content-bound Check identity; observedGitHead is Git HEAD and is not interchangeable.',
2921
- evidenceArtifactPath: record.origin?.workVerificationSnapshot ? undefined : receipt.artifactPath,
2922
- evidenceReceiptId: receipt.receiptId,
2923
- checkResultReceiptId: structuredCheckResult?.receiptId,
2924
- verificationIsolation: record.origin?.workVerificationSnapshot ? 'work_snapshot' : 'shared_checkout',
2925
- boundedStatus,
2926
- evidenceState: evidenceState.state,
2927
- ...(evidenceState.infrastructureReason ? { infrastructureReason: evidenceState.infrastructureReason } : {}),
2928
- ...(record.error?.code ? { processErrorCode: record.error.code } : {}),
2929
- };
2930
-
2931
- if (workId) {
2932
- const sourceRevision = observedGitHead ?? undefined;
2933
- const facade = verifyGoalWorkloop(workloopCtx, {
2934
- workId,
2935
- checkId: normalizedCheckId,
2936
- sourceRevision,
2937
- workspaceFingerprint,
2938
- verificationInputFingerprint: sourceRevision ? verificationRequestFingerprint : undefined,
2939
- commandFingerprint: commandFingerprint(normalizedCheckId, receipt.commandId),
2940
- receipt,
2941
- infrastructureFailed,
2942
- checkFailed,
2943
- });
2944
- const data = facade.data as Record<string, unknown>;
2945
- return result({
2946
- ...facade,
2947
- data: {
2948
- ...data,
2949
- verification: {
2950
- ...(typeof data.verification === 'object' && data.verification ? data.verification as Record<string, unknown> : {}),
2951
- ...commonVerification,
2952
- },
2953
- },
2954
- warnings: infrastructureFailed
2955
- ? [...facade.warnings, evidenceState.warning ?? 'infrastructure_failure is distinct from acceptance failure']
2956
- : facade.warnings,
2957
- } as unknown as Record<string, unknown>, facade.status === 'failed');
2958
- }
2959
-
2960
- return result(buildFacadeResult({
2961
- status: checkFailed ? 'failed' : 'ok',
2962
- summary: infrastructureFailed
2963
- ? `Infrastructure failure while running ${normalizedCheckId}; not an acceptance failure.`
2964
- : receipt.ok
2965
- ? `Check ${normalizedCheckId} passed with persisted Process evidence.`
2966
- : `Check ${normalizedCheckId} failed acceptance.`,
2967
- data: { verification: commonVerification },
2968
- warnings: infrastructureFailed
2969
- ? [evidenceState.warning ?? 'infrastructure_failure is distinct from acceptance failure']
2970
- : [],
2971
- rawAvailable: false,
2972
- }) as unknown as Record<string, unknown>, checkFailed);
2973
- } catch (error) {
2974
- const message = error instanceof Error ? error.message : String(error);
2975
- if (workId) {
2976
- const facade = verifyGoalWorkloop(workloopCtx, {
2977
- workId,
2978
- checkId: classified.normalizedCheckId ?? checkId,
2979
- infrastructureFailed: true,
2800
+ commandFingerprint: commandFingerprint(normalizedCheckId, receipt.commandId),
2801
+ receipt,
2802
+ infrastructureFailed,
2803
+ checkFailed,
2980
2804
  });
2981
- return result({
2982
- ...facade,
2983
- warnings: [...facade.warnings, `check_runner_error: ${message.slice(0, 200)}`],
2984
- data: {
2985
- ...(facade.data as Record<string, unknown>),
2986
- isAcceptanceFailure: false,
2987
- },
2988
- } as unknown as Record<string, unknown>);
2805
+ reconciledProcessIds.push(record.processId);
2806
+ } catch {
2807
+ // Exact receipt/process identity is mandatory. Any malformed, stale, or
2808
+ // mismatched terminal Process remains non-authoritative and is ignored.
2989
2809
  }
2990
- return result(buildFacadeResult({
2991
- status: 'ok',
2992
- summary: `Infrastructure failure invoking Process Runtime for ${classified.normalizedCheckId}; not acceptance failure.`,
2993
- data: {
2994
- verification: {
2995
- checkId: classified.normalizedCheckId,
2996
- outcome: 'infrastructure_failure',
2997
- isAcceptanceFailure: false,
2998
- isInfrastructureIssue: true,
2999
- },
3000
- },
3001
- warnings: [`check_runner_error: ${message.slice(0, 200)}`],
3002
- suggestedNextActions: [{
3003
- label: 'Diagnose runtime (dry-run)',
3004
- tool: 'rh_work',
3005
- operation: 'repair',
3006
- payload: { repair_operation: 'diagnose', dry_run: true },
3007
- risk: 'readonly',
3008
- }],
3009
- }) as unknown as Record<string, unknown>);
3010
2810
  }
3011
- }
3012
2811
 
3013
- /**
3014
- * Runtime Source drift for MCP readiness.
3015
- *
3016
- * `currentRuntimeRoot` is only for tests that pin a Controller Runtime Source
3017
- * fixture. Callers must never pass an execution repository canonicalRoot here.
3018
- */
3019
- export function runtimeSourceSnapshotStatus(
3020
- active: RuntimeSourceIdentity | undefined,
3021
- currentRuntimeRoot?: string,
3022
- ) {
3023
- const drift = evaluateActiveRuntimeSourceDrift(active, {
3024
- currentRuntimeRoot,
3025
- });
3026
- return {
3027
- current: drift.current,
3028
- restartRequired: drift.restartRequired,
3029
- reasons: drift.reasons,
3030
- code: drift.code,
3031
- };
2812
+ return { sourceRevision, workspaceFingerprint, workspaceChangedPaths, reconciledProcessIds, workBoundProcessEvidenceIds };
3032
2813
  }
3033
2814
 
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',
2815
+ async function runFacadeVerify(
2816
+ ctx: MultiRepositoryMcpToolContext,
2817
+ repository: ReturnType<typeof selected>,
2818
+ args: Record<string, unknown>,
2819
+ ): Promise<CallToolResult> {
2820
+ const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
2821
+ const verification = await executeWorkVerification({
2822
+ controllerHome: ctx.controllerHome,
2823
+ repository,
2824
+ workId: workId || undefined,
2825
+ checkId: String(args.check_id ?? args.checkId ?? '').trim(),
2826
+ requestId: typeof args.request_id === 'string' && args.request_id.trim() ? args.request_id.trim() : undefined,
2827
+ timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
2828
+ interactiveWaitMs: 0,
2829
+ leaseWaitMs: RH_WORK_VERIFY_LEASE_WAIT_MS,
2830
+ simulate: args.simulate_check === true || args.infrastructure_failed === true || args.check_failed === true || args.skipped === true
2831
+ ? {
2832
+ infrastructureFailed: args.infrastructure_failed === true,
2833
+ checkFailed: args.check_failed === true,
2834
+ skipped: args.skipped === true,
2835
+ }
2836
+ : undefined,
2837
+ allowDurableCheckExecution: workId ? ({ work }) => {
2838
+ try {
2839
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
2840
+ const owner = getControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, work.workId);
2841
+ return Boolean(
2842
+ owner
2843
+ && owner.controllerId === identity.controllerId
2844
+ && controllerSessionPrincipalId(owner) === identity.principalId
2845
+ && owner.controllerInstanceId === identity.controllerInstanceId,
2846
+ );
2847
+ } catch {
2848
+ return false;
2849
+ }
2850
+ } : undefined,
3072
2851
  });
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
- };
2852
+ return result(verification.facade as unknown as Record<string, unknown>, verification.isError);
3116
2853
  }
3117
2854
 
3118
2855
  export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>): Promise<CallToolResult | undefined> {
@@ -3142,8 +2879,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3142
2879
  ctx.runtimeSourceRoot,
3143
2880
  );
3144
2881
  const sourceSnapshotStale = runtimeSource.restartRequired;
3145
- const toolset = await import('../../../cli/mcp/toolset');
3146
- const exposure = toolset.controllerToolSurfaceStatus(ctx);
2882
+ const exposure = controllerToolSurfaceStatus(ctx);
3147
2883
  const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
3148
2884
  const ready = observation.ready && toolSurfaceReady && !sourceSnapshotStale;
3149
2885
  const reasonCodes = [...observation.reasonCodes];
@@ -3284,10 +3020,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3284
3020
  // never against the selected execution repository.
3285
3021
  const runtimeSource = runtimeSourceSnapshotStatus(readiness.daemon.source, ctx.runtimeSourceRoot);
3286
3022
  const sourceSnapshotStale = runtimeSource.restartRequired;
3287
- // Dynamic import avoids a static cycle: toolset.ts composes runtimeToolDefinitions.
3288
- const toolset = await import('../../../cli/mcp/toolset');
3289
- const exposure = toolset.controllerExposureSnapshot(ctx);
3290
- const localRegisteredToolNames = toolset.allControllerToolDefinitions(ctx).map((tool) => tool.name).sort();
3023
+ const exposure = controllerExposureSnapshot(ctx);
3024
+ const localRegisteredToolNames = allControllerToolDefinitions(ctx).map((tool) => tool.name).sort();
3291
3025
  markDetailPhase('tool_surface');
3292
3026
  const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
3293
3027
  // Ordinary interactive execution depends on the execution axis only.
@@ -3526,8 +3260,12 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
3526
3260
  if (item.workId && ctx.principalId?.trim() && ctx.sessionId?.trim()) {
3527
3261
  const owner = getControllerSession(store, item.workId);
3528
3262
  if (owner && owner.controllerId === ctx.principalId.trim() && owner.sessionId === ctx.sessionId.trim()) {
3529
- releaseControllerSession(store, item.workId, owner.controllerId);
3530
- ownershipReleased = true;
3263
+ const released = releaseObservedControllerSession(store, {
3264
+ workId: item.workId,
3265
+ actor: `handoff-create:${ctx.principalId.trim()}`,
3266
+ owner,
3267
+ });
3268
+ ownershipReleased = released.allowed;
3531
3269
  }
3532
3270
  }
3533
3271
  return result(buildFacadeResult({ summary: `Created handoff ${item.id}.`, data: { item: summarizeHandoffItem(item), ownershipReleased } }) as unknown as Record<string, unknown>);
@@ -4190,13 +3928,28 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4190
3928
  const work = getWorkContract(store, workId);
4191
3929
  if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
4192
3930
  const identity = authenticatedFacadeControllerIdentity(ctx, args);
3931
+ assertFacadeControllerRoundAuthority(ctx, store, workId, args);
3932
+ const observedOwner = getControllerSession(store, workId);
3933
+ const dispatchedRelay = getControllerRoundRelay(store, workId);
3934
+ const crossOwnerRecovery = Boolean(observedOwner)
3935
+ && (
3936
+ observedOwner!.controllerId !== identity.controllerId
3937
+ || controllerSessionPrincipalId(observedOwner!) !== identity.principalId
3938
+ )
3939
+ && dispatchedChatgptRelayAuthorizesStaleControllerRecovery(dispatchedRelay, identity.controllerType);
4193
3940
  const session = resumeControllerSession(store, {
4194
3941
  workId,
4195
3942
  controllerId: identity.controllerId,
4196
- controllerType: ['chatgpt', 'codex', 'grok', 'claude', 'human'].includes(String(args.controller_type)) ? String(args.controller_type) as 'chatgpt' | 'codex' | 'grok' | 'claude' | 'human' : 'chatgpt',
3943
+ controllerType: identity.controllerType,
4197
3944
  sessionId: identity.sessionId,
4198
3945
  principalId: identity.principalId,
4199
3946
  controllerInstanceId: identity.controllerInstanceId,
3947
+ ...(crossOwnerRecovery
3948
+ ? {
3949
+ expectedClaimGeneration: observedOwner!.claimGeneration,
3950
+ allowStaleRecovery: true,
3951
+ }
3952
+ : {}),
4200
3953
  leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
4201
3954
  });
4202
3955
  const permissionSnapshotVersion = currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId);
@@ -4238,7 +3991,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4238
3991
  if (!['continue_immediately', 'wait', 'wait_for_user', 'goal_complete'].includes(disposition)) {
4239
3992
  throw new Error('CONTROLLER_RELAY_DISPOSITION_INVALID');
4240
3993
  }
4241
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
3994
+ const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
4242
3995
  const work = getWorkContract(store, workId);
4243
3996
  if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
4244
3997
  const terminalGoalComplete = work.status === 'completed' && disposition === 'goal_complete';
@@ -4252,7 +4005,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4252
4005
  if (currentOwner) {
4253
4006
  if (currentOwner.controllerType !== 'chatgpt') throw new Error(`CONTROLLER_RELAY_CHATGPT_ONLY: ${workId}`);
4254
4007
  if (currentOwner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
4255
- if ((currentOwner.principalId?.trim() || currentOwner.controllerId) !== identity.principalId) {
4008
+ if ((controllerSessionPrincipalId(currentOwner)) !== identity.principalId) {
4256
4009
  throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
4257
4010
  }
4258
4011
  // Finalization may terminalize the Work before the prompt-required
@@ -4261,21 +4014,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4261
4014
  // rewrite a terminal Work lease here; terminal authorization is fenced
4262
4015
  // by the already-claimed relay lineage in submitControllerRoundDisposition.
4263
4016
  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
- }
4017
+ bindFacadeControllerOwnership(ctx, store, workId, { ...identity, controllerType: 'chatgpt' });
4279
4018
  }
4280
4019
  }
4281
4020
  const chatgptBinding = getChatgptWorkConversationBinding(store, workId);
@@ -4308,20 +4047,42 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4308
4047
  if (operation === 'controller_release') {
4309
4048
  try {
4310
4049
  const workId = String(args.work_id ?? '').trim();
4311
- const identity = authenticatedFacadeControllerIdentity(ctx, args);
4312
- const owner = getControllerSession(store, workId);
4050
+ const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
4051
+ assertFacadeControllerRoundAuthority(ctx, store, workId, args);
4052
+ const observedOwner = getControllerSession(store, workId);
4053
+ const work = getWorkContract(store, workId);
4054
+ const terminalWork = work ? ['completed', 'failed', 'cancelled'].includes(work.status) : false;
4055
+ const owner = observedOwner
4056
+ ? terminalWork
4057
+ ? (() => {
4058
+ // A terminal Work cannot be rebound to a replacement MCP transport.
4059
+ // Authenticate the caller against the observed durable controller epoch
4060
+ // without mutating it; releaseControllerSessionWithAuthority remains the
4061
+ // exact claim-generation CAS authority for physical lease release.
4062
+ const ownerPrincipal = observedOwner.principalId?.trim() || observedOwner.controllerId;
4063
+ const ownerInstanceId = observedOwner.controllerInstanceId?.trim() || '';
4064
+ if (observedOwner.controllerId !== identity.controllerId) {
4065
+ throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
4066
+ }
4067
+ if (observedOwner.controllerType !== identity.controllerType) {
4068
+ throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${workId}`);
4069
+ }
4070
+ if (ownerPrincipal !== identity.principalId) {
4071
+ throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
4072
+ }
4073
+ if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) {
4074
+ throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
4075
+ }
4076
+ if (typeof observedOwner.claimGeneration !== 'number' || observedOwner.claimGeneration < 1) {
4077
+ throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
4078
+ }
4079
+ return observedOwner;
4080
+ })()
4081
+ : bindFacadeControllerOwnership(ctx, store, workId, identity)
4082
+ : undefined;
4313
4083
  if (owner) {
4314
- const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
4084
+ const ownerPrincipal = controllerSessionPrincipalId(owner);
4315
4085
  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
4086
  if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
4326
4087
  throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
4327
4088
  }
@@ -4349,6 +4110,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4349
4110
  relayStore,
4350
4111
  { workId, releasedSession: owner },
4351
4112
  ) : undefined;
4113
+ const abandonedRelay = owner && !relay
4114
+ ? reconcileControllerRoundAfterAbandonedRelease(relayStore, { workId, releasedSession: owner })
4115
+ : undefined;
4352
4116
  if (relay?.status === 'dispatching') {
4353
4117
  try {
4354
4118
  assertAutomatedOperationAllowed('external_controller_wake', {
@@ -4418,8 +4182,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4418
4182
  }
4419
4183
  }
4420
4184
  return result(buildFacadeResult({
4421
- summary: 'Controller lease released.',
4422
- data: { relay: getControllerRoundRelay(relayStore, workId) ?? relay, tabSettlement },
4185
+ summary: abandonedRelay
4186
+ ? 'Controller lease released; the claimed round was mechanically marked abandoned without semantic completion and can be relaunched through the bounded launcher path.'
4187
+ : 'Controller lease released.',
4188
+ data: { relay: getControllerRoundRelay(relayStore, workId) ?? abandonedRelay ?? relay, tabSettlement },
4423
4189
  }) as unknown as Record<string, unknown>);
4424
4190
  } catch (error) {
4425
4191
  return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller release failed.', data: {} }) as unknown as Record<string, unknown>, true);
@@ -4576,57 +4342,46 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4576
4342
  };
4577
4343
 
4578
4344
  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) {
4345
+ const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id : '';
4346
+ const title = typeof args.requirement_title === 'string' ? args.requirement_title : '';
4347
+ const outcomeStatement = typeof args.requirement_outcome === 'string' ? args.requirement_outcome : '';
4348
+ if (!requirementId.trim() || !title.trim() || !outcomeStatement.trim()) {
4592
4349
  return result(buildFacadeResult({
4593
4350
  status: 'blocked',
4594
4351
  summary: 'REQUIREMENT_CREATE_INPUT_REQUIRED: requirement_id, requirement_title, and requirement_outcome are required.',
4595
4352
  data: { requirementCreated: false },
4596
4353
  }) as unknown as Record<string, unknown>, true);
4597
4354
  }
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
- }
4355
+ let admission;
4356
+ try {
4357
+ admission = admitRequirement({ controllerHome: ctx.controllerHome }, {
4358
+ requirementId,
4359
+ title,
4360
+ outcomeStatement,
4361
+ acceptanceCriteria: Array.isArray(args.requirement_acceptance_criteria) ? args.requirement_acceptance_criteria.map(String) : [],
4362
+ requiredDeliveryReferences: Array.isArray(args.requirement_delivery_references) ? args.requirement_delivery_references.map(String) : [],
4363
+ legacyAliases: Array.isArray(args.requirement_legacy_aliases) ? args.requirement_legacy_aliases.map(String) : [],
4364
+ });
4365
+ } catch (error) {
4366
+ return result(buildFacadeResult({
4367
+ status: 'blocked',
4368
+ summary: error instanceof Error ? error.message : String(error),
4369
+ data: { requirementCreated: false },
4370
+ }) as unknown as Record<string, unknown>, true);
4371
+ }
4372
+ if (admission.decision === 'existing_conflict') {
4613
4373
  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' },
4374
+ status: 'blocked',
4375
+ summary: `REQUIREMENT_ALREADY_EXISTS_CONFLICT: ${admission.requirement.requirementId}. Existing Requirement authority was not changed.`,
4376
+ data: { requirement: admission.requirement, requirementCreated: false, admissionDecision: admission.decision },
4616
4377
  suggestedNextActions: [],
4617
- }) as unknown as Record<string, unknown>);
4378
+ }) as unknown as Record<string, unknown>, true);
4618
4379
  }
4619
- const requirement = createRequirement({ controllerHome: ctx.controllerHome }, {
4620
- requirementId,
4621
- title,
4622
- outcomeStatement,
4623
- acceptanceCriteria,
4624
- requiredDeliveryReferences,
4625
- legacyAliases,
4626
- });
4627
4380
  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' },
4381
+ summary: admission.decision === 'created'
4382
+ ? `Requirement ${admission.requirement.requirementId} created. Requirement authority does not imply a Plan; Controller chooses the next action.`
4383
+ : `REQUIREMENT_AUTHORITY_REUSED: ${admission.requirement.requirementId}. Requirement authority does not imply a Plan; Controller chooses the next action.`,
4384
+ data: { requirement: admission.requirement, requirementCreated: admission.created, admissionDecision: admission.decision },
4630
4385
  suggestedNextActions: [],
4631
4386
  }) as unknown as Record<string, unknown>);
4632
4387
  }
@@ -4635,6 +4390,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4635
4390
  try {
4636
4391
  if (operation === 'plan_create') {
4637
4392
  const rawSteps = Array.isArray(args.plan_steps) ? args.plan_steps : [];
4393
+ const requestedPlanId = String(args.plan_id ?? '').trim();
4638
4394
  const requestedRequirementId = typeof args.requirement_id === 'string' && args.requirement_id.trim() ? args.requirement_id.trim() : undefined;
4639
4395
  const requestedPlanRelation: 'extend' | 'parallel' | undefined = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
4640
4396
  ? args.plan_relation
@@ -4657,16 +4413,43 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4657
4413
  const renderPlanAdmission = (admission: ReturnType<typeof resolvePlanAdmission>): CallToolResult | undefined => {
4658
4414
  if (admission.admissionDecision === 'create_new') return undefined;
4659
4415
  if (admission.reason === 'exact_scope_authority' && admission.plan) {
4416
+ const exactDraftRepair = admission.plan.status === 'draft' && requestedPlanId === admission.plan.planId;
4660
4417
  const facade = buildFacadeResult({
4661
- summary: `PLAN_AUTHORITY_REUSED: active Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; no duplicate draft was created.`,
4418
+ summary: exactDraftRepair
4419
+ ? `PLAN_DRAFT_REPAIR_REQUIRED: draft Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; preserve that authority and amend it through rh_work repair.`
4420
+ : `PLAN_AUTHORITY_REUSED: active Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; no duplicate draft was created.`,
4662
4421
  data: {
4663
4422
  plan: summarizePlanContract(admission.plan),
4664
4423
  executionStarted: false,
4665
4424
  planContractCreated: false,
4666
4425
  admissionDecision: 'reuse_existing',
4667
4426
  resolutionRequired: false,
4427
+ ...(exactDraftRepair ? { repairRequired: true } : {}),
4668
4428
  },
4669
- suggestedNextActions: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: admission.plan.planId }, risk: 'readonly', confidence: 'high' }],
4429
+ suggestedNextActions: exactDraftRepair
4430
+ ? [{
4431
+ label: 'Repair this exact draft Plan',
4432
+ tool: 'rh_work',
4433
+ operation: 'repair',
4434
+ payload: {
4435
+ plan_id: admission.plan.planId,
4436
+ repair_operation: 'repair',
4437
+ dry_run: false,
4438
+ scope_key: args.scope_key,
4439
+ source_revision: args.source_revision,
4440
+ objective: args.objective,
4441
+ plan_steps: args.plan_steps,
4442
+ non_goals: args.non_goals,
4443
+ assumptions: args.assumptions,
4444
+ resolved_decisions: args.resolved_decisions,
4445
+ stop_conditions: args.stop_conditions,
4446
+ replan_conditions: args.replan_conditions,
4447
+ integration_strategy: args.integration_strategy,
4448
+ },
4449
+ risk: 'workspace_write',
4450
+ confidence: 'high',
4451
+ }]
4452
+ : [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: admission.plan.planId }, risk: 'readonly', confidence: 'high' }],
4670
4453
  });
4671
4454
  return result(facade as unknown as Record<string, unknown>);
4672
4455
  }
@@ -4828,41 +4611,23 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4828
4611
 
4829
4612
  if (operation === 'stop') {
4830
4613
  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 ?? ''))) {
4614
+ try {
4615
+ if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
4616
+ } catch (error) {
4617
+ const blocked = buildFacadeResult({
4618
+ status: 'blocked',
4619
+ summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`,
4620
+ data: { workId, terminalizationApplied: false },
4621
+ });
4622
+ return result(blocked as unknown as Record<string, unknown>, true);
4623
+ }
4624
+ let terminalizationAuthority: ControllerTerminalizationAuthority;
4625
+ try {
4626
+ terminalizationAuthority = currentFacadeTerminalizationAuthority(ctx, store, workId, args);
4627
+ } catch (error) {
4863
4628
  const blocked = buildFacadeResult({
4864
4629
  status: 'blocked',
4865
- summary: `WORK_CONTROLLER_OWNER_REQUIRED: ${workId}; unclaimed stop requires explicit requested_by=user or requested_by=system authority.`,
4630
+ summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`,
4866
4631
  data: { workId, terminalizationApplied: false },
4867
4632
  });
4868
4633
  return result(blocked as unknown as Record<string, unknown>, true);
@@ -4934,13 +4699,41 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4934
4699
 
4935
4700
  if (operation === 'finalize') {
4936
4701
  const workId = String(args.work_id ?? '').trim();
4937
- if (workId) reconcileTerminalFacadeWorkVerifications(ctx, repository, workId);
4702
+ try {
4703
+ if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
4704
+ } catch (error) {
4705
+ const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`, data: { workId, lifecycleClosed: false } });
4706
+ return result(blocked as unknown as Record<string, unknown>, true);
4707
+ }
4708
+ const finalizeReconciliation = workId
4709
+ ? reconcileTerminalFacadeWorkVerifications(ctx, repository, workId)
4710
+ : undefined;
4711
+ const semanticFinalizeContext = {
4712
+ ...workloopCtx,
4713
+ sourceRevision: finalizeReconciliation?.sourceRevision ?? workloopCtx.sourceRevision ?? undefined,
4714
+ workspaceFingerprint: finalizeReconciliation?.workspaceFingerprint,
4715
+ workspaceChangedPaths: finalizeReconciliation?.workspaceChangedPaths,
4716
+ workBoundProcessEvidenceIds: finalizeReconciliation?.workBoundProcessEvidenceIds,
4717
+ };
4938
4718
  let before = workId ? getWorkContract(store, workId) : undefined;
4719
+ let terminalizationAuthority: ControllerTerminalizationAuthority | undefined;
4720
+ // Finalize may commit, merge, clean resources, and complete the Work.
4721
+ // It therefore shares the same exact-claim authority as stop. Transport
4722
+ // or Runtime recovery remains an explicit controller_claim operation;
4723
+ // terminalization itself must never rebind an unrelated controller scope.
4724
+ if (before && !['completed', 'failed', 'cancelled'].includes(before.status)) {
4725
+ try {
4726
+ terminalizationAuthority = currentFacadeTerminalizationAuthority(ctx, store, workId, args);
4727
+ } catch (error) {
4728
+ const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`, data: { workId, terminalizationApplied: false, lifecycleClosed: false } });
4729
+ return result(blocked as unknown as Record<string, unknown>, true);
4730
+ }
4731
+ }
4939
4732
  if (before && !before.completionReceipt && args.reconcile_historical_delivery === true) {
4940
4733
  try {
4941
4734
  const identity = authenticatedFacadeControllerIdentity(ctx, args);
4942
4735
  const owner = getControllerSession(store, workId);
4943
- if (!owner || (owner.principalId?.trim() || owner.controllerId) !== identity.principalId) {
4736
+ if (!owner || (controllerSessionPrincipalId(owner)) !== identity.principalId) {
4944
4737
  throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
4945
4738
  }
4946
4739
  const historicalHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
@@ -4973,31 +4766,60 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
4973
4766
  : transitionWorkHandle(ctx.controllerHome, historicalHandle, 'committed', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
4974
4767
  transitionWorkHandle(ctx.controllerHome, delivered, 'cleaned', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
4975
4768
  }
4976
- releaseControllerSession(store, workId, owner.controllerId);
4769
+ const released = releaseObservedControllerSession(store, {
4770
+ workId,
4771
+ actor: `direct-edit-reconciliation:${identity.principalId}`,
4772
+ owner,
4773
+ });
4774
+ if (!released.allowed) {
4775
+ throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_RELEASE_FENCED: ${workId}:${released.reason}`);
4776
+ }
4977
4777
  before = getWorkContract(store, workId);
4978
4778
  } catch (error) {
4979
4779
  const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Historical Work delivery reconciliation failed.', data: { workId, lifecycleClosed: false } });
4980
4780
  return result(blocked as unknown as Record<string, unknown>, true);
4981
4781
  }
4982
4782
  }
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
4783
  const repositoryDeliveryHandle = workId
4989
4784
  ? readWorkHandle(ctx.controllerHome, repository.repoId, workId)
4990
4785
  : undefined;
4991
- if (before?.workKind === 'remote_effect' && !before.completionReceipt && !repositoryDeliveryHandle) {
4786
+ const effectHasRepositoryDelta = Boolean(
4787
+ before
4788
+ && ['local_effect', 'remote_effect'].includes(before.workKind)
4789
+ && (
4790
+ (repositoryDeliveryHandle && repositoryWorkHandleHasSourceDelta(repository, repositoryDeliveryHandle))
4791
+ || (finalizeReconciliation?.workspaceChangedPaths?.length ?? 0) > 0
4792
+ )
4793
+ );
4794
+ if (effectHasRepositoryDelta && !repositoryDeliveryHandle) {
4795
+ const blocked = buildFacadeResult({
4796
+ status: 'blocked',
4797
+ summary: `WORK_EFFECT_REPOSITORY_DELIVERY_HANDLE_REQUIRED: ${workId} has repository source delta and cannot use effect-only terminalization without a physical WorkHandle.`,
4798
+ data: { workId, lifecycleClosed: false },
4799
+ });
4800
+ return result(blocked as unknown as Record<string, unknown>, true);
4801
+ }
4802
+ // Pure remote effects may complete from either the canonical plugin
4803
+ // action receipt or the deliberately narrow trusted repository
4804
+ // Process authority (`git push` + exact remote lease). A remote_effect
4805
+ // that acquired source delta is no longer pure and must use physical
4806
+ // repository delivery instead.
4807
+ if (before?.workKind === 'remote_effect' && !before.completionReceipt && !effectHasRepositoryDelta) {
4992
4808
  try {
4993
4809
  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);
4810
+ } catch (pluginError) {
4811
+ const checkoutId = before.checkoutId ?? repositoryDeliveryHandle?.checkoutId ?? repository.activeCheckoutId;
4812
+ const processCompleted = finalizeRemoteEffectWorkFromRepositoryProcessReceipt(ctx, repository, workId, checkoutId);
4813
+ if (processCompleted) {
4814
+ before = processCompleted;
4815
+ } else {
4816
+ const blocked = buildFacadeResult({
4817
+ status: 'blocked',
4818
+ summary: pluginError instanceof Error ? pluginError.message : 'Remote-effect semantic finalization failed.',
4819
+ data: { workId, lifecycleClosed: false },
4820
+ });
4821
+ return result(blocked as unknown as Record<string, unknown>, true);
4822
+ }
5001
4823
  }
5002
4824
  }
5003
4825
  const completedCleanupPending = Boolean(
@@ -5006,7 +4828,15 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5006
4828
  && before.worktreeRef?.trim()
5007
4829
  && existsSync(before.worktreeRef),
5008
4830
  );
5009
- if (before && before.workKind !== 'local_effect' && (!before.completionReceipt || completedCleanupPending)) {
4831
+ const repositoryDeliveryRequired = Boolean(
4832
+ before
4833
+ && before.workKind !== 'read_only_review'
4834
+ && (
4835
+ !['local_effect', 'remote_effect'].includes(before.workKind)
4836
+ || effectHasRepositoryDelta
4837
+ )
4838
+ );
4839
+ if (before && ((repositoryDeliveryRequired && !before.completionReceipt) || completedCleanupPending)) {
5010
4840
  try {
5011
4841
  const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'finalize');
5012
4842
  if (physical?.isError === true) return physical;
@@ -5025,40 +4855,15 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5025
4855
  let facade;
5026
4856
  if (semanticWork && !['completed', 'failed', 'cancelled'].includes(semanticWork.status)) {
5027
4857
  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}`);
4858
+ const authority = terminalizationAuthority ?? currentFacadeTerminalizationAuthority(ctx, store, workId, args);
5048
4859
  const fenced = withControllerSessionTerminalizationFence(
5049
4860
  store,
5050
4861
  {
5051
4862
  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
- },
4863
+ actor: `rh-work-finalize:${authority.controllerId}:${authority.controllerInstanceId}`,
4864
+ authority,
5060
4865
  },
5061
- () => runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'finalize', args),
4866
+ () => runGoalWorkloop(semanticFinalizeContext, 'finalize', args),
5062
4867
  );
5063
4868
  if (!fenced.allowed) {
5064
4869
  throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`);
@@ -5073,9 +4878,29 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5073
4878
  return result(blocked as unknown as Record<string, unknown>, true);
5074
4879
  }
5075
4880
  } else {
5076
- facade = runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'finalize', args);
4881
+ facade = runGoalWorkloop(semanticFinalizeContext, 'finalize', args);
4882
+ }
4883
+ let completed = getWorkContract(store, workId);
4884
+ const postSemanticCleanupPending = Boolean(
4885
+ completed?.completionReceipt
4886
+ && args.cleanup !== false
4887
+ && completed.worktreeRef?.trim()
4888
+ && existsSync(completed.worktreeRef),
4889
+ );
4890
+ if (postSemanticCleanupPending) {
4891
+ try {
4892
+ const physical = await finalizeFacadeWorkHandle(ctx, repository, { ...args, commit: false, merge: false }, 'finalize');
4893
+ if (physical?.isError === true) return physical;
4894
+ completed = getWorkContract(store, workId);
4895
+ } catch (error) {
4896
+ const blocked = buildFacadeResult({
4897
+ status: 'blocked',
4898
+ summary: error instanceof Error ? error.message : `Work ${workId} terminal cleanup reconciliation failed.`,
4899
+ data: { workId, terminalizationApplied: true, lifecycleClosed: false },
4900
+ });
4901
+ return result(blocked as unknown as Record<string, unknown>, true);
4902
+ }
5077
4903
  }
5078
- const completed = getWorkContract(store, workId);
5079
4904
  // Finalizing a Work proves the Work lifecycle only. A Plan step may aggregate
5080
4905
  // acceptance criteria that are broader than this Work (for example, a canary
5081
4906
  // plus a later stabilization soak), so finalize must never synthesize semantic
@@ -5100,12 +4925,23 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5100
4925
  let continuationSourceRevision = workloopCtx.sourceRevision;
5101
4926
  let continuationWorkspaceFingerprint: string | undefined;
5102
4927
  let continuationWorkspaceChangedPaths: string[] | undefined;
4928
+ let continuationWorkBoundProcessEvidenceIds: string[] | undefined;
5103
4929
  if (operation === 'continue') {
5104
4930
  try {
5105
4931
  const workId = String(args.work_id ?? '').trim();
4932
+ if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
5106
4933
  let work = getWorkContract(store, workId);
5107
4934
  if (work?.status === 'cancelled') {
5108
- const resumed = reauthorizeCancelledFacadeWork(ctx, repository, workId, args);
4935
+ const identity = authenticatedFacadeControllerIdentity(ctx, args);
4936
+ const resumed = reauthorizeRetainedCancelledRepositoryWork({
4937
+ controllerHome: ctx.controllerHome,
4938
+ repository,
4939
+ workId,
4940
+ identity,
4941
+ requestedBy: typeof args.requested_by === 'string' ? args.requested_by : undefined,
4942
+ approvalConfirmed: args.approval_confirmed === true,
4943
+ prepareDependencies: args.needs_dependencies === true,
4944
+ });
5109
4945
  cancelledWorkReauthorized = true;
5110
4946
  reconstructedCancelledCheckout = resumed.reconstructedCheckout;
5111
4947
  work = getWorkContract(store, workId);
@@ -5121,14 +4957,12 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5121
4957
  controllerInstanceId: identity.controllerInstanceId,
5122
4958
  leaseMs: 3_600_000,
5123
4959
  });
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
- }
4960
+ rebindRepositoryWorkHandleControllerIdentity({
4961
+ controllerHome: ctx.controllerHome,
4962
+ repositoryId: repository.repoId,
4963
+ workId,
4964
+ identity: { sessionId: identity.sessionId, principalId: identity.principalId },
4965
+ });
5132
4966
  if (work.worktreePolicy.required === true && !work.worktreeRef) {
5133
4967
  materializeFacadeWorkPlacement(ctx, repository, workId, args);
5134
4968
  }
@@ -5138,6 +4972,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5138
4972
  continuationSourceRevision = reconciled.sourceRevision ?? continuationSourceRevision;
5139
4973
  continuationWorkspaceFingerprint = reconciled.workspaceFingerprint ?? continuationWorkspaceFingerprint;
5140
4974
  continuationWorkspaceChangedPaths = reconciled.workspaceChangedPaths ?? continuationWorkspaceChangedPaths;
4975
+ continuationWorkBoundProcessEvidenceIds = reconciled.workBoundProcessEvidenceIds;
5141
4976
  }
5142
4977
  } catch (error) {
5143
4978
  const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller resume failed.', data: { operation, executionStarted: false, ownershipResumed: false } });
@@ -5157,6 +4992,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
5157
4992
  sourceRevision: continuationSourceRevision ?? undefined,
5158
4993
  workspaceFingerprint: continuationWorkspaceFingerprint,
5159
4994
  workspaceChangedPaths: continuationWorkspaceChangedPaths,
4995
+ workBoundProcessEvidenceIds: continuationWorkBoundProcessEvidenceIds,
5160
4996
  semanticAdmissionLocked: semanticAdmissionRequired,
5161
4997
  };
5162
4998
  const facade = semanticAdmissionRequired
@@ -6130,8 +5966,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
6130
5966
  ? selected(ctx, args)
6131
5967
  : (ctx.explicitRepository ?? (registered.length === 1 ? registered[0] : undefined));
6132
5968
  const readiness = await controllerReadiness(ctx, repository);
6133
- const toolset = await import('../../../cli/mcp/toolset');
6134
- const exposure = toolset.controllerExposureSnapshot(ctx);
5969
+ const exposure = controllerExposureSnapshot(ctx);
6135
5970
  const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
6136
5971
  const reasonCodes = new Set(readiness.reasonCodes);
6137
5972
  if (!toolSurfaceReady) reasonCodes.add('MCP_TOOL_SURFACE_INCOMPLETE');
@@ -6511,52 +6346,14 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
6511
6346
  return result(buildWorkflowWatchdogReport(ctx.controllerHome, repository, { staleMinutes: args.stale_minutes, includeProcesses: args.include_processes }) as unknown as Record<string, unknown>);
6512
6347
  }
6513
6348
  case 'ios_xcode_status':
6514
- return legacyIosPluginAction(ctx, name, args, 'xcode_status', {});
6515
6349
  case 'ios_simulators_list':
6516
- return legacyIosPluginAction(ctx, name, args, 'list_simulators', {
6517
- runtime: typeof args.runtime === 'string' ? args.runtime : undefined,
6518
- name: typeof args.name === 'string' ? args.name : undefined,
6519
- });
6520
6350
  case 'ios_project_discover':
6521
- return legacyIosPluginAction(ctx, name, args, 'discover_project', {});
6522
6351
  case 'ios_schemes_list':
6523
- return legacyIosPluginAction(ctx, name, args, 'list_schemes', {
6524
- workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
6525
- project: typeof args.project === 'string' ? args.project : undefined,
6526
- });
6527
6352
  case 'ios_simulator_boot':
6528
- return legacyIosPluginAction(ctx, name, args, 'launch_simulator', {
6529
- udid: String(args.udid ?? '').trim(),
6530
- open_simulator: args.open_simulator !== false,
6531
- timeout_ms: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
6532
- });
6533
6353
  case 'ios_app_build':
6534
- return legacyIosPluginAction(ctx, name, args, 'build', {
6535
- scheme: String(args.scheme ?? '').trim(),
6536
- udid: typeof args.udid === 'string' ? args.udid : undefined,
6537
- simulator_name: typeof args.simulator_name === 'string' ? args.simulator_name : undefined,
6538
- workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
6539
- project: typeof args.project === 'string' ? args.project : undefined,
6540
- configuration: typeof args.configuration === 'string' ? args.configuration : undefined,
6541
- timeout_ms: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
6542
- });
6543
6354
  case 'ios_simulator_screenshot':
6544
- return legacyIosPluginAction(ctx, name, args, 'capture_screenshot', {
6545
- udid: String(args.udid ?? '').trim(),
6546
- label: typeof args.label === 'string' ? args.label : undefined,
6547
- });
6548
6355
  case 'ios_ui_smoke_test':
6549
- return legacyIosPluginAction(ctx, name, args, 'smoke_review', {
6550
- scheme: typeof args.scheme === 'string' ? args.scheme : 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,
6554
- workspace: typeof args.workspace === 'string' ? args.workspace : undefined,
6555
- project: typeof args.project === 'string' ? args.project : undefined,
6556
- configuration: typeof args.configuration === 'string' ? args.configuration : undefined,
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
- });
6356
+ return legacyIosPluginAction(ctx, name, args);
6560
6357
  case 'ios_app_install':
6561
6358
  case 'ios_app_launch':
6562
6359
  case 'ios_simulator_log_tail':
@@ -6595,7 +6392,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
6595
6392
  }
6596
6393
  case 'plugin_action_execute': {
6597
6394
  const pluginId = String(args.plugin_id ?? '').trim();
6395
+ const workId = typeof args.work_id === 'string' && args.work_id.trim() ? args.work_id.trim() : undefined;
6598
6396
  const repository = pluginRepository(ctx, args, pluginId);
6397
+ const workRepository = workId ? selected(ctx, args) : undefined;
6599
6398
  const actionId = String(args.action_id ?? '').trim();
6600
6399
  const requestId = String(args.request_id ?? '').trim();
6601
6400
  const actionArguments = args.arguments && typeof args.arguments === 'object' && !Array.isArray(args.arguments)
@@ -6605,7 +6404,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
6605
6404
  pluginId,
6606
6405
  actionId,
6607
6406
  requestId,
6608
- workId: typeof args.work_id === 'string' && args.work_id.trim() ? args.work_id.trim() : undefined,
6407
+ workId,
6408
+ ...(workId && workRepository ? { workRepoId: workRepository.repoId } : {}),
6609
6409
  args: actionArguments,
6610
6410
  timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
6611
6411
  signal: ctx.signal,