@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
|
@@ -1,54 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
3
|
-
import { spawnSync } from 'child_process';
|
|
4
|
-
import { basename, isAbsolute, relative, resolve } from 'path';
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
5
2
|
import type { McpToolDefinition, CallToolResult } from '../../../cli/mcp/tools';
|
|
6
|
-
import type { CompletionReceipt } from '../../../cli/controller/types';
|
|
7
3
|
import type { MultiRepositoryMcpToolContext } from '../../../cli/mcp/multi-repository';
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
getRepository,
|
|
11
|
-
listRepositories,
|
|
12
|
-
resolveRepositorySelection,
|
|
13
|
-
selectRepositoryCheckout,
|
|
14
|
-
setRepositoryCheckoutLifecycle,
|
|
15
|
-
} from '../../../cli/repositories/registry';
|
|
16
|
-
import { withControllerLock } from '../../../cli/repositories/locks';
|
|
17
|
-
import { repositoryGitCommit, repositoryGitDeleteBranch, repositoryGitFinishWorkflow, repositoryGitMergeBranch, repositoryGitRebaseOnto, repositoryGitStatus, repositoryGitDiff } from '../../../cli/repositories/structured-git';
|
|
18
|
-
import { previewRepositoryCommandExecution } from '../../../cli/repositories/command-executor';
|
|
19
|
-
import { classifyRepositoryCommand } from '../../../cli/repositories/command-classifier';
|
|
20
|
-
import { globMatches } from '../../../cli/mcp/paths';
|
|
21
|
-
import { ensureManagedWorkspace } from '../../execution/managed-workspace';
|
|
22
|
-
import { listControllerChecks, type ControllerCheck } from '../../../cli/controller/check-runner';
|
|
23
|
-
import { readRepositoryAccessPolicy } from '../../control-plane/governance/access-policy';
|
|
24
|
-
import { appendWorkEvidence, createWorkContract, getWorkContract, recordWorkCompletionReceipt, transitionWorkContractPhase, updateWorkContract, appendVerificationRecord } from '../../control-plane/facade/work-contract-store';
|
|
25
|
-
import { completeRequirementFromWork } from '../../control-plane/persistence/requirement-store';
|
|
26
|
-
import { isTerminalWorkContractStatus, type VerificationRecord, type WorkReconciliationRecord } from '../../control-plane/facade/types';
|
|
4
|
+
import { repositoryGitStatus, repositoryGitDiff } from '../../../cli/repositories/structured-git';
|
|
5
|
+
import { listControllerChecks } from '../../../cli/controller/check-runner';
|
|
27
6
|
import { buildWorkContinuationSnapshot } from '../../control-plane/facade/work-continuation';
|
|
28
|
-
import {
|
|
29
|
-
import { currentControllerInstanceId, requireExecutionSession, startExecutionSession, updateExecutionSession, type ExecutionSessionContext, type SessionIdentity } from '../../control-plane/execution/session-store';
|
|
7
|
+
import { currentControllerInstanceId, startExecutionSession, type ExecutionSessionContext } from '../../control-plane/execution/session-store';
|
|
30
8
|
import { currentPermissionSnapshotVersion, validateWorkHandle } from '../../control-plane/execution/validation';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { withWorkPrepareRequest } from '../../control-plane/execution/work-prepare-request-store';
|
|
34
|
-
import { markWorkHandleFailed, newWorkId, readWorkHandle, transitionWorkHandle, workDeliveryBaseRevision, writeWorkHandle, type WorkFinalizationStages, type WorkHandleState, type WorkTerminalOutcome } from '../../control-plane/execution/work-handle-store';
|
|
35
|
-
import { cleanupTerminalWork } from '../../control-plane/execution/work-terminal-cleanup';
|
|
36
|
-
import { pushExactWorkRemoteDelivery, type WorkRemoteDeliveryReceipt } from '../../control-plane/execution/work-remote-delivery';
|
|
37
|
-
import { assertResolvedAuthorization, createGoalDelegation, decideAuthorization, resolveAuthorizationRequest, type AuthorizationDecision, type AuthorizationRiskClass } from '../../control-plane/governance/authorization';
|
|
38
|
-
import { readControllerResult, searchControllerResult, writeControllerResult } from '../../evidence/result-store';
|
|
9
|
+
import { resolveAuthorizationRequest } from '../../control-plane/governance/authorization';
|
|
10
|
+
import { readControllerResult, searchControllerResult } from '../../evidence/result-store';
|
|
39
11
|
import { resumeExecutionJobAfterApproval } from '../../execution/jobs/store';
|
|
40
12
|
import { recordMcpTiming, type McpTimingTrace } from '../../diagnostics/mcp-timing';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
13
|
+
import { compactHandle, contractFor, identityFor, makeBoundedWorkResult, principalFor, requireSession, workForSession } from '../../control-plane/execution/work-execution-support';
|
|
14
|
+
import { finalizeWork } from '../../control-plane/execution/work-finalization-service';
|
|
15
|
+
import { bindSessionRepository, prepareWork } from '../../control-plane/execution/work-preparation-service';
|
|
16
|
+
import { executeWork, validateWork } from '../../control-plane/execution/work-operation-service';
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// Compatibility exports: implementation authority lives in control-plane execution.
|
|
20
|
+
export {
|
|
21
|
+
inspectWorkTargetAdvance,
|
|
22
|
+
targetAdvanceLinearMergeCommits,
|
|
23
|
+
planTargetAdvanceValidationAuthority,
|
|
24
|
+
targetAdvanceWorkScopeViolation,
|
|
25
|
+
inspectDirectTargetDelivery,
|
|
26
|
+
inspectTargetDirtyWorkOwnership,
|
|
27
|
+
inspectDirectCanonicalTargetAdvanceReconciliation,
|
|
28
|
+
completionReceiptChangedPaths,
|
|
29
|
+
inspectCleanupOnlyMergedHead,
|
|
30
|
+
resetFinalizationStagesForRequest,
|
|
31
|
+
} from '../../control-plane/execution/work-finalization-service';
|
|
32
|
+
export { releasePreparedWorkOwnership } from '../../control-plane/execution/work-execution-support';
|
|
33
|
+
export { selectDefaultWorkValidationChecks } from '../../control-plane/execution/work-operation-service';
|
|
34
|
+
export type {
|
|
35
|
+
DirectCanonicalTargetAdvanceInspection,
|
|
36
|
+
DirectTargetDeliveryInspection,
|
|
37
|
+
TargetAdvanceValidationTransferPlan,
|
|
38
|
+
TargetDirtyWorkOwnershipInspection,
|
|
39
|
+
WorkTargetAdvanceInspection,
|
|
40
|
+
} from '../../control-plane/execution/work-finalization-service';
|
|
52
41
|
|
|
53
42
|
function definition(name: string, description: string, properties: Record<string, unknown>, required: string[] = [], readOnlyHint = false, destructiveHint = false): McpToolDefinition {
|
|
54
43
|
return { name, description, inputSchema: { type: 'object', properties, ...(required.length ? { required } : {}), additionalProperties: false }, annotations: { readOnlyHint, openWorldHint: false, destructiveHint } };
|
|
@@ -113,18 +102,6 @@ function failure(error: unknown): CallToolResult {
|
|
|
113
102
|
return result({ error: { code, message } }, true);
|
|
114
103
|
}
|
|
115
104
|
|
|
116
|
-
function principalFor(ctx: MultiRepositoryMcpToolContext): string {
|
|
117
|
-
return ctx.principalId?.trim() || `controller-issued:${ctx.controllerInstanceId ?? currentControllerInstanceId()}`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function identityFor(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): SessionIdentity {
|
|
121
|
-
return {
|
|
122
|
-
sessionId: typeof args.session_id === 'string' && args.session_id.trim() ? args.session_id.trim() : ctx.sessionId,
|
|
123
|
-
principalId: principalFor(ctx),
|
|
124
|
-
controllerInstanceId: ctx.controllerInstanceId ?? currentControllerInstanceId(),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
105
|
function startOrResumeSession(ctx: MultiRepositoryMcpToolContext): ExecutionSessionContext {
|
|
129
106
|
const permissionVersion = ctx.explicitRepository ? currentPermissionSnapshotVersion(ctx.controllerHome, ctx.explicitRepository.repoId) : 0;
|
|
130
107
|
return startExecutionSession(ctx.controllerHome, {
|
|
@@ -136,1153 +113,6 @@ function startOrResumeSession(ctx: MultiRepositoryMcpToolContext): ExecutionSess
|
|
|
136
113
|
});
|
|
137
114
|
}
|
|
138
115
|
|
|
139
|
-
function requireSession(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): ExecutionSessionContext {
|
|
140
|
-
return requireExecutionSession(ctx.controllerHome, identityFor(ctx, args));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function requireExplicitRepoId(args: Record<string, unknown>): string {
|
|
144
|
-
const value = typeof args.repo_id === 'string' ? args.repo_id.trim() : '';
|
|
145
|
-
if (!value) throw new Error('REPOSITORY_ID_REQUIRED: repository selection must be explicit for session binding');
|
|
146
|
-
return value;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function selectedRepository(ctx: MultiRepositoryMcpToolContext, session: ExecutionSessionContext, args: Record<string, unknown>, allowSession = true) {
|
|
150
|
-
const requested = typeof args.repo_id === 'string' && args.repo_id.trim() ? args.repo_id.trim() : undefined;
|
|
151
|
-
const selectedRepoId = requested ?? (allowSession ? session.activeRepositoryId : undefined);
|
|
152
|
-
if (!selectedRepoId) throw new Error('SESSION_REPOSITORY_REQUIRED: bind a repository before using this work tool');
|
|
153
|
-
if (session.activeRepositoryId && requested && session.activeRepositoryId !== requested) {
|
|
154
|
-
throw new Error('SESSION_REPOSITORY_MISMATCH: call session_bind_repository before switching repositories');
|
|
155
|
-
}
|
|
156
|
-
return resolveRepositorySelection({ repoId: selectedRepoId, checkoutId: typeof args.checkout_id === 'string' ? args.checkout_id : session.activeCheckoutId, controllerHome: ctx.controllerHome, allowSoleRepository: false });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function compactHandle(handle: WorkHandleState): Record<string, unknown> {
|
|
160
|
-
return {
|
|
161
|
-
workId: handle.workId, sessionId: handle.sessionId, repoId: handle.repositoryId, checkoutId: handle.checkoutId,
|
|
162
|
-
worktreePath: handle.worktreePath, branch: handle.branch, sourceCheckoutId: handle.sourceCheckoutId, goalId: handle.goalId, delegationVersion: handle.delegationVersion,
|
|
163
|
-
workContractId: handle.workContractId, baseCommit: handle.baseCommit, deliveryBaseCommit: handle.deliveryBaseCommit, expectedHead: handle.expectedHead,
|
|
164
|
-
permissionSnapshotVersion: handle.permissionSnapshotVersion, state: handle.state, managedWorktree: handle.managedWorktree,
|
|
165
|
-
createdAt: handle.createdAt, updatedAt: handle.updatedAt, finalization: handle.finalization,
|
|
166
|
-
...(handle.failureReason ? { failureReason: handle.failureReason } : {}),
|
|
167
|
-
...(handle.cleanupResponsibility ? { cleanupResponsibility: handle.cleanupResponsibility } : {}),
|
|
168
|
-
...(handle.terminalResourceDisposition ? { terminalResourceDisposition: handle.terminalResourceDisposition } : {}),
|
|
169
|
-
...(handle.cleanupReceipt ? { cleanupReceipt: handle.cleanupReceipt } : {}),
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function initialStage(): WorkFinalizationStages {
|
|
174
|
-
return { validation: 'pending', commit: 'pending', merge: 'pending', branchCleanup: 'pending', worktreeCleanup: 'pending' };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function gitRevision(root: string, revision: string): string | undefined {
|
|
178
|
-
const output = spawnSync('git', ['-C', root, 'rev-parse', '--verify', `${revision}^{commit}`], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000 });
|
|
179
|
-
return output.status === 0 && typeof output.stdout === 'string' && output.stdout.trim() ? output.stdout.trim() : undefined;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function gitHead(root: string): string | undefined {
|
|
183
|
-
return gitRevision(root, 'HEAD');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function gitCommit(root: string, revision: string, label: string): string {
|
|
187
|
-
const output = spawnSync('git', ['-C', root, 'rev-parse', '--verify', `${revision}^{commit}`], {
|
|
188
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
189
|
-
});
|
|
190
|
-
if (output.status !== 0 || output.error || typeof output.stdout !== 'string' || !output.stdout.trim()) {
|
|
191
|
-
throw new Error(`WORK_HEAD_ADOPTION_${label}_INVALID: ${revision}`);
|
|
192
|
-
}
|
|
193
|
-
return output.stdout.trim();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function gitChangedPaths(root: string, previousHead: string, candidateHead: string): string[] {
|
|
197
|
-
const output = spawnSync('git', ['-C', root, 'diff', '--name-only', '-z', previousHead, candidateHead], {
|
|
198
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000, maxBuffer: 8 * 1024 * 1024,
|
|
199
|
-
});
|
|
200
|
-
if (output.status !== 0 || output.error || typeof output.stdout !== 'string') {
|
|
201
|
-
throw new Error('WORK_HEAD_ADOPTION_CHANGED_PATHS_UNAVAILABLE');
|
|
202
|
-
}
|
|
203
|
-
return [...new Set(output.stdout.split('\0').filter(Boolean))].sort((left, right) => left.localeCompare(right));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function gitMergeBase(root: string, leftHead: string, rightHead: string): string {
|
|
207
|
-
const output = spawnSync('git', ['-C', root, 'merge-base', leftHead, rightHead], {
|
|
208
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
209
|
-
});
|
|
210
|
-
if (output.status !== 0 || output.error || typeof output.stdout !== 'string' || !output.stdout.trim()) {
|
|
211
|
-
throw new Error('WORK_HEAD_ADOPTION_SCOPE_BASE_UNAVAILABLE');
|
|
212
|
-
}
|
|
213
|
-
return output.stdout.trim();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function gitIsAncestor(root: string, ancestor: string, descendant: string): boolean {
|
|
217
|
-
const result = spawnSync('git', ['-C', root, 'merge-base', '--is-ancestor', ancestor, descendant], {
|
|
218
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
219
|
-
});
|
|
220
|
-
if (result.error || (result.status !== 0 && result.status !== 1)) {
|
|
221
|
-
throw new Error(`WORK_TARGET_ADVANCE_ANCESTRY_UNAVAILABLE: ${ancestor} -> ${descendant}`);
|
|
222
|
-
}
|
|
223
|
-
return result.status === 0;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export interface WorkTargetAdvanceInspection {
|
|
227
|
-
relation: 'candidate_contains_target' | 'target_contains_candidate' | 'diverged_clean' | 'diverged_conflict';
|
|
228
|
-
candidateHead: string;
|
|
229
|
-
targetHead: string;
|
|
230
|
-
mergeBase: string;
|
|
231
|
-
candidateChangedPaths: string[];
|
|
232
|
-
targetChangedPaths: string[];
|
|
233
|
-
mergedTree?: string;
|
|
234
|
-
detail?: string;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function inspectWorkTargetAdvance(root: string, candidateRevision: string, targetRevision: string): WorkTargetAdvanceInspection {
|
|
238
|
-
const candidateHead = gitCommit(root, candidateRevision, 'TARGET_ADVANCE_CANDIDATE');
|
|
239
|
-
const targetHead = gitCommit(root, targetRevision, 'TARGET_ADVANCE_TARGET');
|
|
240
|
-
if (gitIsAncestor(root, targetHead, candidateHead)) {
|
|
241
|
-
return {
|
|
242
|
-
relation: 'candidate_contains_target', candidateHead, targetHead, mergeBase: targetHead,
|
|
243
|
-
candidateChangedPaths: gitChangedPaths(root, targetHead, candidateHead), targetChangedPaths: [],
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
if (gitIsAncestor(root, candidateHead, targetHead)) {
|
|
247
|
-
return {
|
|
248
|
-
relation: 'target_contains_candidate', candidateHead, targetHead, mergeBase: candidateHead,
|
|
249
|
-
candidateChangedPaths: [], targetChangedPaths: gitChangedPaths(root, candidateHead, targetHead),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
const mergeBase = gitMergeBase(root, candidateHead, targetHead);
|
|
253
|
-
const candidateChangedPaths = gitChangedPaths(root, mergeBase, candidateHead);
|
|
254
|
-
const targetChangedPaths = gitChangedPaths(root, mergeBase, targetHead);
|
|
255
|
-
const preflight = spawnSync('git', ['-C', root, 'merge-tree', '--write-tree', candidateHead, targetHead], {
|
|
256
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000, maxBuffer: 512 * 1024,
|
|
257
|
-
});
|
|
258
|
-
if (preflight.error || (preflight.status !== 0 && preflight.status !== 1)) {
|
|
259
|
-
throw new Error(`WORK_TARGET_ADVANCE_PREFLIGHT_UNAVAILABLE: ${preflight.error?.message ?? preflight.stderr ?? 'merge-tree failed'}`);
|
|
260
|
-
}
|
|
261
|
-
if (preflight.status === 0) {
|
|
262
|
-
const mergedTree = typeof preflight.stdout === 'string' ? preflight.stdout.trim().split(/\s+/)[0] : undefined;
|
|
263
|
-
return {
|
|
264
|
-
relation: 'diverged_clean', candidateHead, targetHead, mergeBase, candidateChangedPaths, targetChangedPaths,
|
|
265
|
-
...(mergedTree ? { mergedTree } : {}),
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
const detail = `${preflight.stdout ?? ''}\n${preflight.stderr ?? ''}`.trim().slice(0, 1_000);
|
|
269
|
-
return { relation: 'diverged_conflict', candidateHead, targetHead, mergeBase, candidateChangedPaths, targetChangedPaths, ...(detail ? { detail } : {}) };
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export function targetAdvanceLinearMergeCommits(root: string, targetRevision: string, candidateRevision: string): string[] {
|
|
273
|
-
const targetHead = gitCommit(root, targetRevision, 'TARGET_ADVANCE_LINEAR_TARGET');
|
|
274
|
-
const candidateHead = gitCommit(root, candidateRevision, 'TARGET_ADVANCE_LINEAR_CANDIDATE');
|
|
275
|
-
const output = spawnSync('git', ['-C', root, 'rev-list', '--merges', `${targetHead}..${candidateHead}`], {
|
|
276
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000, maxBuffer: 512 * 1024,
|
|
277
|
-
});
|
|
278
|
-
if (output.status !== 0 || output.error || typeof output.stdout !== 'string') {
|
|
279
|
-
throw new Error('WORK_TARGET_ADVANCE_LINEAR_HISTORY_UNAVAILABLE: could not inspect candidate merge history');
|
|
280
|
-
}
|
|
281
|
-
return output.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function normalizedReadScope(scope: string): string {
|
|
285
|
-
return scope.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '') || '.';
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function pathIsWithinReadScope(path: string, scope: string): boolean {
|
|
289
|
-
const normalized = normalizedReadScope(scope);
|
|
290
|
-
return normalized === '.' || path === normalized || path.startsWith(`${normalized}/`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function stableCheckDefinition(check: ControllerCheck | undefined): string | undefined {
|
|
294
|
-
if (!check) return undefined;
|
|
295
|
-
return JSON.stringify({
|
|
296
|
-
id: check.id,
|
|
297
|
-
command: check.command,
|
|
298
|
-
cwd: check.cwd,
|
|
299
|
-
timeoutMs: check.timeoutMs,
|
|
300
|
-
source: check.source,
|
|
301
|
-
effects: check.effects ?? null,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export interface TargetAdvanceValidationTransferPlan {
|
|
306
|
-
transferredRecords: VerificationRecord[];
|
|
307
|
-
reusableCheckIds: string[];
|
|
308
|
-
invalidatedCheckIds: string[];
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function planTargetAdvanceValidationAuthority(input: {
|
|
312
|
-
checkIds: string[];
|
|
313
|
-
checkRefs: VerificationRecord[];
|
|
314
|
-
checksBefore: ControllerCheck[];
|
|
315
|
-
checksAfter: ControllerCheck[];
|
|
316
|
-
candidateHead: string;
|
|
317
|
-
candidateWorkspaceFingerprint: string;
|
|
318
|
-
integratedHead: string;
|
|
319
|
-
integratedWorkspaceFingerprint: string;
|
|
320
|
-
targetChangedPaths: string[];
|
|
321
|
-
recordedAt?: string;
|
|
322
|
-
}): TargetAdvanceValidationTransferPlan {
|
|
323
|
-
const beforeById = new Map(input.checksBefore.map((check) => [check.id, check]));
|
|
324
|
-
const afterById = new Map(input.checksAfter.map((check) => [check.id, check]));
|
|
325
|
-
const transferredRecords: VerificationRecord[] = [];
|
|
326
|
-
const reusableCheckIds: string[] = [];
|
|
327
|
-
const invalidatedCheckIds: string[] = [];
|
|
328
|
-
const recordedAt = input.recordedAt ?? new Date().toISOString();
|
|
329
|
-
for (const checkId of input.checkIds) {
|
|
330
|
-
const before = beforeById.get(checkId);
|
|
331
|
-
const after = afterById.get(checkId);
|
|
332
|
-
const reads = before?.effects?.reads;
|
|
333
|
-
const definitionUnchanged = stableCheckDefinition(before) !== undefined
|
|
334
|
-
&& stableCheckDefinition(before) === stableCheckDefinition(after);
|
|
335
|
-
const readsGitHistory = before?.effects?.git !== undefined;
|
|
336
|
-
const targetTouchesReadScope = !reads || reads.length === 0
|
|
337
|
-
|| input.targetChangedPaths.some((path) => reads.some((scope) => pathIsWithinReadScope(path, scope)));
|
|
338
|
-
const sourcePass = effectiveVerificationEvidence(input.checkRefs, {
|
|
339
|
-
sourceRevision: input.candidateHead,
|
|
340
|
-
workspaceFingerprint: input.candidateWorkspaceFingerprint,
|
|
341
|
-
checkId,
|
|
342
|
-
requestedChecks: input.checkIds,
|
|
343
|
-
}).find((entry) => entry.current && entry.record.outcome === 'valid_pass' && Boolean(entry.record.receipt))?.record;
|
|
344
|
-
if (!definitionUnchanged || readsGitHistory || targetTouchesReadScope || !sourcePass) {
|
|
345
|
-
invalidatedCheckIds.push(checkId);
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
const transferred: VerificationRecord = {
|
|
349
|
-
...sourcePass,
|
|
350
|
-
summary: `Validation authority transferred across target advancement because target-only changes did not intersect check ${checkId} read inputs.`,
|
|
351
|
-
recordedAt,
|
|
352
|
-
sourceRevision: input.integratedHead,
|
|
353
|
-
workspaceFingerprint: input.integratedWorkspaceFingerprint,
|
|
354
|
-
verificationInputFingerprint: verificationInputFingerprint({
|
|
355
|
-
sourceRevision: input.integratedHead,
|
|
356
|
-
workspaceFingerprint: input.integratedWorkspaceFingerprint,
|
|
357
|
-
checkId,
|
|
358
|
-
requestedChecks: input.checkIds,
|
|
359
|
-
}),
|
|
360
|
-
evidenceRef: {
|
|
361
|
-
title: checkId,
|
|
362
|
-
summary: `Reused prior valid receipt after proving target-only path changes do not affect this check's declared inputs.`,
|
|
363
|
-
detailLevel: 'summary',
|
|
364
|
-
},
|
|
365
|
-
};
|
|
366
|
-
transferredRecords.push(transferred);
|
|
367
|
-
reusableCheckIds.push(checkId);
|
|
368
|
-
}
|
|
369
|
-
return { transferredRecords, reusableCheckIds, invalidatedCheckIds };
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
export function targetAdvanceWorkScopeViolation(
|
|
373
|
-
scope: { allowedPaths: string[]; forbiddenPaths: string[] } | undefined,
|
|
374
|
-
changedPaths: string[],
|
|
375
|
-
): { kind: 'forbidden' | 'out_of_scope'; path: string } | undefined {
|
|
376
|
-
if (!scope) return undefined;
|
|
377
|
-
for (const path of changedPaths) {
|
|
378
|
-
if (scope.forbiddenPaths.some((pattern) => globMatches(pattern, path))) return { kind: 'forbidden', path };
|
|
379
|
-
if (scope.allowedPaths.length > 0 && !scope.allowedPaths.some((pattern) => globMatches(pattern, path))) {
|
|
380
|
-
return { kind: 'out_of_scope', path };
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
return undefined;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function replaceTargetAdvanceScopeEvidence(
|
|
387
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
388
|
-
contract: NonNullable<ReturnType<typeof contractFor>>,
|
|
389
|
-
actualChangedPaths: string[],
|
|
390
|
-
): void {
|
|
391
|
-
updateWorkContract({ controllerHome: ctx.controllerHome, repoId: contract.repoId }, contract.workId, {
|
|
392
|
-
scopeEvidence: {
|
|
393
|
-
initialLikelyPaths: contract.scopeEvidence?.initialLikelyPaths ?? contract.allowedPaths,
|
|
394
|
-
inspectedPaths: contract.scopeEvidence?.inspectedPaths ?? [],
|
|
395
|
-
actualChangedPaths: [...new Set(actualChangedPaths)].sort((left, right) => left.localeCompare(right)).slice(0, 500),
|
|
396
|
-
recordedAt: new Date().toISOString(),
|
|
397
|
-
},
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export interface DirectTargetDeliveryInspection {
|
|
402
|
-
integrated: boolean;
|
|
403
|
-
reason: 'not_direct_target' | 'path_mismatch' | 'branch_mismatch' | 'dirty' | 'revision_unavailable' | 'not_reachable' | 'integrated';
|
|
404
|
-
expectedHead?: string;
|
|
405
|
-
targetHead?: string;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Direct-edit Work may already be executing on the delivery branch. In that
|
|
410
|
-
* case Git merge is not a delivery operation at all: the Work commit is
|
|
411
|
-
* integrated once its exact expected revision remains reachable from the
|
|
412
|
-
* target branch. Keep this proof deliberately strict so a feature Work or a
|
|
413
|
-
* dirty/mismatched checkout still follows the normal fail-closed merge path.
|
|
414
|
-
*/
|
|
415
|
-
export function inspectDirectTargetDelivery(
|
|
416
|
-
root: string,
|
|
417
|
-
worktreePath: string,
|
|
418
|
-
managedWorktree: boolean,
|
|
419
|
-
workBranch: string,
|
|
420
|
-
targetBranch: string,
|
|
421
|
-
expectedRevision: string | undefined,
|
|
422
|
-
): DirectTargetDeliveryInspection {
|
|
423
|
-
if (managedWorktree || workBranch !== targetBranch) return { integrated: false, reason: 'not_direct_target' };
|
|
424
|
-
try {
|
|
425
|
-
if (realpathSync(root) !== realpathSync(worktreePath)) return { integrated: false, reason: 'path_mismatch' };
|
|
426
|
-
} catch {
|
|
427
|
-
return { integrated: false, reason: 'path_mismatch' };
|
|
428
|
-
}
|
|
429
|
-
const branch = spawnSync('git', ['-C', root, 'branch', '--show-current'], {
|
|
430
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
431
|
-
});
|
|
432
|
-
if (branch.status !== 0 || typeof branch.stdout !== 'string' || branch.stdout.trim() !== targetBranch) {
|
|
433
|
-
return { integrated: false, reason: 'branch_mismatch' };
|
|
434
|
-
}
|
|
435
|
-
const status = spawnSync('git', ['-C', root, 'status', '--porcelain'], {
|
|
436
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
437
|
-
});
|
|
438
|
-
if (status.status !== 0 || typeof status.stdout !== 'string' || status.stdout.trim()) {
|
|
439
|
-
return { integrated: false, reason: 'dirty' };
|
|
440
|
-
}
|
|
441
|
-
const expectedHead = expectedRevision ? gitRevision(root, expectedRevision) : undefined;
|
|
442
|
-
const targetHead = gitRevision(root, targetBranch);
|
|
443
|
-
if (!expectedHead || !targetHead) return { integrated: false, reason: 'revision_unavailable' };
|
|
444
|
-
if (!gitIsAncestor(root, expectedHead, targetHead)) {
|
|
445
|
-
return { integrated: false, reason: 'not_reachable', expectedHead, targetHead };
|
|
446
|
-
}
|
|
447
|
-
return { integrated: true, reason: 'integrated', expectedHead, targetHead };
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function normalizedRequiredString(args: Record<string, unknown>, key: string): string | undefined {
|
|
451
|
-
return typeof args[key] === 'string' && args[key].trim() ? args[key].trim() : undefined;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function selectWorkFinalizationTarget(
|
|
455
|
-
repository: ReturnType<typeof getRepository>,
|
|
456
|
-
handle: Pick<WorkHandleState, 'sourceCheckoutId' | 'checkoutId'>,
|
|
457
|
-
) {
|
|
458
|
-
const preferredCheckoutId = handle.sourceCheckoutId?.trim();
|
|
459
|
-
if (preferredCheckoutId) {
|
|
460
|
-
try {
|
|
461
|
-
return selectRepositoryCheckout(repository, preferredCheckoutId, { allowArchived: true });
|
|
462
|
-
} catch (error) {
|
|
463
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
464
|
-
// Legacy Work can outlive the registry entry for the source checkout after
|
|
465
|
-
// an earlier partial cleanup. Git refs/worktree administration are
|
|
466
|
-
// repository-common operations, so fall back only for a positively
|
|
467
|
-
// identified missing/removed source checkout; all other errors fail closed.
|
|
468
|
-
if (!message.startsWith('CHECKOUT_NOT_ACTIVE:') && !message.startsWith('checkout not found for ')) throw error;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
return repository;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
function workReturnCheckoutId(
|
|
475
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
476
|
-
handle: Pick<WorkHandleState, 'repositoryId' | 'sourceCheckoutId' | 'checkoutId'>,
|
|
477
|
-
fallbackCheckoutId?: string,
|
|
478
|
-
): string | undefined {
|
|
479
|
-
const repository = getRepository(handle.repositoryId, ctx.controllerHome, { includeRemoved: true });
|
|
480
|
-
const target = selectWorkFinalizationTarget(repository, handle);
|
|
481
|
-
return target.activeCheckoutId ?? repository.activeCheckoutId ?? fallbackCheckoutId;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function exactCommitChangedPaths(repoRoot: string, revision: string): string[] {
|
|
485
|
-
const result = spawnSync('git', ['-C', repoRoot, 'diff-tree', '--root', '--no-commit-id', '--name-only', '-r', revision], {
|
|
486
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
487
|
-
});
|
|
488
|
-
if (result.status !== 0 || result.error || typeof result.stdout !== 'string') {
|
|
489
|
-
throw new Error(`WORK_COMPLETION_RECEIPT_COMMIT_PATHS_UNAVAILABLE: ${revision}`);
|
|
490
|
-
}
|
|
491
|
-
return Array.from(new Set(result.stdout.split('\n').map((entry) => entry.trim()).filter(Boolean)))
|
|
492
|
-
.sort((left, right) => left.localeCompare(right));
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function workOwnedDirtyPaths(
|
|
496
|
-
contract: ReturnType<typeof contractFor>,
|
|
497
|
-
status: ReturnType<typeof repositoryGitStatus>,
|
|
498
|
-
): { dirtyPaths: string[]; ownedPaths: string[] } {
|
|
499
|
-
const dirtyPaths = [...new Set([...status.staged, ...status.unstaged, ...status.untracked])]
|
|
500
|
-
.sort((left, right) => left.localeCompare(right));
|
|
501
|
-
if (!contract) return { dirtyPaths, ownedPaths: dirtyPaths };
|
|
502
|
-
const ownedPaths = dirtyPaths.filter((path) => {
|
|
503
|
-
if (contract.forbiddenPaths.some((pattern) => globMatches(pattern, path))) return false;
|
|
504
|
-
return contract.allowedPaths.length === 0 || contract.allowedPaths.some((pattern) => globMatches(pattern, path));
|
|
505
|
-
});
|
|
506
|
-
return { dirtyPaths, ownedPaths };
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
export function completionReceiptChangedPaths(repoRoot: string, baseRevision: string, deliveryRevision: string): string[] {
|
|
510
|
-
const result = spawnSync('git', ['-C', repoRoot, 'diff', '--name-only', `${baseRevision}..${deliveryRevision}`], {
|
|
511
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
512
|
-
});
|
|
513
|
-
if (result.status !== 0 || result.error || typeof result.stdout !== 'string') {
|
|
514
|
-
throw new Error(`WORK_COMPLETION_RECEIPT_CHANGED_PATHS_UNAVAILABLE: ${baseRevision}..${deliveryRevision}`);
|
|
515
|
-
}
|
|
516
|
-
return Array.from(new Set(result.stdout.split('\n').map((entry) => entry.trim()).filter(Boolean)))
|
|
517
|
-
.sort((left, right) => left.localeCompare(right));
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function completionReceiptForFinalizedWork(
|
|
521
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
522
|
-
handle: WorkHandleState,
|
|
523
|
-
contract: ReturnType<typeof contractFor>,
|
|
524
|
-
args: Record<string, unknown>,
|
|
525
|
-
): CompletionReceipt {
|
|
526
|
-
const repository = getRepository(handle.repositoryId, ctx.controllerHome, { includeRemoved: true });
|
|
527
|
-
const target = selectWorkFinalizationTarget(repository, handle);
|
|
528
|
-
const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
|
|
529
|
-
? args.target_branch.trim()
|
|
530
|
-
: repository.defaultBranch || 'main';
|
|
531
|
-
const targetRevision = gitRevision(target.canonicalRoot, targetBranch);
|
|
532
|
-
if (!targetRevision) throw new Error(`WORK_COMPLETION_RECEIPT_TARGET_REQUIRED: target branch ${targetBranch} is unavailable`);
|
|
533
|
-
const noChange = args.completion_outcome === 'completed_no_change';
|
|
534
|
-
// Delivery authority is always the Work's own revision, never the target
|
|
535
|
-
// branch's newest HEAD. The target may have advanced because of unrelated
|
|
536
|
-
// concurrent Work; targetRevision records integration identity, while
|
|
537
|
-
// deliveryRevision owns path attribution and reachability proof.
|
|
538
|
-
const deliveryRevision = handle.expectedHead ?? handle.baseCommit;
|
|
539
|
-
if (!deliveryRevision) throw new Error('WORK_COMPLETION_RECEIPT_SOURCE_REQUIRED: Work delivery revision is unavailable');
|
|
540
|
-
const reachable = deliveryRevision === targetRevision || spawnSync('git', ['-C', target.canonicalRoot, 'merge-base', '--is-ancestor', deliveryRevision, targetBranch], {
|
|
541
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
542
|
-
}).status === 0;
|
|
543
|
-
if (!reachable) throw new Error(`WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN: ${deliveryRevision} is not reachable from ${targetBranch}`);
|
|
544
|
-
const deliveryBaseRevision = workDeliveryBaseRevision(handle);
|
|
545
|
-
const changedPaths = noChange
|
|
546
|
-
? []
|
|
547
|
-
: !handle.managedWorktree && handle.finalization.commit === 'done'
|
|
548
|
-
? exactCommitChangedPaths(target.canonicalRoot, deliveryRevision)
|
|
549
|
-
: deliveryBaseRevision
|
|
550
|
-
? completionReceiptChangedPaths(target.canonicalRoot, deliveryBaseRevision, deliveryRevision)
|
|
551
|
-
: (() => { throw new Error('WORK_COMPLETION_RECEIPT_BASE_REQUIRED: Work delivery provenance base is unavailable'); })();
|
|
552
|
-
const recordedAt = new Date().toISOString();
|
|
553
|
-
const retainsExplicitDeliveryBranch = args.merge !== true && targetBranch === handle.branch;
|
|
554
|
-
const warnings = [
|
|
555
|
-
...(handle.finalization.branchCleanup === 'skipped' && !retainsExplicitDeliveryBranch ? [{ code: 'cleanup_retained_by_request' as const, message: 'Branch cleanup was skipped by the finalization request.', resourceKind: 'branch' as const, resourceId: handle.branch, recordedAt }] : []),
|
|
556
|
-
...(handle.finalization.worktreeCleanup === 'skipped' && handle.managedWorktree ? [{ code: 'cleanup_retained_by_request' as const, message: 'Managed worktree cleanup was skipped by the finalization request.', resourceKind: 'worktree' as const, resourceId: handle.worktreePath, recordedAt }] : []),
|
|
557
|
-
];
|
|
558
|
-
return {
|
|
559
|
-
schemaVersion: 1,
|
|
560
|
-
receiptId: `REC-controller_work-${createHash('sha256').update(`${handle.workId}\0${targetRevision}`).digest('hex').slice(0, 16)}`,
|
|
561
|
-
source: 'controller_work',
|
|
562
|
-
issueId: contract?.issueId ?? 'work',
|
|
563
|
-
taskId: contract?.taskId ?? handle.workId,
|
|
564
|
-
workId: handle.workId,
|
|
565
|
-
targetBranch,
|
|
566
|
-
targetRevision,
|
|
567
|
-
sourceRevision: deliveryRevision,
|
|
568
|
-
baseRevision: handle.baseCommit,
|
|
569
|
-
changedPaths,
|
|
570
|
-
delivery: {
|
|
571
|
-
kind: noChange ? 'no_change' : 'commit',
|
|
572
|
-
status: 'integrated',
|
|
573
|
-
strategy: noChange ? 'no_change' : handle.finalization.commit === 'done' ? 'edit_session_commit' : 'already_integrated',
|
|
574
|
-
reachable: true,
|
|
575
|
-
recordedAt,
|
|
576
|
-
},
|
|
577
|
-
cleanup: {
|
|
578
|
-
status: warnings.length > 0 ? 'maintenance_warning' : 'complete',
|
|
579
|
-
warnings,
|
|
580
|
-
blockers: [],
|
|
581
|
-
recordedAt,
|
|
582
|
-
},
|
|
583
|
-
verifiedAt: recordedAt,
|
|
584
|
-
recordedAt,
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
function boundedStringArray(value: unknown, limit: number): string[] {
|
|
589
|
-
return Array.isArray(value) ? value.map(String).slice(0, limit) : [];
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function workPrepareFingerprint(input: {
|
|
593
|
-
repoId: string;
|
|
594
|
-
requestedCheckoutId?: string;
|
|
595
|
-
isolation: 'reuse' | 'new_worktree' | 'auto';
|
|
596
|
-
objective: string;
|
|
597
|
-
goalId?: string;
|
|
598
|
-
acceptanceCriteria: string[];
|
|
599
|
-
allowedPaths: string[];
|
|
600
|
-
checks: string[];
|
|
601
|
-
baseRef?: string;
|
|
602
|
-
needsDependencies: boolean;
|
|
603
|
-
}): string {
|
|
604
|
-
return createHash('sha256').update(JSON.stringify({ schemaVersion: 1, operation: 'work_prepare', ...input })).digest('hex');
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
function makeBoundedResult(ctx: MultiRepositoryMcpToolContext, session: ExecutionSessionContext, repoId: string, workIdValue: string | undefined, kind: 'inspection' | 'command' | 'validation' | 'finalization' | 'generic', value: Record<string, unknown>): Record<string, unknown> {
|
|
608
|
-
const serialized = JSON.stringify(value);
|
|
609
|
-
if (Buffer.byteLength(serialized, 'utf8') <= MAX_INLINE_RESULT_BYTES) return value;
|
|
610
|
-
const started = performance.now();
|
|
611
|
-
const stored = writeControllerResult({ controllerHome: ctx.controllerHome, repoId, sessionId: session.sessionId, principalId: session.principalId, workId: workIdValue, kind, value });
|
|
612
|
-
return {
|
|
613
|
-
summary: { itemCount: Array.isArray(value.items) ? value.items.length : undefined, truncated: true, warnings: ['Full result is available through the secure result reference.'] },
|
|
614
|
-
items: Array.isArray(value.items) ? value.items.slice(0, 25) : { preview: serialized.slice(0, 16_384) },
|
|
615
|
-
resultRef: stored.resultRef,
|
|
616
|
-
resultId: stored.resultId,
|
|
617
|
-
byteLength: stored.byteLength,
|
|
618
|
-
_resultPersistenceMs: Math.round((performance.now() - started) * 100) / 100,
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function contractFor(ctx: MultiRepositoryMcpToolContext, handle: WorkHandleState) {
|
|
623
|
-
return handle.workContractId ? getWorkContract({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, handle.workContractId) : undefined;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function findWorkHandle(
|
|
627
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
628
|
-
session: ExecutionSessionContext,
|
|
629
|
-
args: Record<string, unknown>,
|
|
630
|
-
): WorkHandleState {
|
|
631
|
-
const requested = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
632
|
-
const workIdValue = requested || session.activeWorkId || '';
|
|
633
|
-
if (!workIdValue) throw new Error('WORK_ID_REQUIRED: provide work_id or call work_prepare first');
|
|
634
|
-
const requestedRepoId = typeof args.repo_id === 'string' && args.repo_id.trim() ? args.repo_id.trim() : undefined;
|
|
635
|
-
const requestedCheckoutId = typeof args.checkout_id === 'string' && args.checkout_id.trim()
|
|
636
|
-
? args.checkout_id.trim()
|
|
637
|
-
: undefined;
|
|
638
|
-
const repoCandidates = [
|
|
639
|
-
requestedRepoId,
|
|
640
|
-
session.activeRepositoryId,
|
|
641
|
-
].filter((value, index, values): value is string => Boolean(value) && values.indexOf(value) === index);
|
|
642
|
-
for (const repositoryId of repoCandidates) {
|
|
643
|
-
const handle = readWorkHandle(ctx.controllerHome, repositoryId, workIdValue);
|
|
644
|
-
if (!handle) continue;
|
|
645
|
-
if (requestedCheckoutId && handle.checkoutId !== requestedCheckoutId) {
|
|
646
|
-
throw new Error(
|
|
647
|
-
`WORK_HANDLE_CHECKOUT_DRIFT: work ${workIdValue} is bound to checkout ${handle.checkoutId}, not ${requestedCheckoutId}`,
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
return handle;
|
|
651
|
-
}
|
|
652
|
-
const matches = listRepositories(ctx.controllerHome, { includeRemoved: true })
|
|
653
|
-
.map((repository) => readWorkHandle(ctx.controllerHome, repository.repoId, workIdValue))
|
|
654
|
-
.filter((handle): handle is WorkHandleState => Boolean(handle));
|
|
655
|
-
// Legacy incomplete identity: only a unique exact WorkHandle match may execute.
|
|
656
|
-
return resolveLegacyWorkContractIdentity({
|
|
657
|
-
workId: workIdValue,
|
|
658
|
-
repoId: requestedRepoId,
|
|
659
|
-
checkoutId: requestedCheckoutId,
|
|
660
|
-
candidates: matches,
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function currentControllerClaimAuthorizesTerminalCleanup(
|
|
665
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
666
|
-
session: ExecutionSessionContext,
|
|
667
|
-
handle: WorkHandleState,
|
|
668
|
-
): boolean {
|
|
669
|
-
if (!terminalCleanupOutcome(ctx, handle)) return false;
|
|
670
|
-
const workIdValue = handle.workContractId ?? handle.workId;
|
|
671
|
-
const owner = getControllerSession({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, workIdValue);
|
|
672
|
-
if (!owner) return false;
|
|
673
|
-
return (owner.principalId?.trim() || owner.controllerId) === session.principalId
|
|
674
|
-
&& owner.controllerInstanceId === session.controllerInstanceId;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
function workForSession(
|
|
678
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
679
|
-
session: ExecutionSessionContext,
|
|
680
|
-
args: Record<string, unknown>,
|
|
681
|
-
options: { reconcileValidation?: boolean; allowClaimedTerminalCleanup?: boolean } = {},
|
|
682
|
-
): WorkHandleState {
|
|
683
|
-
let handle = findWorkHandle(ctx, session, args);
|
|
684
|
-
if (
|
|
685
|
-
handle.principalId !== session.principalId
|
|
686
|
-
&& !(options.allowClaimedTerminalCleanup === true && currentControllerClaimAuthorizesTerminalCleanup(ctx, session, handle))
|
|
687
|
-
) throw new Error('WORK_HANDLE_PRINCIPAL_MISMATCH: work handle belongs to another principal');
|
|
688
|
-
if (options.reconcileValidation !== false) handle = reconcileWorkValidation(ctx.controllerHome, handle).handle;
|
|
689
|
-
if (
|
|
690
|
-
session.activeRepositoryId !== handle.repositoryId
|
|
691
|
-
|| session.activeCheckoutId !== handle.checkoutId
|
|
692
|
-
|| session.activeWorkId !== handle.workId
|
|
693
|
-
) {
|
|
694
|
-
updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), {
|
|
695
|
-
activeRepositoryId: handle.repositoryId,
|
|
696
|
-
activeCheckoutId: handle.checkoutId,
|
|
697
|
-
activeWorkId: handle.workId,
|
|
698
|
-
permissionSnapshotVersion: handle.permissionSnapshotVersion,
|
|
699
|
-
lastValidatedAt: new Date().toISOString(),
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
return handle;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function assertWorkControllerOwnership(
|
|
706
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
707
|
-
session: ExecutionSessionContext,
|
|
708
|
-
handle: WorkHandleState,
|
|
709
|
-
args: Record<string, unknown>,
|
|
710
|
-
) {
|
|
711
|
-
const workIdValue = handle.workContractId ?? handle.workId;
|
|
712
|
-
const options = { controllerHome: ctx.controllerHome, repoId: handle.repositoryId };
|
|
713
|
-
const owner = getControllerSession(options, workIdValue);
|
|
714
|
-
const controllerId = typeof args.controller_id === 'string' && args.controller_id.trim()
|
|
715
|
-
? args.controller_id.trim()
|
|
716
|
-
: session.principalId;
|
|
717
|
-
if (controllerId !== session.principalId) {
|
|
718
|
-
throw new Error('WORK_CONTROLLER_IDENTITY_MISMATCH: controller_id must match the authenticated principal');
|
|
719
|
-
}
|
|
720
|
-
if (owner) {
|
|
721
|
-
const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
|
|
722
|
-
const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
|
|
723
|
-
if (owner.controllerId !== controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workIdValue} is owned by ${owner.controllerId}`);
|
|
724
|
-
if (ownerPrincipal !== session.principalId) throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workIdValue}`);
|
|
725
|
-
if (!ownerInstanceId || ownerInstanceId !== session.controllerInstanceId) throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workIdValue}`);
|
|
726
|
-
if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workIdValue}`);
|
|
727
|
-
const resumed = resumeControllerSession(options, {
|
|
728
|
-
workId: workIdValue,
|
|
729
|
-
controllerId,
|
|
730
|
-
controllerType: owner.controllerType,
|
|
731
|
-
sessionId: session.sessionId,
|
|
732
|
-
principalId: session.principalId,
|
|
733
|
-
controllerInstanceId: session.controllerInstanceId,
|
|
734
|
-
expectedClaimGeneration: owner.claimGeneration,
|
|
735
|
-
leaseMs: 3_600_000,
|
|
736
|
-
});
|
|
737
|
-
if (
|
|
738
|
-
resumed.controllerId !== controllerId
|
|
739
|
-
|| resumed.principalId !== session.principalId
|
|
740
|
-
|| resumed.controllerInstanceId !== session.controllerInstanceId
|
|
741
|
-
|| resumed.claimGeneration !== owner.claimGeneration
|
|
742
|
-
) {
|
|
743
|
-
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workIdValue} ownership epoch changed during resume`);
|
|
744
|
-
}
|
|
745
|
-
return resumed;
|
|
746
|
-
}
|
|
747
|
-
return claimControllerSession(options, {
|
|
748
|
-
workId: workIdValue,
|
|
749
|
-
controllerId,
|
|
750
|
-
controllerType: 'chatgpt',
|
|
751
|
-
sessionId: session.sessionId,
|
|
752
|
-
principalId: session.principalId,
|
|
753
|
-
controllerInstanceId: session.controllerInstanceId,
|
|
754
|
-
expectedClaimGeneration: 0,
|
|
755
|
-
leaseMs: 3_600_000,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function claimPreparedWorkOwnership(
|
|
760
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
761
|
-
session: ExecutionSessionContext,
|
|
762
|
-
handle: WorkHandleState,
|
|
763
|
-
args: Record<string, unknown>,
|
|
764
|
-
): void {
|
|
765
|
-
const workIdValue = handle.workContractId ?? handle.workId;
|
|
766
|
-
const controllerId = typeof args.controller_id === 'string' && args.controller_id.trim()
|
|
767
|
-
? args.controller_id.trim()
|
|
768
|
-
: session.principalId;
|
|
769
|
-
claimControllerSession({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, {
|
|
770
|
-
workId: workIdValue,
|
|
771
|
-
controllerId,
|
|
772
|
-
controllerType: 'chatgpt',
|
|
773
|
-
sessionId: session.sessionId,
|
|
774
|
-
principalId: session.principalId,
|
|
775
|
-
controllerInstanceId: session.controllerInstanceId,
|
|
776
|
-
leaseMs: 3_600_000,
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
function claimHeadAdoptionOwnership(
|
|
781
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
782
|
-
session: ExecutionSessionContext,
|
|
783
|
-
handle: WorkHandleState,
|
|
784
|
-
args: Record<string, unknown>,
|
|
785
|
-
): void {
|
|
786
|
-
const workIdValue = handle.workContractId ?? handle.workId;
|
|
787
|
-
const controllerId = normalizedRequiredString(args, 'controller_id') ?? session.principalId;
|
|
788
|
-
if (controllerId !== session.principalId) {
|
|
789
|
-
throw new Error('WORK_CONTROLLER_IDENTITY_MISMATCH: controller_id must match the authenticated principal');
|
|
790
|
-
}
|
|
791
|
-
const options = { controllerHome: ctx.controllerHome, repoId: handle.repositoryId };
|
|
792
|
-
const current = getControllerSession(options, workIdValue);
|
|
793
|
-
const input = {
|
|
794
|
-
workId: workIdValue,
|
|
795
|
-
controllerId,
|
|
796
|
-
controllerType: current?.controllerType ?? 'chatgpt' as const,
|
|
797
|
-
sessionId: session.sessionId,
|
|
798
|
-
principalId: session.principalId,
|
|
799
|
-
controllerInstanceId: session.controllerInstanceId,
|
|
800
|
-
expectedClaimGeneration: current?.claimGeneration ?? 0,
|
|
801
|
-
leaseMs: 3_600_000,
|
|
802
|
-
};
|
|
803
|
-
const claimed = current
|
|
804
|
-
? resumeControllerSession(options, input)
|
|
805
|
-
: claimControllerSession(options, input);
|
|
806
|
-
if (
|
|
807
|
-
claimed.controllerId !== controllerId
|
|
808
|
-
|| claimed.sessionId !== session.sessionId
|
|
809
|
-
|| claimed.principalId !== session.principalId
|
|
810
|
-
|| claimed.controllerInstanceId !== session.controllerInstanceId
|
|
811
|
-
|| (claimed.claimGeneration ?? 0) < 1
|
|
812
|
-
) {
|
|
813
|
-
throw new Error('WORK_HEAD_ADOPTION_OWNERSHIP_FENCE_MISMATCH');
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function adoptExistingWorkHead(
|
|
818
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
819
|
-
session: ExecutionSessionContext,
|
|
820
|
-
repository: RepositoryRecord,
|
|
821
|
-
handle: WorkHandleState,
|
|
822
|
-
args: Record<string, unknown>,
|
|
823
|
-
): Record<string, unknown> | undefined {
|
|
824
|
-
const previousInput = normalizedRequiredString(args, 'expected_previous_head');
|
|
825
|
-
const candidateInput = normalizedRequiredString(args, 'adopt_candidate_head');
|
|
826
|
-
if (!previousInput && !candidateInput) return undefined;
|
|
827
|
-
if (!previousInput || !candidateInput) {
|
|
828
|
-
throw new Error('WORK_HEAD_ADOPTION_ARGUMENTS_REQUIRED: expected_previous_head and adopt_candidate_head must be provided together');
|
|
829
|
-
}
|
|
830
|
-
const requestedCheckoutId = normalizedRequiredString(args, 'checkout_id');
|
|
831
|
-
if (!requestedCheckoutId) throw new Error('WORK_HEAD_ADOPTION_CHECKOUT_REQUIRED: checkout_id must be explicit');
|
|
832
|
-
if (repository.repoId !== handle.repositoryId) throw new Error('WORK_HEAD_ADOPTION_REPOSITORY_MISMATCH');
|
|
833
|
-
if (requestedCheckoutId !== handle.checkoutId || repository.activeCheckoutId !== handle.checkoutId) {
|
|
834
|
-
throw new Error(`WORK_HEAD_ADOPTION_CHECKOUT_MISMATCH: expected ${handle.checkoutId}, found ${requestedCheckoutId}`);
|
|
835
|
-
}
|
|
836
|
-
if (!handle.managedWorktree) throw new Error('WORK_HEAD_ADOPTION_MANAGED_WORKTREE_REQUIRED');
|
|
837
|
-
if (handle.principalId !== session.principalId) throw new Error('WORK_HANDLE_PRINCIPAL_MISMATCH: work handle belongs to another principal');
|
|
838
|
-
if (handle.state !== 'prepared' && handle.state !== 'editing') {
|
|
839
|
-
throw new Error(`WORK_HEAD_ADOPTION_STATE_INVALID: ${handle.state}`);
|
|
840
|
-
}
|
|
841
|
-
const finalizationStages = [
|
|
842
|
-
handle.finalization.validation,
|
|
843
|
-
handle.finalization.commit,
|
|
844
|
-
handle.finalization.merge,
|
|
845
|
-
handle.finalization.branchCleanup,
|
|
846
|
-
handle.finalization.worktreeCleanup,
|
|
847
|
-
];
|
|
848
|
-
if (finalizationStages.some((stage) => stage !== 'pending')) {
|
|
849
|
-
throw new Error('WORK_HEAD_ADOPTION_FINALIZATION_ALREADY_STARTED');
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
const registered = getRepository(handle.repositoryId, ctx.controllerHome, { includeRemoved: true });
|
|
853
|
-
const registeredCheckout = registered.checkouts.find((entry) => entry.checkoutId === handle.checkoutId);
|
|
854
|
-
if (!registeredCheckout || registeredCheckout.lifecycle !== 'active' || registeredCheckout.worktree !== true) {
|
|
855
|
-
throw new Error('WORK_HEAD_ADOPTION_CHECKOUT_NOT_ACTIVE_MANAGED');
|
|
856
|
-
}
|
|
857
|
-
const worktreeRepository = selectRepositoryCheckout(registered, handle.checkoutId);
|
|
858
|
-
const guarded = assertExecutionIdentity({
|
|
859
|
-
controllerHome: ctx.controllerHome,
|
|
860
|
-
identity: executionIdentityFromCoordinates({
|
|
861
|
-
repositoryId: handle.repositoryId,
|
|
862
|
-
checkoutId: handle.checkoutId,
|
|
863
|
-
canonicalRoot: handle.worktreePath,
|
|
864
|
-
workId: handle.workId,
|
|
865
|
-
worktreePath: handle.worktreePath,
|
|
866
|
-
branch: handle.branch,
|
|
867
|
-
}),
|
|
868
|
-
cwd: handle.worktreePath,
|
|
869
|
-
requestedRepoId: repository.repoId,
|
|
870
|
-
requestedCheckoutId,
|
|
871
|
-
});
|
|
872
|
-
const status = repositoryGitStatus(worktreeRepository);
|
|
873
|
-
if (!status.clean) throw new Error('WORK_HEAD_ADOPTION_WORKTREE_DIRTY');
|
|
874
|
-
|
|
875
|
-
const previousHead = gitCommit(handle.worktreePath, previousInput, 'PREVIOUS_HEAD');
|
|
876
|
-
const candidateHead = gitCommit(handle.worktreePath, candidateInput, 'CANDIDATE_HEAD');
|
|
877
|
-
const authoritativePrevious = handle.expectedHead ? gitCommit(handle.worktreePath, handle.expectedHead, 'AUTHORITATIVE_PREVIOUS_HEAD') : undefined;
|
|
878
|
-
if (!authoritativePrevious || authoritativePrevious !== previousHead) {
|
|
879
|
-
throw new Error(`WORK_HEAD_ADOPTION_PREVIOUS_HEAD_MISMATCH: expected ${authoritativePrevious ?? 'missing'}, found ${previousHead}`);
|
|
880
|
-
}
|
|
881
|
-
if (guarded.currentHead !== candidateHead || status.head !== candidateHead) {
|
|
882
|
-
throw new Error(`WORK_HEAD_ADOPTION_CANDIDATE_NOT_CURRENT: expected current HEAD ${candidateHead}, found ${guarded.currentHead ?? status.head ?? 'missing'}`);
|
|
883
|
-
}
|
|
884
|
-
if (previousHead === candidateHead) throw new Error('WORK_HEAD_ADOPTION_SUCCESSOR_REQUIRED');
|
|
885
|
-
const ancestry = spawnSync('git', ['-C', handle.worktreePath, 'merge-base', '--is-ancestor', previousHead, candidateHead], {
|
|
886
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
887
|
-
});
|
|
888
|
-
if (ancestry.status !== 0 || ancestry.error) throw new Error('WORK_HEAD_ADOPTION_NOT_DESCENDANT');
|
|
889
|
-
|
|
890
|
-
const contract = contractFor(ctx, handle);
|
|
891
|
-
if (!contract || contract.repoId !== handle.repositoryId) throw new Error('WORK_HEAD_ADOPTION_CONTRACT_MISSING');
|
|
892
|
-
if (isTerminalWorkContractStatus(contract.status) || contract.completionReceipt) {
|
|
893
|
-
throw new Error('WORK_HEAD_ADOPTION_CONTRACT_TERMINAL');
|
|
894
|
-
}
|
|
895
|
-
// expectedHead can predate unrelated commits that landed on the source checkout
|
|
896
|
-
// before this Work was rebased. Scope adoption to the candidate's unique delta
|
|
897
|
-
// from the current source-checkout merge-base; otherwise those target-branch
|
|
898
|
-
// commits are incorrectly attributed to the Work and fail its allow-list.
|
|
899
|
-
const sourceRepository = selectWorkFinalizationTarget(registered, handle);
|
|
900
|
-
const sourceHead = handle.sourceCheckoutId && handle.sourceCheckoutId !== handle.checkoutId
|
|
901
|
-
? repositoryGitStatus(sourceRepository).head
|
|
902
|
-
: undefined;
|
|
903
|
-
const scopeBaseHead = sourceHead ? gitMergeBase(handle.worktreePath, sourceHead, candidateHead) : previousHead;
|
|
904
|
-
const changedPaths = gitChangedPaths(handle.worktreePath, scopeBaseHead, candidateHead);
|
|
905
|
-
for (const path of changedPaths) {
|
|
906
|
-
if (contract.forbiddenPaths.some((pattern) => globMatches(pattern, path))) {
|
|
907
|
-
throw new Error(`WORK_HEAD_ADOPTION_FORBIDDEN_PATH: ${path}`);
|
|
908
|
-
}
|
|
909
|
-
if (contract.allowedPaths.length === 0 || !contract.allowedPaths.some((pattern) => globMatches(pattern, path))) {
|
|
910
|
-
throw new Error(`WORK_HEAD_ADOPTION_PATH_OUT_OF_SCOPE: ${path}`);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
claimHeadAdoptionOwnership(ctx, session, handle, args);
|
|
915
|
-
|
|
916
|
-
const reviewedAt = new Date().toISOString();
|
|
917
|
-
const reconciliationId = `RECNC-${createHash('sha256').update([
|
|
918
|
-
handle.repositoryId, handle.workId, previousHead, candidateHead, handle.checkoutId, handle.branch,
|
|
919
|
-
].join('\0')).digest('hex').slice(0, 16)}`;
|
|
920
|
-
const reconciliation: WorkReconciliationRecord = {
|
|
921
|
-
schemaVersion: 1,
|
|
922
|
-
reconciliationId,
|
|
923
|
-
originalExpectedRevision: previousHead,
|
|
924
|
-
observedTargetRevision: candidateHead,
|
|
925
|
-
baseRevision: gitCommit(handle.worktreePath, handle.baseCommit ?? previousHead, 'BASE_HEAD'),
|
|
926
|
-
targetBranch: handle.branch,
|
|
927
|
-
reachable: true,
|
|
928
|
-
method: 'exact_commit',
|
|
929
|
-
comparedPaths: changedPaths,
|
|
930
|
-
reviewer: session.principalId.slice(0, 200),
|
|
931
|
-
reviewedAt,
|
|
932
|
-
unrecoverableStages: [],
|
|
933
|
-
cleanupOwnershipProof: `No cleanup was performed; managed checkout ${handle.checkoutId} remains owned by Work finalizer.`,
|
|
934
|
-
rationale: 'Adopted an exact clean successor commit after repository, checkout, worktree, branch, controller ownership, ancestry, and WorkContract path-scope verification. This reconciliation is not completion evidence.',
|
|
935
|
-
outcome: 'accepted_equivalence',
|
|
936
|
-
};
|
|
937
|
-
|
|
938
|
-
const adopted = transitionWorkHandle(ctx.controllerHome, handle, 'editing', {
|
|
939
|
-
deliveryBaseCommit: scopeBaseHead,
|
|
940
|
-
expectedHead: candidateHead,
|
|
941
|
-
failureReason: undefined,
|
|
942
|
-
finalization: initialStage(),
|
|
943
|
-
validationRun: undefined,
|
|
944
|
-
validatedInputFingerprint: undefined,
|
|
945
|
-
});
|
|
946
|
-
try {
|
|
947
|
-
updateWorkContract({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, contract.workId, {
|
|
948
|
-
evidenceState: contract.checkRefs.length === 0
|
|
949
|
-
? contract.evidenceState
|
|
950
|
-
: contract.evidenceState === 'valid' || contract.evidenceState === 'stale'
|
|
951
|
-
? 'stale'
|
|
952
|
-
: 'partial',
|
|
953
|
-
reconciliations: [reconciliation, ...contract.reconciliations.filter((entry) => entry.reconciliationId !== reconciliationId)],
|
|
954
|
-
});
|
|
955
|
-
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, contract.workId, {
|
|
956
|
-
title: 'audited WorkHandle successor HEAD adoption',
|
|
957
|
-
summary: `${previousHead} -> ${candidateHead}; ${changedPaths.length} candidate-unique path(s) from scope base ${scopeBaseHead} remained within the WorkContract allow-list. Historical validation and completion receipts were not rewritten.`,
|
|
958
|
-
detailLevel: 'summary',
|
|
959
|
-
});
|
|
960
|
-
} catch (error) {
|
|
961
|
-
try {
|
|
962
|
-
writeWorkHandle(ctx.controllerHome, { ...handle, recordRevision: adopted.recordRevision });
|
|
963
|
-
} catch (rollbackError) {
|
|
964
|
-
throw new Error(`WORK_HEAD_ADOPTION_AUDIT_WRITE_FAILED_AND_ROLLBACK_FAILED: ${String(error)}; ${String(rollbackError)}`);
|
|
965
|
-
}
|
|
966
|
-
throw error;
|
|
967
|
-
}
|
|
968
|
-
const nextSession = updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), {
|
|
969
|
-
activeRepositoryId: handle.repositoryId,
|
|
970
|
-
activeCheckoutId: handle.checkoutId,
|
|
971
|
-
activeWorkId: handle.workId,
|
|
972
|
-
permissionSnapshotVersion: handle.permissionSnapshotVersion,
|
|
973
|
-
lastValidatedAt: reviewedAt,
|
|
974
|
-
});
|
|
975
|
-
return {
|
|
976
|
-
session: nextSession,
|
|
977
|
-
work: compactHandle(adopted),
|
|
978
|
-
reused: true,
|
|
979
|
-
adopted: true,
|
|
980
|
-
adoption: { previousHead, candidateHead, changedPaths, reconciliationId },
|
|
981
|
-
controllerClaimed: true,
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
export function releasePreparedWorkOwnership(
|
|
986
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
987
|
-
handle: WorkHandleState,
|
|
988
|
-
): 'released' | 'already_released' {
|
|
989
|
-
const workIdValue = handle.workContractId ?? handle.workId;
|
|
990
|
-
const options = { controllerHome: ctx.controllerHome, repoId: handle.repositoryId };
|
|
991
|
-
const current = getControllerSession(options, workIdValue);
|
|
992
|
-
if (!current) return 'already_released';
|
|
993
|
-
|
|
994
|
-
const callerPrincipal = principalFor(ctx);
|
|
995
|
-
const callerInstanceId = ctx.controllerInstanceId ?? currentControllerInstanceId();
|
|
996
|
-
const ownerPrincipal = current.principalId?.trim() || current.controllerId;
|
|
997
|
-
const ownerInstanceId = current.controllerInstanceId?.trim() || '';
|
|
998
|
-
if (ownerPrincipal !== callerPrincipal) {
|
|
999
|
-
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workIdValue}`);
|
|
1000
|
-
}
|
|
1001
|
-
if (!ownerInstanceId || ownerInstanceId !== callerInstanceId) {
|
|
1002
|
-
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workIdValue}`);
|
|
1003
|
-
}
|
|
1004
|
-
if (typeof current.claimGeneration !== 'number' || current.claimGeneration < 1) {
|
|
1005
|
-
throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workIdValue}`);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
const released = releaseControllerSessionWithAuthority(options, {
|
|
1009
|
-
workId: workIdValue,
|
|
1010
|
-
actor: `legacy-work-release:${callerPrincipal}:${callerInstanceId}`,
|
|
1011
|
-
authority: {
|
|
1012
|
-
controllerId: current.controllerId,
|
|
1013
|
-
controllerType: current.controllerType,
|
|
1014
|
-
principalId: ownerPrincipal,
|
|
1015
|
-
controllerInstanceId: ownerInstanceId,
|
|
1016
|
-
claimGeneration: current.claimGeneration,
|
|
1017
|
-
},
|
|
1018
|
-
});
|
|
1019
|
-
if (!released.allowed) {
|
|
1020
|
-
throw new Error(`WORK_CONTROLLER_RELEASE_FENCED: ${workIdValue}:${released.reason}`);
|
|
1021
|
-
}
|
|
1022
|
-
return 'released';
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
function terminalCleanupOutcome(
|
|
1026
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
1027
|
-
handle: WorkHandleState,
|
|
1028
|
-
): WorkTerminalOutcome | undefined {
|
|
1029
|
-
if (handle.cleanupReceipt) return handle.cleanupReceipt.terminalOutcome;
|
|
1030
|
-
const contract = contractFor(ctx, handle);
|
|
1031
|
-
if (contract?.status === 'cancelled') return 'cancelled';
|
|
1032
|
-
if (contract?.status === 'completed') return 'completed_cleanup';
|
|
1033
|
-
const reason = `${handle.failureReason ?? ''} ${handle.finalization.lastError ?? ''}`.toLowerCase();
|
|
1034
|
-
if (contract?.status === 'blocked' && reason.includes('terminal')) return 'blocked_terminal';
|
|
1035
|
-
// `failed` is a retryable execution-handle state (its transition table allows
|
|
1036
|
-
// failed -> validating/editing). Do not reinterpret it as terminal cleanup
|
|
1037
|
-
// unless the durable Work itself is terminal. `failed_terminal_cleanup` is
|
|
1038
|
-
// the explicit point of no return for resource cleanup reconciliation.
|
|
1039
|
-
if (contract?.status === 'failed' || handle.state === 'failed_terminal_cleanup') {
|
|
1040
|
-
if (reason.includes('infrastructure') || reason.includes('timed out') || reason.includes('unavailable')) {
|
|
1041
|
-
return 'infrastructure_failed';
|
|
1042
|
-
}
|
|
1043
|
-
if (handle.finalization.validation === 'failed') return 'validation_failed';
|
|
1044
|
-
return 'failed';
|
|
1045
|
-
}
|
|
1046
|
-
return undefined;
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
async function reconcileTerminalCleanup(
|
|
1050
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
1051
|
-
session: ExecutionSessionContext,
|
|
1052
|
-
handle: WorkHandleState,
|
|
1053
|
-
args: Record<string, unknown>,
|
|
1054
|
-
outcome: WorkTerminalOutcome,
|
|
1055
|
-
): Promise<Record<string, unknown>> {
|
|
1056
|
-
const wasComplete = handle.cleanupReceipt?.complete === true;
|
|
1057
|
-
const cleaned = await cleanupTerminalWork({
|
|
1058
|
-
controllerHome: ctx.controllerHome,
|
|
1059
|
-
handle,
|
|
1060
|
-
targetBranch: typeof args.target_branch === 'string' ? args.target_branch : undefined,
|
|
1061
|
-
deleteBranch: args.delete_branch !== false,
|
|
1062
|
-
terminalOutcome: outcome,
|
|
1063
|
-
failureReason: handle.failureReason ?? handle.finalization.lastError,
|
|
1064
|
-
});
|
|
1065
|
-
const ownership = releasePreparedWorkOwnership(ctx, cleaned.handle);
|
|
1066
|
-
cleaned.receipt.ownership.controllerLease = ownership;
|
|
1067
|
-
const persisted = writeWorkHandle(ctx.controllerHome, {
|
|
1068
|
-
...cleaned.handle,
|
|
1069
|
-
cleanupReceipt: cleaned.receipt,
|
|
1070
|
-
});
|
|
1071
|
-
updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), {
|
|
1072
|
-
activeWorkId: undefined,
|
|
1073
|
-
activeCheckoutId: workReturnCheckoutId(ctx, persisted, session.activeCheckoutId),
|
|
1074
|
-
});
|
|
1075
|
-
if (cleaned.receipt.complete && !wasComplete) {
|
|
1076
|
-
appendWorkEvidence(
|
|
1077
|
-
{ controllerHome: ctx.controllerHome, repoId: persisted.repositoryId },
|
|
1078
|
-
persisted.workContractId ?? persisted.workId,
|
|
1079
|
-
{
|
|
1080
|
-
title: 'terminal cleanup receipt',
|
|
1081
|
-
summary: `Cleanup ${cleaned.receipt.receiptId} completed for ${outcome}; Work outcome was preserved and no completion receipt was fabricated.`,
|
|
1082
|
-
detailLevel: 'summary',
|
|
1083
|
-
},
|
|
1084
|
-
);
|
|
1085
|
-
}
|
|
1086
|
-
return {
|
|
1087
|
-
work: compactHandle(persisted),
|
|
1088
|
-
stages: persisted.finalization,
|
|
1089
|
-
completed: false,
|
|
1090
|
-
cleanupCompleted: cleaned.receipt.complete,
|
|
1091
|
-
cleanupPartial: cleaned.receipt.partial,
|
|
1092
|
-
failurePreserved: true,
|
|
1093
|
-
cleanupReceipt: cleaned.receipt,
|
|
1094
|
-
idempotent: wasComplete,
|
|
1095
|
-
};
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
function invalidateActiveWork(ctx: MultiRepositoryMcpToolContext, session: ExecutionSessionContext, reason: string): void {
|
|
1099
|
-
if (!session.activeRepositoryId || !session.activeWorkId) return;
|
|
1100
|
-
const handle = readWorkHandle(ctx.controllerHome, session.activeRepositoryId, session.activeWorkId);
|
|
1101
|
-
if (!handle || handle.state === 'cleaned') return;
|
|
1102
|
-
const contract = contractFor(ctx, handle);
|
|
1103
|
-
if (contract?.status === 'completed') return;
|
|
1104
|
-
markWorkHandleFailed(ctx.controllerHome, handle, reason);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
function bindSessionRepository(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Record<string, unknown> {
|
|
1108
|
-
const session = requireSession(ctx, args);
|
|
1109
|
-
const repository = resolveRepositorySelection({ repoId: requireExplicitRepoId(args), checkoutId: typeof args.checkout_id === 'string' ? args.checkout_id : undefined, controllerHome: ctx.controllerHome, allowSoleRepository: false });
|
|
1110
|
-
const switching = session.activeRepositoryId !== undefined && (session.activeRepositoryId !== repository.repoId || session.activeCheckoutId !== repository.activeCheckoutId);
|
|
1111
|
-
if (switching) invalidateActiveWork(ctx, session, 'explicit repository or checkout switch invalidated the previous active work handle');
|
|
1112
|
-
const next = updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), {
|
|
1113
|
-
activeRepositoryId: repository.repoId,
|
|
1114
|
-
activeCheckoutId: repository.activeCheckoutId,
|
|
1115
|
-
activeWorkId: undefined,
|
|
1116
|
-
goalDelegation: undefined,
|
|
1117
|
-
permissionSnapshotVersion: currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId),
|
|
1118
|
-
lastValidatedAt: new Date().toISOString(),
|
|
1119
|
-
});
|
|
1120
|
-
return { session: next, repository: { repoId: repository.repoId, checkoutId: repository.activeCheckoutId, canonicalRoot: repository.canonicalRoot, branch: repository.checkouts.find((entry) => entry.checkoutId === repository.activeCheckoutId)?.branch ?? null }, switched: switching };
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Record<string, unknown> {
|
|
1124
|
-
const session = requireSession(ctx, args);
|
|
1125
|
-
const repository = selectedRepository(ctx, session, args, true);
|
|
1126
|
-
if (!session.activeRepositoryId) {
|
|
1127
|
-
updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), { activeRepositoryId: repository.repoId, activeCheckoutId: repository.activeCheckoutId, permissionSnapshotVersion: currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId) });
|
|
1128
|
-
}
|
|
1129
|
-
const existingId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
1130
|
-
if (existingId) {
|
|
1131
|
-
const existing = readWorkHandle(ctx.controllerHome, repository.repoId, existingId)
|
|
1132
|
-
?? findWorkHandle(ctx, session, { ...args, work_id: existingId, repo_id: repository.repoId });
|
|
1133
|
-
if (existing.principalId !== session.principalId) throw new Error('WORK_HANDLE_ACCESS_DENIED');
|
|
1134
|
-
const adopted = adoptExistingWorkHead(ctx, session, repository, existing, args);
|
|
1135
|
-
if (adopted) return adopted;
|
|
1136
|
-
validateWorkHandle(ctx.controllerHome, existing, identityFor(ctx, args), 'cheap', 'inspect');
|
|
1137
|
-
updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), { activeRepositoryId: existing.repositoryId, activeCheckoutId: existing.checkoutId, activeWorkId: existing.workId, permissionSnapshotVersion: existing.permissionSnapshotVersion });
|
|
1138
|
-
claimPreparedWorkOwnership(ctx, session, existing, args);
|
|
1139
|
-
return { session: requireSession(ctx, args), work: compactHandle(existing), reused: true, controllerClaimed: true };
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
const requestId = typeof args.request_id === 'string' ? args.request_id.trim() : '';
|
|
1143
|
-
if (!requestId) throw new Error('WORK_PREPARE_REQUEST_ID_REQUIRED: new work preparation requires request_id');
|
|
1144
|
-
const isolation = args.isolation === 'reuse' || args.isolation === 'new_worktree' || args.isolation === 'auto' ? args.isolation : 'auto';
|
|
1145
|
-
const objective = String(args.objective ?? 'Controller-managed repository work').trim().slice(0, 2_000);
|
|
1146
|
-
const goalId = typeof args.goal_id === 'string' && args.goal_id.trim() ? args.goal_id.trim() : undefined;
|
|
1147
|
-
const acceptanceCriteria = boundedStringArray(args.acceptance_criteria, 20);
|
|
1148
|
-
const allowedPaths = boundedStringArray(args.allowed_paths, 50);
|
|
1149
|
-
const checks = boundedStringArray(args.checks, 30);
|
|
1150
|
-
const baseRef = typeof args.base_ref === 'string' && args.base_ref.trim() ? args.base_ref.trim() : undefined;
|
|
1151
|
-
const requestedCheckoutId = typeof args.checkout_id === 'string' && args.checkout_id.trim() ? args.checkout_id.trim() : undefined;
|
|
1152
|
-
const needsDependencies = args.needs_dependencies === true;
|
|
1153
|
-
const baseCheckoutId = repository.activeCheckoutId;
|
|
1154
|
-
const baseStatus = repositoryGitStatus(repository);
|
|
1155
|
-
if (isolation === 'reuse' && !baseStatus.clean) throw new Error('WORKTREE_DIRTY: reuse was requested but the selected checkout is dirty; choose new_worktree or auto');
|
|
1156
|
-
const useWorktree = isolation === 'new_worktree' || (isolation === 'auto' && !baseStatus.clean);
|
|
1157
|
-
const policy = readRepositoryAccessPolicy(ctx.controllerHome, repository.repoId);
|
|
1158
|
-
const fingerprint = workPrepareFingerprint({
|
|
1159
|
-
repoId: repository.repoId,
|
|
1160
|
-
requestedCheckoutId,
|
|
1161
|
-
isolation,
|
|
1162
|
-
objective,
|
|
1163
|
-
goalId,
|
|
1164
|
-
acceptanceCriteria,
|
|
1165
|
-
allowedPaths,
|
|
1166
|
-
checks,
|
|
1167
|
-
baseRef,
|
|
1168
|
-
needsDependencies,
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
return withWorkPrepareRequest({
|
|
1172
|
-
controllerHome: ctx.controllerHome,
|
|
1173
|
-
repoId: repository.repoId,
|
|
1174
|
-
sessionId: session.sessionId,
|
|
1175
|
-
principalId: session.principalId,
|
|
1176
|
-
requestId,
|
|
1177
|
-
fingerprint,
|
|
1178
|
-
proposedWorkId: newWorkId(),
|
|
1179
|
-
}, (request, requestReused) => {
|
|
1180
|
-
const createdWorkId = request.workId;
|
|
1181
|
-
const existingHandle = readWorkHandle(ctx.controllerHome, repository.repoId, createdWorkId);
|
|
1182
|
-
if (existingHandle) {
|
|
1183
|
-
if (existingHandle.principalId !== session.principalId || existingHandle.sessionId !== session.sessionId) {
|
|
1184
|
-
throw new Error(`WORK_PREPARE_REQUEST_INDEX_CORRUPT: ${requestId}`);
|
|
1185
|
-
}
|
|
1186
|
-
const existingContract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, createdWorkId);
|
|
1187
|
-
if (!existingContract) throw new Error(`WORK_PREPARE_RESULT_LOST: ${requestId} has a Work handle without its WorkContract`);
|
|
1188
|
-
const terminal = isTerminalWorkContractStatus(existingContract.status)
|
|
1189
|
-
&& !(existingContract.status === 'failed' && request.status === 'claimed');
|
|
1190
|
-
if (terminal) {
|
|
1191
|
-
return {
|
|
1192
|
-
session: requireSession(ctx, args),
|
|
1193
|
-
work: compactHandle(existingHandle),
|
|
1194
|
-
reused: true,
|
|
1195
|
-
terminal: true,
|
|
1196
|
-
workContractStatus: existingContract.status,
|
|
1197
|
-
controllerClaimed: false,
|
|
1198
|
-
};
|
|
1199
|
-
}
|
|
1200
|
-
if (existingContract.status === 'open' || existingContract.status === 'failed') {
|
|
1201
|
-
updateWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, createdWorkId, { status: 'running', worktreeRef: existingHandle.worktreePath });
|
|
1202
|
-
}
|
|
1203
|
-
const delegation = createGoalDelegation({
|
|
1204
|
-
sessionId: session.sessionId,
|
|
1205
|
-
repositoryId: repository.repoId,
|
|
1206
|
-
workId: createdWorkId,
|
|
1207
|
-
goalId,
|
|
1208
|
-
allowedRiskClasses: ['readonly', 'local_repo_write', 'workspace_write', 'local_command', 'dependency_change', 'local_git'],
|
|
1209
|
-
deniedRiskClasses: ['remote_write', 'destructive', 'secret_access', 'outside_repository'],
|
|
1210
|
-
permissionSnapshotVersion: policy.revision,
|
|
1211
|
-
source: 'gpt_risk_delegate',
|
|
1212
|
-
});
|
|
1213
|
-
const nextSession = updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), { activeRepositoryId: repository.repoId, activeCheckoutId: existingHandle.checkoutId, activeWorkId: createdWorkId, permissionSnapshotVersion: policy.revision, goalDelegation: delegation, lastValidatedAt: new Date().toISOString() });
|
|
1214
|
-
claimPreparedWorkOwnership(ctx, nextSession, existingHandle, args);
|
|
1215
|
-
return { session: nextSession, work: compactHandle(existingHandle), reused: true, isolation: existingHandle.managedWorktree ? 'isolated' : 'current', controllerClaimed: true };
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
let contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, createdWorkId);
|
|
1219
|
-
if (contract?.requestId && contract.requestId !== requestId) throw new Error(`WORK_PREPARE_REQUEST_INDEX_CORRUPT: ${requestId}`);
|
|
1220
|
-
if (request.status === 'prepared') {
|
|
1221
|
-
throw new Error(`WORK_PREPARE_RESULT_LOST: ${requestId} completed without a readable Work handle`);
|
|
1222
|
-
}
|
|
1223
|
-
if (contract && (contract.status === 'completed' || contract.status === 'cancelled')) {
|
|
1224
|
-
throw new Error(`WORK_PREPARE_REQUEST_TERMINAL: ${requestId} belongs to ${contract.status} Work ${createdWorkId}`);
|
|
1225
|
-
}
|
|
1226
|
-
if (!contract) {
|
|
1227
|
-
contract = createWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
|
|
1228
|
-
workId: createdWorkId,
|
|
1229
|
-
repoId: repository.repoId,
|
|
1230
|
-
mode: useWorktree ? 'goal_workloop' : 'direct_control',
|
|
1231
|
-
objective,
|
|
1232
|
-
acceptanceCriteria,
|
|
1233
|
-
allowedPaths,
|
|
1234
|
-
forbiddenPaths: [],
|
|
1235
|
-
checks,
|
|
1236
|
-
constraints: { accessMode: policy.mode, workspaceMode: useWorktree ? 'isolated' : 'current', requireWorktree: useWorktree, allowCommit: true, allowMerge: true, allowCleanup: true },
|
|
1237
|
-
worktreePolicy: { required: useWorktree, reason: useWorktree ? 'work_prepare selected isolated worktree execution' : 'explicitly reused a registered checkout' },
|
|
1238
|
-
requestedBy: 'chatgpt',
|
|
1239
|
-
requestId,
|
|
1240
|
-
});
|
|
1241
|
-
}
|
|
1242
|
-
const delegation = createGoalDelegation({
|
|
1243
|
-
sessionId: session.sessionId,
|
|
1244
|
-
repositoryId: repository.repoId,
|
|
1245
|
-
workId: createdWorkId,
|
|
1246
|
-
goalId,
|
|
1247
|
-
allowedRiskClasses: ['readonly', 'local_repo_write', 'workspace_write', 'local_command', 'dependency_change', 'local_git'],
|
|
1248
|
-
deniedRiskClasses: ['remote_write', 'destructive', 'secret_access', 'outside_repository'],
|
|
1249
|
-
permissionSnapshotVersion: policy.revision,
|
|
1250
|
-
source: 'gpt_risk_delegate',
|
|
1251
|
-
});
|
|
1252
|
-
try {
|
|
1253
|
-
const workspace = useWorktree
|
|
1254
|
-
? ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
1255
|
-
requestId: createdWorkId,
|
|
1256
|
-
title: objective,
|
|
1257
|
-
baseRef,
|
|
1258
|
-
prepareDependencies: needsDependencies,
|
|
1259
|
-
})
|
|
1260
|
-
: { mode: 'current' as const, checkoutId: baseCheckoutId, root: repository.canonicalRoot, branch: baseStatus.branch ?? 'detached', baseRevision: baseStatus.head ?? undefined, managed: false };
|
|
1261
|
-
const refreshed = getRepository(repository.repoId, ctx.controllerHome);
|
|
1262
|
-
const checkout = selectRepositoryCheckout(refreshed, workspace.checkoutId);
|
|
1263
|
-
const branch = workspace.branch || repositoryGitStatus(checkout).branch;
|
|
1264
|
-
if (!branch) throw new Error('WORKTREE_DETACHED: selected worktree has no branch');
|
|
1265
|
-
const head = gitHead(checkout.canonicalRoot);
|
|
1266
|
-
const handle: WorkHandleState = {
|
|
1267
|
-
schemaVersion: 1, workId: createdWorkId, sessionId: session.sessionId, principalId: session.principalId,
|
|
1268
|
-
repositoryId: repository.repoId, checkoutId: checkout.activeCheckoutId, worktreePath: checkout.canonicalRoot, branch,
|
|
1269
|
-
sourceCheckoutId: baseCheckoutId, managedWorktree: workspace.managed, workContractId: contract.workId, goalId, delegationVersion: delegation.version,
|
|
1270
|
-
baseCommit: workspace.baseRevision ?? head, deliveryBaseCommit: workspace.baseRevision ?? head, expectedHead: head, permissionSnapshotVersion: policy.revision,
|
|
1271
|
-
state: 'prepared', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), finalization: initialStage(),
|
|
1272
|
-
cleanupResponsibility: { owner: 'work_finalizer', registeredAt: new Date().toISOString() },
|
|
1273
|
-
};
|
|
1274
|
-
writeWorkHandle(ctx.controllerHome, handle);
|
|
1275
|
-
updateWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, contract.workId, { status: 'running', worktreeRef: checkout.canonicalRoot });
|
|
1276
|
-
const nextSession = updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), { activeRepositoryId: repository.repoId, activeCheckoutId: checkout.activeCheckoutId, activeWorkId: createdWorkId, permissionSnapshotVersion: policy.revision, goalDelegation: delegation, lastValidatedAt: new Date().toISOString() });
|
|
1277
|
-
claimPreparedWorkOwnership(ctx, nextSession, handle, args);
|
|
1278
|
-
return { session: nextSession, work: compactHandle(handle), reused: requestReused, isolation: workspace.mode, controllerClaimed: true };
|
|
1279
|
-
} catch (error) {
|
|
1280
|
-
updateWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, contract.workId, { status: 'failed' });
|
|
1281
|
-
throw error;
|
|
1282
|
-
}
|
|
1283
|
-
});
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
116
|
function inspectWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Record<string, unknown> {
|
|
1287
117
|
const session = requireSession(ctx, args);
|
|
1288
118
|
// work_inspect is authorized read-only evidence; it must not require the write lease.
|
|
@@ -1317,1737 +147,12 @@ function inspectWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
|
|
|
1317
147
|
checks: checks.map((checkId) => ({ checkId, registered: listControllerChecks(validated.worktreeRepository.canonicalRoot).some((check) => check.id === checkId) })),
|
|
1318
148
|
package: packageManifest ? { name: packageManifest.name, scripts: packageManifest.scripts } : undefined,
|
|
1319
149
|
};
|
|
1320
|
-
const response =
|
|
150
|
+
const response = makeBoundedWorkResult(ctx, session, handle.repositoryId, handle.workId, 'inspection', value);
|
|
1321
151
|
const trace: McpTimingTrace = { tool: 'work_inspect', sessionResolutionMs: 0, repositoryResolutionMs: 0, workHandleValidationMs: Math.round(validationMs * 100) / 100, resultSerializationMs: 0, totalToolDurationMs: Math.round((performance.now() - started) * 100) / 100, sessionId: session.sessionId, repoId: handle.repositoryId, workId: handle.workId };
|
|
1322
152
|
recordMcpTiming(ctx.controllerHome, trace);
|
|
1323
153
|
return response;
|
|
1324
154
|
}
|
|
1325
155
|
|
|
1326
|
-
function commandInputs(args: Record<string, unknown>): Array<Record<string, unknown>> {
|
|
1327
|
-
if (Array.isArray(args.commands)) return args.commands.filter((value): value is Record<string, unknown> => Boolean(value && typeof value === 'object' && !Array.isArray(value)));
|
|
1328
|
-
if (args.command !== undefined) return [{ command: args.command, cwd: args.cwd, approval_token: args.approval_token, timeout_ms: args.timeout_ms, max_output_bytes: args.max_output_bytes }];
|
|
1329
|
-
throw new Error('COMMAND_REQUIRED: provide command or commands');
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
function authorizationRisk(command: RepositoryCommandValue, classification: ReturnType<typeof classifyRepositoryCommand>): AuthorizationRiskClass {
|
|
1333
|
-
if (classification.risk === 'readonly') return 'readonly';
|
|
1334
|
-
if (classification.risk === 'remote_write') return 'remote_write';
|
|
1335
|
-
if (classification.risk === 'destructive') return 'destructive';
|
|
1336
|
-
const executable = typeof command === 'string' ? command : command[0] ?? '';
|
|
1337
|
-
if (typeof command === 'string' && /\b(?:npm|bun|pnpm|yarn)\s+(?:install|add|remove|update)\b/i.test(command)) return 'dependency_change';
|
|
1338
|
-
if (/^\s*(?:git|.*[\\/]git)(?:\s|$)/i.test(executable)) return 'local_git';
|
|
1339
|
-
return 'workspace_write';
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1343
|
-
const session = requireSession(ctx, args);
|
|
1344
|
-
const handle = workForSession(ctx, session, args);
|
|
1345
|
-
assertWorkControllerOwnership(ctx, session, handle, args);
|
|
1346
|
-
const commands = commandInputs(args);
|
|
1347
|
-
if (commands.length > 16) throw new Error('COMMAND_BATCH_TOO_LARGE: at most 16 commands per work_execute');
|
|
1348
|
-
const cheap = validateWorkHandle(ctx.controllerHome, handle, identityFor(ctx, args), 'cheap', 'execute');
|
|
1349
|
-
const inputs = commands.map((entry) => ({
|
|
1350
|
-
command: commandValue(normalizeRepositoryCommand(entry.command)),
|
|
1351
|
-
cwd: typeof entry.cwd === 'string' ? entry.cwd : undefined,
|
|
1352
|
-
approvalToken: typeof entry.approval_token === 'string' ? entry.approval_token : undefined,
|
|
1353
|
-
}));
|
|
1354
|
-
const classifications = inputs.map((entry) => classifyRepositoryCommand(entry.command, cheap.repository.defaultBranch));
|
|
1355
|
-
const requiresFull = classifications.some((classification) => classification.risk !== 'readonly');
|
|
1356
|
-
if (requiresFull) validateWorkHandle(ctx.controllerHome, handle, identityFor(ctx, args), 'full', 'execute');
|
|
1357
|
-
const decisions: AuthorizationDecision[] = [];
|
|
1358
|
-
const approvalRequestId = typeof args.approval_request_id === 'string' ? args.approval_request_id.trim() : '';
|
|
1359
|
-
const resolvedRequest = approvalRequestId
|
|
1360
|
-
? assertResolvedAuthorization({ controllerHome: ctx.controllerHome, repositoryId: handle.repositoryId, approvalRequestId, sessionId: session.sessionId, principalId: session.principalId, workId: handle.workId, permissionSnapshotVersion: handle.permissionSnapshotVersion, command: inputs[0]?.command })
|
|
1361
|
-
: undefined;
|
|
1362
|
-
for (const [index, entry] of inputs.entries()) {
|
|
1363
|
-
const classification = classifications[index]!;
|
|
1364
|
-
const outsideCwd = Boolean(entry.cwd && (isAbsolute(entry.cwd) || (() => {
|
|
1365
|
-
const rel = relative(resolve(handle.worktreePath), resolve(handle.worktreePath, entry.cwd));
|
|
1366
|
-
return rel === '..' || rel.startsWith('../') || rel.startsWith('..\\');
|
|
1367
|
-
})()));
|
|
1368
|
-
const risk = outsideCwd ? 'outside_repository' : authorizationRisk(entry.command, classification);
|
|
1369
|
-
const decision = resolvedRequest
|
|
1370
|
-
? { decision: 'allow', source: 'user_confirmation', reason: 'The user resolved the exact approval request for this command.' } as const
|
|
1371
|
-
: decideAuthorization({
|
|
1372
|
-
controllerHome: ctx.controllerHome,
|
|
1373
|
-
accessMode: readRepositoryAccessPolicy(ctx.controllerHome, handle.repositoryId).mode,
|
|
1374
|
-
risk,
|
|
1375
|
-
repositoryId: handle.repositoryId,
|
|
1376
|
-
currentRepositoryId: handle.repositoryId,
|
|
1377
|
-
workId: handle.workId,
|
|
1378
|
-
boundWorkId: handle.workId,
|
|
1379
|
-
goalId: handle.goalId,
|
|
1380
|
-
boundGoalId: handle.goalId,
|
|
1381
|
-
sessionId: session.sessionId,
|
|
1382
|
-
principalId: session.principalId,
|
|
1383
|
-
permissionSnapshotVersion: handle.permissionSnapshotVersion,
|
|
1384
|
-
delegation: session.goalDelegation,
|
|
1385
|
-
worktreePath: handle.worktreePath,
|
|
1386
|
-
cwd: entry.cwd,
|
|
1387
|
-
command: entry.command,
|
|
1388
|
-
approvedByUser: Boolean(resolvedRequest),
|
|
1389
|
-
});
|
|
1390
|
-
decisions.push(decision);
|
|
1391
|
-
if (decision.decision !== 'allow') return { authorization: decision, work: compactHandle(handle), command: entry.command };
|
|
1392
|
-
}
|
|
1393
|
-
const invocationId = typeof args.request_id === 'string' && args.request_id.trim()
|
|
1394
|
-
? args.request_id.trim()
|
|
1395
|
-
: `invoke-${session.sessionId}-${handle.workId}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
1396
|
-
const invocationFingerprint = createHash('sha256')
|
|
1397
|
-
.update(JSON.stringify({
|
|
1398
|
-
tool: 'work_execute',
|
|
1399
|
-
workId: handle.workId,
|
|
1400
|
-
commands: inputs.map((entry) => ({ command: entry.command, cwd: entry.cwd ?? null })),
|
|
1401
|
-
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : null,
|
|
1402
|
-
maxOutputBytes: typeof args.max_output_bytes === 'number' ? args.max_output_bytes : null,
|
|
1403
|
-
}))
|
|
1404
|
-
.digest('hex');
|
|
1405
|
-
claimProcessInvocation({
|
|
1406
|
-
controllerHome: ctx.controllerHome,
|
|
1407
|
-
repoId: handle.repositoryId,
|
|
1408
|
-
checkoutId: handle.checkoutId,
|
|
1409
|
-
requestId: invocationId,
|
|
1410
|
-
invocationFingerprint,
|
|
1411
|
-
});
|
|
1412
|
-
const run = async (entry: typeof inputs[number], index: number) => {
|
|
1413
|
-
const commandId = `${invocationId}:command:${index + 1}`;
|
|
1414
|
-
const execution = await executeRepositoryCommandViaProcessRuntime({
|
|
1415
|
-
controllerHome: ctx.controllerHome,
|
|
1416
|
-
repository: cheap.worktreeRepository,
|
|
1417
|
-
executionIdentity: executionIdentityForWork(cheap.worktreeRepository, handle),
|
|
1418
|
-
command: entry.command,
|
|
1419
|
-
cwd: entry.cwd,
|
|
1420
|
-
workId: handle.workId,
|
|
1421
|
-
commandId,
|
|
1422
|
-
requestId: commandId,
|
|
1423
|
-
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
1424
|
-
maxOutputBytes: typeof args.max_output_bytes === 'number' ? args.max_output_bytes : undefined,
|
|
1425
|
-
});
|
|
1426
|
-
const process = execution.process;
|
|
1427
|
-
const ok = process ? process.ok === true : execution.ok === true;
|
|
1428
|
-
const status = process
|
|
1429
|
-
? (process.completed
|
|
1430
|
-
? (process.cancelled ? 'cancelled' : process.timedOut ? 'timed_out' : ok ? 'executed' : 'failed')
|
|
1431
|
-
: 'running')
|
|
1432
|
-
: execution.route === 'process_direct'
|
|
1433
|
-
? (ok ? 'executed' : 'failed')
|
|
1434
|
-
: execution.route === 'durable'
|
|
1435
|
-
? 'deferred_durable'
|
|
1436
|
-
: 'rejected';
|
|
1437
|
-
return {
|
|
1438
|
-
processId: process?.processId,
|
|
1439
|
-
commandId,
|
|
1440
|
-
requestId: commandId,
|
|
1441
|
-
status,
|
|
1442
|
-
ok,
|
|
1443
|
-
exitCode: process?.exitCode ?? execution.exitCode,
|
|
1444
|
-
timedOut: process?.timedOut === true,
|
|
1445
|
-
cancelled: process?.cancelled === true,
|
|
1446
|
-
startedAt: process?.startedAt,
|
|
1447
|
-
finishedAt: process?.completed ? process.startedAt : undefined,
|
|
1448
|
-
stdout: process?.stdout ?? execution.stdout,
|
|
1449
|
-
stderr: process?.stderr ?? execution.stderr,
|
|
1450
|
-
logArtifact: process?.processId ? { processId: process.processId, kind: 'process_logs' } : undefined,
|
|
1451
|
-
route: execution.route,
|
|
1452
|
-
reason: execution.reason,
|
|
1453
|
-
authorizationDecision: decisions[index],
|
|
1454
|
-
approvalRequestId: resolvedRequest?.approvalRequestId,
|
|
1455
|
-
authorization: decisions[index]?.decision === 'allow'
|
|
1456
|
-
? (resolvedRequest ? 'confirmed_plan' : decisions[index]?.source)
|
|
1457
|
-
: 'explicit_user_request',
|
|
1458
|
-
durableSideEffects: execution.durableSideEffects,
|
|
1459
|
-
process,
|
|
1460
|
-
};
|
|
1461
|
-
};
|
|
1462
|
-
const started = performance.now();
|
|
1463
|
-
const executions = classifications.every((classification) => classification.risk === 'readonly')
|
|
1464
|
-
? await Promise.all(inputs.map((entry, index) => run(entry, index)))
|
|
1465
|
-
: await (async () => {
|
|
1466
|
-
const ordered: Awaited<ReturnType<typeof run>>[] = [];
|
|
1467
|
-
for (const [index, entry] of inputs.entries()) {
|
|
1468
|
-
const execution = await run(entry, index);
|
|
1469
|
-
ordered.push(execution);
|
|
1470
|
-
// Do not launch another mutating command while a Work-owned Lightweight
|
|
1471
|
-
// process is still changing the workspace. This is sequencing, not a
|
|
1472
|
-
// durable Lease boundary; the caller resumes through process_wait.
|
|
1473
|
-
if (execution.process && !execution.process.completed) break;
|
|
1474
|
-
}
|
|
1475
|
-
return ordered;
|
|
1476
|
-
})();
|
|
1477
|
-
const branch = repositoryGitStatus(cheap.worktreeRepository).branch;
|
|
1478
|
-
const head = gitHead(cheap.worktreeRepository.canonicalRoot);
|
|
1479
|
-
let nextHandle = handle;
|
|
1480
|
-
if (branch !== handle.branch) nextHandle = markWorkHandleFailed(ctx.controllerHome, handle, `command changed the bound branch to ${branch ?? 'detached'}`);
|
|
1481
|
-
else nextHandle = transitionWorkHandle(ctx.controllerHome, handle, 'editing', { expectedHead: head, failureReason: undefined });
|
|
1482
|
-
const value = {
|
|
1483
|
-
work: compactHandle(nextHandle),
|
|
1484
|
-
commands: executions,
|
|
1485
|
-
executedCount: executions.filter((entry) => entry.status === 'executed' && entry.ok === true).length,
|
|
1486
|
-
managedProcessCount: executions.filter((entry) => entry.process && !entry.process.completed).length,
|
|
1487
|
-
deferredCommandCount: Math.max(0, inputs.length - executions.length),
|
|
1488
|
-
authorization: decisions[0],
|
|
1489
|
-
requestId: invocationId,
|
|
1490
|
-
};
|
|
1491
|
-
const response = makeBoundedResult(ctx, session, handle.repositoryId, handle.workId, 'command', value);
|
|
1492
|
-
recordMcpTiming(ctx.controllerHome, { tool: 'work_execute', workHandleValidationMs: 0, commandExecutionMs: Math.round((performance.now() - started) * 100) / 100, totalToolDurationMs: Math.round((performance.now() - started) * 100) / 100, sessionId: session.sessionId, repoId: handle.repositoryId, workId: handle.workId });
|
|
1493
|
-
return response;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
export function selectDefaultWorkValidationChecks(
|
|
1497
|
-
contract: ReturnType<typeof contractFor>,
|
|
1498
|
-
changedPaths: string[],
|
|
1499
|
-
): string[] {
|
|
1500
|
-
if (!contract || changedPaths.length === 0) return [];
|
|
1501
|
-
if (contract.risk === 'medium' || contract.risk === 'high' || contract.risk === 'destructive') {
|
|
1502
|
-
return [...contract.checks];
|
|
1503
|
-
}
|
|
1504
|
-
const sourceTypeScriptChanged = changedPaths.some((path) =>
|
|
1505
|
-
/\.(?:ts|tsx|mts|cts)$/.test(path)
|
|
1506
|
-
&& !/(?:^|\/)(?:tests?|__tests__|fixtures)(?:\/|$)/.test(path));
|
|
1507
|
-
return contract.checks.filter((checkId) => {
|
|
1508
|
-
const normalized = checkId.toLowerCase();
|
|
1509
|
-
if (normalized.includes('package:test') || normalized.includes('full') || normalized.includes('architecture') || normalized.includes('release') || normalized.includes('runtime')) {
|
|
1510
|
-
return false;
|
|
1511
|
-
}
|
|
1512
|
-
if (normalized.includes('type')) return sourceTypeScriptChanged;
|
|
1513
|
-
return normalized.includes('focused') || normalized.includes('unit') || normalized.includes('changed') || normalized.includes('target');
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1518
|
-
const session = requireSession(ctx, args);
|
|
1519
|
-
const handle = workForSession(ctx, session, args, {
|
|
1520
|
-
reconcileValidation: false,
|
|
1521
|
-
allowClaimedTerminalCleanup: args.cleanup !== false,
|
|
1522
|
-
});
|
|
1523
|
-
const terminalOutcome = terminalCleanupOutcome(ctx, handle);
|
|
1524
|
-
if (terminalOutcome && args.cleanup !== false) {
|
|
1525
|
-
return await reconcileTerminalCleanup(ctx, session, handle, args, terminalOutcome);
|
|
1526
|
-
}
|
|
1527
|
-
const validated = validateWorkHandle(ctx.controllerHome, handle, identityFor(ctx, args), 'full', 'validate');
|
|
1528
|
-
const contract = contractFor(ctx, handle);
|
|
1529
|
-
const changed = repositoryGitDiff(validated.worktreeRepository, { maxBytes: 64 * 1024 });
|
|
1530
|
-
const changedPaths = Array.isArray(changed.nameOnly) ? changed.nameOnly.map(String) : [];
|
|
1531
|
-
const requestedChecks = Array.isArray(args.check_ids)
|
|
1532
|
-
? args.check_ids.map(String).filter(Boolean)
|
|
1533
|
-
: selectDefaultWorkValidationChecks(contract, changedPaths);
|
|
1534
|
-
const validationInvocationId = typeof args.request_id === 'string' && args.request_id.trim()
|
|
1535
|
-
? args.request_id.trim()
|
|
1536
|
-
: `validate-${session.sessionId}-${handle.workId}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
1537
|
-
const validationStatus = repositoryGitStatus(validated.worktreeRepository);
|
|
1538
|
-
const validationHead = validationStatus.head
|
|
1539
|
-
?? handle.expectedHead
|
|
1540
|
-
?? handle.baseCommit
|
|
1541
|
-
?? 'unknown';
|
|
1542
|
-
const workspaceFingerprint = workspaceValidationFingerprint(
|
|
1543
|
-
validated.worktreeRepository.canonicalRoot,
|
|
1544
|
-
validationStatus,
|
|
1545
|
-
);
|
|
1546
|
-
const validationFingerprint = workValidationInputFingerprint(
|
|
1547
|
-
validationHead,
|
|
1548
|
-
workspaceFingerprint,
|
|
1549
|
-
requestedChecks,
|
|
1550
|
-
);
|
|
1551
|
-
const previousRun = handle.validationRun?.fingerprint === validationFingerprint
|
|
1552
|
-
? handle.validationRun
|
|
1553
|
-
: undefined;
|
|
1554
|
-
let validationRun: NonNullable<WorkHandleState['validationRun']> = previousRun ?? {
|
|
1555
|
-
fingerprint: validationFingerprint,
|
|
1556
|
-
head: validationHead,
|
|
1557
|
-
workspaceFingerprint,
|
|
1558
|
-
requestedChecks,
|
|
1559
|
-
resumeState: handle.state === 'committed' || handle.state === 'merged' ? handle.state : 'editing',
|
|
1560
|
-
processes: {},
|
|
1561
|
-
};
|
|
1562
|
-
let current = transitionWorkHandle(ctx.controllerHome, handle, 'validating', {
|
|
1563
|
-
finalization: { ...handle.finalization, validation: 'pending', lastError: undefined },
|
|
1564
|
-
validationRun,
|
|
1565
|
-
});
|
|
1566
|
-
markWorkValidationPending(ctx.controllerHome, current);
|
|
1567
|
-
const available = new Set(listControllerChecks(validated.worktreeRepository.canonicalRoot).map((check) => check.id));
|
|
1568
|
-
const checks: Array<Record<string, unknown>> = [];
|
|
1569
|
-
for (const [index, checkId] of requestedChecks.entries()) {
|
|
1570
|
-
if (!available.has(checkId)) {
|
|
1571
|
-
checks.push({ checkId, ok: false, status: 'missing', summary: `Check not found: ${checkId}` });
|
|
1572
|
-
break;
|
|
1573
|
-
}
|
|
1574
|
-
const reusablePass = contract
|
|
1575
|
-
? [...effectiveVerificationEvidence(contract.checkRefs, {
|
|
1576
|
-
sourceRevision: validationHead,
|
|
1577
|
-
workspaceFingerprint,
|
|
1578
|
-
checkId,
|
|
1579
|
-
requestedChecks,
|
|
1580
|
-
})].reverse().find((entry) => entry.current && entry.record.outcome === 'valid_pass' && Boolean(entry.record.receipt))
|
|
1581
|
-
: undefined;
|
|
1582
|
-
if (reusablePass) {
|
|
1583
|
-
checks.push({
|
|
1584
|
-
checkId,
|
|
1585
|
-
ok: true,
|
|
1586
|
-
status: 'passed',
|
|
1587
|
-
reusedEvidence: true,
|
|
1588
|
-
receipt: reusablePass.record.receipt,
|
|
1589
|
-
});
|
|
1590
|
-
continue;
|
|
1591
|
-
}
|
|
1592
|
-
const existingBinding = validationRun.processes[checkId];
|
|
1593
|
-
let process = existingBinding
|
|
1594
|
-
? getCheckProcessHandle(ctx.controllerHome, handle.repositoryId, existingBinding.processId)
|
|
1595
|
-
: undefined;
|
|
1596
|
-
if (existingBinding && !process) {
|
|
1597
|
-
checks.push({
|
|
1598
|
-
checkId,
|
|
1599
|
-
ok: false,
|
|
1600
|
-
status: 'infrastructure_failure',
|
|
1601
|
-
summary: `Validation process record is unavailable: ${existingBinding.processId}`,
|
|
1602
|
-
});
|
|
1603
|
-
break;
|
|
1604
|
-
}
|
|
1605
|
-
if (!process) {
|
|
1606
|
-
const processRequestId = `${validationInvocationId}:check:${index + 1}`;
|
|
1607
|
-
const executed = await runPersistedCheckViaProcessRuntime({
|
|
1608
|
-
controllerHome: ctx.controllerHome,
|
|
1609
|
-
repoId: handle.repositoryId,
|
|
1610
|
-
checkoutId: handle.checkoutId,
|
|
1611
|
-
repoRoot: validated.worktreeRepository.canonicalRoot,
|
|
1612
|
-
executionIdentity: Object.freeze({
|
|
1613
|
-
...executionIdentityForWork(validated.worktreeRepository, handle),
|
|
1614
|
-
expectedHead: validationHead === 'unknown' ? handle.expectedHead : validationHead,
|
|
1615
|
-
}),
|
|
1616
|
-
checkId,
|
|
1617
|
-
requestId: processRequestId,
|
|
1618
|
-
workId: handle.workId,
|
|
1619
|
-
commandId: processRequestId,
|
|
1620
|
-
verificationBinding: { executionSessionId: session.sessionId },
|
|
1621
|
-
verificationSnapshot: contract && contract.allowedPaths.length > 0 ? {
|
|
1622
|
-
workId: contract.workId,
|
|
1623
|
-
allowedPaths: contract.allowedPaths,
|
|
1624
|
-
forbiddenPaths: contract.forbiddenPaths,
|
|
1625
|
-
} : undefined,
|
|
1626
|
-
});
|
|
1627
|
-
if (executed.mode === 'durable') {
|
|
1628
|
-
checks.push({ checkId, ok: undefined, status: 'deferred', summary: executed.durable?.reason, durable: executed.durable });
|
|
1629
|
-
break;
|
|
1630
|
-
}
|
|
1631
|
-
process = executed.process!;
|
|
1632
|
-
const launchedRecord = getProcessRecord(ctx.controllerHome, handle.repositoryId, process.processId);
|
|
1633
|
-
validationRun = {
|
|
1634
|
-
...validationRun,
|
|
1635
|
-
processes: {
|
|
1636
|
-
...validationRun.processes,
|
|
1637
|
-
[checkId]: {
|
|
1638
|
-
processId: process.processId,
|
|
1639
|
-
requestId: processRequestId,
|
|
1640
|
-
...(launchedRecord?.checkExecution ? { checkExecution: { ...launchedRecord.checkExecution } } : {}),
|
|
1641
|
-
},
|
|
1642
|
-
},
|
|
1643
|
-
};
|
|
1644
|
-
current = transitionWorkHandle(ctx.controllerHome, current, 'validating', { validationRun });
|
|
1645
|
-
}
|
|
1646
|
-
if (!process.completed) {
|
|
1647
|
-
checks.push({ checkId, ok: undefined, status: 'running', process });
|
|
1648
|
-
break;
|
|
1649
|
-
}
|
|
1650
|
-
const record = getProcessRecord(ctx.controllerHome, handle.repositoryId, process.processId);
|
|
1651
|
-
if (!record) {
|
|
1652
|
-
checks.push({ checkId, ok: false, status: 'infrastructure_failure', summary: `Validation process record is unavailable: ${process.processId}` });
|
|
1653
|
-
break;
|
|
1654
|
-
}
|
|
1655
|
-
const boundCheckExecution = validationRun.processes[checkId]?.checkExecution;
|
|
1656
|
-
const receipt = processCheckCompletionReceipt(record, {
|
|
1657
|
-
repoId: handle.repositoryId,
|
|
1658
|
-
checkoutId: handle.checkoutId,
|
|
1659
|
-
workId: handle.workId,
|
|
1660
|
-
checkId,
|
|
1661
|
-
processId: process.processId,
|
|
1662
|
-
...(boundCheckExecution ? {
|
|
1663
|
-
checkExecution: {
|
|
1664
|
-
cacheKey: boundCheckExecution.cacheKey,
|
|
1665
|
-
revision: boundCheckExecution.revision,
|
|
1666
|
-
definitionDigest: boundCheckExecution.definitionDigest,
|
|
1667
|
-
environmentFingerprint: boundCheckExecution.environmentFingerprint,
|
|
1668
|
-
timeoutMs: boundCheckExecution.timeoutMs,
|
|
1669
|
-
scopeKey: boundCheckExecution.scopeKey,
|
|
1670
|
-
},
|
|
1671
|
-
} : {}),
|
|
1672
|
-
});
|
|
1673
|
-
const terminalEvidence = classifyPersistedCheckTerminalEvidence(record, checkId);
|
|
1674
|
-
const infrastructureFailed = receipt.timedOut
|
|
1675
|
-
|| receipt.cancelled
|
|
1676
|
-
|| terminalEvidence.state !== 'matched'
|
|
1677
|
-
|| (!receipt.ok && terminalEvidence.failureClass !== 'acceptance_failure');
|
|
1678
|
-
appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, handle.workId, {
|
|
1679
|
-
checkId,
|
|
1680
|
-
outcome: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'valid_pass' : 'valid_fail',
|
|
1681
|
-
summary: receipt.summary,
|
|
1682
|
-
recordedAt: receipt.finishedAt,
|
|
1683
|
-
sourceRevision: validationHead,
|
|
1684
|
-
workspaceFingerprint,
|
|
1685
|
-
verificationInputFingerprint: verificationInputFingerprint({
|
|
1686
|
-
sourceRevision: validationHead,
|
|
1687
|
-
workspaceFingerprint,
|
|
1688
|
-
checkId,
|
|
1689
|
-
requestedChecks,
|
|
1690
|
-
}),
|
|
1691
|
-
commandFingerprint: commandFingerprint(checkId, receipt.commandId),
|
|
1692
|
-
resultArtifactId: receipt.receiptId,
|
|
1693
|
-
startedAt: receipt.startedAt,
|
|
1694
|
-
completedAt: receipt.finishedAt,
|
|
1695
|
-
evidenceRef: { title: checkId, summary: `${receipt.receiptId}; artifact=${receipt.artifactPath}`, detailLevel: 'summary' },
|
|
1696
|
-
receipt,
|
|
1697
|
-
});
|
|
1698
|
-
checks.push({
|
|
1699
|
-
checkId,
|
|
1700
|
-
ok: receipt.ok,
|
|
1701
|
-
status: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'passed' : 'failed',
|
|
1702
|
-
process,
|
|
1703
|
-
receipt,
|
|
1704
|
-
...(terminalEvidence.warning ? { warning: terminalEvidence.warning } : {}),
|
|
1705
|
-
});
|
|
1706
|
-
if (!receipt.ok) break;
|
|
1707
|
-
}
|
|
1708
|
-
const infrastructureFailure = checks.find((check) => (
|
|
1709
|
-
check.status === 'missing'
|
|
1710
|
-
|| check.status === 'infrastructure_failure'
|
|
1711
|
-
|| check.status === 'deferred'
|
|
1712
|
-
));
|
|
1713
|
-
const acceptedFailure = checks.find((check) => check.status === 'failed');
|
|
1714
|
-
const allObserved = checks.length === requestedChecks.length && checks.every((check) => check.ok !== undefined);
|
|
1715
|
-
const completed = Boolean(infrastructureFailure || acceptedFailure || allObserved);
|
|
1716
|
-
const passed = completed
|
|
1717
|
-
&& !infrastructureFailure
|
|
1718
|
-
&& !acceptedFailure
|
|
1719
|
-
&& checks.every((check) => check.ok === true);
|
|
1720
|
-
const failureSummary = infrastructureFailure
|
|
1721
|
-
? String(infrastructureFailure.summary ?? 'validation infrastructure failure')
|
|
1722
|
-
: acceptedFailure
|
|
1723
|
-
? String(acceptedFailure.summary ?? 'targeted validation failed')
|
|
1724
|
-
: undefined;
|
|
1725
|
-
const nextState = !completed
|
|
1726
|
-
? 'validating'
|
|
1727
|
-
: passed
|
|
1728
|
-
? validationRun.resumeState
|
|
1729
|
-
: 'failed';
|
|
1730
|
-
const validation = !completed ? 'pending' : passed ? 'done' : 'failed';
|
|
1731
|
-
const next = transitionWorkHandle(ctx.controllerHome, current, nextState, {
|
|
1732
|
-
finalization: {
|
|
1733
|
-
...current.finalization,
|
|
1734
|
-
validation,
|
|
1735
|
-
lastError: failureSummary,
|
|
1736
|
-
},
|
|
1737
|
-
validationRun: completed ? undefined : validationRun,
|
|
1738
|
-
...(passed ? { validatedInputFingerprint: validationFingerprint } : {}),
|
|
1739
|
-
...(failureSummary ? { failureReason: failureSummary } : { failureReason: undefined }),
|
|
1740
|
-
});
|
|
1741
|
-
if (completed) {
|
|
1742
|
-
projectWorkValidationOutcome(
|
|
1743
|
-
ctx.controllerHome,
|
|
1744
|
-
next,
|
|
1745
|
-
infrastructureFailure ? 'infrastructure_failure' : passed ? 'passed' : 'failed',
|
|
1746
|
-
failureSummary,
|
|
1747
|
-
);
|
|
1748
|
-
}
|
|
1749
|
-
if (completed && acceptedFailure) {
|
|
1750
|
-
const cleanup = await reconcileTerminalCleanup(ctx, session, next, args, 'validation_failed');
|
|
1751
|
-
const value = {
|
|
1752
|
-
...cleanup,
|
|
1753
|
-
validation: { passed, completed, checks, targeted: true, changedPaths, cleanupTriggered: true },
|
|
1754
|
-
};
|
|
1755
|
-
return makeBoundedResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
|
|
1756
|
-
}
|
|
1757
|
-
const value = {
|
|
1758
|
-
work: compactHandle(next),
|
|
1759
|
-
validation: {
|
|
1760
|
-
passed,
|
|
1761
|
-
completed,
|
|
1762
|
-
checks,
|
|
1763
|
-
targeted: true,
|
|
1764
|
-
changedPaths,
|
|
1765
|
-
...(infrastructureFailure ? { retryable: true, cleanupTriggered: false } : {}),
|
|
1766
|
-
},
|
|
1767
|
-
};
|
|
1768
|
-
return makeBoundedResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
function runCleanup(targetRoot: string, worktreePath: string): { ok: boolean; message?: string } {
|
|
1772
|
-
if (targetRoot === worktreePath) return { ok: true };
|
|
1773
|
-
if (!existsSync(worktreePath)) return { ok: true, message: 'managed worktree already removed' };
|
|
1774
|
-
const status = repositoryGitStatus({ repoId: 'cleanup', activeCheckoutId: 'cleanup', canonicalRoot: worktreePath, localRoot: worktreePath, checkouts: [], schemaVersion: 1, displayName: basename(worktreePath), repositoryType: 'git', enabled: true, createdAt: '', updatedAt: '', lastSeenAt: '', configurationPath: '', stateStorageStrategy: 'controller-home' });
|
|
1775
|
-
if (!status.clean) return { ok: false, message: 'managed worktree is dirty; cleanup preserved it' };
|
|
1776
|
-
const process = spawnSync('git', ['-C', targetRoot, 'worktree', 'remove', worktreePath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000 });
|
|
1777
|
-
return process.status === 0 ? { ok: true } : { ok: false, message: String(process.stderr ?? 'git worktree remove failed').trim() };
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
/**
|
|
1781
|
-
* Reconcile a stale Work Handle only for a cleanup-only request whose exact
|
|
1782
|
-
* branch HEAD is already reachable from the requested target branch.
|
|
1783
|
-
*
|
|
1784
|
-
* Two bounded recovery cases are supported:
|
|
1785
|
-
* 1. an older controller committed/merged successfully but failed before
|
|
1786
|
-
* recording the finalization stages; and
|
|
1787
|
-
* 2. a terminal cancelled WorkContract owns an unchanged, clean duplicate
|
|
1788
|
-
* worktree created by an idempotency defect.
|
|
1789
|
-
*
|
|
1790
|
-
* Missing worktrees are accepted only when this handle already recorded a
|
|
1791
|
-
* successful worktree cleanup. This lets a retry finish branch cleanup after a
|
|
1792
|
-
* crash without turning a missing checkout into proof of safety.
|
|
1793
|
-
*/
|
|
1794
|
-
function cleanupOnlyMergedHead(
|
|
1795
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
1796
|
-
current: WorkHandleState,
|
|
1797
|
-
args: Record<string, unknown>,
|
|
1798
|
-
): { currentHead: string; cancelledContract: boolean; worktreeMissing: boolean } | undefined {
|
|
1799
|
-
if (args.cleanup !== true || args.commit === true || args.merge === true || !current.managedWorktree) return undefined;
|
|
1800
|
-
const contract = contractFor(ctx, current);
|
|
1801
|
-
const cancelledContract = contract?.status === 'cancelled';
|
|
1802
|
-
|
|
1803
|
-
const repository = getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true });
|
|
1804
|
-
const target = selectWorkFinalizationTarget(repository, current);
|
|
1805
|
-
const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
|
|
1806
|
-
? args.target_branch.trim()
|
|
1807
|
-
: repository.defaultBranch || 'main';
|
|
1808
|
-
const branchHeadResult = spawnSync('git', ['-C', target.canonicalRoot, 'rev-parse', '--verify', `refs/heads/${current.branch}`], {
|
|
1809
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
1810
|
-
});
|
|
1811
|
-
const currentHead = branchHeadResult.status === 0 ? String(branchHeadResult.stdout ?? '').trim() : '';
|
|
1812
|
-
if (!currentHead) return undefined;
|
|
1813
|
-
const merged = spawnSync('git', ['-C', target.canonicalRoot, 'merge-base', '--is-ancestor', currentHead, targetBranch], {
|
|
1814
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
1815
|
-
});
|
|
1816
|
-
if (merged.status !== 0) return undefined;
|
|
1817
|
-
|
|
1818
|
-
const worktreeMissing = !existsSync(current.worktreePath);
|
|
1819
|
-
if (worktreeMissing) {
|
|
1820
|
-
if (!cancelledContract || current.finalization.worktreeCleanup !== 'done') return undefined;
|
|
1821
|
-
return { currentHead, cancelledContract, worktreeMissing: true };
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
const worktree = selectRepositoryCheckout(repository, current.checkoutId, { allowArchived: true });
|
|
1825
|
-
if (!repositoryGitStatus(worktree).clean) return undefined;
|
|
1826
|
-
const worktreeHead = gitHead(worktree.canonicalRoot);
|
|
1827
|
-
if (!worktreeHead || worktreeHead !== currentHead) return undefined;
|
|
1828
|
-
const currentBranch = spawnSync('git', ['-C', worktree.canonicalRoot, 'branch', '--show-current'], {
|
|
1829
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
1830
|
-
});
|
|
1831
|
-
if (currentBranch.status !== 0 || String(currentBranch.stdout ?? '').trim() !== current.branch) return undefined;
|
|
1832
|
-
if (current.expectedHead && currentHead !== current.expectedHead && cancelledContract) return undefined;
|
|
1833
|
-
return { currentHead, cancelledContract, worktreeMissing: false };
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
interface FailedCleanupProof {
|
|
1837
|
-
currentHead: string;
|
|
1838
|
-
worktreeMissing: boolean;
|
|
1839
|
-
targetBranch: string;
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
/**
|
|
1843
|
-
* Prove that a failed Work owns an unchanged, clean managed worktree whose
|
|
1844
|
-
* branch is already contained in the target branch. This authorizes resource
|
|
1845
|
-
* cleanup only; it never proves successful verification or delivery.
|
|
1846
|
-
*/
|
|
1847
|
-
function failedCleanupOnlyHead(
|
|
1848
|
-
ctx: MultiRepositoryMcpToolContext,
|
|
1849
|
-
current: WorkHandleState,
|
|
1850
|
-
args: Record<string, unknown>,
|
|
1851
|
-
): FailedCleanupProof | undefined {
|
|
1852
|
-
if (
|
|
1853
|
-
current.state !== 'failed'
|
|
1854
|
-
|| args.cleanup !== true
|
|
1855
|
-
|| args.commit === true
|
|
1856
|
-
|| args.merge === true
|
|
1857
|
-
|| !current.managedWorktree
|
|
1858
|
-
) return undefined;
|
|
1859
|
-
const contract = contractFor(ctx, current);
|
|
1860
|
-
if (contract?.status !== 'failed') return undefined;
|
|
1861
|
-
|
|
1862
|
-
const expectedHead = current.expectedHead ?? current.baseCommit;
|
|
1863
|
-
if (!expectedHead) return undefined;
|
|
1864
|
-
const repository = getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true });
|
|
1865
|
-
const target = selectWorkFinalizationTarget(repository, current);
|
|
1866
|
-
const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
|
|
1867
|
-
? args.target_branch.trim()
|
|
1868
|
-
: repository.defaultBranch || 'main';
|
|
1869
|
-
const merged = spawnSync('git', ['-C', target.canonicalRoot, 'merge-base', '--is-ancestor', expectedHead, targetBranch], {
|
|
1870
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
1871
|
-
});
|
|
1872
|
-
if (merged.status !== 0) return undefined;
|
|
1873
|
-
|
|
1874
|
-
const worktreeMissing = !existsSync(current.worktreePath);
|
|
1875
|
-
if (worktreeMissing) {
|
|
1876
|
-
return current.finalization.worktreeCleanup === 'done'
|
|
1877
|
-
? { currentHead: expectedHead, worktreeMissing: true, targetBranch }
|
|
1878
|
-
: undefined;
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
const worktree = selectRepositoryCheckout(repository, current.checkoutId, { allowArchived: true });
|
|
1882
|
-
if (resolve(worktree.canonicalRoot) !== resolve(current.worktreePath)) return undefined;
|
|
1883
|
-
if (!repositoryGitStatus(worktree).clean) return undefined;
|
|
1884
|
-
if (gitHead(worktree.canonicalRoot) !== expectedHead) return undefined;
|
|
1885
|
-
const currentBranch = spawnSync('git', ['-C', worktree.canonicalRoot, 'branch', '--show-current'], {
|
|
1886
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
1887
|
-
});
|
|
1888
|
-
if (currentBranch.status !== 0 || String(currentBranch.stdout ?? '').trim() !== current.branch) return undefined;
|
|
1889
|
-
return { currentHead: expectedHead, worktreeMissing: false, targetBranch };
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
type RetryableFinalizationStage = 'commit' | 'merge' | 'branchCleanup' | 'worktreeCleanup';
|
|
1893
|
-
|
|
1894
|
-
function requestedFailedFinalizationRetry(
|
|
1895
|
-
stages: WorkFinalizationStages,
|
|
1896
|
-
wants: { commit: boolean; merge: boolean; cleanup: boolean },
|
|
1897
|
-
): RetryableFinalizationStage | undefined {
|
|
1898
|
-
if (stages.validation === 'failed') return undefined;
|
|
1899
|
-
if (wants.commit && stages.commit === 'failed') return 'commit';
|
|
1900
|
-
if (wants.merge && stages.merge === 'failed') return 'merge';
|
|
1901
|
-
if (wants.cleanup && stages.worktreeCleanup === 'failed') return 'worktreeCleanup';
|
|
1902
|
-
if (wants.cleanup && stages.branchCleanup === 'failed') return 'branchCleanup';
|
|
1903
|
-
return undefined;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
function retryPhaseForFinalizationStage(stage: RetryableFinalizationStage): 'delivery' | 'cleanup' {
|
|
1907
|
-
return stage === 'commit' || stage === 'merge' ? 'delivery' : 'cleanup';
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
function retryHandleStateForFinalization(stages: WorkFinalizationStages): WorkHandleState['state'] {
|
|
1911
|
-
if (stages.merge === 'done') return 'merged';
|
|
1912
|
-
if (stages.commit === 'done') return 'committed';
|
|
1913
|
-
return 'validating';
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
export function resetFinalizationStagesForRequest(
|
|
1917
|
-
stages: WorkFinalizationStages,
|
|
1918
|
-
wants: { commit: boolean; merge: boolean; cleanup: boolean },
|
|
1919
|
-
options: { managedWorktree?: boolean; deleteBranchRequested?: boolean; retainedByRequest?: boolean; workspaceDirty?: boolean } = {},
|
|
1920
|
-
): WorkFinalizationStages {
|
|
1921
|
-
const next = { ...stages };
|
|
1922
|
-
let reset = false;
|
|
1923
|
-
if (next.validation === 'failed') {
|
|
1924
|
-
next.validation = 'pending';
|
|
1925
|
-
reset = true;
|
|
1926
|
-
}
|
|
1927
|
-
if (wants.commit && next.commit === 'failed') {
|
|
1928
|
-
next.commit = 'pending';
|
|
1929
|
-
reset = true;
|
|
1930
|
-
}
|
|
1931
|
-
if (wants.merge && next.merge === 'failed') {
|
|
1932
|
-
next.merge = 'pending';
|
|
1933
|
-
reset = true;
|
|
1934
|
-
}
|
|
1935
|
-
if (wants.cleanup && next.worktreeCleanup === 'failed') {
|
|
1936
|
-
next.worktreeCleanup = 'pending';
|
|
1937
|
-
reset = true;
|
|
1938
|
-
}
|
|
1939
|
-
if (wants.cleanup && next.branchCleanup === 'failed') {
|
|
1940
|
-
next.branchCleanup = 'pending';
|
|
1941
|
-
reset = true;
|
|
1942
|
-
}
|
|
1943
|
-
// A Work may be semantically repaired and revalidated after an earlier delivery
|
|
1944
|
-
// attempt already marked commit/merge done. Exact validation of a dirty current
|
|
1945
|
-
// workspace proves there is new Work-owned content still requiring delivery;
|
|
1946
|
-
// re-arm only the requested Git stages instead of letting cleanup discard it.
|
|
1947
|
-
if (options.workspaceDirty === true && wants.commit && next.commit === 'done') {
|
|
1948
|
-
next.commit = 'pending';
|
|
1949
|
-
reset = true;
|
|
1950
|
-
if (wants.merge && next.merge === 'done') next.merge = 'pending';
|
|
1951
|
-
}
|
|
1952
|
-
// A prior finalize(cleanup=false) intentionally records managed resources as
|
|
1953
|
-
// skipped/retained. A later explicit cleanup=true is a new resource-disposal
|
|
1954
|
-
// request, not an idempotent replay of the earlier retention decision. Re-arm
|
|
1955
|
-
// only cleanup stages that are applicable to this managed Work; commit/merge
|
|
1956
|
-
// skipped states keep their original delivery semantics.
|
|
1957
|
-
if (wants.cleanup && options.retainedByRequest === true && options.managedWorktree === true && next.worktreeCleanup === 'skipped') {
|
|
1958
|
-
next.worktreeCleanup = 'pending';
|
|
1959
|
-
reset = true;
|
|
1960
|
-
}
|
|
1961
|
-
if (wants.cleanup && options.retainedByRequest === true && options.managedWorktree === true && options.deleteBranchRequested === true && next.branchCleanup === 'skipped') {
|
|
1962
|
-
next.branchCleanup = 'pending';
|
|
1963
|
-
reset = true;
|
|
1964
|
-
}
|
|
1965
|
-
if (reset) delete next.lastError;
|
|
1966
|
-
return next;
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
function finalizationComplete(stages: WorkFinalizationStages): boolean {
|
|
1970
|
-
return stages.validation === 'done'
|
|
1971
|
-
&& stages.commit !== 'pending'
|
|
1972
|
-
&& stages.merge !== 'pending'
|
|
1973
|
-
&& stages.branchCleanup !== 'pending'
|
|
1974
|
-
&& stages.worktreeCleanup !== 'pending'
|
|
1975
|
-
&& stages.commit !== 'failed'
|
|
1976
|
-
&& stages.merge !== 'failed'
|
|
1977
|
-
&& stages.branchCleanup !== 'failed'
|
|
1978
|
-
&& stages.worktreeCleanup !== 'failed'
|
|
1979
|
-
&& !stages.lastError;
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
function finalStateForStages(stages: WorkFinalizationStages, fallback: WorkHandleState['state']): WorkHandleState['state'] {
|
|
1983
|
-
if (stages.worktreeCleanup === 'done') return 'cleaned';
|
|
1984
|
-
if (stages.merge === 'done') return 'merged';
|
|
1985
|
-
if (stages.commit === 'done') return 'committed';
|
|
1986
|
-
return fallback === 'failed' ? 'editing' : fallback;
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
function currentWorkValidationInput(
|
|
1990
|
-
repository: RepositoryRecord,
|
|
1991
|
-
handle: WorkHandleState,
|
|
1992
|
-
requestedChecks: string[],
|
|
1993
|
-
): { head: string; workspaceFingerprint: string; fingerprint: string; clean: boolean } {
|
|
1994
|
-
const status = repositoryGitStatus(repository);
|
|
1995
|
-
const head = status.head ?? handle.expectedHead ?? handle.baseCommit ?? 'unknown';
|
|
1996
|
-
const workspaceFingerprint = workspaceValidationFingerprint(repository.canonicalRoot, status);
|
|
1997
|
-
return {
|
|
1998
|
-
head,
|
|
1999
|
-
workspaceFingerprint,
|
|
2000
|
-
fingerprint: workValidationInputFingerprint(head, workspaceFingerprint, requestedChecks),
|
|
2001
|
-
clean: status.clean,
|
|
2002
|
-
};
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
2006
|
-
const session = requireSession(ctx, args);
|
|
2007
|
-
let current = workForSession(ctx, session, args, { allowClaimedTerminalCleanup: args.cleanup !== false });
|
|
2008
|
-
const requestedWants = { commit: args.commit === true, merge: args.merge === true, cleanup: args.cleanup === true };
|
|
2009
|
-
const retryStage = current.state === 'failed'
|
|
2010
|
-
? requestedFailedFinalizationRetry(current.finalization, requestedWants)
|
|
2011
|
-
: undefined;
|
|
2012
|
-
const retryContract = retryStage ? contractFor(ctx, current) : undefined;
|
|
2013
|
-
if (
|
|
2014
|
-
retryStage
|
|
2015
|
-
&& retryContract?.status === 'failed'
|
|
2016
|
-
&& !retryContract.completionOutcome
|
|
2017
|
-
&& !retryContract.completionReceipt
|
|
2018
|
-
) {
|
|
2019
|
-
transitionWorkContractPhase(
|
|
2020
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
2021
|
-
retryContract.workId,
|
|
2022
|
-
{
|
|
2023
|
-
phase: retryPhaseForFinalizationStage(retryStage),
|
|
2024
|
-
status: 'running',
|
|
2025
|
-
state: 'active',
|
|
2026
|
-
summary: `Retrying previously failed Work finalization stage ${retryStage}.`,
|
|
2027
|
-
},
|
|
2028
|
-
);
|
|
2029
|
-
}
|
|
2030
|
-
const terminalOutcome = terminalCleanupOutcome(ctx, current);
|
|
2031
|
-
if (terminalOutcome && args.cleanup !== false) {
|
|
2032
|
-
return await reconcileTerminalCleanup(ctx, session, current, args, terminalOutcome);
|
|
2033
|
-
}
|
|
2034
|
-
const terminalizationOwner = assertWorkControllerOwnership(ctx, session, current, args);
|
|
2035
|
-
if (terminalOutcome && args.cleanup === false) {
|
|
2036
|
-
const recordedAt = new Date().toISOString();
|
|
2037
|
-
current = withControllerLock(
|
|
2038
|
-
ctx.controllerHome,
|
|
2039
|
-
{ scope: 'worktree', repoId: current.repositoryId, worktreeId: current.checkoutId },
|
|
2040
|
-
`work-finalize:${current.workId}:retain-terminal-resources`,
|
|
2041
|
-
() => {
|
|
2042
|
-
const fresh = readWorkHandle(ctx.controllerHome, current.repositoryId, current.workId) ?? current;
|
|
2043
|
-
return writeWorkHandle(ctx.controllerHome, {
|
|
2044
|
-
...fresh,
|
|
2045
|
-
finalization: {
|
|
2046
|
-
...fresh.finalization,
|
|
2047
|
-
validation: fresh.finalization.validation === 'failed' ? 'failed' : 'done',
|
|
2048
|
-
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
2049
|
-
merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
|
|
2050
|
-
branchCleanup: 'skipped',
|
|
2051
|
-
worktreeCleanup: 'skipped',
|
|
2052
|
-
lastError: fresh.finalization.validation === 'failed' ? fresh.finalization.lastError : undefined,
|
|
2053
|
-
},
|
|
2054
|
-
terminalResourceDisposition: {
|
|
2055
|
-
mode: 'retained_by_request',
|
|
2056
|
-
retainWorktree: fresh.managedWorktree,
|
|
2057
|
-
retainBranch: true,
|
|
2058
|
-
recordedAt,
|
|
2059
|
-
},
|
|
2060
|
-
});
|
|
2061
|
-
},
|
|
2062
|
-
10_000,
|
|
2063
|
-
);
|
|
2064
|
-
appendWorkEvidence(
|
|
2065
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
2066
|
-
current.workContractId ?? current.workId,
|
|
2067
|
-
{
|
|
2068
|
-
title: 'terminal resources retained by request',
|
|
2069
|
-
summary: `Controller explicitly retained ${current.managedWorktree ? 'the managed worktree and ' : ''}local branch after terminal Work; automatic terminal cleanup must not reclaim them.`,
|
|
2070
|
-
detailLevel: 'summary',
|
|
2071
|
-
},
|
|
2072
|
-
);
|
|
2073
|
-
releasePreparedWorkOwnership(ctx, current);
|
|
2074
|
-
updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), {
|
|
2075
|
-
activeWorkId: undefined,
|
|
2076
|
-
activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
|
|
2077
|
-
});
|
|
2078
|
-
return {
|
|
2079
|
-
work: compactHandle(current),
|
|
2080
|
-
stages: current.finalization,
|
|
2081
|
-
completed: true,
|
|
2082
|
-
cleanupCompleted: false,
|
|
2083
|
-
cleanupRetained: true,
|
|
2084
|
-
cleanupPending: false,
|
|
2085
|
-
idempotent: true,
|
|
2086
|
-
};
|
|
2087
|
-
}
|
|
2088
|
-
if (current.state === 'cleaned') {
|
|
2089
|
-
const terminalContract = contractFor(ctx, current);
|
|
2090
|
-
if (
|
|
2091
|
-
terminalContract?.status === 'failed'
|
|
2092
|
-
&& current.finalization.validation === 'failed'
|
|
2093
|
-
&& current.finalization.worktreeCleanup === 'done'
|
|
2094
|
-
) {
|
|
2095
|
-
releasePreparedWorkOwnership(ctx, current);
|
|
2096
|
-
updateExecutionSession(ctx.controllerHome, identityFor(ctx, args), {
|
|
2097
|
-
activeWorkId: undefined,
|
|
2098
|
-
activeCheckoutId: workReturnCheckoutId(ctx, current, session.activeCheckoutId),
|
|
2099
|
-
});
|
|
2100
|
-
return {
|
|
2101
|
-
idempotent: true,
|
|
2102
|
-
work: compactHandle(current),
|
|
2103
|
-
stages: current.finalization,
|
|
2104
|
-
completed: false,
|
|
2105
|
-
cleanupCompleted: true,
|
|
2106
|
-
failurePreserved: true,
|
|
2107
|
-
};
|
|
2108
|
-
}
|
|
2109
|
-
validateWorkHandle(ctx.controllerHome, current, identityFor(ctx, args), 'none', 'finalize');
|
|
2110
|
-
return { idempotent: true, work: compactHandle(current) };
|
|
2111
|
-
}
|
|
2112
|
-
const identity = identityFor(ctx, args);
|
|
2113
|
-
const requestedOutcome = args.completion_outcome === 'completed_no_change' || args.completion_outcome === 'completed_changed'
|
|
2114
|
-
? args.completion_outcome
|
|
2115
|
-
: undefined;
|
|
2116
|
-
const noChangeProof = typeof args.no_change_evidence === 'string' ? args.no_change_evidence.trim().slice(0, 1_000) : '';
|
|
2117
|
-
if (requestedOutcome === 'completed_no_change' && (!noChangeProof || args.commit === true || args.merge === true)) {
|
|
2118
|
-
throw new Error('WORK_NO_CHANGE_PROOF_REQUIRED: completed_no_change requires objective-specific evidence and forbids commit/merge');
|
|
2119
|
-
}
|
|
2120
|
-
const wants = requestedWants;
|
|
2121
|
-
const explicitTargetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
|
|
2122
|
-
? args.target_branch.trim()
|
|
2123
|
-
: undefined;
|
|
2124
|
-
// With merge=false an explicitly selected Work branch is the delivery target,
|
|
2125
|
-
// not a disposable feature branch. Keep the ref reachable for the receipt.
|
|
2126
|
-
const retainExplicitWorkTargetBranch = !wants.merge && explicitTargetBranch === current.branch;
|
|
2127
|
-
const deleteBranchRequested = args.delete_branch !== false && !retainExplicitWorkTargetBranch;
|
|
2128
|
-
const noChangeFastPath = requestedOutcome === 'completed_no_change'
|
|
2129
|
-
&& !wants.commit
|
|
2130
|
-
&& !wants.merge
|
|
2131
|
-
&& current.state !== 'failed';
|
|
2132
|
-
|
|
2133
|
-
const transact = (label: string, update: (fresh: WorkHandleState) => WorkHandleState): WorkHandleState =>
|
|
2134
|
-
withControllerLock(ctx.controllerHome, { scope: 'worktree', repoId: current.repositoryId, worktreeId: current.checkoutId }, `work-finalize:${current.workId}:${label}`, () => {
|
|
2135
|
-
const fresh = readWorkHandle(ctx.controllerHome, current.repositoryId, current.workId) ?? current;
|
|
2136
|
-
return update(fresh);
|
|
2137
|
-
}, 10_000);
|
|
2138
|
-
|
|
2139
|
-
const failStage = (stage: keyof WorkFinalizationStages, reason: string): Record<string, unknown> => {
|
|
2140
|
-
current = transact(`fail:${String(stage)}`, (fresh) => {
|
|
2141
|
-
const finalization = { ...fresh.finalization, [stage]: 'failed', lastError: reason } as WorkFinalizationStages;
|
|
2142
|
-
return markWorkHandleFailed(ctx.controllerHome, { ...fresh, finalization }, reason);
|
|
2143
|
-
});
|
|
2144
|
-
if (current.workContractId) {
|
|
2145
|
-
if (stage === 'validation') {
|
|
2146
|
-
updateWorkContract({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId, { status: 'failed' });
|
|
2147
|
-
} else if (stage === 'commit' || stage === 'merge' || stage === 'branchCleanup' || stage === 'worktreeCleanup') {
|
|
2148
|
-
transitionWorkContractPhase(
|
|
2149
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
2150
|
-
current.workContractId,
|
|
2151
|
-
{
|
|
2152
|
-
phase: retryPhaseForFinalizationStage(stage),
|
|
2153
|
-
status: 'blocked',
|
|
2154
|
-
state: 'blocked',
|
|
2155
|
-
summary: `Retryable Work finalization stage ${stage} failed: ${reason}`,
|
|
2156
|
-
},
|
|
2157
|
-
);
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
markRepositoryProjectionDirty(ctx.controllerHome, current.repositoryId, `finalize:${current.workId}:${String(stage)}-failed`);
|
|
2161
|
-
return { work: compactHandle(current), stages: current.finalization, completed: false };
|
|
2162
|
-
};
|
|
2163
|
-
|
|
2164
|
-
const failedCleanupRequested = current.state === 'failed'
|
|
2165
|
-
&& wants.cleanup
|
|
2166
|
-
&& !wants.commit
|
|
2167
|
-
&& !wants.merge
|
|
2168
|
-
&& retryStage === undefined;
|
|
2169
|
-
const failedCleanupProof = failedCleanupRequested
|
|
2170
|
-
? failedCleanupOnlyHead(ctx, current, args)
|
|
2171
|
-
: undefined;
|
|
2172
|
-
if (failedCleanupRequested && !failedCleanupProof) {
|
|
2173
|
-
throw new Error('WORK_FAILED_CLEANUP_UNSAFE: failed Work cleanup requires exact checkout/branch ownership and an unchanged clean controller-owned managed worktree whose exact HEAD is already contained in the target branch');
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
if (!failedCleanupRequested && !noChangeFastPath) {
|
|
2177
|
-
current = transact('begin', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2178
|
-
...fresh,
|
|
2179
|
-
state: fresh.state === 'failed' ? retryHandleStateForFinalization(fresh.finalization) : fresh.state,
|
|
2180
|
-
failureReason: undefined,
|
|
2181
|
-
finalization: resetFinalizationStagesForRequest(fresh.finalization, wants, {
|
|
2182
|
-
managedWorktree: fresh.managedWorktree,
|
|
2183
|
-
deleteBranchRequested,
|
|
2184
|
-
retainedByRequest: fresh.terminalResourceDisposition?.mode === 'retained_by_request',
|
|
2185
|
-
}),
|
|
2186
|
-
...(wants.cleanup && fresh.terminalResourceDisposition?.mode === 'retained_by_request' ? { terminalResourceDisposition: undefined } : {}),
|
|
2187
|
-
}));
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
const approvalRequestId = typeof args.approval_request_id === 'string' ? args.approval_request_id.trim() : '';
|
|
2191
|
-
const resolvedAuthorization = approvalRequestId
|
|
2192
|
-
? assertResolvedAuthorization({ controllerHome: ctx.controllerHome, repositoryId: current.repositoryId, approvalRequestId, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, permissionSnapshotVersion: current.permissionSnapshotVersion, command: 'work_finalize' })
|
|
2193
|
-
: undefined;
|
|
2194
|
-
const gitAuthorization = resolvedAuthorization
|
|
2195
|
-
? { decision: 'allow', source: 'user_confirmation', reason: 'The user resolved the exact finalization approval request.' } as const
|
|
2196
|
-
: decideAuthorization({
|
|
2197
|
-
controllerHome: ctx.controllerHome,
|
|
2198
|
-
accessMode: readRepositoryAccessPolicy(ctx.controllerHome, current.repositoryId).mode,
|
|
2199
|
-
risk: 'local_git',
|
|
2200
|
-
repositoryId: current.repositoryId,
|
|
2201
|
-
currentRepositoryId: current.repositoryId,
|
|
2202
|
-
workId: current.workId,
|
|
2203
|
-
boundWorkId: current.workId,
|
|
2204
|
-
goalId: current.goalId,
|
|
2205
|
-
boundGoalId: current.goalId,
|
|
2206
|
-
sessionId: session.sessionId,
|
|
2207
|
-
principalId: session.principalId,
|
|
2208
|
-
permissionSnapshotVersion: current.permissionSnapshotVersion,
|
|
2209
|
-
delegation: session.goalDelegation,
|
|
2210
|
-
command: 'work_finalize',
|
|
2211
|
-
});
|
|
2212
|
-
if (gitAuthorization.decision !== 'allow') return { authorization: gitAuthorization, work: compactHandle(current), stages: current.finalization };
|
|
2213
|
-
|
|
2214
|
-
if (failedCleanupProof) {
|
|
2215
|
-
const preservedFailure = current.failureReason ?? current.finalization.lastError ?? 'Work validation failed.';
|
|
2216
|
-
current = transact('failed-cleanup-begin', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2217
|
-
...fresh,
|
|
2218
|
-
state: 'failed',
|
|
2219
|
-
failureReason: preservedFailure,
|
|
2220
|
-
finalization: {
|
|
2221
|
-
...fresh.finalization,
|
|
2222
|
-
validation: 'failed',
|
|
2223
|
-
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
2224
|
-
merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
|
|
2225
|
-
branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
|
|
2226
|
-
lastError: preservedFailure,
|
|
2227
|
-
},
|
|
2228
|
-
}));
|
|
2229
|
-
|
|
2230
|
-
if (!failedCleanupProof.worktreeMissing && current.finalization.worktreeCleanup !== 'done') {
|
|
2231
|
-
const target = selectWorkFinalizationTarget(
|
|
2232
|
-
getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true }),
|
|
2233
|
-
current,
|
|
2234
|
-
);
|
|
2235
|
-
const cleanup = runCleanup(target.canonicalRoot, current.worktreePath);
|
|
2236
|
-
if (!cleanup.ok) {
|
|
2237
|
-
throw new Error(`WORK_FAILED_CLEANUP_UNSAFE: ${cleanup.message ?? 'managed worktree cleanup failed'}`);
|
|
2238
|
-
}
|
|
2239
|
-
current = transact('failed-worktree-cleanup-done', (fresh) => {
|
|
2240
|
-
setRepositoryCheckoutLifecycle({
|
|
2241
|
-
controllerHome: ctx.controllerHome,
|
|
2242
|
-
repoId: fresh.repositoryId,
|
|
2243
|
-
checkoutId: fresh.checkoutId,
|
|
2244
|
-
lifecycle: 'removed',
|
|
2245
|
-
reason: `Failed Work ${fresh.workId} cleanup completed without changing delivery state.`,
|
|
2246
|
-
});
|
|
2247
|
-
markRepositoryProjectionDirty(ctx.controllerHome, fresh.repositoryId, `cleanup:${fresh.workId}:failed-worktree`);
|
|
2248
|
-
return writeWorkHandle(ctx.controllerHome, {
|
|
2249
|
-
...fresh,
|
|
2250
|
-
state: 'failed',
|
|
2251
|
-
failureReason: preservedFailure,
|
|
2252
|
-
finalization: { ...fresh.finalization, worktreeCleanup: 'done', lastError: preservedFailure },
|
|
2253
|
-
});
|
|
2254
|
-
});
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
if (deleteBranchRequested && current.finalization.branchCleanup !== 'done') {
|
|
2258
|
-
const target = selectWorkFinalizationTarget(
|
|
2259
|
-
getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true }),
|
|
2260
|
-
current,
|
|
2261
|
-
);
|
|
2262
|
-
const deleted = repositoryGitDeleteBranch(ctx.controllerHome, target, {
|
|
2263
|
-
branch: current.branch,
|
|
2264
|
-
force: false,
|
|
2265
|
-
authorizationDecision: gitAuthorization,
|
|
2266
|
-
sessionId: session.sessionId,
|
|
2267
|
-
principalId: session.principalId,
|
|
2268
|
-
workId: current.workId,
|
|
2269
|
-
goalId: current.goalId,
|
|
2270
|
-
});
|
|
2271
|
-
if (deleted.execution.authorizationDecision?.decision === 'user_confirmation_required') {
|
|
2272
|
-
return { authorization: deleted.execution.authorizationDecision, work: compactHandle(current), stages: current.finalization };
|
|
2273
|
-
}
|
|
2274
|
-
if (deleted.execution.status !== 'executed' || deleted.execution.ok !== true) {
|
|
2275
|
-
current = transact('failed-branch-cleanup-failed', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2276
|
-
...fresh,
|
|
2277
|
-
state: 'failed',
|
|
2278
|
-
failureReason: preservedFailure,
|
|
2279
|
-
finalization: {
|
|
2280
|
-
...fresh.finalization,
|
|
2281
|
-
branchCleanup: 'failed',
|
|
2282
|
-
lastError: String(deleted.execution.stderr || 'feature branch cleanup failed').slice(0, 1_000),
|
|
2283
|
-
},
|
|
2284
|
-
}));
|
|
2285
|
-
return {
|
|
2286
|
-
work: compactHandle(current),
|
|
2287
|
-
stages: current.finalization,
|
|
2288
|
-
completed: false,
|
|
2289
|
-
cleanupCompleted: false,
|
|
2290
|
-
failurePreserved: true,
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
|
-
current = transact('failed-branch-cleanup-done', (fresh) => {
|
|
2294
|
-
markRepositoryProjectionDirty(ctx.controllerHome, fresh.repositoryId, `cleanup:${fresh.workId}:failed-branch`);
|
|
2295
|
-
return writeWorkHandle(ctx.controllerHome, {
|
|
2296
|
-
...fresh,
|
|
2297
|
-
state: 'failed',
|
|
2298
|
-
failureReason: preservedFailure,
|
|
2299
|
-
finalization: { ...fresh.finalization, branchCleanup: 'done', lastError: preservedFailure },
|
|
2300
|
-
});
|
|
2301
|
-
});
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
current = transact('failed-cleanup-complete', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'cleaned', {
|
|
2305
|
-
failureReason: preservedFailure,
|
|
2306
|
-
finalization: {
|
|
2307
|
-
...fresh.finalization,
|
|
2308
|
-
validation: 'failed',
|
|
2309
|
-
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
2310
|
-
merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
|
|
2311
|
-
branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
|
|
2312
|
-
worktreeCleanup: 'done',
|
|
2313
|
-
lastError: preservedFailure,
|
|
2314
|
-
},
|
|
2315
|
-
}));
|
|
2316
|
-
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
|
|
2317
|
-
title: 'failed work cleanup completed',
|
|
2318
|
-
summary: `Controller preserved the failed Work outcome while removing its unchanged clean managed worktree and ${deleteBranchRequested ? 'removing' : 'retaining'} the local branch after proving ${failedCleanupProof.currentHead} is contained in ${failedCleanupProof.targetBranch}.`,
|
|
2319
|
-
detailLevel: 'summary',
|
|
2320
|
-
});
|
|
2321
|
-
releasePreparedWorkOwnership(ctx, current);
|
|
2322
|
-
updateExecutionSession(ctx.controllerHome, identity, {
|
|
2323
|
-
activeWorkId: undefined,
|
|
2324
|
-
activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
|
|
2325
|
-
});
|
|
2326
|
-
return {
|
|
2327
|
-
work: compactHandle(current),
|
|
2328
|
-
stages: current.finalization,
|
|
2329
|
-
completed: false,
|
|
2330
|
-
cleanupCompleted: true,
|
|
2331
|
-
failurePreserved: true,
|
|
2332
|
-
idempotent: false,
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
const contractAtStart = contractFor(ctx, current);
|
|
2337
|
-
const cancelledCleanupRequested = contractAtStart?.status === 'cancelled'
|
|
2338
|
-
&& wants.cleanup
|
|
2339
|
-
&& !wants.commit
|
|
2340
|
-
&& !wants.merge;
|
|
2341
|
-
const cleanupReconciliation = requestedOutcome === 'completed_no_change'
|
|
2342
|
-
? undefined
|
|
2343
|
-
: cleanupOnlyMergedHead(ctx, current, args);
|
|
2344
|
-
if (cancelledCleanupRequested && !cleanupReconciliation) {
|
|
2345
|
-
throw new Error('WORK_CANCELLED_CLEANUP_UNSAFE: cancelled Work cleanup requires an unchanged clean managed worktree (or a previously recorded cleanup) and a branch HEAD already contained in the target branch');
|
|
2346
|
-
}
|
|
2347
|
-
if (cleanupReconciliation) {
|
|
2348
|
-
current = transact('cleanup-reconcile-merged-head', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2349
|
-
...fresh,
|
|
2350
|
-
expectedHead: cleanupReconciliation.currentHead,
|
|
2351
|
-
state: 'merged',
|
|
2352
|
-
failureReason: undefined,
|
|
2353
|
-
finalization: {
|
|
2354
|
-
...fresh.finalization,
|
|
2355
|
-
validation: cleanupReconciliation.worktreeMissing ? 'done' : fresh.finalization.validation,
|
|
2356
|
-
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
2357
|
-
merge: 'done',
|
|
2358
|
-
branchCleanup: deleteBranchRequested ? 'pending' : 'skipped',
|
|
2359
|
-
lastError: undefined,
|
|
2360
|
-
},
|
|
2361
|
-
}));
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
const changedCleanupOnly = wants.cleanup
|
|
2365
|
-
&& !wants.commit
|
|
2366
|
-
&& !wants.merge
|
|
2367
|
-
&& requestedOutcome !== 'completed_no_change'
|
|
2368
|
-
&& current.state !== 'merged'
|
|
2369
|
-
&& current.state !== 'cleaned';
|
|
2370
|
-
if (changedCleanupOnly && !cleanupReconciliation) {
|
|
2371
|
-
throw new Error('WORK_CLEANUP_DELIVERY_NOT_PROVEN: cleanup-only completion requires the exact Work branch HEAD to already be contained in the target branch before any managed worktree is removed');
|
|
2372
|
-
}
|
|
2373
|
-
|
|
2374
|
-
const terminalCleanupOnly = Boolean(cleanupReconciliation && wants.cleanup && !wants.commit && !wants.merge);
|
|
2375
|
-
let exactValidationInput: ReturnType<typeof currentWorkValidationInput> | undefined;
|
|
2376
|
-
if (terminalCleanupOnly) {
|
|
2377
|
-
current = transact('terminal-cleanup-validation-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2378
|
-
...fresh,
|
|
2379
|
-
finalization: { ...fresh.finalization, validation: 'done', lastError: undefined },
|
|
2380
|
-
}));
|
|
2381
|
-
} else {
|
|
2382
|
-
let validatedRepository: RepositoryRecord;
|
|
2383
|
-
try {
|
|
2384
|
-
validatedRepository = validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize').worktreeRepository;
|
|
2385
|
-
} catch (error) {
|
|
2386
|
-
return failStage('validation', error instanceof Error ? error.message : String(error));
|
|
2387
|
-
}
|
|
2388
|
-
const validationContract = contractFor(ctx, current);
|
|
2389
|
-
if (!validationContract) throw new Error(`WORK_VALIDATION_CONTRACT_MISSING: ${current.workContractId ?? current.workId}`);
|
|
2390
|
-
const validationInput = currentWorkValidationInput(validatedRepository, current, validationContract.checks);
|
|
2391
|
-
exactValidationInput = validationInput;
|
|
2392
|
-
if (validationContract.checks.length === 0) {
|
|
2393
|
-
if (!noChangeFastPath) {
|
|
2394
|
-
current = transact('validation-no-checks', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2395
|
-
...fresh,
|
|
2396
|
-
validatedInputFingerprint: validationInput.fingerprint,
|
|
2397
|
-
finalization: { ...fresh.finalization, validation: 'done', lastError: undefined },
|
|
2398
|
-
}));
|
|
2399
|
-
}
|
|
2400
|
-
projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required.');
|
|
2401
|
-
} else if (!hasCurrentWorkValidationAuthority({
|
|
2402
|
-
finalizationValidation: current.finalization.validation,
|
|
2403
|
-
validatedInputFingerprint: current.validatedInputFingerprint,
|
|
2404
|
-
evidenceState: validationContract.evidenceState,
|
|
2405
|
-
expectedFingerprint: validationInput.fingerprint,
|
|
2406
|
-
})) {
|
|
2407
|
-
current = transact('validation-required', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2408
|
-
...fresh,
|
|
2409
|
-
validatedInputFingerprint: undefined,
|
|
2410
|
-
finalization: { ...fresh.finalization, validation: 'pending', lastError: undefined },
|
|
2411
|
-
}));
|
|
2412
|
-
markWorkValidationPending(ctx.controllerHome, current);
|
|
2413
|
-
throw new Error('WORK_VALIDATION_REQUIRED: run work_validate against the exact current workspace before finalization');
|
|
2414
|
-
}
|
|
2415
|
-
if (!validationInput.clean && wants.commit && current.finalization.commit === 'done') {
|
|
2416
|
-
current = transact('rearm-delivery-after-validated-repair', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2417
|
-
...fresh,
|
|
2418
|
-
state: 'validating',
|
|
2419
|
-
failureReason: undefined,
|
|
2420
|
-
finalization: resetFinalizationStagesForRequest(fresh.finalization, wants, {
|
|
2421
|
-
managedWorktree: fresh.managedWorktree,
|
|
2422
|
-
deleteBranchRequested,
|
|
2423
|
-
retainedByRequest: fresh.terminalResourceDisposition?.mode === 'retained_by_request',
|
|
2424
|
-
workspaceDirty: true,
|
|
2425
|
-
}),
|
|
2426
|
-
}));
|
|
2427
|
-
}
|
|
2428
|
-
if (wants.merge && !wants.commit && !validationInput.clean) {
|
|
2429
|
-
throw new Error('WORK_MERGE_UNCOMMITTED_CHANGES: commit the validated workspace before merging');
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
if (noChangeFastPath) {
|
|
2434
|
-
if (!exactValidationInput?.clean) {
|
|
2435
|
-
throw new Error('WORK_NO_CHANGE_DIRTY: completed_no_change cannot retain an owned dirty worktree');
|
|
2436
|
-
}
|
|
2437
|
-
const noChangeFinalization: WorkFinalizationStages = {
|
|
2438
|
-
...current.finalization,
|
|
2439
|
-
validation: 'done',
|
|
2440
|
-
commit: 'skipped',
|
|
2441
|
-
merge: 'skipped',
|
|
2442
|
-
branchCleanup: wants.cleanup && deleteBranchRequested ? 'pending' : 'skipped',
|
|
2443
|
-
lastError: undefined,
|
|
2444
|
-
};
|
|
2445
|
-
// Collapse the no-op Git stages into the same durable lifecycle write used
|
|
2446
|
-
// to enter/establish the delivery boundary. Physical cleanup remains
|
|
2447
|
-
// separately persisted because it has real side effects and crash semantics.
|
|
2448
|
-
if (current.state === 'prepared') {
|
|
2449
|
-
current = transact('no-change-validated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'validating', {
|
|
2450
|
-
finalization: noChangeFinalization,
|
|
2451
|
-
validatedInputFingerprint: exactValidationInput.fingerprint,
|
|
2452
|
-
failureReason: undefined,
|
|
2453
|
-
}));
|
|
2454
|
-
} else if (current.state === 'editing' || current.state === 'validating' || current.state === 'committed') {
|
|
2455
|
-
current = transact('no-change-delivery-integrated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
|
|
2456
|
-
finalization: noChangeFinalization,
|
|
2457
|
-
validatedInputFingerprint: exactValidationInput.fingerprint,
|
|
2458
|
-
failureReason: undefined,
|
|
2459
|
-
}));
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
|
|
2463
|
-
if (wants.commit && current.finalization.commit === 'pending') {
|
|
2464
|
-
const validated = validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize');
|
|
2465
|
-
const contract = contractFor(ctx, current);
|
|
2466
|
-
if (contract?.constraints.allowCommit === false) throw new Error('WORK_COMMIT_NOT_ALLOWED: WorkContract disallows commit');
|
|
2467
|
-
const status = repositoryGitStatus(validated.worktreeRepository);
|
|
2468
|
-
const { dirtyPaths, ownedPaths: commitPaths } = workOwnedDirtyPaths(contract, status);
|
|
2469
|
-
if (dirtyPaths.length > 0 && commitPaths.length === 0) {
|
|
2470
|
-
throw new Error(`WORK_COMMIT_NO_OWNED_DIRTY_PATHS: ${current.workId} has ${dirtyPaths.length} dirty path(s), but none are owned by its WorkContract path scope`);
|
|
2471
|
-
}
|
|
2472
|
-
const committed = repositoryGitCommit(ctx.controllerHome, validated.worktreeRepository, { message: String(args.message ?? `Complete ${current.workId}`), paths: commitPaths, allowEmpty: false, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
|
|
2473
|
-
const pendingAuthorization = [committed.stage, committed.commit].find((execution) => execution?.authorizationDecision?.decision === 'user_confirmation_required')?.authorizationDecision;
|
|
2474
|
-
if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
|
|
2475
|
-
if (!committed.committed) return { ...failStage('commit', committed.error?.message ?? 'commit failed'), commit: committed };
|
|
2476
|
-
const checks = contract?.checks ?? [];
|
|
2477
|
-
const committedValidatedWorkspaceFingerprint = exactValidationInput && committed.after.clean
|
|
2478
|
-
? workspaceValidationFingerprint(validated.worktreeRepository.canonicalRoot, committed.before)
|
|
2479
|
-
: undefined;
|
|
2480
|
-
const validationPreservedAcrossCommit = Boolean(
|
|
2481
|
-
checks.length > 0
|
|
2482
|
-
&& exactValidationInput
|
|
2483
|
-
&& committedValidatedWorkspaceFingerprint === exactValidationInput.workspaceFingerprint
|
|
2484
|
-
);
|
|
2485
|
-
const committedHead = gitHead(validated.worktreeRepository.canonicalRoot);
|
|
2486
|
-
const postCommitInput = currentWorkValidationInput(validated.worktreeRepository, { ...current, expectedHead: committedHead }, checks);
|
|
2487
|
-
current = transact('commit-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'committed', {
|
|
2488
|
-
expectedHead: committedHead,
|
|
2489
|
-
finalization: {
|
|
2490
|
-
...fresh.finalization,
|
|
2491
|
-
validation: checks.length ? (validationPreservedAcrossCommit ? 'done' : 'pending') : 'done',
|
|
2492
|
-
commit: 'done',
|
|
2493
|
-
lastError: undefined,
|
|
2494
|
-
},
|
|
2495
|
-
validatedInputFingerprint: checks.length
|
|
2496
|
-
? (validationPreservedAcrossCommit ? postCommitInput.fingerprint : undefined)
|
|
2497
|
-
: postCommitInput.fingerprint,
|
|
2498
|
-
failureReason: undefined,
|
|
2499
|
-
}));
|
|
2500
|
-
if (checks.length && !validationPreservedAcrossCommit) {
|
|
2501
|
-
markWorkValidationPending(ctx.controllerHome, current);
|
|
2502
|
-
return {
|
|
2503
|
-
work: compactHandle(current),
|
|
2504
|
-
stages: current.finalization,
|
|
2505
|
-
completed: false,
|
|
2506
|
-
continuation: 'WORK_COMMITTED_REVALIDATION_REQUIRED: committed content changed from the exact validated workspace; run work_validate on the committed HEAD before merge or completion',
|
|
2507
|
-
};
|
|
2508
|
-
}
|
|
2509
|
-
if (validationPreservedAcrossCommit && exactValidationInput) {
|
|
2510
|
-
const checkDefinitions = listControllerChecks(validated.worktreeRepository.canonicalRoot);
|
|
2511
|
-
for (const checkId of checks) {
|
|
2512
|
-
const check = checkDefinitions.find((entry) => entry.id === checkId);
|
|
2513
|
-
if (check?.effects?.git !== undefined) continue;
|
|
2514
|
-
const sourcePass = contract
|
|
2515
|
-
? effectiveVerificationEvidence(contract.checkRefs, {
|
|
2516
|
-
sourceRevision: exactValidationInput.head,
|
|
2517
|
-
workspaceFingerprint: exactValidationInput.workspaceFingerprint,
|
|
2518
|
-
checkId,
|
|
2519
|
-
requestedChecks: checks,
|
|
2520
|
-
}).find((entry) => entry.current && entry.record.outcome === 'valid_pass' && Boolean(entry.record.receipt))?.record
|
|
2521
|
-
: undefined;
|
|
2522
|
-
if (!sourcePass) continue;
|
|
2523
|
-
appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
|
|
2524
|
-
...sourcePass,
|
|
2525
|
-
summary: `Validation authority transferred across content-equivalent commit for ${checkId}.`,
|
|
2526
|
-
recordedAt: new Date().toISOString(),
|
|
2527
|
-
sourceRevision: postCommitInput.head,
|
|
2528
|
-
workspaceFingerprint: postCommitInput.workspaceFingerprint,
|
|
2529
|
-
verificationInputFingerprint: verificationInputFingerprint({
|
|
2530
|
-
sourceRevision: postCommitInput.head,
|
|
2531
|
-
workspaceFingerprint: postCommitInput.workspaceFingerprint,
|
|
2532
|
-
checkId,
|
|
2533
|
-
requestedChecks: checks,
|
|
2534
|
-
}),
|
|
2535
|
-
evidenceRef: {
|
|
2536
|
-
title: checkId,
|
|
2537
|
-
summary: 'Reused prior valid receipt after proving the validated filesystem content was committed without drift.',
|
|
2538
|
-
detailLevel: 'summary',
|
|
2539
|
-
},
|
|
2540
|
-
});
|
|
2541
|
-
}
|
|
2542
|
-
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
|
|
2543
|
-
title: 'validation authority preserved across content-equivalent commit',
|
|
2544
|
-
summary: `Exact validated workspace ${exactValidationInput.workspaceFingerprint.slice(0, 16)} was committed without content/status drift; non-Git-sensitive per-check authority transferred to committed HEAD ${postCommitInput.head}.`,
|
|
2545
|
-
detailLevel: 'summary',
|
|
2546
|
-
});
|
|
2547
|
-
} else if (checks.length === 0) {
|
|
2548
|
-
projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required after commit.');
|
|
2549
|
-
}
|
|
2550
|
-
} else if (!wants.commit && current.finalization.commit === 'pending') {
|
|
2551
|
-
current = transact('commit-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, commit: 'skipped' } }));
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
if (wants.merge && current.finalization.merge === 'pending' && current.finalization.commit === 'done') {
|
|
2555
|
-
const repository = getRepository(current.repositoryId, ctx.controllerHome);
|
|
2556
|
-
const target = selectWorkFinalizationTarget(repository, current);
|
|
2557
|
-
const targetBranch = explicitTargetBranch ?? repository.defaultBranch ?? 'main';
|
|
2558
|
-
const directTarget = inspectDirectTargetDelivery(
|
|
2559
|
-
target.canonicalRoot,
|
|
2560
|
-
current.worktreePath,
|
|
2561
|
-
current.managedWorktree,
|
|
2562
|
-
current.branch,
|
|
2563
|
-
targetBranch,
|
|
2564
|
-
current.expectedHead,
|
|
2565
|
-
);
|
|
2566
|
-
if (directTarget.integrated) {
|
|
2567
|
-
current = transact('direct-target-delivery-integrated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
|
|
2568
|
-
finalization: { ...fresh.finalization, merge: 'done', branchCleanup: 'skipped', lastError: undefined },
|
|
2569
|
-
failureReason: undefined,
|
|
2570
|
-
}));
|
|
2571
|
-
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
|
|
2572
|
-
title: 'direct target delivery already integrated',
|
|
2573
|
-
summary: `Work ${current.workId} runs directly on ${targetBranch}; exact delivery revision ${directTarget.expectedHead} is reachable from current target ${directTarget.targetHead}, so no self-merge or target-branch deletion is required.`,
|
|
2574
|
-
detailLevel: 'summary',
|
|
2575
|
-
});
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
if (wants.merge && current.finalization.merge === 'pending') {
|
|
2580
|
-
const mergeValidated = validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize');
|
|
2581
|
-
if (mergeValidated.currentHead && mergeValidated.currentHead !== current.expectedHead) {
|
|
2582
|
-
current = transact('merge-adopt-work-delivery-head', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2583
|
-
...fresh,
|
|
2584
|
-
expectedHead: mergeValidated.currentHead,
|
|
2585
|
-
failureReason: undefined,
|
|
2586
|
-
}));
|
|
2587
|
-
}
|
|
2588
|
-
const contract = contractFor(ctx, current);
|
|
2589
|
-
if (contract?.constraints.allowMerge === false) throw new Error('WORK_MERGE_NOT_ALLOWED: WorkContract disallows merge');
|
|
2590
|
-
const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
|
|
2591
|
-
const deleteAfterWorktreeCleanup = current.managedWorktree && deleteBranchRequested;
|
|
2592
|
-
const targetBranch = explicitTargetBranch ?? target.defaultBranch ?? 'main';
|
|
2593
|
-
const activeMergeHead = retryStage === 'merge' ? gitRevision(target.canonicalRoot, 'MERGE_HEAD') : undefined;
|
|
2594
|
-
if (!activeMergeHead && current.managedWorktree && current.expectedHead) {
|
|
2595
|
-
const targetHead = gitRevision(target.canonicalRoot, targetBranch);
|
|
2596
|
-
if (targetHead) {
|
|
2597
|
-
const advance = inspectWorkTargetAdvance(mergeValidated.worktreeRepository.canonicalRoot, current.expectedHead, targetHead);
|
|
2598
|
-
if (advance.relation === 'diverged_conflict') {
|
|
2599
|
-
if (contract) {
|
|
2600
|
-
transitionWorkContractPhase(
|
|
2601
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
2602
|
-
contract.workId,
|
|
2603
|
-
{
|
|
2604
|
-
phase: 'delivery',
|
|
2605
|
-
status: 'blocked',
|
|
2606
|
-
state: 'blocked',
|
|
2607
|
-
summary: `Target branch ${targetBranch} advanced to ${advance.targetHead} and conflicts with candidate ${advance.candidateHead}; canonical target was not mutated.`,
|
|
2608
|
-
},
|
|
2609
|
-
);
|
|
2610
|
-
}
|
|
2611
|
-
return {
|
|
2612
|
-
work: compactHandle(current),
|
|
2613
|
-
stages: current.finalization,
|
|
2614
|
-
completed: false,
|
|
2615
|
-
blocked: true,
|
|
2616
|
-
recoverable: true,
|
|
2617
|
-
error: {
|
|
2618
|
-
code: 'WORK_TARGET_ADVANCE_CONFLICT',
|
|
2619
|
-
message: `Target branch ${targetBranch} advanced and cannot be integrated cleanly into the isolated Work candidate. Canonical target was left unchanged.${advance.detail ? ` ${advance.detail}` : ''}`,
|
|
2620
|
-
},
|
|
2621
|
-
continuation: 'WORK_TARGET_ADVANCE_REVIEW_REQUIRED: inspect the target/candidate conflict, repair only inside the managed Work checkout, then revalidate before retrying finalize.',
|
|
2622
|
-
};
|
|
2623
|
-
}
|
|
2624
|
-
if (advance.relation === 'candidate_contains_target') {
|
|
2625
|
-
const nonLinearCommits = targetAdvanceLinearMergeCommits(
|
|
2626
|
-
mergeValidated.worktreeRepository.canonicalRoot,
|
|
2627
|
-
advance.targetHead,
|
|
2628
|
-
advance.candidateHead,
|
|
2629
|
-
);
|
|
2630
|
-
if (nonLinearCommits.length > 0) {
|
|
2631
|
-
return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_HISTORY_VIOLATION: candidate already contains merge commit(s) above reconciled target ${advance.targetHead}: ${nonLinearCommits.slice(0, 8).join(', ')}`);
|
|
2632
|
-
}
|
|
2633
|
-
const scopeViolation = targetAdvanceWorkScopeViolation(contract, advance.candidateChangedPaths);
|
|
2634
|
-
if (scopeViolation) {
|
|
2635
|
-
return failStage('merge', `WORK_TARGET_ADVANCE_SCOPE_VIOLATION: Work-owned ${scopeViolation.kind} path ${scopeViolation.path}`);
|
|
2636
|
-
}
|
|
2637
|
-
if (contract) replaceTargetAdvanceScopeEvidence(ctx, contract, advance.candidateChangedPaths);
|
|
2638
|
-
current = transact('target-advance-provenance-confirmed', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2639
|
-
...fresh,
|
|
2640
|
-
deliveryBaseCommit: advance.targetHead,
|
|
2641
|
-
}));
|
|
2642
|
-
}
|
|
2643
|
-
if (advance.relation === 'diverged_clean') {
|
|
2644
|
-
const preIntegrationScopeViolation = targetAdvanceWorkScopeViolation(contract, advance.candidateChangedPaths);
|
|
2645
|
-
if (preIntegrationScopeViolation) {
|
|
2646
|
-
return failStage('merge', `WORK_TARGET_ADVANCE_SCOPE_VIOLATION: Work-owned ${preIntegrationScopeViolation.kind} path ${preIntegrationScopeViolation.path}`);
|
|
2647
|
-
}
|
|
2648
|
-
const checks = contract?.checks ?? [];
|
|
2649
|
-
const candidateInput = currentWorkValidationInput(mergeValidated.worktreeRepository, current, checks);
|
|
2650
|
-
const checksBefore = checks.length > 0 ? listControllerChecks(mergeValidated.worktreeRepository.canonicalRoot) : [];
|
|
2651
|
-
const integrated = repositoryGitRebaseOnto(ctx.controllerHome, mergeValidated.worktreeRepository, {
|
|
2652
|
-
onto: advance.targetHead,
|
|
2653
|
-
upstream: advance.mergeBase,
|
|
2654
|
-
abortOnFailure: true,
|
|
2655
|
-
authorizationDecision: gitAuthorization,
|
|
2656
|
-
sessionId: session.sessionId,
|
|
2657
|
-
principalId: session.principalId,
|
|
2658
|
-
workId: current.workId,
|
|
2659
|
-
goalId: current.goalId,
|
|
2660
|
-
});
|
|
2661
|
-
if (!integrated.rebased) {
|
|
2662
|
-
if (contract) {
|
|
2663
|
-
transitionWorkContractPhase(
|
|
2664
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
2665
|
-
contract.workId,
|
|
2666
|
-
{
|
|
2667
|
-
phase: 'delivery',
|
|
2668
|
-
status: 'blocked',
|
|
2669
|
-
state: 'blocked',
|
|
2670
|
-
summary: `Linear target-advance integration failed inside the isolated Work checkout; canonical ${targetBranch} was not mutated.`,
|
|
2671
|
-
},
|
|
2672
|
-
);
|
|
2673
|
-
}
|
|
2674
|
-
return {
|
|
2675
|
-
work: compactHandle(current),
|
|
2676
|
-
stages: current.finalization,
|
|
2677
|
-
completed: false,
|
|
2678
|
-
blocked: true,
|
|
2679
|
-
recoverable: integrated.restored,
|
|
2680
|
-
error: {
|
|
2681
|
-
code: 'WORK_TARGET_ADVANCE_LINEAR_INTEGRATION_FAILED',
|
|
2682
|
-
message: integrated.restored
|
|
2683
|
-
? 'Target advancement could not be rebased safely; the isolated Work checkout was restored to its original candidate HEAD.'
|
|
2684
|
-
: 'Target advancement rebase failed and exact checkout restoration could not be proven; inspect the isolated Work checkout before retrying.',
|
|
2685
|
-
},
|
|
2686
|
-
continuation: 'WORK_TARGET_ADVANCE_REVIEW_REQUIRED: inspect the isolated Work checkout and retry only after its exact branch/HEAD/cleanliness are proven.',
|
|
2687
|
-
};
|
|
2688
|
-
}
|
|
2689
|
-
const integratedHead = gitHead(mergeValidated.worktreeRepository.canonicalRoot);
|
|
2690
|
-
if (!integratedHead || !gitIsAncestor(mergeValidated.worktreeRepository.canonicalRoot, advance.targetHead, integratedHead)) {
|
|
2691
|
-
return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_ANCESTRY_MISMATCH: target ${advance.targetHead} is not an ancestor of ${integratedHead ?? 'unavailable'}`);
|
|
2692
|
-
}
|
|
2693
|
-
const mergeCommits = targetAdvanceLinearMergeCommits(
|
|
2694
|
-
mergeValidated.worktreeRepository.canonicalRoot,
|
|
2695
|
-
advance.targetHead,
|
|
2696
|
-
integratedHead,
|
|
2697
|
-
);
|
|
2698
|
-
const integratedTree = spawnSync('git', ['-C', mergeValidated.worktreeRepository.canonicalRoot, 'rev-parse', '--verify', `${integratedHead}^{tree}`], {
|
|
2699
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
2700
|
-
});
|
|
2701
|
-
if (mergeCommits.length > 0) {
|
|
2702
|
-
return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_HISTORY_VIOLATION: integrated candidate contains merge commit(s): ${mergeCommits.slice(0, 8).join(', ')}`);
|
|
2703
|
-
}
|
|
2704
|
-
if (
|
|
2705
|
-
!advance.mergedTree
|
|
2706
|
-
|| integratedTree.status !== 0
|
|
2707
|
-
|| integratedTree.error
|
|
2708
|
-
|| typeof integratedTree.stdout !== 'string'
|
|
2709
|
-
|| integratedTree.stdout.trim() !== advance.mergedTree
|
|
2710
|
-
) {
|
|
2711
|
-
return failStage('merge', `WORK_TARGET_ADVANCE_LINEAR_TREE_MISMATCH: rebased candidate tree does not equal the preflight clean integration tree ${advance.mergedTree ?? 'unavailable'}`);
|
|
2712
|
-
}
|
|
2713
|
-
const workOwnedChangedPaths = gitChangedPaths(mergeValidated.worktreeRepository.canonicalRoot, advance.targetHead, integratedHead);
|
|
2714
|
-
const postIntegrationScopeViolation = targetAdvanceWorkScopeViolation(contract, workOwnedChangedPaths);
|
|
2715
|
-
if (postIntegrationScopeViolation) {
|
|
2716
|
-
return failStage('merge', `WORK_TARGET_ADVANCE_SCOPE_VIOLATION: integrated Work-owned ${postIntegrationScopeViolation.kind} path ${postIntegrationScopeViolation.path}`);
|
|
2717
|
-
}
|
|
2718
|
-
const integratedInput = currentWorkValidationInput(
|
|
2719
|
-
mergeValidated.worktreeRepository,
|
|
2720
|
-
{ ...current, expectedHead: integratedHead },
|
|
2721
|
-
checks,
|
|
2722
|
-
);
|
|
2723
|
-
const checksAfter = checks.length > 0 ? listControllerChecks(mergeValidated.worktreeRepository.canonicalRoot) : [];
|
|
2724
|
-
const transferPlan = contract
|
|
2725
|
-
? planTargetAdvanceValidationAuthority({
|
|
2726
|
-
checkIds: checks,
|
|
2727
|
-
checkRefs: contract.checkRefs,
|
|
2728
|
-
checksBefore,
|
|
2729
|
-
checksAfter,
|
|
2730
|
-
candidateHead: advance.candidateHead,
|
|
2731
|
-
candidateWorkspaceFingerprint: candidateInput.workspaceFingerprint,
|
|
2732
|
-
integratedHead,
|
|
2733
|
-
integratedWorkspaceFingerprint: integratedInput.workspaceFingerprint,
|
|
2734
|
-
targetChangedPaths: advance.targetChangedPaths,
|
|
2735
|
-
})
|
|
2736
|
-
: { transferredRecords: [], reusableCheckIds: [], invalidatedCheckIds: checks };
|
|
2737
|
-
for (const record of transferPlan.transferredRecords) {
|
|
2738
|
-
appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, record);
|
|
2739
|
-
}
|
|
2740
|
-
if (contract) replaceTargetAdvanceScopeEvidence(ctx, contract, workOwnedChangedPaths);
|
|
2741
|
-
const validationPreserved = transferPlan.invalidatedCheckIds.length === 0;
|
|
2742
|
-
current = transact('target-advance-integrated', (fresh) => transitionWorkHandle(
|
|
2743
|
-
ctx.controllerHome,
|
|
2744
|
-
fresh,
|
|
2745
|
-
validationPreserved ? 'committed' : 'validating',
|
|
2746
|
-
{
|
|
2747
|
-
deliveryBaseCommit: advance.targetHead,
|
|
2748
|
-
expectedHead: integratedHead,
|
|
2749
|
-
failureReason: undefined,
|
|
2750
|
-
finalization: { ...fresh.finalization, validation: validationPreserved ? 'done' : 'pending', merge: 'pending', lastError: undefined },
|
|
2751
|
-
validationRun: undefined,
|
|
2752
|
-
validatedInputFingerprint: validationPreserved ? integratedInput.fingerprint : undefined,
|
|
2753
|
-
},
|
|
2754
|
-
));
|
|
2755
|
-
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
|
|
2756
|
-
title: 'target advancement linearly integrated into isolated Work candidate',
|
|
2757
|
-
summary: `Canonical ${targetBranch} at ${advance.targetHead} advanced independently. Forge rebased Work candidate ${advance.candidateHead} onto it as ${integratedHead} with no merge commits; Work scope is ${workOwnedChangedPaths.length} target-relative path(s). Validation authority transferred for ${transferPlan.reusableCheckIds.length}/${checks.length} check(s); ${transferPlan.invalidatedCheckIds.length} check(s) require fresh evidence.`,
|
|
2758
|
-
detailLevel: 'summary',
|
|
2759
|
-
});
|
|
2760
|
-
if (!validationPreserved) {
|
|
2761
|
-
markWorkValidationPending(ctx.controllerHome, current);
|
|
2762
|
-
return {
|
|
2763
|
-
work: compactHandle(current),
|
|
2764
|
-
stages: current.finalization,
|
|
2765
|
-
completed: false,
|
|
2766
|
-
continuation: `WORK_COMMITTED_REVALIDATION_REQUIRED: target branch ${targetBranch} changed inputs for [${transferPlan.invalidatedCheckIds.join(', ')}]; unaffected check evidence was transferred to ${integratedHead} and may be reused`,
|
|
2767
|
-
};
|
|
2768
|
-
}
|
|
2769
|
-
projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', checks.length === 0
|
|
2770
|
-
? 'Target advancement was linearly integrated and no validation checks were required.'
|
|
2771
|
-
: `Target advancement was linearly integrated; all ${checks.length} check result(s) retained authority because target-only changes did not affect their declared inputs.`);
|
|
2772
|
-
}
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
let merged: ReturnType<typeof repositoryGitFinishWorkflow>;
|
|
2776
|
-
if (activeMergeHead) {
|
|
2777
|
-
const expectedMergeHead = current.expectedHead ? gitRevision(target.canonicalRoot, current.expectedHead) : undefined;
|
|
2778
|
-
const featureHead = gitRevision(target.canonicalRoot, current.branch);
|
|
2779
|
-
const currentBranchResult = spawnSync('git', ['-C', target.canonicalRoot, 'branch', '--show-current'], {
|
|
2780
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
2781
|
-
});
|
|
2782
|
-
const currentBranchName = currentBranchResult.status === 0 && typeof currentBranchResult.stdout === 'string'
|
|
2783
|
-
? currentBranchResult.stdout.trim()
|
|
2784
|
-
: '';
|
|
2785
|
-
if (!expectedMergeHead || activeMergeHead !== expectedMergeHead || featureHead !== expectedMergeHead || currentBranchName !== targetBranch) {
|
|
2786
|
-
return failStage(
|
|
2787
|
-
'merge',
|
|
2788
|
-
`WORK_MERGE_HEAD_OWNERSHIP_MISMATCH: retry may conclude only Work ${current.workId} merge head ${expectedMergeHead ?? 'unavailable'} from ${current.branch} while on ${targetBranch}; observed MERGE_HEAD ${activeMergeHead}, feature ${featureHead ?? 'unavailable'}, branch ${currentBranchName || 'unavailable'}`,
|
|
2789
|
-
);
|
|
2790
|
-
}
|
|
2791
|
-
const unmerged = spawnSync('git', ['-C', target.canonicalRoot, 'diff', '--name-only', '--diff-filter=U'], {
|
|
2792
|
-
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
2793
|
-
});
|
|
2794
|
-
if (unmerged.status !== 0 || unmerged.error || typeof unmerged.stdout !== 'string') {
|
|
2795
|
-
return failStage('merge', 'WORK_MERGE_CONFLICT_STATE_UNAVAILABLE: unable to prove that the owned merge has no unmerged paths');
|
|
2796
|
-
}
|
|
2797
|
-
if (unmerged.stdout.trim()) {
|
|
2798
|
-
return failStage('merge', `WORK_MERGE_CONFLICTS_UNRESOLVED: resolve and stage all paths before retrying finalize (${unmerged.stdout.trim().split('\n').filter(Boolean).join(', ')})`);
|
|
2799
|
-
}
|
|
2800
|
-
const beforeConclude = repositoryGitStatus(target);
|
|
2801
|
-
const concluded = repositoryGitCommit(ctx.controllerHome, target, {
|
|
2802
|
-
message: `Merge ${current.branch} into ${targetBranch}`,
|
|
2803
|
-
allowEmpty: true,
|
|
2804
|
-
authorizationDecision: gitAuthorization,
|
|
2805
|
-
sessionId: session.sessionId,
|
|
2806
|
-
principalId: session.principalId,
|
|
2807
|
-
workId: current.workId,
|
|
2808
|
-
goalId: current.goalId,
|
|
2809
|
-
});
|
|
2810
|
-
if (!concluded.committed || !concluded.commit) {
|
|
2811
|
-
return failStage('merge', concluded.error?.message ?? 'WORK_MERGE_CONCLUDE_FAILED: git commit did not conclude the owned merge');
|
|
2812
|
-
}
|
|
2813
|
-
const concludedSecondParent = gitRevision(target.canonicalRoot, 'HEAD^2');
|
|
2814
|
-
if (concludedSecondParent !== expectedMergeHead || gitRevision(target.canonicalRoot, 'MERGE_HEAD')) {
|
|
2815
|
-
return failStage('merge', `WORK_MERGE_CONCLUSION_OWNERSHIP_MISMATCH: concluded merge must retain ${expectedMergeHead} as its second parent and clear MERGE_HEAD`);
|
|
2816
|
-
}
|
|
2817
|
-
const steps: ReturnType<typeof repositoryGitFinishWorkflow>['steps'] = [{ name: 'conclude_owned_merge', execution: concluded.commit }];
|
|
2818
|
-
if (!deleteAfterWorktreeCleanup && deleteBranchRequested) {
|
|
2819
|
-
const deleted = repositoryGitDeleteBranch(ctx.controllerHome, target, { branch: current.branch, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
|
|
2820
|
-
steps.push({ name: 'delete_feature_branch', execution: deleted.execution });
|
|
2821
|
-
if (deleted.execution.status !== 'executed' || deleted.execution.ok !== true) {
|
|
2822
|
-
return failStage('merge', deleted.execution.stderr || 'WORK_MERGE_BRANCH_CLEANUP_FAILED: owned merge concluded but feature branch deletion failed');
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
merged = {
|
|
2826
|
-
repoId: target.repoId,
|
|
2827
|
-
checkoutId: target.activeCheckoutId,
|
|
2828
|
-
featureBranch: current.branch,
|
|
2829
|
-
targetBranch,
|
|
2830
|
-
before: beforeConclude,
|
|
2831
|
-
steps,
|
|
2832
|
-
after: repositoryGitStatus(target),
|
|
2833
|
-
completed: true,
|
|
2834
|
-
};
|
|
2835
|
-
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
|
|
2836
|
-
title: 'owned resolved merge concluded',
|
|
2837
|
-
summary: `Finalizer retry proved MERGE_HEAD ${activeMergeHead} belongs to the exact Work delivery head, found no unmerged paths, and concluded the existing merge instead of replaying git merge.`,
|
|
2838
|
-
detailLevel: 'summary',
|
|
2839
|
-
});
|
|
2840
|
-
} else {
|
|
2841
|
-
merged = repositoryGitFinishWorkflow(ctx.controllerHome, target, { featureBranch: current.branch, targetBranch: explicitTargetBranch, deleteBranch: !deleteAfterWorktreeCleanup && deleteBranchRequested, noFf: args.no_ff === true, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
|
|
2842
|
-
}
|
|
2843
|
-
const pendingAuthorization = merged.steps.find((step) => step.execution.authorizationDecision?.decision === 'user_confirmation_required')?.execution.authorizationDecision;
|
|
2844
|
-
if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
|
|
2845
|
-
if (!merged.completed) return { ...failStage('merge', merged.error?.message ?? 'merge failed'), merge: merged };
|
|
2846
|
-
current = transact('merge-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
|
|
2847
|
-
finalization: { ...fresh.finalization, merge: 'done', branchCleanup: !deleteBranchRequested ? 'skipped' : deleteAfterWorktreeCleanup ? 'pending' : 'done', lastError: undefined },
|
|
2848
|
-
failureReason: undefined,
|
|
2849
|
-
}));
|
|
2850
|
-
} else if (!wants.merge && current.finalization.merge === 'pending') {
|
|
2851
|
-
current = transact('merge-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
2852
|
-
...fresh,
|
|
2853
|
-
finalization: {
|
|
2854
|
-
...fresh.finalization,
|
|
2855
|
-
merge: 'skipped',
|
|
2856
|
-
branchCleanup: requestedOutcome === 'completed_no_change' && wants.cleanup && deleteBranchRequested ? 'pending' : 'skipped',
|
|
2857
|
-
},
|
|
2858
|
-
}));
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
let remoteDelivery: WorkRemoteDeliveryReceipt | undefined;
|
|
2862
|
-
const remoteDeliveryRequired = args.remote_write === true
|
|
2863
|
-
|| contractFor(ctx, current)?.constraints.remoteDeliveryRequired === true;
|
|
2864
|
-
if (remoteDeliveryRequired && requestedOutcome !== 'completed_no_change') {
|
|
2865
|
-
const repository = getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true });
|
|
2866
|
-
const target = selectWorkFinalizationTarget(repository, current);
|
|
2867
|
-
const targetBranch = explicitTargetBranch ?? repository.defaultBranch ?? 'main';
|
|
2868
|
-
const targetRevision = gitRevision(target.canonicalRoot, targetBranch);
|
|
2869
|
-
if (!targetRevision) throw new Error(`WORK_REMOTE_DELIVERY_TARGET_REQUIRED: target branch ${targetBranch} is unavailable`);
|
|
2870
|
-
const authorityRepository = selectRepositoryCheckout(repository, current.checkoutId, { allowArchived: true });
|
|
2871
|
-
remoteDelivery = await pushExactWorkRemoteDelivery({
|
|
2872
|
-
controllerHome: ctx.controllerHome,
|
|
2873
|
-
repository: authorityRepository,
|
|
2874
|
-
workId: current.workId,
|
|
2875
|
-
targetBranch,
|
|
2876
|
-
targetRevision,
|
|
2877
|
-
});
|
|
2878
|
-
appendWorkEvidence(
|
|
2879
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
2880
|
-
current.workContractId ?? current.workId,
|
|
2881
|
-
{
|
|
2882
|
-
title: 'remote delivery verified before Work terminalization',
|
|
2883
|
-
summary: remoteDelivery.pushed
|
|
2884
|
-
? `Exact integrated revision ${remoteDelivery.targetRevision} was pushed to origin/${targetBranch} while Work ${current.workId} still held active delivery authority.`
|
|
2885
|
-
: `origin/${targetBranch} already contained exact integrated revision ${remoteDelivery.targetRevision}; remote delivery was verified idempotently before Work terminalization.`,
|
|
2886
|
-
detailLevel: 'summary',
|
|
2887
|
-
},
|
|
2888
|
-
);
|
|
2889
|
-
}
|
|
2890
|
-
|
|
2891
|
-
if (
|
|
2892
|
-
requestedOutcome === 'completed_no_change'
|
|
2893
|
-
&& current.finalization.validation === 'done'
|
|
2894
|
-
&& current.finalization.commit === 'skipped'
|
|
2895
|
-
&& current.finalization.merge === 'skipped'
|
|
2896
|
-
&& current.state !== 'merged'
|
|
2897
|
-
&& current.state !== 'cleaned'
|
|
2898
|
-
) {
|
|
2899
|
-
// `merged` is the WorkHandle's durable integrated-delivery boundary. A
|
|
2900
|
-
// no-change completion has no Git merge, but it must cross that boundary
|
|
2901
|
-
// before physical cleanup so an earlier failure preserves retryability.
|
|
2902
|
-
if (current.state === 'prepared') {
|
|
2903
|
-
current = transact('no-change-validation-state', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'validating', {
|
|
2904
|
-
finalization: fresh.finalization,
|
|
2905
|
-
failureReason: undefined,
|
|
2906
|
-
}));
|
|
2907
|
-
}
|
|
2908
|
-
if (current.state === 'editing' || current.state === 'validating' || current.state === 'committed') {
|
|
2909
|
-
current = transact('no-change-delivery-integrated', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
|
|
2910
|
-
finalization: fresh.finalization,
|
|
2911
|
-
failureReason: undefined,
|
|
2912
|
-
}));
|
|
2913
|
-
} else if (current.state !== 'merged') {
|
|
2914
|
-
throw new Error(`WORK_NO_CHANGE_DELIVERY_STATE_INVALID: cannot establish no-change delivery from ${current.state}`);
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
if (wants.cleanup && current.finalization.worktreeCleanup === 'pending') {
|
|
2919
|
-
const contract = contractFor(ctx, current);
|
|
2920
|
-
if (contract?.constraints.allowCleanup === false) throw new Error('WORK_CLEANUP_NOT_ALLOWED: WorkContract disallows cleanup');
|
|
2921
|
-
if (!current.managedWorktree) {
|
|
2922
|
-
current = transact('worktree-cleanup-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'skipped' } }));
|
|
2923
|
-
} else {
|
|
2924
|
-
const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
|
|
2925
|
-
const cleanup = runCleanup(target.canonicalRoot, current.worktreePath);
|
|
2926
|
-
if (!cleanup.ok) return failStage('worktreeCleanup', cleanup.message ?? 'worktree cleanup failed');
|
|
2927
|
-
current = transact('worktree-cleanup-done', (fresh) => {
|
|
2928
|
-
setRepositoryCheckoutLifecycle({ controllerHome: ctx.controllerHome, repoId: fresh.repositoryId, checkoutId: fresh.checkoutId, lifecycle: 'removed', reason: `Work ${fresh.workId} cleanup completed.` });
|
|
2929
|
-
markRepositoryProjectionDirty(ctx.controllerHome, fresh.repositoryId, `cleanup:${fresh.workId}:worktree`);
|
|
2930
|
-
return writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'done', lastError: undefined } });
|
|
2931
|
-
});
|
|
2932
|
-
}
|
|
2933
|
-
} else if (!wants.cleanup && current.finalization.worktreeCleanup === 'pending') {
|
|
2934
|
-
current = transact('worktree-cleanup-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'skipped' } }));
|
|
2935
|
-
}
|
|
2936
|
-
|
|
2937
|
-
if (
|
|
2938
|
-
wants.cleanup
|
|
2939
|
-
&& current.finalization.branchCleanup === 'pending'
|
|
2940
|
-
&& (current.finalization.merge === 'done' || requestedOutcome === 'completed_no_change')
|
|
2941
|
-
) {
|
|
2942
|
-
const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
|
|
2943
|
-
const deleted = repositoryGitDeleteBranch(ctx.controllerHome, target, { branch: current.branch, force: false, authorizationDecision: gitAuthorization, sessionId: session.sessionId, principalId: session.principalId, workId: current.workId, goalId: current.goalId });
|
|
2944
|
-
if (deleted.execution.authorizationDecision?.decision === 'user_confirmation_required') return { authorization: deleted.execution.authorizationDecision, work: compactHandle(current), stages: current.finalization };
|
|
2945
|
-
if (deleted.execution.status !== 'executed' || deleted.execution.ok !== true) return failStage('branchCleanup', deleted.execution.stderr || 'feature branch cleanup failed');
|
|
2946
|
-
current = transact('branch-cleanup-done', (fresh) => {
|
|
2947
|
-
markRepositoryProjectionDirty(ctx.controllerHome, fresh.repositoryId, `cleanup:${fresh.workId}:branch`);
|
|
2948
|
-
return writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, branchCleanup: 'done', lastError: undefined } });
|
|
2949
|
-
});
|
|
2950
|
-
}
|
|
2951
|
-
|
|
2952
|
-
const complete = finalizationComplete(current.finalization);
|
|
2953
|
-
if (complete) {
|
|
2954
|
-
const finalState = finalStateForStages(current.finalization, current.state);
|
|
2955
|
-
current = transact('complete', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, finalState, { finalization: current.finalization, failureReason: undefined }));
|
|
2956
|
-
const completedContract = contractFor(ctx, current);
|
|
2957
|
-
if (completedContract?.status === 'cancelled') {
|
|
2958
|
-
appendWorkEvidence(
|
|
2959
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
2960
|
-
completedContract.workId,
|
|
2961
|
-
{
|
|
2962
|
-
title: 'cancelled work cleanup completed',
|
|
2963
|
-
summary: 'Controller verified terminal ownership, a clean or previously removed managed worktree, and a branch HEAD already contained in the target branch before deleting retained workspace references.',
|
|
2964
|
-
detailLevel: 'summary',
|
|
2965
|
-
},
|
|
2966
|
-
);
|
|
2967
|
-
} else {
|
|
2968
|
-
const workId = current.workContractId ?? current.workId;
|
|
2969
|
-
const completionContract = contractFor(ctx, current);
|
|
2970
|
-
if (!completionContract) throw new Error(`WORK_COMPLETION_CONTRACT_MISSING: ${workId}`);
|
|
2971
|
-
if (requestedOutcome === 'completed_no_change') {
|
|
2972
|
-
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, workId, {
|
|
2973
|
-
title: 'objective-specific no-change proof',
|
|
2974
|
-
summary: noChangeProof,
|
|
2975
|
-
detailLevel: 'summary',
|
|
2976
|
-
});
|
|
2977
|
-
}
|
|
2978
|
-
const receipt = completionReceiptForFinalizedWork(ctx, current, completionContract, args);
|
|
2979
|
-
const ownerPrincipal = terminalizationOwner.principalId?.trim() || terminalizationOwner.controllerId;
|
|
2980
|
-
const ownerInstanceId = terminalizationOwner.controllerInstanceId?.trim() || '';
|
|
2981
|
-
const ownerClaimGeneration = terminalizationOwner.claimGeneration;
|
|
2982
|
-
if (!ownerInstanceId || typeof ownerClaimGeneration !== 'number' || ownerClaimGeneration < 1) {
|
|
2983
|
-
throw new Error(`WORK_CONTROLLER_TERMINALIZATION_AUTHORITY_INVALID: ${workId}`);
|
|
2984
|
-
}
|
|
2985
|
-
const fencedCompletion = withControllerSessionTerminalizationFence(
|
|
2986
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
2987
|
-
{
|
|
2988
|
-
workId,
|
|
2989
|
-
actor: `work-finalize-completion:${terminalizationOwner.controllerId}:${ownerInstanceId}`,
|
|
2990
|
-
authority: {
|
|
2991
|
-
controllerId: terminalizationOwner.controllerId,
|
|
2992
|
-
controllerType: terminalizationOwner.controllerType,
|
|
2993
|
-
principalId: ownerPrincipal,
|
|
2994
|
-
controllerInstanceId: ownerInstanceId,
|
|
2995
|
-
claimGeneration: ownerClaimGeneration,
|
|
2996
|
-
},
|
|
2997
|
-
},
|
|
2998
|
-
() => recordWorkCompletionReceipt(
|
|
2999
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
3000
|
-
workId,
|
|
3001
|
-
receipt,
|
|
3002
|
-
requestedOutcome === 'completed_no_change' ? 'completed_no_change' : 'completed_changed',
|
|
3003
|
-
requestedOutcome === 'completed_no_change' ? 'completed_no_change' : 'repository_change',
|
|
3004
|
-
),
|
|
3005
|
-
);
|
|
3006
|
-
if (!fencedCompletion.allowed) {
|
|
3007
|
-
throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fencedCompletion.reason}`);
|
|
3008
|
-
}
|
|
3009
|
-
const recorded = fencedCompletion.value;
|
|
3010
|
-
if (recorded.requirementId) {
|
|
3011
|
-
try {
|
|
3012
|
-
completeRequirementFromWork(
|
|
3013
|
-
{ controllerHome: ctx.controllerHome },
|
|
3014
|
-
{ requirementId: recorded.requirementId, work: recorded },
|
|
3015
|
-
);
|
|
3016
|
-
} catch (error) {
|
|
3017
|
-
// Work completion + its receipt are the canonical machine-delivery facts.
|
|
3018
|
-
// Requirement completion is a downstream projection and may legitimately
|
|
3019
|
-
// be absent for callers that use requirement_id only as a semantic
|
|
3020
|
-
// identity. Never turn an already integrated/cleaned Work back into a
|
|
3021
|
-
// failed finalize because this projection is missing or stale.
|
|
3022
|
-
try {
|
|
3023
|
-
appendWorkEvidence(
|
|
3024
|
-
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
3025
|
-
workId,
|
|
3026
|
-
{
|
|
3027
|
-
title: 'requirement completion projection pending',
|
|
3028
|
-
summary: `Work completion remains authoritative; Requirement projection could not be applied: ${error instanceof Error ? error.message : String(error)}`.slice(0, 2_000),
|
|
3029
|
-
detailLevel: 'summary',
|
|
3030
|
-
},
|
|
3031
|
-
);
|
|
3032
|
-
} catch {
|
|
3033
|
-
// Diagnostic persistence is also downstream of the completion
|
|
3034
|
-
// receipt and therefore cannot invalidate the terminal Work fact.
|
|
3035
|
-
}
|
|
3036
|
-
}
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
// Successful WorkContract completion always ends controller ownership.
|
|
3040
|
-
// Physical branch/worktree retention is represented by finalization stages
|
|
3041
|
-
// and completion-receipt warnings; it must not keep a mutation owner live.
|
|
3042
|
-
releasePreparedWorkOwnership(ctx, current);
|
|
3043
|
-
updateExecutionSession(ctx.controllerHome, identity, {
|
|
3044
|
-
activeWorkId: undefined,
|
|
3045
|
-
activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
|
|
3046
|
-
});
|
|
3047
|
-
}
|
|
3048
|
-
return { work: compactHandle(current), stages: current.finalization, completed: complete, ...(remoteDelivery ? { remoteDelivery } : {}), idempotent: !wants.commit && !wants.merge && !wants.cleanup && current.finalization.validation === 'done' };
|
|
3049
|
-
}
|
|
3050
|
-
|
|
3051
156
|
export async function callExecutionTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>): Promise<CallToolResult | undefined> {
|
|
3052
157
|
if (!executionToolNames.has(name)) return undefined;
|
|
3053
158
|
try {
|