@moretea-labs/forge 1.7.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +13 -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 +85 -89
- 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 +18 -25
- 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 +96 -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 +805 -1009
- 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 +1 -1
- package/src/runtime/root/package-runtime-service.ts +33 -2
- 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,6 +1103,7 @@ 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
1109
|
const sessionId = ctx.sessionId?.trim();
|
|
@@ -1129,7 +1115,7 @@ function authenticatedFacadeControllerIdentity(
|
|
|
1129
1115
|
if (requestedControllerId && requestedControllerId !== principalId) {
|
|
1130
1116
|
throw new Error('CONTROLLER_ID_CONTEXT_MISMATCH: controller_id must match the authenticated principal');
|
|
1131
1117
|
}
|
|
1132
|
-
if (requestedSessionId && requestedSessionId !== sessionId) {
|
|
1118
|
+
if (!options.allowTransportSessionRollover && requestedSessionId && requestedSessionId !== sessionId) {
|
|
1133
1119
|
throw new Error('CONTROLLER_SESSION_CONTEXT_MISMATCH: session_id must match the authenticated MCP session');
|
|
1134
1120
|
}
|
|
1135
1121
|
const requestedControllerType = typeof args.controller_type === 'string' && ['chatgpt', 'codex', 'claude', 'grok', 'human'].includes(args.controller_type)
|
|
@@ -1148,245 +1134,174 @@ function authenticatedFacadeControllerIdentity(
|
|
|
1148
1134
|
};
|
|
1149
1135
|
}
|
|
1150
1136
|
|
|
1151
|
-
function
|
|
1137
|
+
export function dispatchedChatgptRelayAuthorizesStaleControllerRecovery(
|
|
1138
|
+
relay: ControllerRoundRelayRecord | undefined,
|
|
1139
|
+
controllerType: 'chatgpt' | 'codex' | 'claude' | 'grok' | 'human',
|
|
1140
|
+
): boolean {
|
|
1141
|
+
if (controllerType !== 'chatgpt' || relay?.status !== 'dispatched' || !relay.browserSessionId || !relay.conversationUrl) {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
return hasChatgptConversationIdentity(relay.conversationUrl);
|
|
1146
|
+
} catch {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function assertFacadeControllerRoundAuthority(
|
|
1152
1152
|
ctx: MultiRepositoryMcpToolContext,
|
|
1153
|
-
|
|
1153
|
+
store: { controllerHome: string; repoId: string },
|
|
1154
1154
|
workId: string,
|
|
1155
1155
|
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
|
-
|
|
1156
|
+
): ControllerRoundRelayRecord | undefined {
|
|
1157
|
+
const relay = getControllerRoundRelay(store, workId);
|
|
1158
|
+
if (!relay) return undefined;
|
|
1159
|
+
|
|
1160
|
+
const expectedAuthorityId = relay.authorityId?.trim() || '';
|
|
1161
|
+
if (expectedAuthorityId) {
|
|
1162
|
+
const requestedScopeId = typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : '';
|
|
1163
|
+
const requestedAuthorityId = typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '';
|
|
1164
|
+
if (!requestedScopeId || requestedScopeId !== relay.relayScopeId) {
|
|
1165
|
+
throw new Error(`WORK_CONTROLLER_RELAY_SCOPE_MISMATCH: ${workId}:expected=${relay.relayScopeId}`);
|
|
1166
|
+
}
|
|
1167
|
+
if (!requestedAuthorityId) {
|
|
1168
|
+
throw new Error(`WORK_CONTROLLER_ROUND_AUTHORITY_REQUIRED: ${workId}`);
|
|
1169
|
+
}
|
|
1170
|
+
if (requestedAuthorityId !== expectedAuthorityId) {
|
|
1171
|
+
throw new Error(`WORK_CONTROLLER_ROUND_AUTHORITY_MISMATCH: ${workId}`);
|
|
1172
|
+
}
|
|
1173
|
+
return relay;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Legacy relay records predate the per-round capability. Preserve only the
|
|
1177
|
+
// exact already-claimed durable epoch; never rebind a replacement transport by
|
|
1178
|
+
// shared OAuth principal alone. A fresh launcher/recovery round upgrades the
|
|
1179
|
+
// record and receives an opaque authority capability.
|
|
1180
|
+
const owner = getControllerSession(store, workId);
|
|
1181
|
+
const principalId = ctx.principalId?.trim() || '';
|
|
1182
|
+
const sessionId = ctx.sessionId?.trim() || '';
|
|
1183
|
+
const controllerInstanceId = ctx.controllerInstanceId?.trim() || '';
|
|
1184
|
+
if (
|
|
1185
|
+
owner
|
|
1186
|
+
&& owner.sessionId === sessionId
|
|
1187
|
+
&& controllerSessionPrincipalId(owner) === principalId
|
|
1188
|
+
&& (owner.controllerInstanceId?.trim() || '') === controllerInstanceId
|
|
1189
|
+
) return relay;
|
|
1190
|
+
throw new Error(`WORK_CONTROLLER_ROUND_AUTHORITY_UPGRADE_REQUIRED: ${workId}`);
|
|
1190
1191
|
}
|
|
1191
1192
|
|
|
1192
|
-
function
|
|
1193
|
+
function bindFacadeControllerOwnership(
|
|
1193
1194
|
ctx: MultiRepositoryMcpToolContext,
|
|
1194
|
-
|
|
1195
|
+
store: { controllerHome: string; repoId: string },
|
|
1195
1196
|
workId: string,
|
|
1196
|
-
|
|
1197
|
+
identity: ReturnType<typeof authenticatedFacadeControllerIdentity>,
|
|
1198
|
+
options: { allowClaimIfMissing?: boolean; leaseMs?: number } = {},
|
|
1197
1199
|
) {
|
|
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 },
|
|
1200
|
+
const runtime = runtimeIdentitySnapshot(ctx);
|
|
1201
|
+
return bindControllerSessionToCurrentRuntime(store, {
|
|
1202
|
+
workId,
|
|
1203
|
+
controllerId: identity.controllerId,
|
|
1204
|
+
controllerType: identity.controllerType,
|
|
1205
|
+
sessionId: identity.sessionId,
|
|
1206
|
+
principalId: identity.principalId,
|
|
1207
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
1208
|
+
currentRuntimeInstanceId: runtime.running ? runtime.runtimeInstanceId : undefined,
|
|
1209
|
+
allowClaimIfMissing: options.allowClaimIfMissing,
|
|
1210
|
+
leaseMs: options.leaseMs ?? 3_600_000,
|
|
1215
1211
|
});
|
|
1216
1212
|
}
|
|
1217
1213
|
|
|
1218
|
-
function
|
|
1214
|
+
function currentFacadeTerminalizationAuthority(
|
|
1219
1215
|
ctx: MultiRepositoryMcpToolContext,
|
|
1220
|
-
|
|
1216
|
+
store: { controllerHome: string; repoId: string },
|
|
1221
1217
|
workId: string,
|
|
1222
1218
|
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`);
|
|
1219
|
+
): ControllerTerminalizationAuthority {
|
|
1220
|
+
let owner = getControllerSession(store, workId);
|
|
1221
|
+
if (!owner) {
|
|
1222
|
+
throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${workId}; terminalization requires an explicit controller_claim for this exact Work.`);
|
|
1229
1223
|
}
|
|
1230
1224
|
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}`);
|
|
1225
|
+
const ownerPrincipal = controllerSessionPrincipalId(owner);
|
|
1226
|
+
if (owner.controllerId !== identity.controllerId) {
|
|
1227
|
+
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId} is owned by ${owner.controllerId}`);
|
|
1237
1228
|
}
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
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}`);
|
|
1229
|
+
if (owner.controllerType !== identity.controllerType) {
|
|
1230
|
+
throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${workId} is owned by ${owner.controllerType}`);
|
|
1242
1231
|
}
|
|
1243
|
-
if (
|
|
1244
|
-
throw new Error(`
|
|
1232
|
+
if (ownerPrincipal !== identity.principalId) {
|
|
1233
|
+
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
1245
1234
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
if (handle.repositoryId !== repository.repoId || (handle.workContractId && handle.workContractId !== workId)) {
|
|
1250
|
-
throw new Error(`WORK_CANCELLED_RESUME_HANDLE_IDENTITY_MISMATCH: ${workId}`);
|
|
1251
|
-
}
|
|
1252
|
-
if (handle.principalId !== identity.principalId) throw new Error(`WORK_CANCELLED_RESUME_HANDLE_PRINCIPAL_MISMATCH: ${workId}`);
|
|
1253
|
-
if (['committed', 'merged', 'cleaned', 'failed_terminal_cleanup'].includes(handle.state)
|
|
1254
|
-
|| handle.finalization.commit === 'done'
|
|
1255
|
-
|| handle.finalization.merge === 'done'
|
|
1256
|
-
|| handle.cleanupReceipt?.complete === true) {
|
|
1257
|
-
throw new Error(`WORK_CANCELLED_RESUME_DELIVERY_OR_CLEANUP_CONFLICT: ${workId}`);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
const registryRepository = listRepositories(ctx.controllerHome, { includeRemoved: true })
|
|
1262
|
-
.find((candidate) => candidate.repoId === repository.repoId);
|
|
1263
|
-
if (!registryRepository) throw new Error(`WORK_CANCELLED_RESUME_REPOSITORY_MISSING: ${workId}`);
|
|
1264
|
-
const recordedCheckoutId = work.checkoutId?.trim();
|
|
1265
|
-
const recordedWorktree = work.worktreeRef?.trim();
|
|
1266
|
-
const checkoutRecord = recordedCheckoutId
|
|
1267
|
-
? registryRepository.checkouts.find((candidate) => candidate.checkoutId === recordedCheckoutId)
|
|
1268
|
-
: undefined;
|
|
1269
|
-
const checkoutActive = checkoutRecord?.lifecycle === 'active';
|
|
1270
|
-
const worktreePresent = Boolean(recordedWorktree && existsSync(recordedWorktree));
|
|
1271
|
-
|
|
1272
|
-
if (checkoutActive && worktreePresent && recordedCheckoutId && recordedWorktree) {
|
|
1273
|
-
const retained = selectRepositoryCheckout(registryRepository, recordedCheckoutId);
|
|
1274
|
-
if (resolve(retained.canonicalRoot) !== resolve(recordedWorktree)) {
|
|
1275
|
-
throw new Error(`WORK_CANCELLED_RESUME_CHECKOUT_REUSED: ${workId}`);
|
|
1276
|
-
}
|
|
1277
|
-
if (!handle) throw new Error(`WORK_CANCELLED_RESUME_HANDLE_REQUIRED: ${workId}`);
|
|
1278
|
-
if (handle.checkoutId !== recordedCheckoutId || resolve(handle.worktreePath) !== resolve(recordedWorktree)) {
|
|
1279
|
-
throw new Error(`WORK_CANCELLED_RESUME_CHECKOUT_OWNERSHIP_MISMATCH: ${workId}`);
|
|
1280
|
-
}
|
|
1281
|
-
const status = repositoryGitStatus(retained);
|
|
1282
|
-
if (!status.branch || status.branch !== handle.branch) throw new Error(`WORK_CANCELLED_RESUME_BRANCH_OWNERSHIP_MISMATCH: ${workId}`);
|
|
1283
|
-
writeWorkHandle(ctx.controllerHome, {
|
|
1284
|
-
...handle,
|
|
1285
|
-
principalId: identity.principalId,
|
|
1286
|
-
sessionId: identity.sessionId,
|
|
1287
|
-
updatedAt: new Date().toISOString(),
|
|
1288
|
-
});
|
|
1289
|
-
resumeRetainedCancelledWorkContract(store, workId, {
|
|
1290
|
-
principalId: identity.principalId,
|
|
1291
|
-
controllerInstanceId: identity.controllerInstanceId,
|
|
1292
|
-
summary: 'Explicit current user re-authorized the same retained cancelled Work; exact managed checkout ownership was revalidated.',
|
|
1293
|
-
});
|
|
1294
|
-
return { reconstructedCheckout: false };
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
if (checkoutActive) throw new Error(`WORK_CANCELLED_RESUME_PRESERVATION_AMBIGUOUS: ${workId}`);
|
|
1298
|
-
if (!recordedCheckoutId || !recordedWorktree || !handle) throw new Error(`WORK_CANCELLED_RESUME_HANDLE_REQUIRED: ${workId}`);
|
|
1299
|
-
|
|
1300
|
-
const actualChangedPaths = work.scopeEvidence?.actualChangedPaths;
|
|
1301
|
-
if (!actualChangedPaths || actualChangedPaths.length !== 0
|
|
1302
|
-
|| work.checks.length !== 0
|
|
1303
|
-
|| work.checkRefs.length !== 0
|
|
1304
|
-
|| work.evidenceState !== 'none') {
|
|
1305
|
-
throw new Error(`WORK_CANCELLED_RESUME_ZERO_DELTA_PROOF_REQUIRED: ${workId}`);
|
|
1306
|
-
}
|
|
1307
|
-
const recordedBase = work.baseRevision?.trim();
|
|
1308
|
-
if (!recordedBase) throw new Error(`WORK_CANCELLED_RESUME_BASE_REVISION_REQUIRED: ${workId}`);
|
|
1309
|
-
const baseRevision = gitCommitAtRef(repository.canonicalRoot, recordedBase);
|
|
1310
|
-
if (!baseRevision) throw new Error(`WORK_CANCELLED_RESUME_BASE_REVISION_MISSING: ${workId}`);
|
|
1311
|
-
const handleBase = handle.baseCommit ? gitCommitAtRef(repository.canonicalRoot, handle.baseCommit) : undefined;
|
|
1312
|
-
const handleHead = handle.expectedHead ? gitCommitAtRef(repository.canonicalRoot, handle.expectedHead) : undefined;
|
|
1313
|
-
if ((handle.baseCommit && handleBase !== baseRevision) || (handle.expectedHead && handleHead !== baseRevision)) {
|
|
1314
|
-
throw new Error(`WORK_CANCELLED_RESUME_REVISION_AMBIGUOUS: ${workId}`);
|
|
1235
|
+
const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
|
|
1236
|
+
if (!ownerInstanceId) {
|
|
1237
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
1315
1238
|
}
|
|
1316
|
-
if (
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
throw new Error(`
|
|
1239
|
+
if (ownerInstanceId !== identity.controllerInstanceId || owner.sessionId !== identity.sessionId) {
|
|
1240
|
+
const relay = assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
1241
|
+
if (!relay?.authorityId?.trim()) {
|
|
1242
|
+
if (ownerInstanceId !== identity.controllerInstanceId) {
|
|
1243
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
1244
|
+
}
|
|
1245
|
+
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
1246
|
}
|
|
1247
|
+
// The opaque per-round relay capability is the durable conversation-round
|
|
1248
|
+
// authority. MCP transport sessions are replaceable execution bindings, so
|
|
1249
|
+
// an exact upgraded relay may reconcile the same authenticated owner onto
|
|
1250
|
+
// the current canonical Runtime/session before the claim-generation fence is
|
|
1251
|
+
// captured for terminalization. Foreign or legacy rounds fail above.
|
|
1252
|
+
owner = bindFacadeControllerOwnership(ctx, store, workId, identity);
|
|
1324
1253
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
1254
|
+
const reboundPrincipal = controllerSessionPrincipalId(owner);
|
|
1255
|
+
const reboundInstanceId = owner.controllerInstanceId?.trim() || '';
|
|
1256
|
+
if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
|
|
1257
|
+
throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
1330
1258
|
}
|
|
1259
|
+
return {
|
|
1260
|
+
controllerId: owner.controllerId,
|
|
1261
|
+
controllerType: owner.controllerType,
|
|
1262
|
+
principalId: reboundPrincipal,
|
|
1263
|
+
controllerInstanceId: reboundInstanceId,
|
|
1264
|
+
claimGeneration: owner.claimGeneration,
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1331
1267
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1268
|
+
function ensureFacadeWorkHandle(
|
|
1269
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
1270
|
+
repository: ReturnType<typeof selected>,
|
|
1271
|
+
workId: string,
|
|
1272
|
+
args: Record<string, unknown>,
|
|
1273
|
+
): WorkHandleState | undefined {
|
|
1274
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
1275
|
+
return ensureRepositoryWorkHandle({
|
|
1276
|
+
controllerHome: ctx.controllerHome,
|
|
1277
|
+
repository,
|
|
1278
|
+
workId,
|
|
1279
|
+
identity: { sessionId: identity.sessionId, principalId: identity.principalId },
|
|
1343
1280
|
});
|
|
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
|
-
}
|
|
1281
|
+
}
|
|
1355
1282
|
|
|
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 };
|
|
1283
|
+
function materializeFacadeWorkPlacement(
|
|
1284
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
1285
|
+
repository: ReturnType<typeof selected>,
|
|
1286
|
+
workId: string,
|
|
1287
|
+
args: Record<string, unknown>,
|
|
1288
|
+
) {
|
|
1289
|
+
return materializeRepositoryWorkPlacement(
|
|
1290
|
+
{ controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
1291
|
+
workId,
|
|
1292
|
+
(contract) => {
|
|
1293
|
+
const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
1294
|
+
requestId: workId,
|
|
1295
|
+
title: contract.objective,
|
|
1296
|
+
baseRef: contract.baseRevision,
|
|
1297
|
+
prepareDependencies: args.needs_dependencies === true,
|
|
1298
|
+
});
|
|
1299
|
+
if (workspace.checkoutId === repository.activeCheckoutId) {
|
|
1300
|
+
throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
|
|
1301
|
+
}
|
|
1302
|
+
return workspace;
|
|
1303
|
+
},
|
|
1304
|
+
);
|
|
1390
1305
|
}
|
|
1391
1306
|
|
|
1392
1307
|
function claimNewFacadeWork(
|
|
@@ -1465,11 +1380,42 @@ async function finalizeFacadeWorkHandle(
|
|
|
1465
1380
|
});
|
|
1466
1381
|
}
|
|
1467
1382
|
|
|
1468
|
-
|
|
1383
|
+
let worktree: ReturnType<typeof selectRepositoryCheckout>;
|
|
1384
|
+
try {
|
|
1385
|
+
worktree = selectRepositoryCheckout(repository, handle.checkoutId, { allowArchived: true });
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1388
|
+
const exactChangedHead = Boolean(handle.managedWorktree
|
|
1389
|
+
&& handle.expectedHead
|
|
1390
|
+
&& handle.baseCommit
|
|
1391
|
+
&& handle.expectedHead !== handle.baseCommit);
|
|
1392
|
+
if (
|
|
1393
|
+
!exactChangedHead
|
|
1394
|
+
|| args.cleanup === false
|
|
1395
|
+
|| (!message.startsWith('CHECKOUT_NOT_ACTIVE:') && !message.startsWith('checkout not found for '))
|
|
1396
|
+
) throw error;
|
|
1397
|
+
// Physical cleanup can succeed before the Work completion receipt is
|
|
1398
|
+
// persisted. Retry through the lower finalizer's exact target-containment
|
|
1399
|
+
// reconciliation instead of requiring a checkout that is already gone.
|
|
1400
|
+
// commit/merge are intentionally disabled here: the missing checkout may
|
|
1401
|
+
// never become permission to replay mutation. If expectedHead is not
|
|
1402
|
+
// already contained by targetBranch, work_finalize fails closed.
|
|
1403
|
+
return callExecutionTool(ctx, 'work_finalize', {
|
|
1404
|
+
...common,
|
|
1405
|
+
commit: false,
|
|
1406
|
+
merge: false,
|
|
1407
|
+
completion_outcome: 'completed_changed',
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1469
1410
|
const status = repositoryGitStatus(worktree);
|
|
1470
1411
|
const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
|
|
1471
1412
|
const committedDelta = Boolean(head && handle.baseCommit && head !== handle.baseCommit);
|
|
1472
|
-
const
|
|
1413
|
+
const requestedCommit = typeof args.commit === 'boolean' ? args.commit : !status.clean;
|
|
1414
|
+
// A clean checkout whose HEAD already differs from base is already committed.
|
|
1415
|
+
// Treat commit=true as satisfied by that exact candidate instead of trying to
|
|
1416
|
+
// manufacture an empty commit and recording GIT_NOTHING_STAGED as a delivery
|
|
1417
|
+
// failure. The candidate is still adopted/revalidated and merge-fenced below.
|
|
1418
|
+
const commit = status.clean && committedDelta ? false : requestedCommit;
|
|
1473
1419
|
const workContract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
|
|
1474
1420
|
if (
|
|
1475
1421
|
status.clean
|
|
@@ -1585,29 +1531,22 @@ function pluginRepository(
|
|
|
1585
1531
|
: selected(ctx, args);
|
|
1586
1532
|
}
|
|
1587
1533
|
|
|
1588
|
-
function
|
|
1589
|
-
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
function legacyIosPluginAction(
|
|
1534
|
+
async function legacyIosPluginAction(
|
|
1593
1535
|
ctx: MultiRepositoryMcpToolContext,
|
|
1594
1536
|
legacyTool: string,
|
|
1595
1537
|
args: Record<string, unknown>,
|
|
1596
|
-
actionId: string,
|
|
1597
|
-
actionArgs: Record<string, unknown>,
|
|
1598
1538
|
): Promise<CallToolResult | undefined> {
|
|
1599
1539
|
const repository = selected(ctx, args);
|
|
1600
|
-
const
|
|
1601
|
-
|
|
1602
|
-
: `legacy-${legacyTool}-${Date.now()}`;
|
|
1540
|
+
const invocation = legacyIosPluginInvocation(legacyTool, args);
|
|
1541
|
+
if (!invocation) return undefined;
|
|
1603
1542
|
return callRuntimeTool(ctx, 'plugin_action_execute', {
|
|
1604
1543
|
repo_id: repository.repoId,
|
|
1605
1544
|
checkout_id: repository.activeCheckoutId,
|
|
1606
1545
|
plugin_id: 'ios',
|
|
1607
|
-
action_id: actionId,
|
|
1608
|
-
request_id: requestId,
|
|
1609
|
-
arguments:
|
|
1610
|
-
...(
|
|
1546
|
+
action_id: invocation.actionId,
|
|
1547
|
+
request_id: invocation.requestId,
|
|
1548
|
+
arguments: invocation.arguments,
|
|
1549
|
+
...(invocation.confirmAuthorization ? { confirm_authorization: true } : {}),
|
|
1611
1550
|
});
|
|
1612
1551
|
}
|
|
1613
1552
|
|
|
@@ -1817,6 +1756,111 @@ export async function controllerReadinessEvidence(
|
|
|
1817
1756
|
};
|
|
1818
1757
|
}
|
|
1819
1758
|
|
|
1759
|
+
/**
|
|
1760
|
+
* Runtime Source drift for MCP readiness.
|
|
1761
|
+
*
|
|
1762
|
+
* `currentRuntimeRoot` is only for tests that pin a Controller Runtime Source
|
|
1763
|
+
* fixture. Callers must never pass an execution repository canonicalRoot here.
|
|
1764
|
+
*/
|
|
1765
|
+
export function runtimeSourceSnapshotStatus(
|
|
1766
|
+
active: RuntimeSourceIdentity | undefined,
|
|
1767
|
+
currentRuntimeRoot?: string,
|
|
1768
|
+
) {
|
|
1769
|
+
const drift = evaluateActiveRuntimeSourceDrift(active, {
|
|
1770
|
+
currentRuntimeRoot,
|
|
1771
|
+
});
|
|
1772
|
+
return {
|
|
1773
|
+
current: drift.current,
|
|
1774
|
+
restartRequired: drift.restartRequired,
|
|
1775
|
+
reasons: drift.reasons,
|
|
1776
|
+
code: drift.code,
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
export function repositoryExecutionReadiness(
|
|
1781
|
+
repoRoot: string,
|
|
1782
|
+
availableChecks: ReturnType<typeof listControllerChecks>,
|
|
1783
|
+
requestedCheckIds: string[] = [],
|
|
1784
|
+
schedulingScope: { repoId?: string; checkoutId?: string } = {},
|
|
1785
|
+
): Record<string, unknown> {
|
|
1786
|
+
const git = gitSnapshot(repoRoot);
|
|
1787
|
+
const registeredCheckIds = availableChecks.map((check) => check.id);
|
|
1788
|
+
const normalizedChecks = normalizeCheckIds(requestedCheckIds, availableChecks);
|
|
1789
|
+
const hasPackageJson = existsSync(join(repoRoot, 'package.json'));
|
|
1790
|
+
const nodeModulesReady = !hasPackageJson || existsSync(join(repoRoot, 'node_modules'));
|
|
1791
|
+
const lockCandidates = [
|
|
1792
|
+
['bun', 'bun.lock'],
|
|
1793
|
+
['bun', 'bun.lockb'],
|
|
1794
|
+
['pnpm', 'pnpm-lock.yaml'],
|
|
1795
|
+
['npm', 'package-lock.json'],
|
|
1796
|
+
['yarn', 'yarn.lock'],
|
|
1797
|
+
] as const;
|
|
1798
|
+
const detectedLock = lockCandidates.find(([, path]) => existsSync(join(repoRoot, path)));
|
|
1799
|
+
const packageManager = detectedLock?.[0];
|
|
1800
|
+
const bootstrapCommand = hasPackageJson && !nodeModulesReady
|
|
1801
|
+
? packageManager === 'bun' ? ['bun', 'install', '--frozen-lockfile']
|
|
1802
|
+
: packageManager === 'pnpm' ? ['pnpm', 'install', '--frozen-lockfile']
|
|
1803
|
+
: packageManager === 'npm' ? ['npm', 'ci']
|
|
1804
|
+
: packageManager === 'yarn' ? ['yarn', 'install', '--frozen-lockfile']
|
|
1805
|
+
: undefined
|
|
1806
|
+
: undefined;
|
|
1807
|
+
const hasPythonManifest = existsSync(join(repoRoot, 'pyproject.toml'))
|
|
1808
|
+
|| existsSync(join(repoRoot, 'requirements.txt'))
|
|
1809
|
+
|| existsSync(join(repoRoot, 'requirements-dev.txt'));
|
|
1810
|
+
const localPythonReady = !hasPythonManifest
|
|
1811
|
+
|| existsSync(join(repoRoot, '.venv', 'bin', 'python'))
|
|
1812
|
+
|| existsSync(join(repoRoot, '.venv', 'Scripts', 'python.exe'));
|
|
1813
|
+
const checkScheduling = buildCheckExecutionSchedule({
|
|
1814
|
+
checks: availableChecks,
|
|
1815
|
+
requestedCheckIds,
|
|
1816
|
+
repoId: schedulingScope.repoId?.trim() || 'selected-repository',
|
|
1817
|
+
checkoutId: schedulingScope.checkoutId?.trim() || 'active',
|
|
1818
|
+
});
|
|
1819
|
+
const blockers = [
|
|
1820
|
+
...(!nodeModulesReady ? [{ code: 'NODE_DEPENDENCIES_MISSING', message: 'package.json is present but node_modules is not materialized in this checkout.' }] : []),
|
|
1821
|
+
...normalizedChecks.invalidCheckIds.map((checkId) => ({ code: 'CHECK_NOT_REGISTERED', message: `Requested check is not registered: ${checkId}`, checkId })),
|
|
1822
|
+
];
|
|
1823
|
+
return {
|
|
1824
|
+
readyForFocusedExecution: blockers.length === 0,
|
|
1825
|
+
git: { head: git.head, branch: git.branch, dirty: git.dirty },
|
|
1826
|
+
checks: {
|
|
1827
|
+
registeredCount: registeredCheckIds.length,
|
|
1828
|
+
registeredCheckIds: registeredCheckIds.slice(0, 80),
|
|
1829
|
+
requestedCheckIds: requestedCheckIds.slice(0, 40),
|
|
1830
|
+
normalized: normalizedChecks,
|
|
1831
|
+
},
|
|
1832
|
+
checkScheduling: {
|
|
1833
|
+
waveCount: checkScheduling.waves.length,
|
|
1834
|
+
maxParallel: checkScheduling.maxParallel,
|
|
1835
|
+
waveSummaries: checkScheduling.waves.map((wave) => `wave ${wave.wave}: ${wave.checkIds.join(', ')}`),
|
|
1836
|
+
conflictSummaries: checkScheduling.conflicts.map((conflict) => {
|
|
1837
|
+
const resources = [...new Set(conflict.resources.flatMap(({ left, right }) => [left.resourceKey, right.resourceKey]))];
|
|
1838
|
+
return `${conflict.leftCheckId} <> ${conflict.rightCheckId}: ${resources.join(', ')}`;
|
|
1839
|
+
}),
|
|
1840
|
+
invalidCheckIds: checkScheduling.invalidCheckIds,
|
|
1841
|
+
guidance: checkScheduling.guidance,
|
|
1842
|
+
},
|
|
1843
|
+
dependencies: {
|
|
1844
|
+
node: {
|
|
1845
|
+
applicable: hasPackageJson,
|
|
1846
|
+
ready: nodeModulesReady,
|
|
1847
|
+
packageManager: packageManager ?? null,
|
|
1848
|
+
lockfile: detectedLock?.[1] ?? null,
|
|
1849
|
+
...(bootstrapCommand ? { bootstrapCommand } : {}),
|
|
1850
|
+
},
|
|
1851
|
+
python: {
|
|
1852
|
+
applicable: hasPythonManifest,
|
|
1853
|
+
localVirtualEnvReady: localPythonReady,
|
|
1854
|
+
advisoryOnly: true,
|
|
1855
|
+
},
|
|
1856
|
+
},
|
|
1857
|
+
blockers,
|
|
1858
|
+
guidance: bootstrapCommand
|
|
1859
|
+
? [`Materialize checkout dependencies before tests/builds: ${bootstrapCommand.join(' ')}`]
|
|
1860
|
+
: [],
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1820
1864
|
export async function controllerReadiness(
|
|
1821
1865
|
ctx: MultiRepositoryMcpToolContext,
|
|
1822
1866
|
repository = ctx.explicitRepository,
|
|
@@ -2308,11 +2352,85 @@ async function runFacadeRepair(
|
|
|
2308
2352
|
const planId = typeof args.plan_id === 'string' ? args.plan_id.trim() : '';
|
|
2309
2353
|
const planStepId = typeof args.plan_step_id === 'string' ? args.plan_step_id.trim() : '';
|
|
2310
2354
|
|
|
2311
|
-
if (planId
|
|
2312
|
-
|
|
2313
|
-
|
|
2355
|
+
if (planId && !planStepId) {
|
|
2356
|
+
const plan = getPlanContract(store, planId);
|
|
2357
|
+
if (!plan) {
|
|
2358
|
+
const facade = buildFacadeResult({ status: 'not_found', summary: `PlanContract ${planId} not found.`, data: { operation: repairOperation, dryRun, planId, repaired: false } });
|
|
2359
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2360
|
+
}
|
|
2361
|
+
if (plan.status !== 'draft') {
|
|
2362
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: `PLAN_DRAFT_REPAIR_STATUS_INVALID: ${plan.planId}:${plan.status}`, data: { operation: repairOperation, dryRun, planId, repaired: false } });
|
|
2363
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2364
|
+
}
|
|
2365
|
+
if (repairOperation !== 'repair' || dryRun) {
|
|
2366
|
+
const facade = buildFacadeResult({
|
|
2367
|
+
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.`,
|
|
2368
|
+
data: { operation: repairOperation, dryRun, plan: summarizePlanContract(plan), repaired: false, repairRequired: true },
|
|
2369
|
+
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' }],
|
|
2370
|
+
});
|
|
2371
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2372
|
+
}
|
|
2373
|
+
const rawSteps = Array.isArray(args.plan_steps)
|
|
2374
|
+
? args.plan_steps
|
|
2375
|
+
: plan.steps.map((step) => ({
|
|
2376
|
+
id: step.id, objective: step.objective, dependencies: step.dependencies, authoritative_files: step.authoritativeFiles,
|
|
2377
|
+
allowed_paths: step.allowedPaths, forbidden_paths: step.forbiddenPaths, check_ids: step.checks, acceptance_criteria: step.acceptanceCriteria,
|
|
2378
|
+
}));
|
|
2379
|
+
const steps = rawSteps
|
|
2380
|
+
.filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step))
|
|
2381
|
+
.map((step) => ({
|
|
2382
|
+
id: String(step.id ?? ''),
|
|
2383
|
+
objective: String(step.objective ?? ''),
|
|
2384
|
+
dependencies: Array.isArray(step.dependencies) ? step.dependencies.map(String) : [],
|
|
2385
|
+
authoritativeFiles: Array.isArray(step.authoritative_files) ? step.authoritative_files.map(String) : [],
|
|
2386
|
+
allowedPaths: Array.isArray(step.allowed_paths) ? step.allowed_paths.map(String) : [],
|
|
2387
|
+
forbiddenPaths: Array.isArray(step.forbidden_paths) ? step.forbidden_paths.map(String) : [],
|
|
2388
|
+
checks: Array.isArray(step.check_ids) ? step.check_ids.map(String) : [],
|
|
2389
|
+
acceptanceCriteria: Array.isArray(step.acceptance_criteria) ? step.acceptance_criteria.map(String) : [],
|
|
2390
|
+
}));
|
|
2391
|
+
const availableChecks = listControllerChecks(repository.canonicalRoot);
|
|
2392
|
+
const normalizedPlanChecks = normalizeCheckIds(steps.flatMap((step) => step.checks), availableChecks);
|
|
2393
|
+
if (normalizedPlanChecks.invalidCheckIds.length > 0) {
|
|
2394
|
+
const facade = buildFacadeResult({
|
|
2395
|
+
status: 'failed',
|
|
2396
|
+
summary: `PLAN_CHECKS_INVALID: ${normalizedPlanChecks.invalidCheckIds.join(', ')}. Draft repair was not persisted.`,
|
|
2397
|
+
data: { operation: repairOperation, dryRun: false, planId, repaired: false, normalizedChecks: normalizedPlanChecks, registeredCheckIds: availableChecks.map((check) => check.id).slice(0, 80) },
|
|
2398
|
+
suggestedNextActions: [],
|
|
2399
|
+
});
|
|
2400
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2401
|
+
}
|
|
2402
|
+
try {
|
|
2403
|
+
const repaired = await repairDraftPlanContractAsync(store, planId, {
|
|
2404
|
+
expectedSourceRevision: plan.sourceRevision,
|
|
2405
|
+
scopeKey: typeof args.scope_key === 'string' ? args.scope_key : plan.scopeKey,
|
|
2406
|
+
sourceRevision: typeof args.source_revision === 'string' ? args.source_revision : plan.sourceRevision,
|
|
2407
|
+
goal: typeof args.objective === 'string' ? args.objective : plan.goal,
|
|
2408
|
+
nonGoals: Array.isArray(args.non_goals) ? args.non_goals.map(String) : plan.nonGoals,
|
|
2409
|
+
assumptions: Array.isArray(args.assumptions) ? args.assumptions.map(String) : plan.assumptions,
|
|
2410
|
+
resolvedDecisions: Array.isArray(args.resolved_decisions) ? args.resolved_decisions.map(String) : plan.resolvedDecisions,
|
|
2411
|
+
stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : plan.stopConditions,
|
|
2412
|
+
replanConditions: Array.isArray(args.replan_conditions) ? args.replan_conditions.map(String) : plan.replanConditions,
|
|
2413
|
+
integrationStrategy: typeof args.integration_strategy === 'string' ? args.integration_strategy : plan.integrationStrategy,
|
|
2414
|
+
steps,
|
|
2415
|
+
});
|
|
2416
|
+
const facade = buildFacadeResult({
|
|
2417
|
+
summary: `PlanContract ${repaired.planId} draft repaired in place; identity and Requirement authority were preserved.`,
|
|
2418
|
+
data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(repaired), repaired: true, replacementPlanCreated: false },
|
|
2419
|
+
suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: repaired.planId }, risk: 'workspace_write', confidence: 'medium' }],
|
|
2420
|
+
});
|
|
2421
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_DRAFT_REPAIR_FAILED', data: { operation: repairOperation, dryRun: false, planId, repaired: false } });
|
|
2314
2424
|
return result(facade as unknown as Record<string, unknown>, true);
|
|
2315
2425
|
}
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
if (planStepId && !planId) {
|
|
2429
|
+
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 } });
|
|
2430
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
if (planId && planStepId) {
|
|
2316
2434
|
const plan = getPlanContract(store, planId);
|
|
2317
2435
|
const step = plan?.steps.find((candidate) => candidate.id === planStepId);
|
|
2318
2436
|
if (!plan || !step) {
|
|
@@ -2496,26 +2614,85 @@ async function runFacadeRepair(
|
|
|
2496
2614
|
return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked' || facade.status === 'approval_required' || facade.status === 'failed');
|
|
2497
2615
|
}
|
|
2498
2616
|
|
|
2499
|
-
function
|
|
2617
|
+
function repositoryWorkHandleHasSourceDelta(
|
|
2618
|
+
repository: ReturnType<typeof selected>,
|
|
2619
|
+
handle: WorkHandleState,
|
|
2620
|
+
): boolean {
|
|
2621
|
+
try {
|
|
2622
|
+
const checkout = selectRepositoryCheckout(repository, handle.checkoutId, { allowArchived: true });
|
|
2623
|
+
const status = repositoryGitStatus(checkout);
|
|
2624
|
+
const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
|
|
2625
|
+
return !status.clean || Boolean(head && handle.baseCommit && head !== handle.baseCommit);
|
|
2626
|
+
} catch (_error) {
|
|
2627
|
+
// A checkout may already have been removed by an earlier bounded cleanup
|
|
2628
|
+
// attempt. The WorkHandle's fenced expectedHead remains authoritative for
|
|
2629
|
+
// whether repository delivery existed and must not be downgraded to an
|
|
2630
|
+
// effect-only semantic completion.
|
|
2631
|
+
return Boolean(handle.expectedHead && handle.baseCommit && handle.expectedHead !== handle.baseCommit);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
function finalizeRemoteEffectWorkFromRepositoryProcessReceipt(
|
|
2500
2636
|
ctx: MultiRepositoryMcpToolContext,
|
|
2501
2637
|
repository: ReturnType<typeof selected>,
|
|
2502
2638
|
workId: string,
|
|
2503
|
-
|
|
2639
|
+
checkoutId: string,
|
|
2640
|
+
) {
|
|
2504
2641
|
const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
2505
|
-
const
|
|
2506
|
-
if (!
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2642
|
+
const work = getWorkContract(store, workId);
|
|
2643
|
+
if (!work) throw new Error(`WORK_REMOTE_EFFECT_PROCESS_BINDING_NOT_FOUND: ${workId}`);
|
|
2644
|
+
if (work.workKind !== 'remote_effect') {
|
|
2645
|
+
throw new Error(`WORK_REMOTE_EFFECT_PROCESS_KIND_MISMATCH: ${workId} is ${work.workKind}, expected remote_effect`);
|
|
2646
|
+
}
|
|
2647
|
+
if (work.status === 'completed' && work.completionReceipt?.source === 'remote_effect') return work;
|
|
2648
|
+
const evidence = listWorkBoundRepositoryRemoteEffectProcessEvidence({
|
|
2649
|
+
controllerHome: ctx.controllerHome,
|
|
2650
|
+
repoId: repository.repoId,
|
|
2651
|
+
checkoutId,
|
|
2652
|
+
workId,
|
|
2653
|
+
})[0];
|
|
2654
|
+
if (!evidence) return undefined;
|
|
2655
|
+
if (!work.evidenceRefs.some((candidate) => candidate.evidenceId === evidence.processId)) {
|
|
2656
|
+
appendWorkEvidence(store, workId, {
|
|
2657
|
+
evidenceId: evidence.processId,
|
|
2658
|
+
title: 'trusted repository remote effect completed',
|
|
2659
|
+
summary: `Trusted Work-attributed git push completed with durable Process ${evidence.processId}.`,
|
|
2660
|
+
detailLevel: 'summary',
|
|
2661
|
+
});
|
|
2515
2662
|
}
|
|
2663
|
+
return recordWorkCompletionReceipt(
|
|
2664
|
+
store,
|
|
2665
|
+
workId,
|
|
2666
|
+
{
|
|
2667
|
+
schemaVersion: 1,
|
|
2668
|
+
receiptId: evidence.processId,
|
|
2669
|
+
source: 'remote_effect',
|
|
2670
|
+
workId,
|
|
2671
|
+
authority: 'repository_process',
|
|
2672
|
+
actionId: evidence.actionId,
|
|
2673
|
+
requestId: evidence.requestId,
|
|
2674
|
+
semanticKey: evidence.semanticKey,
|
|
2675
|
+
resultDigest: evidence.resultDigest,
|
|
2676
|
+
processId: evidence.processId,
|
|
2677
|
+
recordedAt: evidence.finishedAt ?? new Date().toISOString(),
|
|
2678
|
+
},
|
|
2679
|
+
'completed_remote',
|
|
2680
|
+
'remote_effect',
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
function reconcileTerminalFacadeWorkVerifications(
|
|
2685
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
2686
|
+
repository: ReturnType<typeof selected>,
|
|
2687
|
+
workId: string,
|
|
2688
|
+
): { sourceRevision?: string; workspaceFingerprint?: string; workspaceChangedPaths?: string[]; reconciledProcessIds: string[]; workBoundProcessEvidenceIds: string[] } {
|
|
2689
|
+
const resolvedVerification = resolveWorkVerificationContext({ controllerHome: ctx.controllerHome, repository, workId });
|
|
2690
|
+
if (!resolvedVerification.ok) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
|
|
2691
|
+
const { store, workContract, repository: verificationRepository, checks: availableChecks } = resolvedVerification.context;
|
|
2692
|
+
if (!workContract || workContract.completionReceipt) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
|
|
2516
2693
|
const verificationStatus = repositoryGitStatus(verificationRepository);
|
|
2517
2694
|
const sourceRevision = verificationStatus.head ?? undefined;
|
|
2518
|
-
if (!sourceRevision) return { reconciledProcessIds: [] };
|
|
2695
|
+
if (!sourceRevision) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
|
|
2519
2696
|
const verificationHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
2520
2697
|
const deliveryBaseRevision = verificationHandle
|
|
2521
2698
|
? workDeliveryBaseRevision(verificationHandle)
|
|
@@ -2530,7 +2707,22 @@ function reconcileTerminalFacadeWorkVerifications(
|
|
|
2530
2707
|
...verificationStatus.untracked,
|
|
2531
2708
|
])].sort();
|
|
2532
2709
|
const workspaceFingerprint = workspaceValidationFingerprint(verificationRepository.canonicalRoot, verificationStatus);
|
|
2533
|
-
|
|
2710
|
+
// Work-bound repository Process evidence remains semantic/result evidence,
|
|
2711
|
+
// never a typed check receipt. repository_change may use it only when no
|
|
2712
|
+
// checks are declared. local_effect may bind the same exact durable Process
|
|
2713
|
+
// ids for Controller semantic review even when checks are declared; the
|
|
2714
|
+
// normal missing-check gate below still requires typed verification receipts.
|
|
2715
|
+
const workBoundProcessEvidenceIds = (
|
|
2716
|
+
workContract.workKind === 'local_effect'
|
|
2717
|
+
|| (workContract.workKind === 'repository_change' && workContract.checks.length === 0)
|
|
2718
|
+
)
|
|
2719
|
+
? listWorkBoundRepositoryProcessEvidence({
|
|
2720
|
+
controllerHome: ctx.controllerHome,
|
|
2721
|
+
repoId: repository.repoId,
|
|
2722
|
+
checkoutId: verificationRepository.activeCheckoutId,
|
|
2723
|
+
workId,
|
|
2724
|
+
}).map((evidence) => evidence.processId)
|
|
2725
|
+
: [];
|
|
2534
2726
|
const workloopCtx = {
|
|
2535
2727
|
workStore: store,
|
|
2536
2728
|
handoffStore: store,
|
|
@@ -2585,29 +2777,11 @@ function reconcileTerminalFacadeWorkVerifications(
|
|
|
2585
2777
|
const latestContract = getWorkContract(store, workId);
|
|
2586
2778
|
if (latestContract?.checkRefs.some((entry) => entry.receipt?.receiptId === receipt.receiptId)) continue;
|
|
2587
2779
|
|
|
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
2780
|
const legacyEvidence = record.origin?.checkResultReceiptPath
|
|
2595
2781
|
? undefined
|
|
2596
2782
|
: 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;
|
|
2783
|
+
const evidenceState = classifyPersistedCheckTerminalEvidence(record, normalizedCheckId, { legacyEvidence });
|
|
2784
|
+
const failureClass = evidenceState.failureClass;
|
|
2611
2785
|
const infrastructureFailed = receipt.timedOut
|
|
2612
2786
|
|| receipt.cancelled
|
|
2613
2787
|
|| evidenceState.state !== 'matched'
|
|
@@ -2619,500 +2793,59 @@ function reconcileTerminalFacadeWorkVerifications(
|
|
|
2619
2793
|
sourceRevision,
|
|
2620
2794
|
workspaceFingerprint,
|
|
2621
2795
|
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,
|
|
2796
|
+
commandFingerprint: commandFingerprint(normalizedCheckId, receipt.commandId),
|
|
2797
|
+
receipt,
|
|
2798
|
+
infrastructureFailed,
|
|
2799
|
+
checkFailed,
|
|
2980
2800
|
});
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
...(facade.data as Record<string, unknown>),
|
|
2986
|
-
isAcceptanceFailure: false,
|
|
2987
|
-
},
|
|
2988
|
-
} as unknown as Record<string, unknown>);
|
|
2801
|
+
reconciledProcessIds.push(record.processId);
|
|
2802
|
+
} catch {
|
|
2803
|
+
// Exact receipt/process identity is mandatory. Any malformed, stale, or
|
|
2804
|
+
// mismatched terminal Process remains non-authoritative and is ignored.
|
|
2989
2805
|
}
|
|
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
2806
|
}
|
|
3011
|
-
}
|
|
3012
2807
|
|
|
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
|
-
};
|
|
2808
|
+
return { sourceRevision, workspaceFingerprint, workspaceChangedPaths, reconciledProcessIds, workBoundProcessEvidenceIds };
|
|
3032
2809
|
}
|
|
3033
2810
|
|
|
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',
|
|
2811
|
+
async function runFacadeVerify(
|
|
2812
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
2813
|
+
repository: ReturnType<typeof selected>,
|
|
2814
|
+
args: Record<string, unknown>,
|
|
2815
|
+
): Promise<CallToolResult> {
|
|
2816
|
+
const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
2817
|
+
const verification = await executeWorkVerification({
|
|
2818
|
+
controllerHome: ctx.controllerHome,
|
|
2819
|
+
repository,
|
|
2820
|
+
workId: workId || undefined,
|
|
2821
|
+
checkId: String(args.check_id ?? args.checkId ?? '').trim(),
|
|
2822
|
+
requestId: typeof args.request_id === 'string' && args.request_id.trim() ? args.request_id.trim() : undefined,
|
|
2823
|
+
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
2824
|
+
interactiveWaitMs: 0,
|
|
2825
|
+
leaseWaitMs: RH_WORK_VERIFY_LEASE_WAIT_MS,
|
|
2826
|
+
simulate: args.simulate_check === true || args.infrastructure_failed === true || args.check_failed === true || args.skipped === true
|
|
2827
|
+
? {
|
|
2828
|
+
infrastructureFailed: args.infrastructure_failed === true,
|
|
2829
|
+
checkFailed: args.check_failed === true,
|
|
2830
|
+
skipped: args.skipped === true,
|
|
2831
|
+
}
|
|
2832
|
+
: undefined,
|
|
2833
|
+
allowDurableCheckExecution: workId ? ({ work }) => {
|
|
2834
|
+
try {
|
|
2835
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
2836
|
+
const owner = getControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, work.workId);
|
|
2837
|
+
return Boolean(
|
|
2838
|
+
owner
|
|
2839
|
+
&& owner.controllerId === identity.controllerId
|
|
2840
|
+
&& controllerSessionPrincipalId(owner) === identity.principalId
|
|
2841
|
+
&& owner.controllerInstanceId === identity.controllerInstanceId,
|
|
2842
|
+
);
|
|
2843
|
+
} catch {
|
|
2844
|
+
return false;
|
|
2845
|
+
}
|
|
2846
|
+
} : undefined,
|
|
3072
2847
|
});
|
|
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
|
-
};
|
|
2848
|
+
return result(verification.facade as unknown as Record<string, unknown>, verification.isError);
|
|
3116
2849
|
}
|
|
3117
2850
|
|
|
3118
2851
|
export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>): Promise<CallToolResult | undefined> {
|
|
@@ -3142,8 +2875,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3142
2875
|
ctx.runtimeSourceRoot,
|
|
3143
2876
|
);
|
|
3144
2877
|
const sourceSnapshotStale = runtimeSource.restartRequired;
|
|
3145
|
-
const
|
|
3146
|
-
const exposure = toolset.controllerToolSurfaceStatus(ctx);
|
|
2878
|
+
const exposure = controllerToolSurfaceStatus(ctx);
|
|
3147
2879
|
const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
|
|
3148
2880
|
const ready = observation.ready && toolSurfaceReady && !sourceSnapshotStale;
|
|
3149
2881
|
const reasonCodes = [...observation.reasonCodes];
|
|
@@ -3284,10 +3016,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3284
3016
|
// never against the selected execution repository.
|
|
3285
3017
|
const runtimeSource = runtimeSourceSnapshotStatus(readiness.daemon.source, ctx.runtimeSourceRoot);
|
|
3286
3018
|
const sourceSnapshotStale = runtimeSource.restartRequired;
|
|
3287
|
-
|
|
3288
|
-
const
|
|
3289
|
-
const exposure = toolset.controllerExposureSnapshot(ctx);
|
|
3290
|
-
const localRegisteredToolNames = toolset.allControllerToolDefinitions(ctx).map((tool) => tool.name).sort();
|
|
3019
|
+
const exposure = controllerExposureSnapshot(ctx);
|
|
3020
|
+
const localRegisteredToolNames = allControllerToolDefinitions(ctx).map((tool) => tool.name).sort();
|
|
3291
3021
|
markDetailPhase('tool_surface');
|
|
3292
3022
|
const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
|
|
3293
3023
|
// Ordinary interactive execution depends on the execution axis only.
|
|
@@ -3526,8 +3256,12 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
3526
3256
|
if (item.workId && ctx.principalId?.trim() && ctx.sessionId?.trim()) {
|
|
3527
3257
|
const owner = getControllerSession(store, item.workId);
|
|
3528
3258
|
if (owner && owner.controllerId === ctx.principalId.trim() && owner.sessionId === ctx.sessionId.trim()) {
|
|
3529
|
-
|
|
3530
|
-
|
|
3259
|
+
const released = releaseObservedControllerSession(store, {
|
|
3260
|
+
workId: item.workId,
|
|
3261
|
+
actor: `handoff-create:${ctx.principalId.trim()}`,
|
|
3262
|
+
owner,
|
|
3263
|
+
});
|
|
3264
|
+
ownershipReleased = released.allowed;
|
|
3531
3265
|
}
|
|
3532
3266
|
}
|
|
3533
3267
|
return result(buildFacadeResult({ summary: `Created handoff ${item.id}.`, data: { item: summarizeHandoffItem(item), ownershipReleased } }) as unknown as Record<string, unknown>);
|
|
@@ -4190,13 +3924,28 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4190
3924
|
const work = getWorkContract(store, workId);
|
|
4191
3925
|
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
4192
3926
|
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
3927
|
+
assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
3928
|
+
const observedOwner = getControllerSession(store, workId);
|
|
3929
|
+
const dispatchedRelay = getControllerRoundRelay(store, workId);
|
|
3930
|
+
const crossOwnerRecovery = Boolean(observedOwner)
|
|
3931
|
+
&& (
|
|
3932
|
+
observedOwner!.controllerId !== identity.controllerId
|
|
3933
|
+
|| controllerSessionPrincipalId(observedOwner!) !== identity.principalId
|
|
3934
|
+
)
|
|
3935
|
+
&& dispatchedChatgptRelayAuthorizesStaleControllerRecovery(dispatchedRelay, identity.controllerType);
|
|
4193
3936
|
const session = resumeControllerSession(store, {
|
|
4194
3937
|
workId,
|
|
4195
3938
|
controllerId: identity.controllerId,
|
|
4196
|
-
controllerType:
|
|
3939
|
+
controllerType: identity.controllerType,
|
|
4197
3940
|
sessionId: identity.sessionId,
|
|
4198
3941
|
principalId: identity.principalId,
|
|
4199
3942
|
controllerInstanceId: identity.controllerInstanceId,
|
|
3943
|
+
...(crossOwnerRecovery
|
|
3944
|
+
? {
|
|
3945
|
+
expectedClaimGeneration: observedOwner!.claimGeneration,
|
|
3946
|
+
allowStaleRecovery: true,
|
|
3947
|
+
}
|
|
3948
|
+
: {}),
|
|
4200
3949
|
leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
4201
3950
|
});
|
|
4202
3951
|
const permissionSnapshotVersion = currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId);
|
|
@@ -4238,7 +3987,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4238
3987
|
if (!['continue_immediately', 'wait', 'wait_for_user', 'goal_complete'].includes(disposition)) {
|
|
4239
3988
|
throw new Error('CONTROLLER_RELAY_DISPOSITION_INVALID');
|
|
4240
3989
|
}
|
|
4241
|
-
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
3990
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
|
|
4242
3991
|
const work = getWorkContract(store, workId);
|
|
4243
3992
|
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
4244
3993
|
const terminalGoalComplete = work.status === 'completed' && disposition === 'goal_complete';
|
|
@@ -4252,7 +4001,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4252
4001
|
if (currentOwner) {
|
|
4253
4002
|
if (currentOwner.controllerType !== 'chatgpt') throw new Error(`CONTROLLER_RELAY_CHATGPT_ONLY: ${workId}`);
|
|
4254
4003
|
if (currentOwner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
|
|
4255
|
-
if ((
|
|
4004
|
+
if ((controllerSessionPrincipalId(currentOwner)) !== identity.principalId) {
|
|
4256
4005
|
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
4257
4006
|
}
|
|
4258
4007
|
// Finalization may terminalize the Work before the prompt-required
|
|
@@ -4261,21 +4010,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4261
4010
|
// rewrite a terminal Work lease here; terminal authorization is fenced
|
|
4262
4011
|
// by the already-claimed relay lineage in submitControllerRoundDisposition.
|
|
4263
4012
|
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
|
-
}
|
|
4013
|
+
bindFacadeControllerOwnership(ctx, store, workId, { ...identity, controllerType: 'chatgpt' });
|
|
4279
4014
|
}
|
|
4280
4015
|
}
|
|
4281
4016
|
const chatgptBinding = getChatgptWorkConversationBinding(store, workId);
|
|
@@ -4308,20 +4043,42 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4308
4043
|
if (operation === 'controller_release') {
|
|
4309
4044
|
try {
|
|
4310
4045
|
const workId = String(args.work_id ?? '').trim();
|
|
4311
|
-
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
4312
|
-
|
|
4046
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
|
|
4047
|
+
assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
4048
|
+
const observedOwner = getControllerSession(store, workId);
|
|
4049
|
+
const work = getWorkContract(store, workId);
|
|
4050
|
+
const terminalWork = work ? ['completed', 'failed', 'cancelled'].includes(work.status) : false;
|
|
4051
|
+
const owner = observedOwner
|
|
4052
|
+
? terminalWork
|
|
4053
|
+
? (() => {
|
|
4054
|
+
// A terminal Work cannot be rebound to a replacement MCP transport.
|
|
4055
|
+
// Authenticate the caller against the observed durable controller epoch
|
|
4056
|
+
// without mutating it; releaseControllerSessionWithAuthority remains the
|
|
4057
|
+
// exact claim-generation CAS authority for physical lease release.
|
|
4058
|
+
const ownerPrincipal = observedOwner.principalId?.trim() || observedOwner.controllerId;
|
|
4059
|
+
const ownerInstanceId = observedOwner.controllerInstanceId?.trim() || '';
|
|
4060
|
+
if (observedOwner.controllerId !== identity.controllerId) {
|
|
4061
|
+
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
|
|
4062
|
+
}
|
|
4063
|
+
if (observedOwner.controllerType !== identity.controllerType) {
|
|
4064
|
+
throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${workId}`);
|
|
4065
|
+
}
|
|
4066
|
+
if (ownerPrincipal !== identity.principalId) {
|
|
4067
|
+
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
4068
|
+
}
|
|
4069
|
+
if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) {
|
|
4070
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
4071
|
+
}
|
|
4072
|
+
if (typeof observedOwner.claimGeneration !== 'number' || observedOwner.claimGeneration < 1) {
|
|
4073
|
+
throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
4074
|
+
}
|
|
4075
|
+
return observedOwner;
|
|
4076
|
+
})()
|
|
4077
|
+
: bindFacadeControllerOwnership(ctx, store, workId, identity)
|
|
4078
|
+
: undefined;
|
|
4313
4079
|
if (owner) {
|
|
4314
|
-
const ownerPrincipal = owner
|
|
4080
|
+
const ownerPrincipal = controllerSessionPrincipalId(owner);
|
|
4315
4081
|
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
4082
|
if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
|
|
4326
4083
|
throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
4327
4084
|
}
|
|
@@ -4349,6 +4106,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4349
4106
|
relayStore,
|
|
4350
4107
|
{ workId, releasedSession: owner },
|
|
4351
4108
|
) : undefined;
|
|
4109
|
+
const abandonedRelay = owner && !relay
|
|
4110
|
+
? reconcileControllerRoundAfterAbandonedRelease(relayStore, { workId, releasedSession: owner })
|
|
4111
|
+
: undefined;
|
|
4352
4112
|
if (relay?.status === 'dispatching') {
|
|
4353
4113
|
try {
|
|
4354
4114
|
assertAutomatedOperationAllowed('external_controller_wake', {
|
|
@@ -4418,8 +4178,10 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4418
4178
|
}
|
|
4419
4179
|
}
|
|
4420
4180
|
return result(buildFacadeResult({
|
|
4421
|
-
summary:
|
|
4422
|
-
|
|
4181
|
+
summary: abandonedRelay
|
|
4182
|
+
? 'Controller lease released; the claimed round was mechanically marked abandoned without semantic completion and can be relaunched through the bounded launcher path.'
|
|
4183
|
+
: 'Controller lease released.',
|
|
4184
|
+
data: { relay: getControllerRoundRelay(relayStore, workId) ?? abandonedRelay ?? relay, tabSettlement },
|
|
4423
4185
|
}) as unknown as Record<string, unknown>);
|
|
4424
4186
|
} catch (error) {
|
|
4425
4187
|
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller release failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
@@ -4576,57 +4338,46 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4576
4338
|
};
|
|
4577
4339
|
|
|
4578
4340
|
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) {
|
|
4341
|
+
const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id : '';
|
|
4342
|
+
const title = typeof args.requirement_title === 'string' ? args.requirement_title : '';
|
|
4343
|
+
const outcomeStatement = typeof args.requirement_outcome === 'string' ? args.requirement_outcome : '';
|
|
4344
|
+
if (!requirementId.trim() || !title.trim() || !outcomeStatement.trim()) {
|
|
4592
4345
|
return result(buildFacadeResult({
|
|
4593
4346
|
status: 'blocked',
|
|
4594
4347
|
summary: 'REQUIREMENT_CREATE_INPUT_REQUIRED: requirement_id, requirement_title, and requirement_outcome are required.',
|
|
4595
4348
|
data: { requirementCreated: false },
|
|
4596
4349
|
}) as unknown as Record<string, unknown>, true);
|
|
4597
4350
|
}
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4351
|
+
let admission;
|
|
4352
|
+
try {
|
|
4353
|
+
admission = admitRequirement({ controllerHome: ctx.controllerHome }, {
|
|
4354
|
+
requirementId,
|
|
4355
|
+
title,
|
|
4356
|
+
outcomeStatement,
|
|
4357
|
+
acceptanceCriteria: Array.isArray(args.requirement_acceptance_criteria) ? args.requirement_acceptance_criteria.map(String) : [],
|
|
4358
|
+
requiredDeliveryReferences: Array.isArray(args.requirement_delivery_references) ? args.requirement_delivery_references.map(String) : [],
|
|
4359
|
+
legacyAliases: Array.isArray(args.requirement_legacy_aliases) ? args.requirement_legacy_aliases.map(String) : [],
|
|
4360
|
+
});
|
|
4361
|
+
} catch (error) {
|
|
4362
|
+
return result(buildFacadeResult({
|
|
4363
|
+
status: 'blocked',
|
|
4364
|
+
summary: error instanceof Error ? error.message : String(error),
|
|
4365
|
+
data: { requirementCreated: false },
|
|
4366
|
+
}) as unknown as Record<string, unknown>, true);
|
|
4367
|
+
}
|
|
4368
|
+
if (admission.decision === 'existing_conflict') {
|
|
4613
4369
|
return result(buildFacadeResult({
|
|
4614
|
-
|
|
4615
|
-
|
|
4370
|
+
status: 'blocked',
|
|
4371
|
+
summary: `REQUIREMENT_ALREADY_EXISTS_CONFLICT: ${admission.requirement.requirementId}. Existing Requirement authority was not changed.`,
|
|
4372
|
+
data: { requirement: admission.requirement, requirementCreated: false, admissionDecision: admission.decision },
|
|
4616
4373
|
suggestedNextActions: [],
|
|
4617
|
-
}) as unknown as Record<string, unknown
|
|
4374
|
+
}) as unknown as Record<string, unknown>, true);
|
|
4618
4375
|
}
|
|
4619
|
-
const requirement = createRequirement({ controllerHome: ctx.controllerHome }, {
|
|
4620
|
-
requirementId,
|
|
4621
|
-
title,
|
|
4622
|
-
outcomeStatement,
|
|
4623
|
-
acceptanceCriteria,
|
|
4624
|
-
requiredDeliveryReferences,
|
|
4625
|
-
legacyAliases,
|
|
4626
|
-
});
|
|
4627
4376
|
return result(buildFacadeResult({
|
|
4628
|
-
summary:
|
|
4629
|
-
|
|
4377
|
+
summary: admission.decision === 'created'
|
|
4378
|
+
? `Requirement ${admission.requirement.requirementId} created. Requirement authority does not imply a Plan; Controller chooses the next action.`
|
|
4379
|
+
: `REQUIREMENT_AUTHORITY_REUSED: ${admission.requirement.requirementId}. Requirement authority does not imply a Plan; Controller chooses the next action.`,
|
|
4380
|
+
data: { requirement: admission.requirement, requirementCreated: admission.created, admissionDecision: admission.decision },
|
|
4630
4381
|
suggestedNextActions: [],
|
|
4631
4382
|
}) as unknown as Record<string, unknown>);
|
|
4632
4383
|
}
|
|
@@ -4635,6 +4386,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4635
4386
|
try {
|
|
4636
4387
|
if (operation === 'plan_create') {
|
|
4637
4388
|
const rawSteps = Array.isArray(args.plan_steps) ? args.plan_steps : [];
|
|
4389
|
+
const requestedPlanId = String(args.plan_id ?? '').trim();
|
|
4638
4390
|
const requestedRequirementId = typeof args.requirement_id === 'string' && args.requirement_id.trim() ? args.requirement_id.trim() : undefined;
|
|
4639
4391
|
const requestedPlanRelation: 'extend' | 'parallel' | undefined = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
|
|
4640
4392
|
? args.plan_relation
|
|
@@ -4657,16 +4409,43 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4657
4409
|
const renderPlanAdmission = (admission: ReturnType<typeof resolvePlanAdmission>): CallToolResult | undefined => {
|
|
4658
4410
|
if (admission.admissionDecision === 'create_new') return undefined;
|
|
4659
4411
|
if (admission.reason === 'exact_scope_authority' && admission.plan) {
|
|
4412
|
+
const exactDraftRepair = admission.plan.status === 'draft' && requestedPlanId === admission.plan.planId;
|
|
4660
4413
|
const facade = buildFacadeResult({
|
|
4661
|
-
summary:
|
|
4414
|
+
summary: exactDraftRepair
|
|
4415
|
+
? `PLAN_DRAFT_REPAIR_REQUIRED: draft Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; preserve that authority and amend it through rh_work repair.`
|
|
4416
|
+
: `PLAN_AUTHORITY_REUSED: active Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; no duplicate draft was created.`,
|
|
4662
4417
|
data: {
|
|
4663
4418
|
plan: summarizePlanContract(admission.plan),
|
|
4664
4419
|
executionStarted: false,
|
|
4665
4420
|
planContractCreated: false,
|
|
4666
4421
|
admissionDecision: 'reuse_existing',
|
|
4667
4422
|
resolutionRequired: false,
|
|
4423
|
+
...(exactDraftRepair ? { repairRequired: true } : {}),
|
|
4668
4424
|
},
|
|
4669
|
-
suggestedNextActions:
|
|
4425
|
+
suggestedNextActions: exactDraftRepair
|
|
4426
|
+
? [{
|
|
4427
|
+
label: 'Repair this exact draft Plan',
|
|
4428
|
+
tool: 'rh_work',
|
|
4429
|
+
operation: 'repair',
|
|
4430
|
+
payload: {
|
|
4431
|
+
plan_id: admission.plan.planId,
|
|
4432
|
+
repair_operation: 'repair',
|
|
4433
|
+
dry_run: false,
|
|
4434
|
+
scope_key: args.scope_key,
|
|
4435
|
+
source_revision: args.source_revision,
|
|
4436
|
+
objective: args.objective,
|
|
4437
|
+
plan_steps: args.plan_steps,
|
|
4438
|
+
non_goals: args.non_goals,
|
|
4439
|
+
assumptions: args.assumptions,
|
|
4440
|
+
resolved_decisions: args.resolved_decisions,
|
|
4441
|
+
stop_conditions: args.stop_conditions,
|
|
4442
|
+
replan_conditions: args.replan_conditions,
|
|
4443
|
+
integration_strategy: args.integration_strategy,
|
|
4444
|
+
},
|
|
4445
|
+
risk: 'workspace_write',
|
|
4446
|
+
confidence: 'high',
|
|
4447
|
+
}]
|
|
4448
|
+
: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: admission.plan.planId }, risk: 'readonly', confidence: 'high' }],
|
|
4670
4449
|
});
|
|
4671
4450
|
return result(facade as unknown as Record<string, unknown>);
|
|
4672
4451
|
}
|
|
@@ -4828,41 +4607,23 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4828
4607
|
|
|
4829
4608
|
if (operation === 'stop') {
|
|
4830
4609
|
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 ?? ''))) {
|
|
4610
|
+
try {
|
|
4611
|
+
if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
4612
|
+
} catch (error) {
|
|
4613
|
+
const blocked = buildFacadeResult({
|
|
4614
|
+
status: 'blocked',
|
|
4615
|
+
summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`,
|
|
4616
|
+
data: { workId, terminalizationApplied: false },
|
|
4617
|
+
});
|
|
4618
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4619
|
+
}
|
|
4620
|
+
let terminalizationAuthority: ControllerTerminalizationAuthority;
|
|
4621
|
+
try {
|
|
4622
|
+
terminalizationAuthority = currentFacadeTerminalizationAuthority(ctx, store, workId, args);
|
|
4623
|
+
} catch (error) {
|
|
4863
4624
|
const blocked = buildFacadeResult({
|
|
4864
4625
|
status: 'blocked',
|
|
4865
|
-
summary:
|
|
4626
|
+
summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`,
|
|
4866
4627
|
data: { workId, terminalizationApplied: false },
|
|
4867
4628
|
});
|
|
4868
4629
|
return result(blocked as unknown as Record<string, unknown>, true);
|
|
@@ -4934,13 +4695,41 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4934
4695
|
|
|
4935
4696
|
if (operation === 'finalize') {
|
|
4936
4697
|
const workId = String(args.work_id ?? '').trim();
|
|
4937
|
-
|
|
4698
|
+
try {
|
|
4699
|
+
if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
4700
|
+
} catch (error) {
|
|
4701
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`, data: { workId, lifecycleClosed: false } });
|
|
4702
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4703
|
+
}
|
|
4704
|
+
const finalizeReconciliation = workId
|
|
4705
|
+
? reconcileTerminalFacadeWorkVerifications(ctx, repository, workId)
|
|
4706
|
+
: undefined;
|
|
4707
|
+
const semanticFinalizeContext = {
|
|
4708
|
+
...workloopCtx,
|
|
4709
|
+
sourceRevision: finalizeReconciliation?.sourceRevision ?? workloopCtx.sourceRevision ?? undefined,
|
|
4710
|
+
workspaceFingerprint: finalizeReconciliation?.workspaceFingerprint,
|
|
4711
|
+
workspaceChangedPaths: finalizeReconciliation?.workspaceChangedPaths,
|
|
4712
|
+
workBoundProcessEvidenceIds: finalizeReconciliation?.workBoundProcessEvidenceIds,
|
|
4713
|
+
};
|
|
4938
4714
|
let before = workId ? getWorkContract(store, workId) : undefined;
|
|
4715
|
+
let terminalizationAuthority: ControllerTerminalizationAuthority | undefined;
|
|
4716
|
+
// Finalize may commit, merge, clean resources, and complete the Work.
|
|
4717
|
+
// It therefore shares the same exact-claim authority as stop. Transport
|
|
4718
|
+
// or Runtime recovery remains an explicit controller_claim operation;
|
|
4719
|
+
// terminalization itself must never rebind an unrelated controller scope.
|
|
4720
|
+
if (before && !['completed', 'failed', 'cancelled'].includes(before.status)) {
|
|
4721
|
+
try {
|
|
4722
|
+
terminalizationAuthority = currentFacadeTerminalizationAuthority(ctx, store, workId, args);
|
|
4723
|
+
} catch (error) {
|
|
4724
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`, data: { workId, terminalizationApplied: false, lifecycleClosed: false } });
|
|
4725
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4939
4728
|
if (before && !before.completionReceipt && args.reconcile_historical_delivery === true) {
|
|
4940
4729
|
try {
|
|
4941
4730
|
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
4942
4731
|
const owner = getControllerSession(store, workId);
|
|
4943
|
-
if (!owner || (
|
|
4732
|
+
if (!owner || (controllerSessionPrincipalId(owner)) !== identity.principalId) {
|
|
4944
4733
|
throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
|
|
4945
4734
|
}
|
|
4946
4735
|
const historicalHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
@@ -4973,31 +4762,60 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
4973
4762
|
: transitionWorkHandle(ctx.controllerHome, historicalHandle, 'committed', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
|
|
4974
4763
|
transitionWorkHandle(ctx.controllerHome, delivered, 'cleaned', { expectedHead: reconciliation.receipt.targetRevision, finalization, failureReason: undefined });
|
|
4975
4764
|
}
|
|
4976
|
-
|
|
4765
|
+
const released = releaseObservedControllerSession(store, {
|
|
4766
|
+
workId,
|
|
4767
|
+
actor: `direct-edit-reconciliation:${identity.principalId}`,
|
|
4768
|
+
owner,
|
|
4769
|
+
});
|
|
4770
|
+
if (!released.allowed) {
|
|
4771
|
+
throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_RELEASE_FENCED: ${workId}:${released.reason}`);
|
|
4772
|
+
}
|
|
4977
4773
|
before = getWorkContract(store, workId);
|
|
4978
4774
|
} catch (error) {
|
|
4979
4775
|
const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Historical Work delivery reconciliation failed.', data: { workId, lifecycleClosed: false } });
|
|
4980
4776
|
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4981
4777
|
}
|
|
4982
4778
|
}
|
|
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
4779
|
const repositoryDeliveryHandle = workId
|
|
4989
4780
|
? readWorkHandle(ctx.controllerHome, repository.repoId, workId)
|
|
4990
4781
|
: undefined;
|
|
4991
|
-
|
|
4782
|
+
const effectHasRepositoryDelta = Boolean(
|
|
4783
|
+
before
|
|
4784
|
+
&& ['local_effect', 'remote_effect'].includes(before.workKind)
|
|
4785
|
+
&& (
|
|
4786
|
+
(repositoryDeliveryHandle && repositoryWorkHandleHasSourceDelta(repository, repositoryDeliveryHandle))
|
|
4787
|
+
|| (finalizeReconciliation?.workspaceChangedPaths?.length ?? 0) > 0
|
|
4788
|
+
)
|
|
4789
|
+
);
|
|
4790
|
+
if (effectHasRepositoryDelta && !repositoryDeliveryHandle) {
|
|
4791
|
+
const blocked = buildFacadeResult({
|
|
4792
|
+
status: 'blocked',
|
|
4793
|
+
summary: `WORK_EFFECT_REPOSITORY_DELIVERY_HANDLE_REQUIRED: ${workId} has repository source delta and cannot use effect-only terminalization without a physical WorkHandle.`,
|
|
4794
|
+
data: { workId, lifecycleClosed: false },
|
|
4795
|
+
});
|
|
4796
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4797
|
+
}
|
|
4798
|
+
// Pure remote effects may complete from either the canonical plugin
|
|
4799
|
+
// action receipt or the deliberately narrow trusted repository
|
|
4800
|
+
// Process authority (`git push` + exact remote lease). A remote_effect
|
|
4801
|
+
// that acquired source delta is no longer pure and must use physical
|
|
4802
|
+
// repository delivery instead.
|
|
4803
|
+
if (before?.workKind === 'remote_effect' && !before.completionReceipt && !effectHasRepositoryDelta) {
|
|
4992
4804
|
try {
|
|
4993
4805
|
before = finalizeRemoteEffectWorkFromActionReceipt(ctx.controllerHome, repository.repoId, workId);
|
|
4994
|
-
} catch (
|
|
4995
|
-
const
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
}
|
|
5000
|
-
|
|
4806
|
+
} catch (pluginError) {
|
|
4807
|
+
const checkoutId = before.checkoutId ?? repositoryDeliveryHandle?.checkoutId ?? repository.activeCheckoutId;
|
|
4808
|
+
const processCompleted = finalizeRemoteEffectWorkFromRepositoryProcessReceipt(ctx, repository, workId, checkoutId);
|
|
4809
|
+
if (processCompleted) {
|
|
4810
|
+
before = processCompleted;
|
|
4811
|
+
} else {
|
|
4812
|
+
const blocked = buildFacadeResult({
|
|
4813
|
+
status: 'blocked',
|
|
4814
|
+
summary: pluginError instanceof Error ? pluginError.message : 'Remote-effect semantic finalization failed.',
|
|
4815
|
+
data: { workId, lifecycleClosed: false },
|
|
4816
|
+
});
|
|
4817
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4818
|
+
}
|
|
5001
4819
|
}
|
|
5002
4820
|
}
|
|
5003
4821
|
const completedCleanupPending = Boolean(
|
|
@@ -5006,7 +4824,15 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5006
4824
|
&& before.worktreeRef?.trim()
|
|
5007
4825
|
&& existsSync(before.worktreeRef),
|
|
5008
4826
|
);
|
|
5009
|
-
|
|
4827
|
+
const repositoryDeliveryRequired = Boolean(
|
|
4828
|
+
before
|
|
4829
|
+
&& before.workKind !== 'read_only_review'
|
|
4830
|
+
&& (
|
|
4831
|
+
!['local_effect', 'remote_effect'].includes(before.workKind)
|
|
4832
|
+
|| effectHasRepositoryDelta
|
|
4833
|
+
)
|
|
4834
|
+
);
|
|
4835
|
+
if (before && ((repositoryDeliveryRequired && !before.completionReceipt) || completedCleanupPending)) {
|
|
5010
4836
|
try {
|
|
5011
4837
|
const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'finalize');
|
|
5012
4838
|
if (physical?.isError === true) return physical;
|
|
@@ -5025,40 +4851,15 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5025
4851
|
let facade;
|
|
5026
4852
|
if (semanticWork && !['completed', 'failed', 'cancelled'].includes(semanticWork.status)) {
|
|
5027
4853
|
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}`);
|
|
4854
|
+
const authority = terminalizationAuthority ?? currentFacadeTerminalizationAuthority(ctx, store, workId, args);
|
|
5048
4855
|
const fenced = withControllerSessionTerminalizationFence(
|
|
5049
4856
|
store,
|
|
5050
4857
|
{
|
|
5051
4858
|
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
|
-
},
|
|
4859
|
+
actor: `rh-work-finalize:${authority.controllerId}:${authority.controllerInstanceId}`,
|
|
4860
|
+
authority,
|
|
5060
4861
|
},
|
|
5061
|
-
() => runGoalWorkloop(
|
|
4862
|
+
() => runGoalWorkloop(semanticFinalizeContext, 'finalize', args),
|
|
5062
4863
|
);
|
|
5063
4864
|
if (!fenced.allowed) {
|
|
5064
4865
|
throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`);
|
|
@@ -5073,9 +4874,29 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5073
4874
|
return result(blocked as unknown as Record<string, unknown>, true);
|
|
5074
4875
|
}
|
|
5075
4876
|
} else {
|
|
5076
|
-
facade = runGoalWorkloop(
|
|
4877
|
+
facade = runGoalWorkloop(semanticFinalizeContext, 'finalize', args);
|
|
4878
|
+
}
|
|
4879
|
+
let completed = getWorkContract(store, workId);
|
|
4880
|
+
const postSemanticCleanupPending = Boolean(
|
|
4881
|
+
completed?.completionReceipt
|
|
4882
|
+
&& args.cleanup !== false
|
|
4883
|
+
&& completed.worktreeRef?.trim()
|
|
4884
|
+
&& existsSync(completed.worktreeRef),
|
|
4885
|
+
);
|
|
4886
|
+
if (postSemanticCleanupPending) {
|
|
4887
|
+
try {
|
|
4888
|
+
const physical = await finalizeFacadeWorkHandle(ctx, repository, { ...args, commit: false, merge: false }, 'finalize');
|
|
4889
|
+
if (physical?.isError === true) return physical;
|
|
4890
|
+
completed = getWorkContract(store, workId);
|
|
4891
|
+
} catch (error) {
|
|
4892
|
+
const blocked = buildFacadeResult({
|
|
4893
|
+
status: 'blocked',
|
|
4894
|
+
summary: error instanceof Error ? error.message : `Work ${workId} terminal cleanup reconciliation failed.`,
|
|
4895
|
+
data: { workId, terminalizationApplied: true, lifecycleClosed: false },
|
|
4896
|
+
});
|
|
4897
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
4898
|
+
}
|
|
5077
4899
|
}
|
|
5078
|
-
const completed = getWorkContract(store, workId);
|
|
5079
4900
|
// Finalizing a Work proves the Work lifecycle only. A Plan step may aggregate
|
|
5080
4901
|
// acceptance criteria that are broader than this Work (for example, a canary
|
|
5081
4902
|
// plus a later stabilization soak), so finalize must never synthesize semantic
|
|
@@ -5100,12 +4921,23 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5100
4921
|
let continuationSourceRevision = workloopCtx.sourceRevision;
|
|
5101
4922
|
let continuationWorkspaceFingerprint: string | undefined;
|
|
5102
4923
|
let continuationWorkspaceChangedPaths: string[] | undefined;
|
|
4924
|
+
let continuationWorkBoundProcessEvidenceIds: string[] | undefined;
|
|
5103
4925
|
if (operation === 'continue') {
|
|
5104
4926
|
try {
|
|
5105
4927
|
const workId = String(args.work_id ?? '').trim();
|
|
4928
|
+
if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
5106
4929
|
let work = getWorkContract(store, workId);
|
|
5107
4930
|
if (work?.status === 'cancelled') {
|
|
5108
|
-
const
|
|
4931
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
4932
|
+
const resumed = reauthorizeRetainedCancelledRepositoryWork({
|
|
4933
|
+
controllerHome: ctx.controllerHome,
|
|
4934
|
+
repository,
|
|
4935
|
+
workId,
|
|
4936
|
+
identity,
|
|
4937
|
+
requestedBy: typeof args.requested_by === 'string' ? args.requested_by : undefined,
|
|
4938
|
+
approvalConfirmed: args.approval_confirmed === true,
|
|
4939
|
+
prepareDependencies: args.needs_dependencies === true,
|
|
4940
|
+
});
|
|
5109
4941
|
cancelledWorkReauthorized = true;
|
|
5110
4942
|
reconstructedCancelledCheckout = resumed.reconstructedCheckout;
|
|
5111
4943
|
work = getWorkContract(store, workId);
|
|
@@ -5121,14 +4953,12 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5121
4953
|
controllerInstanceId: identity.controllerInstanceId,
|
|
5122
4954
|
leaseMs: 3_600_000,
|
|
5123
4955
|
});
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
});
|
|
5131
|
-
}
|
|
4956
|
+
rebindRepositoryWorkHandleControllerIdentity({
|
|
4957
|
+
controllerHome: ctx.controllerHome,
|
|
4958
|
+
repositoryId: repository.repoId,
|
|
4959
|
+
workId,
|
|
4960
|
+
identity: { sessionId: identity.sessionId, principalId: identity.principalId },
|
|
4961
|
+
});
|
|
5132
4962
|
if (work.worktreePolicy.required === true && !work.worktreeRef) {
|
|
5133
4963
|
materializeFacadeWorkPlacement(ctx, repository, workId, args);
|
|
5134
4964
|
}
|
|
@@ -5138,6 +4968,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5138
4968
|
continuationSourceRevision = reconciled.sourceRevision ?? continuationSourceRevision;
|
|
5139
4969
|
continuationWorkspaceFingerprint = reconciled.workspaceFingerprint ?? continuationWorkspaceFingerprint;
|
|
5140
4970
|
continuationWorkspaceChangedPaths = reconciled.workspaceChangedPaths ?? continuationWorkspaceChangedPaths;
|
|
4971
|
+
continuationWorkBoundProcessEvidenceIds = reconciled.workBoundProcessEvidenceIds;
|
|
5141
4972
|
}
|
|
5142
4973
|
} catch (error) {
|
|
5143
4974
|
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller resume failed.', data: { operation, executionStarted: false, ownershipResumed: false } });
|
|
@@ -5157,6 +4988,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
5157
4988
|
sourceRevision: continuationSourceRevision ?? undefined,
|
|
5158
4989
|
workspaceFingerprint: continuationWorkspaceFingerprint,
|
|
5159
4990
|
workspaceChangedPaths: continuationWorkspaceChangedPaths,
|
|
4991
|
+
workBoundProcessEvidenceIds: continuationWorkBoundProcessEvidenceIds,
|
|
5160
4992
|
semanticAdmissionLocked: semanticAdmissionRequired,
|
|
5161
4993
|
};
|
|
5162
4994
|
const facade = semanticAdmissionRequired
|
|
@@ -6130,8 +5962,7 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
6130
5962
|
? selected(ctx, args)
|
|
6131
5963
|
: (ctx.explicitRepository ?? (registered.length === 1 ? registered[0] : undefined));
|
|
6132
5964
|
const readiness = await controllerReadiness(ctx, repository);
|
|
6133
|
-
const
|
|
6134
|
-
const exposure = toolset.controllerExposureSnapshot(ctx);
|
|
5965
|
+
const exposure = controllerExposureSnapshot(ctx);
|
|
6135
5966
|
const toolSurfaceReady = exposure.ready && exposure.missingToolNames.length === 0;
|
|
6136
5967
|
const reasonCodes = new Set(readiness.reasonCodes);
|
|
6137
5968
|
if (!toolSurfaceReady) reasonCodes.add('MCP_TOOL_SURFACE_INCOMPLETE');
|
|
@@ -6511,52 +6342,14 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
6511
6342
|
return result(buildWorkflowWatchdogReport(ctx.controllerHome, repository, { staleMinutes: args.stale_minutes, includeProcesses: args.include_processes }) as unknown as Record<string, unknown>);
|
|
6512
6343
|
}
|
|
6513
6344
|
case 'ios_xcode_status':
|
|
6514
|
-
return legacyIosPluginAction(ctx, name, args, 'xcode_status', {});
|
|
6515
6345
|
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
6346
|
case 'ios_project_discover':
|
|
6521
|
-
return legacyIosPluginAction(ctx, name, args, 'discover_project', {});
|
|
6522
6347
|
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
6348
|
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
6349
|
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
6350
|
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
6351
|
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
|
-
});
|
|
6352
|
+
return legacyIosPluginAction(ctx, name, args);
|
|
6560
6353
|
case 'ios_app_install':
|
|
6561
6354
|
case 'ios_app_launch':
|
|
6562
6355
|
case 'ios_simulator_log_tail':
|
|
@@ -6595,7 +6388,9 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
6595
6388
|
}
|
|
6596
6389
|
case 'plugin_action_execute': {
|
|
6597
6390
|
const pluginId = String(args.plugin_id ?? '').trim();
|
|
6391
|
+
const workId = typeof args.work_id === 'string' && args.work_id.trim() ? args.work_id.trim() : undefined;
|
|
6598
6392
|
const repository = pluginRepository(ctx, args, pluginId);
|
|
6393
|
+
const workRepository = workId ? selected(ctx, args) : undefined;
|
|
6599
6394
|
const actionId = String(args.action_id ?? '').trim();
|
|
6600
6395
|
const requestId = String(args.request_id ?? '').trim();
|
|
6601
6396
|
const actionArguments = args.arguments && typeof args.arguments === 'object' && !Array.isArray(args.arguments)
|
|
@@ -6605,7 +6400,8 @@ export async function callRuntimeTool(ctx: MultiRepositoryMcpToolContext, name:
|
|
|
6605
6400
|
pluginId,
|
|
6606
6401
|
actionId,
|
|
6607
6402
|
requestId,
|
|
6608
|
-
workId
|
|
6403
|
+
workId,
|
|
6404
|
+
...(workId && workRepository ? { workRepoId: workRepository.repoId } : {}),
|
|
6609
6405
|
args: actionArguments,
|
|
6610
6406
|
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
6611
6407
|
signal: ctx.signal,
|