@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.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +26 -0
- package/CLAUDE.md +2 -0
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/docs/architecture/CURRENT.md +12 -4
- package/docs/forge-plugin-management.md +1 -1
- package/docs/operations/controller-performance-and-502.md +19 -0
- package/docs/operations/homebrew.md +2 -2
- package/package.json +5 -2
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-work-efficiency.ts +0 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1531
- package/scripts/check-architecture-sync.sh +2 -302
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-runtime-architecture.mjs +243 -16
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +0 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -702
- package/scripts/ensure-task-workflow.sh +1 -1192
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/lib/project-init-lib.sh +7 -2
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -33
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -771
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +3 -0
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -531
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -593
- package/scripts/verify-sprint.sh +1 -500
- package/scripts/workflow-contract.ts +4 -201
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/controller/check-runner.ts +7 -2
- package/src/cli/controller/context-pack.ts +46 -21
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +2 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/local-bridge/facade-api.ts +37 -47
- package/src/cli/local-bridge/server.ts +2 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -33
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +125 -101
- package/src/cli/mcp/server.ts +57 -13
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset.ts +2 -2
- package/src/cli/mcp/transports/http.ts +23 -0
- package/src/cli/repositories/command-classifier.ts +2 -2
- package/src/cli/repositories/command-executor.ts +24 -6
- package/src/cli/repositories/repository-snapshot.ts +5 -2
- package/src/cli/repositories/structured-git.ts +57 -3
- package/src/cli/repository/inspector.ts +11 -3
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +9 -61
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +69 -29
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +150 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
- package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
- package/src/runtime/control-plane/facade/index.ts +2 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/types.ts +68 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
- package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +18 -6
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/process-runtime/check-result.ts +53 -18
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
- package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
- package/src/runtime/execution/process-runtime/runtime.ts +10 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -12
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -13
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/router.ts +2 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +815 -1015
- package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
- package/src/runtime/plugins/browser-adapter.ts +256 -1083
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +18 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +10 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
- package/src/runtime/plugins/external-adapter.ts +92 -58
- package/src/runtime/plugins/macos-capability-broker.ts +0 -8
- package/src/runtime/plugins/store.ts +66 -9
- package/src/runtime/plugins/types.ts +3 -1
- package/src/runtime/recovery/maintenance-executor.ts +61 -25
- package/src/runtime/root/package-connector-service.ts +46 -4
- package/src/runtime/root/package-runtime-release.ts +3 -2
- package/src/runtime/root/package-runtime-service.ts +48 -3
- package/src/runtime/root/release-manifest.ts +137 -16
- package/src/runtime/root/release-materialize.ts +109 -35
- package/src/runtime/root/types.ts +10 -0
- package/src/runtime/standalone-recovery/core.ts +425 -194
- package/src/runtime/standalone-recovery/entry.ts +1 -1
- package/src/runtime/standalone-recovery/installer.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +113 -87
- package/src/runtime/workflow/schedules/settlement.ts +26 -23
- 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/
|
|
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,
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
1157
|
+
store: { controllerHome: string; repoId: string },
|
|
1154
1158
|
workId: string,
|
|
1155
1159
|
args: Record<string, unknown>,
|
|
1156
|
-
):
|
|
1157
|
-
const
|
|
1158
|
-
if (
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
|
1197
|
+
function bindFacadeControllerOwnership(
|
|
1193
1198
|
ctx: MultiRepositoryMcpToolContext,
|
|
1194
|
-
|
|
1199
|
+
store: { controllerHome: string; repoId: string },
|
|
1195
1200
|
workId: string,
|
|
1196
|
-
|
|
1201
|
+
identity: ReturnType<typeof authenticatedFacadeControllerIdentity>,
|
|
1202
|
+
options: { allowClaimIfMissing?: boolean; leaseMs?: number } = {},
|
|
1197
1203
|
) {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
|
1218
|
+
function currentFacadeTerminalizationAuthority(
|
|
1219
1219
|
ctx: MultiRepositoryMcpToolContext,
|
|
1220
|
-
|
|
1220
|
+
store: { controllerHome: string; repoId: string },
|
|
1221
1221
|
workId: string,
|
|
1222
1222
|
args: Record<string, unknown>,
|
|
1223
|
-
):
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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
|
-
|
|
1232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1317
|
-
|
|
1239
|
+
const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
|
|
1240
|
+
if (!ownerInstanceId) {
|
|
1241
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
1318
1242
|
}
|
|
1319
|
-
if (
|
|
1320
|
-
const
|
|
1321
|
-
if (
|
|
1322
|
-
|
|
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
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1601
|
-
|
|
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:
|
|
1610
|
-
...(
|
|
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
|
|
2312
|
-
|
|
2313
|
-
|
|
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
|
|
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
|
-
|
|
2643
|
+
checkoutId: string,
|
|
2644
|
+
) {
|
|
2504
2645
|
const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
2505
|
-
const
|
|
2506
|
-
if (!
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
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
|
-
|
|
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
|
|
2598
|
-
|
|
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
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
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
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
const
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3288
|
-
const
|
|
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
|
-
|
|
3530
|
-
|
|
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:
|
|
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 ((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
4422
|
-
|
|
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
|
|
4580
|
-
const title = typeof args.requirement_title === 'string' ? args.requirement_title
|
|
4581
|
-
const outcomeStatement = typeof args.requirement_outcome === 'string' ? args.requirement_outcome
|
|
4582
|
-
|
|
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
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
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
|
-
|
|
4615
|
-
|
|
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:
|
|
4629
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
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:
|
|
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
|
-
|
|
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 || (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
4995
|
-
const
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
}
|
|
5000
|
-
|
|
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
|
-
|
|
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
|
|
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:${
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|