@moretea-labs/forge 1.5.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +46 -0
- package/CLAUDE.md +2 -0
- package/DESIGN.md +7 -2
- package/README.md +4 -4
- package/README.zh-CN.md +2 -2
- package/SKILL.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/hooks/hook-input.sh +11 -8
- package/assets/hooks/lib/session-state.sh +10 -7
- package/assets/hooks/lib/workflow-state.sh +13 -177
- package/assets/hooks/post-tool-observer.sh +8 -2
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +7 -83
- package/assets/hooks/session-start-context.sh +56 -1
- package/assets/partials/05-workflow.partial.md +1 -1
- package/assets/partials-agents/03-orchestration.partial.md +1 -1
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/harness-overview.md +1 -1
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/contract.template.md +2 -7
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-task-workflow.sh +4 -4
- package/assets/templates/helpers/contract-worktree.sh +1 -21
- package/assets/templates/helpers/ensure-task-workflow.sh +13 -38
- package/assets/templates/helpers/migrate-project-template.sh +22 -45
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +12 -37
- package/assets/templates/helpers/ship-worktrees.sh +2 -29
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/assets/templates/helpers/verify-contract.sh +1 -111
- package/assets/templates/helpers/verify-sprint.sh +3 -180
- package/assets/templates/plan.template.md +1 -1
- package/assets/templates/review.template.md +9 -34
- package/assets/workflow-contract.v1.json +10 -19
- package/docs/README.md +10 -10
- package/docs/architecture/CURRENT.md +170 -0
- package/docs/architecture/EVOLUTION.md +42 -0
- package/docs/architecture/current/README.md +4 -156
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
- package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
- package/docs/architecture/history/README.md +7 -0
- package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
- package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
- package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
- package/docs/architecture/history.md +3 -34
- package/docs/architecture/index.md +15 -98
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
- package/docs/architecture/versions/1.6.md +38 -0
- package/docs/architecture/versions/1.7.md +41 -0
- package/docs/forge-chatgpt-browser-engine.md +2 -2
- package/docs/forge-chatgpt-mcp-setup.md +4 -28
- package/docs/forge-chatgpt-supervised-automation.md +3 -3
- package/docs/forge-github-issue-launcher.md +1 -1
- package/docs/forge-runtime-self-healing-loop.md +1 -1
- package/docs/operations/controller-browser-plugin.md +38 -22
- package/docs/operations/controller-performance-and-502.md +124 -0
- package/docs/operations/controller-reliability-runbook.md +1 -1
- package/docs/operations/features.md +3 -3
- package/docs/operations/github-repository.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +5 -3
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/wiki/Architecture.md +6 -6
- package/docs/wiki/Implementation.md +2 -2
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Integrations.md +2 -2
- package/docs/wiki/Quick-Start.md +1 -1
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/package.json +29 -10
- package/scripts/activate-source-baseline.ts +17 -13
- package/scripts/benchmark-background-check-overlap.ts +228 -0
- package/scripts/benchmark-mcp-host.sh +117 -0
- package/scripts/benchmark-mcp-transport.ts +170 -0
- package/scripts/benchmark-quality-harness.ts +146 -0
- package/scripts/benchmark-route-session-concurrency.ts +207 -6
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
- package/scripts/benchmark-typescript-navigation.ts +186 -0
- package/scripts/benchmark-work-efficiency.ts +334 -0
- package/scripts/capture-plan.sh +3 -3
- package/scripts/check-agent-tooling.sh +8 -35
- package/scripts/check-architecture-sync.sh +17 -86
- package/scripts/check-mcp-compatibility.ts +14 -0
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/check-runtime-architecture.mjs +107 -64
- package/scripts/check-stable-baseline.ts +92 -0
- package/scripts/check-task-workflow.sh +4 -4
- package/scripts/contract-worktree.sh +1 -21
- package/scripts/ensure-task-workflow.sh +22 -55
- package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
- package/scripts/install-standalone-recovery.ts +24 -0
- package/scripts/lib/project-init-lib.sh +15 -44
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/migrate-project-template.sh +23 -1179
- package/scripts/new-plan.sh +1 -1
- package/scripts/plan-to-todo.sh +11 -36
- package/scripts/run-governed-gate.ts +2 -1
- package/scripts/ship-worktrees.sh +2 -29
- package/scripts/smoke-ios-physical-device.ts +4 -2
- package/scripts/smoke-runtime-recovery.ts +1 -35
- package/scripts/smoke-schedule-engine.ts +1 -20
- package/scripts/sprint-backlog.sh +1 -1
- package/scripts/verify-contract.sh +1 -111
- package/scripts/verify-forge-runtime.sh +0 -1
- package/scripts/verify-sprint.sh +3 -180
- package/scripts/workflow-contract.ts +1 -5
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +37 -10
- package/src/cli/commands/recovery.ts +25 -3
- package/src/cli/commands/runtime.ts +55 -3
- package/src/cli/commands/security.ts +12 -1
- package/src/cli/commands/setup-profile.ts +31 -21
- package/src/cli/controller/check-runner.ts +106 -3
- package/src/cli/controller/composite-result.ts +1 -1
- package/src/cli/controller/context/focus.ts +51 -0
- package/src/cli/controller/context/instruction-context.ts +114 -0
- package/src/cli/controller/context/known-paths.ts +76 -0
- package/src/cli/controller/context/query-planning.ts +110 -0
- package/src/cli/controller/context/rule-resolution.ts +532 -0
- package/src/cli/controller/context/source-materializer.ts +278 -0
- package/src/cli/controller/context/structural.ts +16 -0
- package/src/cli/controller/context/types.ts +286 -0
- package/src/cli/controller/context-pack.ts +898 -430
- package/src/cli/controller/launch-agents.ts +77 -2
- package/src/cli/controller/operational-plan.ts +2 -2
- package/src/cli/controller/work-mode.ts +6 -4
- package/src/cli/editing/edit-session.ts +60 -7
- package/src/cli/github/github.ts +133 -0
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/installer/managed-entries.ts +15 -1
- package/src/cli/local-bridge/console-automations.ts +225 -67
- package/src/cli/local-bridge/console-view-models.ts +3 -35
- package/src/cli/local-bridge/facade-api.ts +2 -217
- package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +153 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +183 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -11
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +8 -7
- package/src/cli/local-bridge/job-store.ts +1 -490
- package/src/cli/local-bridge/server.ts +29 -532
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5101 -4967
- package/src/cli/mcp/auth.ts +5 -5
- package/src/cli/mcp/instructions.ts +3 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -189
- package/src/cli/mcp/multi-repository.ts +16 -5
- package/src/cli/mcp/oauth.ts +175 -26
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +356 -101
- package/src/cli/mcp/server.ts +625 -54
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/toolset-selection.ts +1 -1
- package/src/cli/mcp/toolset.ts +42 -5
- package/src/cli/mcp/transports/http.ts +125 -15
- package/src/cli/mcp/transports/session-registry.ts +67 -5
- package/src/cli/mcp/types.ts +3 -3
- package/src/cli/repositories/command-classifier.ts +209 -14
- package/src/cli/repositories/command-executor.ts +55 -521
- package/src/cli/repositories/command-process.ts +108 -0
- package/src/cli/repositories/command-scope.ts +99 -41
- package/src/cli/repositories/controller-home.ts +7 -8
- package/src/cli/repositories/git-command-executor.ts +2 -0
- package/src/cli/repositories/locks.ts +53 -16
- package/src/cli/repositories/repository-snapshot.ts +121 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +110 -10
- package/src/cli/repository/inspector.ts +565 -62
- package/src/cli/repository/session-cache.ts +58 -17
- package/src/cli/v81-entry.ts +1 -4
- package/src/effects/fs-transaction.ts +85 -17
- package/src/runtime/context/codegraph-read-provider.ts +233 -2
- package/src/runtime/context/codegraph-sidecar.cjs +74 -40
- package/src/runtime/context/generic-lsp-provider.ts +355 -0
- package/src/runtime/context/lsp-client.ts +283 -0
- package/src/runtime/context/semantic-navigation.ts +275 -0
- package/src/runtime/context/swift-navigation.ts +565 -0
- package/src/runtime/context/typescript-navigation.ts +226 -0
- package/src/runtime/control-plane/dispatch-priority.ts +28 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +196 -2
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/session-store.ts +18 -0
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
- package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +175 -8
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -21
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +87 -9
- package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
- package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +144 -14
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +9 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +885 -105
- package/src/runtime/control-plane/facade/index.ts +1 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +211 -26
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
- package/src/runtime/control-plane/facade/types.ts +55 -11
- package/src/runtime/control-plane/facade/work-contract-store.ts +204 -73
- package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
- package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
- package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
- package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
- package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
- package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
- package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
- package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
- package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +642 -51
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
- package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
- package/src/runtime/control-plane/release-retention.ts +62 -2
- package/src/runtime/control-plane/routing/route-policy.ts +7 -16
- package/src/runtime/control-plane/runtime-status-client.ts +0 -2
- package/src/runtime/diagnostics/mcp-timing.ts +19 -0
- package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
- package/src/runtime/diagnostics/performance.ts +24 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/execution/jobs/store.ts +58 -4
- package/src/runtime/execution/jobs/types.ts +1 -1
- package/src/runtime/execution/managed-workspace.ts +203 -1
- package/src/runtime/execution/process-runtime/check-facade.ts +52 -14
- package/src/runtime/execution/process-runtime/check-result.ts +63 -2
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +346 -40
- package/src/runtime/execution/process-runtime/gc.ts +98 -12
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +57 -6
- package/src/runtime/execution/process-runtime/resource-claims.ts +97 -8
- package/src/runtime/execution/process-runtime/runtime.ts +59 -4
- package/src/runtime/execution/process-runtime/store.ts +7 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -0
- package/src/runtime/execution/thin-harness/async-process.ts +12 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
- package/src/runtime/execution/workers/executor.ts +0 -48
- package/src/runtime/gateway/mcp/execution-tools.ts +1219 -126
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +185 -83
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2555 -1616
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
- package/src/runtime/plugins/browser-adapter.ts +2925 -318
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +92 -5
- package/src/runtime/plugins/browser-macos-bridge.ts +586 -45
- package/src/runtime/plugins/browser-node-bridge.ts +6 -34
- package/src/runtime/plugins/browser-provider-registry.ts +109 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
- package/src/runtime/plugins/browser-runtime.ts +183 -0
- package/src/runtime/plugins/browser-session-authority.ts +332 -0
- package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +15 -12
- package/src/runtime/plugins/external-adapter.ts +324 -3
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/external-unix-socket.ts +7 -1
- package/src/runtime/plugins/first-party-registry.ts +4 -2
- package/src/runtime/plugins/github-adapter.ts +125 -0
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
- package/src/runtime/plugins/ios-adapter.ts +106 -16
- package/src/runtime/plugins/ios-agent-device.ts +234 -64
- package/src/runtime/plugins/ios-physical-device.ts +1143 -392
- package/src/runtime/plugins/lightweight-action.ts +153 -0
- package/src/runtime/plugins/local-system-adapter.ts +125 -8
- package/src/runtime/plugins/macos-capability-broker.ts +55 -9
- package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
- package/src/runtime/plugins/plugin-management-adapter.ts +181 -0
- package/src/runtime/plugins/resend-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +366 -24
- package/src/runtime/plugins/types.ts +39 -2
- package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
- package/src/runtime/projections/git-status-sampler.ts +16 -16
- package/src/runtime/projections/materialized-view.ts +28 -10
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +415 -18
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/mcp-transport.ts +180 -10
- package/src/runtime/root/package-connector-service.ts +186 -8
- package/src/runtime/root/package-runtime-release.ts +143 -17
- package/src/runtime/root/package-runtime-service.ts +430 -22
- package/src/runtime/root/release-manifest.ts +7 -2
- package/src/runtime/root/release-materialize.ts +162 -19
- package/src/runtime/root/release-store.ts +48 -11
- package/src/runtime/root/runtime.ts +23 -5
- package/src/runtime/root/service.ts +100 -27
- package/src/runtime/root/types.ts +2 -0
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/ios-development.ts +321 -63
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/shared/trusted-executable.ts +62 -0
- package/src/runtime/shared/trusted-node-executable.ts +48 -0
- package/src/runtime/standalone-recovery/core.ts +702 -94
- package/src/runtime/standalone-recovery/entry.ts +99 -23
- package/src/runtime/standalone-recovery/installer.ts +19 -3
- package/src/runtime/standalone-recovery/service-labels.ts +2 -0
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
- package/src/runtime/workflow/schedules/browser-probe.ts +258 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +509 -80
- package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
- package/src/runtime/workflow/schedules/settlement.ts +48 -4
- package/src/runtime/workflow/schedules/store.ts +131 -10
- package/src/runtime/workflow/schedules/types.ts +6 -3
- package/src/runtime/workflow/schedules/work-continuation.ts +120 -34
- package/src/testing/test-governance.ts +8 -40
- package/assets/reference-configs/heartbeat-triage.md +0 -57
- package/assets/templates/helpers/harness-trace-grade.sh +0 -150
- package/assets/templates/helpers/heartbeat-triage.sh +0 -361
- package/assets/templates/helpers/maintenance-triage.sh +0 -167
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
- package/docs/architecture/current/architecture-invariants.md +0 -331
- package/docs/architecture/current/automation-and-schedule-engine.md +0 -453
- package/docs/architecture/current/codegraph-context-provider.md +0 -226
- package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
- package/docs/architecture/current/controller-repository-decoupling.md +0 -148
- package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -354
- package/docs/architecture/current/entity-model.md +0 -592
- package/docs/architecture/current/external-plugin-governance.md +0 -396
- package/docs/architecture/current/failure-recovery.md +0 -447
- package/docs/architecture/current/governance.md +0 -213
- package/docs/architecture/current/human-interaction-plane.md +0 -87
- package/docs/architecture/current/implementation-status.md +0 -128
- package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
- package/docs/architecture/current/legacy-governance.md +0 -58
- package/docs/architecture/current/local-workspace-target-grants.md +0 -70
- package/docs/architecture/current/migration-roadmap.md +0 -105
- package/docs/architecture/current/multi-repository-execution.md +0 -371
- package/docs/architecture/current/operations-runbook.md +0 -45
- package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
- package/docs/architecture/current/repository-access-modes.md +0 -102
- package/docs/architecture/current/route-policy.md +0 -15
- package/docs/architecture/current/route-session-concurrency.md +0 -85
- package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
- package/docs/architecture/current/runtime-directory-map.md +0 -49
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
- package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
- package/docs/architecture/current/system-overview.md +0 -144
- package/docs/architecture/current/target-requirements-traceability.md +0 -202
- package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
- package/docs/architecture/current/thin-harness-v1.md +0 -311
- package/docs/architecture/current/verification-and-release-gates.md +0 -391
- package/docs/forge-autonomous-goal-loop.md +0 -9
- package/docs/forge-provider-configuration.md +0 -7
- package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -83
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/docs/operations/gmail-assistant-routines.md +0 -186
- package/docs/operations/personal-assistant-operator-guide.md +0 -61
- package/docs/operations/provider-configuration.md +0 -31
- package/scripts/harness-trace-grade.sh +0 -150
- package/scripts/heartbeat-triage.sh +0 -361
- package/scripts/maintenance-triage.sh +0 -167
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
- package/src/cli/repositories/goal-registry.ts +0 -239
- package/src/runtime/assistant/action-proposals.ts +0 -279
- package/src/runtime/assistant/intent.ts +0 -395
- package/src/runtime/assistant/model-provider.ts +0 -460
- package/src/runtime/assistant/openapi.ts +0 -223
- package/src/runtime/assistant/policy.ts +0 -89
- package/src/runtime/assistant/readiness.ts +0 -258
- package/src/runtime/assistant/routine-runtime.ts +0 -477
- package/src/runtime/assistant/schedule-binding.ts +0 -171
- package/src/runtime/assistant/standing-grants.ts +0 -375
- package/src/runtime/assistant/store.ts +0 -243
- package/src/runtime/assistant/types.ts +0 -135
- package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
- package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
- package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
- package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
- package/src/runtime/control-plane/goal-loop/index.ts +0 -13
- package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
- package/src/runtime/control-plane/goal-loop/types.ts +0 -378
- package/src/runtime/control-plane/startup-recovery.ts +0 -198
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
- package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
- package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- package/src/runtime/safe-tooling/browser-safe.ts +0 -133
- package/src/runtime/workflow/findings/store.ts +0 -204
- package/src/runtime/workflow/findings/types.ts +0 -58
- package/src/runtime/workflow/portfolio/engine.ts +0 -20
- package/src/runtime/workflow/portfolio/store.ts +0 -185
- package/src/runtime/workflow/portfolio/types.ts +0 -29
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'crypto';
|
|
2
|
-
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
3
3
|
import { spawnSync } from 'child_process';
|
|
4
4
|
import { basename, isAbsolute, relative, resolve } from 'path';
|
|
5
5
|
import type { McpToolDefinition, CallToolResult } from '../../../cli/mcp/tools';
|
|
@@ -14,25 +14,26 @@ import {
|
|
|
14
14
|
setRepositoryCheckoutLifecycle,
|
|
15
15
|
} from '../../../cli/repositories/registry';
|
|
16
16
|
import { withControllerLock } from '../../../cli/repositories/locks';
|
|
17
|
-
import { repositoryGitCommit, repositoryGitDeleteBranch, repositoryGitFinishWorkflow, repositoryGitStatus, repositoryGitDiff } from '../../../cli/repositories/structured-git';
|
|
17
|
+
import { repositoryGitCommit, repositoryGitDeleteBranch, repositoryGitFinishWorkflow, repositoryGitMergeBranch, repositoryGitRebaseOnto, repositoryGitStatus, repositoryGitDiff } from '../../../cli/repositories/structured-git';
|
|
18
18
|
import { previewRepositoryCommandExecution } from '../../../cli/repositories/command-executor';
|
|
19
19
|
import { classifyRepositoryCommand } from '../../../cli/repositories/command-classifier';
|
|
20
20
|
import { globMatches } from '../../../cli/mcp/paths';
|
|
21
21
|
import { ensureManagedWorkspace } from '../../execution/managed-workspace';
|
|
22
|
-
import { listControllerChecks } from '../../../cli/controller/check-runner';
|
|
22
|
+
import { listControllerChecks, type ControllerCheck } from '../../../cli/controller/check-runner';
|
|
23
23
|
import { readRepositoryAccessPolicy } from '../../control-plane/governance/access-policy';
|
|
24
|
-
import { appendWorkEvidence, createWorkContract, getWorkContract, recordWorkCompletionReceipt, updateWorkContract, appendVerificationRecord } from '../../control-plane/facade/work-contract-store';
|
|
24
|
+
import { appendWorkEvidence, createWorkContract, getWorkContract, recordWorkCompletionReceipt, transitionWorkContractPhase, updateWorkContract, appendVerificationRecord } from '../../control-plane/facade/work-contract-store';
|
|
25
25
|
import { completeRequirementFromWork } from '../../control-plane/persistence/requirement-store';
|
|
26
|
-
import { isTerminalWorkContractStatus, type WorkReconciliationRecord } from '../../control-plane/facade/types';
|
|
26
|
+
import { isTerminalWorkContractStatus, type VerificationRecord, type WorkReconciliationRecord } from '../../control-plane/facade/types';
|
|
27
27
|
import { buildWorkContinuationSnapshot } from '../../control-plane/facade/work-continuation';
|
|
28
|
-
import { claimControllerSession, getControllerSession, releaseControllerSession, resumeControllerSession } from '../../control-plane/facade/controller-session-store';
|
|
28
|
+
import { claimControllerSession, getControllerSession, releaseControllerSession, releaseControllerSessionWithAuthority, resumeControllerSession, withControllerSessionTerminalizationFence } from '../../control-plane/facade/controller-session-store';
|
|
29
29
|
import { currentControllerInstanceId, requireExecutionSession, startExecutionSession, updateExecutionSession, type ExecutionSessionContext, type SessionIdentity } from '../../control-plane/execution/session-store';
|
|
30
30
|
import { currentPermissionSnapshotVersion, validateWorkHandle } from '../../control-plane/execution/validation';
|
|
31
|
-
import { commandFingerprint, verificationInputFingerprint, workspaceValidationFingerprint, workValidationInputFingerprint } from '../../control-plane/execution/verification-evidence';
|
|
31
|
+
import { commandFingerprint, effectiveVerificationEvidence, verificationInputFingerprint, workspaceValidationFingerprint, workValidationInputFingerprint } from '../../control-plane/execution/verification-evidence';
|
|
32
32
|
import { assertExecutionIdentity, executionIdentityForWork, executionIdentityFromCoordinates, resolveLegacyWorkContractIdentity } from '../../control-plane/execution/execution-identity';
|
|
33
33
|
import { withWorkPrepareRequest } from '../../control-plane/execution/work-prepare-request-store';
|
|
34
|
-
import { markWorkHandleFailed, newWorkId, readWorkHandle, transitionWorkHandle, writeWorkHandle, type WorkFinalizationStages, type WorkHandleState, type WorkTerminalOutcome } from '../../control-plane/execution/work-handle-store';
|
|
34
|
+
import { markWorkHandleFailed, newWorkId, readWorkHandle, transitionWorkHandle, workDeliveryBaseRevision, writeWorkHandle, type WorkFinalizationStages, type WorkHandleState, type WorkTerminalOutcome } from '../../control-plane/execution/work-handle-store';
|
|
35
35
|
import { cleanupTerminalWork } from '../../control-plane/execution/work-terminal-cleanup';
|
|
36
|
+
import { pushExactWorkRemoteDelivery, type WorkRemoteDeliveryReceipt } from '../../control-plane/execution/work-remote-delivery';
|
|
36
37
|
import { assertResolvedAuthorization, createGoalDelegation, decideAuthorization, resolveAuthorizationRequest, type AuthorizationDecision, type AuthorizationRiskClass } from '../../control-plane/governance/authorization';
|
|
37
38
|
import { readControllerResult, searchControllerResult, writeControllerResult } from '../../evidence/result-store';
|
|
38
39
|
import { resumeExecutionJobAfterApproval } from '../../execution/jobs/store';
|
|
@@ -42,6 +43,7 @@ import { markRepositoryProjectionDirty } from '../../projections/invalidation';
|
|
|
42
43
|
import { executeRepositoryCommandViaProcessRuntime } from '../../execution/process-runtime/command-facade';
|
|
43
44
|
import { getCheckProcessHandle } from '../../execution/process-runtime/check-facade';
|
|
44
45
|
import { processCheckCompletionReceipt } from '../../execution/process-runtime/check-receipt';
|
|
46
|
+
import { classifyPersistedCheckTerminalEvidence } from '../../execution/process-runtime/check-result';
|
|
45
47
|
import { claimProcessInvocation, getProcessRecord } from '../../execution/process-runtime/store';
|
|
46
48
|
import { runPersistedCheckViaProcessRuntime } from './persisted-check-process';
|
|
47
49
|
import { hasCurrentWorkValidationAuthority, markWorkValidationPending, projectWorkValidationOutcome, reconcileWorkValidation } from './work-validation-reconciler';
|
|
@@ -62,7 +64,7 @@ export const executionToolDefinitions: McpToolDefinition[] = [
|
|
|
62
64
|
definition('work_prepare', 'Prepare or reuse one controller-owned work handle and bind it to a WorkContract, checkout, branch, and permission snapshot.', {
|
|
63
65
|
session_id: sessionId, repo_id: repoId, checkout_id: { type: 'string' }, work_id: workId,
|
|
64
66
|
objective: { type: 'string' }, goal_id: { type: 'string' }, acceptance_criteria: { type: 'array', items: { type: 'string' } }, allowed_paths: { type: 'array', items: { type: 'string' } }, checks: { type: 'array', items: { type: 'string' } },
|
|
65
|
-
isolation: { type: 'string', enum: ['reuse', 'new_worktree', 'auto'] }, base_ref: { type: 'string' },
|
|
67
|
+
isolation: { type: 'string', enum: ['reuse', 'new_worktree', 'auto'] }, base_ref: { type: 'string' }, needs_dependencies: { type: 'boolean' },
|
|
66
68
|
expected_previous_head: { type: 'string', description: 'Explicit prior WorkHandle HEAD required for audited successor adoption of an existing work_id.' },
|
|
67
69
|
adopt_candidate_head: { type: 'string', description: 'Explicit current successor commit to adopt after exact identity, ownership, cleanliness, ancestry, and path-scope validation.' },
|
|
68
70
|
}, [], false),
|
|
@@ -76,7 +78,7 @@ export const executionToolDefinitions: McpToolDefinition[] = [
|
|
|
76
78
|
session_id: sessionId, controller_id: { type: 'string', description: 'Controller identity that holds the Work lease. Defaults to the authenticated principal.' }, repo_id: repoId, work_id: workId, check_ids: { type: 'array', items: { type: 'string' } }, commands: { type: 'array', items: { type: 'object' } },
|
|
77
79
|
}, ['work_id'], false),
|
|
78
80
|
definition('work_finalize', 'Idempotently validate, commit, merge, clean a managed worktree, and complete the existing WorkContract in independently recorded stages.', {
|
|
79
|
-
session_id: sessionId, controller_id: { type: 'string', description: 'Controller identity that holds the Work lease. Defaults to the authenticated principal.' }, repo_id: repoId, work_id: workId, commit: { type: 'boolean' }, message: { type: 'string' }, merge: { type: 'boolean' }, target_branch: { type: 'string' }, delete_branch: { type: 'boolean' }, cleanup: { type: 'boolean' }, no_ff: { type: 'boolean' }, approval_request_id: { type: 'string' },
|
|
81
|
+
session_id: sessionId, controller_id: { type: 'string', description: 'Controller identity that holds the Work lease. Defaults to the authenticated principal.' }, repo_id: repoId, work_id: workId, commit: { type: 'boolean' }, message: { type: 'string' }, merge: { type: 'boolean' }, target_branch: { type: 'string' }, remote_write: { type: 'boolean', description: 'When true, push the exact locally integrated target revision to origin before cleanup and Work terminalization.' }, delete_branch: { type: 'boolean' }, cleanup: { type: 'boolean' }, no_ff: { type: 'boolean' }, approval_request_id: { type: 'string' },
|
|
80
82
|
completion_outcome: { type: 'string', enum: ['completed_changed', 'completed_no_change'] }, no_change_evidence: { type: 'string', description: 'Objective-specific proof that the requested state already holds; required for completed_no_change.' },
|
|
81
83
|
}, ['work_id'], false, true),
|
|
82
84
|
definition('approval_resolve', 'Resolve a controller approval request from the current conversation; GUI approval is optional and not required for continuation.', { session_id: sessionId, repo_id: repoId, work_id: workId, approval_request_id: { type: 'string' }, confirm_authorization: { type: 'boolean' } }, ['approval_request_id', 'confirm_authorization'], false),
|
|
@@ -158,11 +160,12 @@ function compactHandle(handle: WorkHandleState): Record<string, unknown> {
|
|
|
158
160
|
return {
|
|
159
161
|
workId: handle.workId, sessionId: handle.sessionId, repoId: handle.repositoryId, checkoutId: handle.checkoutId,
|
|
160
162
|
worktreePath: handle.worktreePath, branch: handle.branch, sourceCheckoutId: handle.sourceCheckoutId, goalId: handle.goalId, delegationVersion: handle.delegationVersion,
|
|
161
|
-
workContractId: handle.workContractId, baseCommit: handle.baseCommit, expectedHead: handle.expectedHead,
|
|
163
|
+
workContractId: handle.workContractId, baseCommit: handle.baseCommit, deliveryBaseCommit: handle.deliveryBaseCommit, expectedHead: handle.expectedHead,
|
|
162
164
|
permissionSnapshotVersion: handle.permissionSnapshotVersion, state: handle.state, managedWorktree: handle.managedWorktree,
|
|
163
165
|
createdAt: handle.createdAt, updatedAt: handle.updatedAt, finalization: handle.finalization,
|
|
164
166
|
...(handle.failureReason ? { failureReason: handle.failureReason } : {}),
|
|
165
167
|
...(handle.cleanupResponsibility ? { cleanupResponsibility: handle.cleanupResponsibility } : {}),
|
|
168
|
+
...(handle.terminalResourceDisposition ? { terminalResourceDisposition: handle.terminalResourceDisposition } : {}),
|
|
166
169
|
...(handle.cleanupReceipt ? { cleanupReceipt: handle.cleanupReceipt } : {}),
|
|
167
170
|
};
|
|
168
171
|
}
|
|
@@ -171,9 +174,13 @@ function initialStage(): WorkFinalizationStages {
|
|
|
171
174
|
return { validation: 'pending', commit: 'pending', merge: 'pending', branchCleanup: 'pending', worktreeCleanup: 'pending' };
|
|
172
175
|
}
|
|
173
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
|
+
|
|
174
182
|
function gitHead(root: string): string | undefined {
|
|
175
|
-
|
|
176
|
-
return output.status === 0 && typeof output.stdout === 'string' ? output.stdout.trim() : undefined;
|
|
183
|
+
return gitRevision(root, 'HEAD');
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
function gitCommit(root: string, revision: string, label: string): string {
|
|
@@ -196,6 +203,250 @@ function gitChangedPaths(root: string, previousHead: string, candidateHead: stri
|
|
|
196
203
|
return [...new Set(output.stdout.split('\0').filter(Boolean))].sort((left, right) => left.localeCompare(right));
|
|
197
204
|
}
|
|
198
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
|
+
|
|
199
450
|
function normalizedRequiredString(args: Record<string, unknown>, key: string): string | undefined {
|
|
200
451
|
return typeof args[key] === 'string' && args[key].trim() ? args[key].trim() : undefined;
|
|
201
452
|
}
|
|
@@ -230,6 +481,42 @@ function workReturnCheckoutId(
|
|
|
230
481
|
return target.activeCheckoutId ?? repository.activeCheckoutId ?? fallbackCheckoutId;
|
|
231
482
|
}
|
|
232
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
|
+
|
|
233
520
|
function completionReceiptForFinalizedWork(
|
|
234
521
|
ctx: MultiRepositoryMcpToolContext,
|
|
235
522
|
handle: WorkHandleState,
|
|
@@ -241,21 +528,31 @@ function completionReceiptForFinalizedWork(
|
|
|
241
528
|
const targetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
|
|
242
529
|
? args.target_branch.trim()
|
|
243
530
|
: repository.defaultBranch || 'main';
|
|
244
|
-
const targetRevision =
|
|
245
|
-
if (!targetRevision) throw new Error(
|
|
246
|
-
const
|
|
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], {
|
|
247
541
|
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
248
542
|
}).status === 0;
|
|
249
|
-
if (!reachable) throw new Error(`WORK_COMPLETION_RECEIPT_DELIVERY_NOT_PROVEN: ${
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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'); })();
|
|
255
552
|
const recordedAt = new Date().toISOString();
|
|
256
|
-
const
|
|
553
|
+
const retainsExplicitDeliveryBranch = args.merge !== true && targetBranch === handle.branch;
|
|
257
554
|
const warnings = [
|
|
258
|
-
...(handle.finalization.branchCleanup === 'skipped' ? [{ code: 'cleanup_retained_by_request' as const, message: 'Branch cleanup was skipped by the finalization request.', resourceKind: 'branch' as const, resourceId: handle.branch, recordedAt }] : []),
|
|
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 }] : []),
|
|
259
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 }] : []),
|
|
260
557
|
];
|
|
261
558
|
return {
|
|
@@ -267,7 +564,7 @@ function completionReceiptForFinalizedWork(
|
|
|
267
564
|
workId: handle.workId,
|
|
268
565
|
targetBranch,
|
|
269
566
|
targetRevision,
|
|
270
|
-
sourceRevision:
|
|
567
|
+
sourceRevision: deliveryRevision,
|
|
271
568
|
baseRevision: handle.baseCommit,
|
|
272
569
|
changedPaths,
|
|
273
570
|
delivery: {
|
|
@@ -302,6 +599,7 @@ function workPrepareFingerprint(input: {
|
|
|
302
599
|
allowedPaths: string[];
|
|
303
600
|
checks: string[];
|
|
304
601
|
baseRef?: string;
|
|
602
|
+
needsDependencies: boolean;
|
|
305
603
|
}): string {
|
|
306
604
|
return createHash('sha256').update(JSON.stringify({ schemaVersion: 1, operation: 'work_prepare', ...input })).digest('hex');
|
|
307
605
|
}
|
|
@@ -363,14 +661,30 @@ function findWorkHandle(
|
|
|
363
661
|
});
|
|
364
662
|
}
|
|
365
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
|
+
|
|
366
677
|
function workForSession(
|
|
367
678
|
ctx: MultiRepositoryMcpToolContext,
|
|
368
679
|
session: ExecutionSessionContext,
|
|
369
680
|
args: Record<string, unknown>,
|
|
370
|
-
options: { reconcileValidation?: boolean } = {},
|
|
681
|
+
options: { reconcileValidation?: boolean; allowClaimedTerminalCleanup?: boolean } = {},
|
|
371
682
|
): WorkHandleState {
|
|
372
683
|
let handle = findWorkHandle(ctx, session, args);
|
|
373
|
-
if (
|
|
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');
|
|
374
688
|
if (options.reconcileValidation !== false) handle = reconcileWorkValidation(ctx.controllerHome, handle).handle;
|
|
375
689
|
if (
|
|
376
690
|
session.activeRepositoryId !== handle.repositoryId
|
|
@@ -393,27 +707,53 @@ function assertWorkControllerOwnership(
|
|
|
393
707
|
session: ExecutionSessionContext,
|
|
394
708
|
handle: WorkHandleState,
|
|
395
709
|
args: Record<string, unknown>,
|
|
396
|
-
)
|
|
710
|
+
) {
|
|
397
711
|
const workIdValue = handle.workContractId ?? handle.workId;
|
|
398
|
-
const
|
|
712
|
+
const options = { controllerHome: ctx.controllerHome, repoId: handle.repositoryId };
|
|
713
|
+
const owner = getControllerSession(options, workIdValue);
|
|
399
714
|
const controllerId = typeof args.controller_id === 'string' && args.controller_id.trim()
|
|
400
715
|
? args.controller_id.trim()
|
|
401
716
|
: session.principalId;
|
|
402
717
|
if (controllerId !== session.principalId) {
|
|
403
718
|
throw new Error('WORK_CONTROLLER_IDENTITY_MISMATCH: controller_id must match the authenticated principal');
|
|
404
719
|
}
|
|
405
|
-
|
|
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, {
|
|
406
748
|
workId: workIdValue,
|
|
407
749
|
controllerId,
|
|
408
|
-
controllerType:
|
|
750
|
+
controllerType: 'chatgpt',
|
|
409
751
|
sessionId: session.sessionId,
|
|
410
752
|
principalId: session.principalId,
|
|
411
753
|
controllerInstanceId: session.controllerInstanceId,
|
|
754
|
+
expectedClaimGeneration: 0,
|
|
412
755
|
leaseMs: 3_600_000,
|
|
413
756
|
});
|
|
414
|
-
if (resumed.controllerId !== controllerId || resumed.sessionId !== session.sessionId) {
|
|
415
|
-
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workIdValue} is owned by ${resumed.controllerId}/${resumed.sessionId}`);
|
|
416
|
-
}
|
|
417
757
|
}
|
|
418
758
|
|
|
419
759
|
function claimPreparedWorkOwnership(
|
|
@@ -552,7 +892,16 @@ function adoptExistingWorkHead(
|
|
|
552
892
|
if (isTerminalWorkContractStatus(contract.status) || contract.completionReceipt) {
|
|
553
893
|
throw new Error('WORK_HEAD_ADOPTION_CONTRACT_TERMINAL');
|
|
554
894
|
}
|
|
555
|
-
|
|
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);
|
|
556
905
|
for (const path of changedPaths) {
|
|
557
906
|
if (contract.forbiddenPaths.some((pattern) => globMatches(pattern, path))) {
|
|
558
907
|
throw new Error(`WORK_HEAD_ADOPTION_FORBIDDEN_PATH: ${path}`);
|
|
@@ -587,6 +936,7 @@ function adoptExistingWorkHead(
|
|
|
587
936
|
};
|
|
588
937
|
|
|
589
938
|
const adopted = transitionWorkHandle(ctx.controllerHome, handle, 'editing', {
|
|
939
|
+
deliveryBaseCommit: scopeBaseHead,
|
|
590
940
|
expectedHead: candidateHead,
|
|
591
941
|
failureReason: undefined,
|
|
592
942
|
finalization: initialStage(),
|
|
@@ -604,7 +954,7 @@ function adoptExistingWorkHead(
|
|
|
604
954
|
});
|
|
605
955
|
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, contract.workId, {
|
|
606
956
|
title: 'audited WorkHandle successor HEAD adoption',
|
|
607
|
-
summary: `${previousHead} -> ${candidateHead}; ${changedPaths.length}
|
|
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.`,
|
|
608
958
|
detailLevel: 'summary',
|
|
609
959
|
});
|
|
610
960
|
} catch (error) {
|
|
@@ -632,21 +982,43 @@ function adoptExistingWorkHead(
|
|
|
632
982
|
};
|
|
633
983
|
}
|
|
634
984
|
|
|
635
|
-
function releasePreparedWorkOwnership(
|
|
985
|
+
export function releasePreparedWorkOwnership(
|
|
636
986
|
ctx: MultiRepositoryMcpToolContext,
|
|
637
987
|
handle: WorkHandleState,
|
|
638
988
|
): 'released' | 'already_released' {
|
|
639
989
|
const workIdValue = handle.workContractId ?? handle.workId;
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
workIdValue,
|
|
643
|
-
);
|
|
990
|
+
const options = { controllerHome: ctx.controllerHome, repoId: handle.repositoryId };
|
|
991
|
+
const current = getControllerSession(options, workIdValue);
|
|
644
992
|
if (!current) return 'already_released';
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
);
|
|
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
|
+
}
|
|
650
1022
|
return 'released';
|
|
651
1023
|
}
|
|
652
1024
|
|
|
@@ -657,9 +1029,14 @@ function terminalCleanupOutcome(
|
|
|
657
1029
|
if (handle.cleanupReceipt) return handle.cleanupReceipt.terminalOutcome;
|
|
658
1030
|
const contract = contractFor(ctx, handle);
|
|
659
1031
|
if (contract?.status === 'cancelled') return 'cancelled';
|
|
1032
|
+
if (contract?.status === 'completed') return 'completed_cleanup';
|
|
660
1033
|
const reason = `${handle.failureReason ?? ''} ${handle.finalization.lastError ?? ''}`.toLowerCase();
|
|
661
1034
|
if (contract?.status === 'blocked' && reason.includes('terminal')) return 'blocked_terminal';
|
|
662
|
-
|
|
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') {
|
|
663
1040
|
if (reason.includes('infrastructure') || reason.includes('timed out') || reason.includes('unavailable')) {
|
|
664
1041
|
return 'infrastructure_failed';
|
|
665
1042
|
}
|
|
@@ -772,6 +1149,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
|
|
|
772
1149
|
const checks = boundedStringArray(args.checks, 30);
|
|
773
1150
|
const baseRef = typeof args.base_ref === 'string' && args.base_ref.trim() ? args.base_ref.trim() : undefined;
|
|
774
1151
|
const requestedCheckoutId = typeof args.checkout_id === 'string' && args.checkout_id.trim() ? args.checkout_id.trim() : undefined;
|
|
1152
|
+
const needsDependencies = args.needs_dependencies === true;
|
|
775
1153
|
const baseCheckoutId = repository.activeCheckoutId;
|
|
776
1154
|
const baseStatus = repositoryGitStatus(repository);
|
|
777
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');
|
|
@@ -787,6 +1165,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
|
|
|
787
1165
|
allowedPaths,
|
|
788
1166
|
checks,
|
|
789
1167
|
baseRef,
|
|
1168
|
+
needsDependencies,
|
|
790
1169
|
});
|
|
791
1170
|
|
|
792
1171
|
return withWorkPrepareRequest({
|
|
@@ -872,7 +1251,12 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
|
|
|
872
1251
|
});
|
|
873
1252
|
try {
|
|
874
1253
|
const workspace = useWorktree
|
|
875
|
-
? ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
1254
|
+
? ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
1255
|
+
requestId: createdWorkId,
|
|
1256
|
+
title: objective,
|
|
1257
|
+
baseRef,
|
|
1258
|
+
prepareDependencies: needsDependencies,
|
|
1259
|
+
})
|
|
876
1260
|
: { mode: 'current' as const, checkoutId: baseCheckoutId, root: repository.canonicalRoot, branch: baseStatus.branch ?? 'detached', baseRevision: baseStatus.head ?? undefined, managed: false };
|
|
877
1261
|
const refreshed = getRepository(repository.repoId, ctx.controllerHome);
|
|
878
1262
|
const checkout = selectRepositoryCheckout(refreshed, workspace.checkoutId);
|
|
@@ -883,7 +1267,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
|
|
|
883
1267
|
schemaVersion: 1, workId: createdWorkId, sessionId: session.sessionId, principalId: session.principalId,
|
|
884
1268
|
repositoryId: repository.repoId, checkoutId: checkout.activeCheckoutId, worktreePath: checkout.canonicalRoot, branch,
|
|
885
1269
|
sourceCheckoutId: baseCheckoutId, managedWorktree: workspace.managed, workContractId: contract.workId, goalId, delegationVersion: delegation.version,
|
|
886
|
-
baseCommit: workspace.baseRevision ?? head, expectedHead: head, permissionSnapshotVersion: policy.revision,
|
|
1270
|
+
baseCommit: workspace.baseRevision ?? head, deliveryBaseCommit: workspace.baseRevision ?? head, expectedHead: head, permissionSnapshotVersion: policy.revision,
|
|
887
1271
|
state: 'prepared', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), finalization: initialStage(),
|
|
888
1272
|
cleanupResponsibility: { owner: 'work_finalizer', registeredAt: new Date().toISOString() },
|
|
889
1273
|
};
|
|
@@ -1040,15 +1424,16 @@ async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<stri
|
|
|
1040
1424
|
maxOutputBytes: typeof args.max_output_bytes === 'number' ? args.max_output_bytes : undefined,
|
|
1041
1425
|
});
|
|
1042
1426
|
const process = execution.process;
|
|
1043
|
-
const completed = process?.completed === true || execution.route === 'process_direct';
|
|
1044
1427
|
const ok = process ? process.ok === true : execution.ok === true;
|
|
1045
1428
|
const status = process
|
|
1046
1429
|
? (process.completed
|
|
1047
1430
|
? (process.cancelled ? 'cancelled' : process.timedOut ? 'timed_out' : ok ? 'executed' : 'failed')
|
|
1048
1431
|
: 'running')
|
|
1049
|
-
: execution.route === '
|
|
1050
|
-
? '
|
|
1051
|
-
: '
|
|
1432
|
+
: execution.route === 'process_direct'
|
|
1433
|
+
? (ok ? 'executed' : 'failed')
|
|
1434
|
+
: execution.route === 'durable'
|
|
1435
|
+
? 'deferred_durable'
|
|
1436
|
+
: 'rejected';
|
|
1052
1437
|
return {
|
|
1053
1438
|
processId: process?.processId,
|
|
1054
1439
|
commandId,
|
|
@@ -1082,8 +1467,9 @@ async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<stri
|
|
|
1082
1467
|
for (const [index, entry] of inputs.entries()) {
|
|
1083
1468
|
const execution = await run(entry, index);
|
|
1084
1469
|
ordered.push(execution);
|
|
1085
|
-
// Do not launch another mutating command while
|
|
1086
|
-
// still
|
|
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.
|
|
1087
1473
|
if (execution.process && !execution.process.completed) break;
|
|
1088
1474
|
}
|
|
1089
1475
|
return ordered;
|
|
@@ -1130,7 +1516,10 @@ export function selectDefaultWorkValidationChecks(
|
|
|
1130
1516
|
|
|
1131
1517
|
async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1132
1518
|
const session = requireSession(ctx, args);
|
|
1133
|
-
const handle = workForSession(ctx, session, args, {
|
|
1519
|
+
const handle = workForSession(ctx, session, args, {
|
|
1520
|
+
reconcileValidation: false,
|
|
1521
|
+
allowClaimedTerminalCleanup: args.cleanup !== false,
|
|
1522
|
+
});
|
|
1134
1523
|
const terminalOutcome = terminalCleanupOutcome(ctx, handle);
|
|
1135
1524
|
if (terminalOutcome && args.cleanup !== false) {
|
|
1136
1525
|
return await reconcileTerminalCleanup(ctx, session, handle, args, terminalOutcome);
|
|
@@ -1182,6 +1571,24 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1182
1571
|
checks.push({ checkId, ok: false, status: 'missing', summary: `Check not found: ${checkId}` });
|
|
1183
1572
|
break;
|
|
1184
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
|
+
}
|
|
1185
1592
|
const existingBinding = validationRun.processes[checkId];
|
|
1186
1593
|
let process = existingBinding
|
|
1187
1594
|
? getCheckProcessHandle(ctx.controllerHome, handle.repositoryId, existingBinding.processId)
|
|
@@ -1202,23 +1609,36 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1202
1609
|
repoId: handle.repositoryId,
|
|
1203
1610
|
checkoutId: handle.checkoutId,
|
|
1204
1611
|
repoRoot: validated.worktreeRepository.canonicalRoot,
|
|
1205
|
-
executionIdentity:
|
|
1612
|
+
executionIdentity: Object.freeze({
|
|
1613
|
+
...executionIdentityForWork(validated.worktreeRepository, handle),
|
|
1614
|
+
expectedHead: validationHead === 'unknown' ? handle.expectedHead : validationHead,
|
|
1615
|
+
}),
|
|
1206
1616
|
checkId,
|
|
1207
1617
|
requestId: processRequestId,
|
|
1208
1618
|
workId: handle.workId,
|
|
1209
1619
|
commandId: processRequestId,
|
|
1210
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,
|
|
1211
1626
|
});
|
|
1212
1627
|
if (executed.mode === 'durable') {
|
|
1213
1628
|
checks.push({ checkId, ok: undefined, status: 'deferred', summary: executed.durable?.reason, durable: executed.durable });
|
|
1214
1629
|
break;
|
|
1215
1630
|
}
|
|
1216
1631
|
process = executed.process!;
|
|
1632
|
+
const launchedRecord = getProcessRecord(ctx.controllerHome, handle.repositoryId, process.processId);
|
|
1217
1633
|
validationRun = {
|
|
1218
1634
|
...validationRun,
|
|
1219
1635
|
processes: {
|
|
1220
1636
|
...validationRun.processes,
|
|
1221
|
-
[checkId]: {
|
|
1637
|
+
[checkId]: {
|
|
1638
|
+
processId: process.processId,
|
|
1639
|
+
requestId: processRequestId,
|
|
1640
|
+
...(launchedRecord?.checkExecution ? { checkExecution: { ...launchedRecord.checkExecution } } : {}),
|
|
1641
|
+
},
|
|
1222
1642
|
},
|
|
1223
1643
|
};
|
|
1224
1644
|
current = transitionWorkHandle(ctx.controllerHome, current, 'validating', { validationRun });
|
|
@@ -1232,17 +1652,32 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1232
1652
|
checks.push({ checkId, ok: false, status: 'infrastructure_failure', summary: `Validation process record is unavailable: ${process.processId}` });
|
|
1233
1653
|
break;
|
|
1234
1654
|
}
|
|
1655
|
+
const boundCheckExecution = validationRun.processes[checkId]?.checkExecution;
|
|
1235
1656
|
const receipt = processCheckCompletionReceipt(record, {
|
|
1236
1657
|
repoId: handle.repositoryId,
|
|
1237
1658
|
checkoutId: handle.checkoutId,
|
|
1238
1659
|
workId: handle.workId,
|
|
1239
|
-
executionSessionId: session.sessionId,
|
|
1240
1660
|
checkId,
|
|
1241
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
|
+
} : {}),
|
|
1242
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');
|
|
1243
1678
|
appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, handle.workId, {
|
|
1244
1679
|
checkId,
|
|
1245
|
-
outcome:
|
|
1680
|
+
outcome: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'valid_pass' : 'valid_fail',
|
|
1246
1681
|
summary: receipt.summary,
|
|
1247
1682
|
recordedAt: receipt.finishedAt,
|
|
1248
1683
|
sourceRevision: validationHead,
|
|
@@ -1252,7 +1687,6 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1252
1687
|
workspaceFingerprint,
|
|
1253
1688
|
checkId,
|
|
1254
1689
|
requestedChecks,
|
|
1255
|
-
commandId: receipt.commandId,
|
|
1256
1690
|
}),
|
|
1257
1691
|
commandFingerprint: commandFingerprint(checkId, receipt.commandId),
|
|
1258
1692
|
resultArtifactId: receipt.receiptId,
|
|
@@ -1264,9 +1698,10 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1264
1698
|
checks.push({
|
|
1265
1699
|
checkId,
|
|
1266
1700
|
ok: receipt.ok,
|
|
1267
|
-
status:
|
|
1701
|
+
status: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'passed' : 'failed',
|
|
1268
1702
|
process,
|
|
1269
1703
|
receipt,
|
|
1704
|
+
...(terminalEvidence.warning ? { warning: terminalEvidence.warning } : {}),
|
|
1270
1705
|
});
|
|
1271
1706
|
if (!receipt.ok) break;
|
|
1272
1707
|
}
|
|
@@ -1311,21 +1746,25 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1311
1746
|
failureSummary,
|
|
1312
1747
|
);
|
|
1313
1748
|
}
|
|
1314
|
-
if (completed &&
|
|
1315
|
-
const cleanup = await reconcileTerminalCleanup(
|
|
1316
|
-
ctx,
|
|
1317
|
-
session,
|
|
1318
|
-
next,
|
|
1319
|
-
args,
|
|
1320
|
-
infrastructureFailure ? 'infrastructure_failed' : 'validation_failed',
|
|
1321
|
-
);
|
|
1749
|
+
if (completed && acceptedFailure) {
|
|
1750
|
+
const cleanup = await reconcileTerminalCleanup(ctx, session, next, args, 'validation_failed');
|
|
1322
1751
|
const value = {
|
|
1323
1752
|
...cleanup,
|
|
1324
1753
|
validation: { passed, completed, checks, targeted: true, changedPaths, cleanupTriggered: true },
|
|
1325
1754
|
};
|
|
1326
1755
|
return makeBoundedResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
|
|
1327
1756
|
}
|
|
1328
|
-
const value = {
|
|
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
|
+
};
|
|
1329
1768
|
return makeBoundedResult(ctx, session, handle.repositoryId, handle.workId, 'validation', value);
|
|
1330
1769
|
}
|
|
1331
1770
|
|
|
@@ -1390,7 +1829,6 @@ function cleanupOnlyMergedHead(
|
|
|
1390
1829
|
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
1391
1830
|
});
|
|
1392
1831
|
if (currentBranch.status !== 0 || String(currentBranch.stdout ?? '').trim() !== current.branch) return undefined;
|
|
1393
|
-
if (current.expectedHead && currentHead === current.expectedHead && !cancelledContract) return undefined;
|
|
1394
1832
|
if (current.expectedHead && currentHead !== current.expectedHead && cancelledContract) return undefined;
|
|
1395
1833
|
return { currentHead, cancelledContract, worktreeMissing: false };
|
|
1396
1834
|
}
|
|
@@ -1451,7 +1889,35 @@ function failedCleanupOnlyHead(
|
|
|
1451
1889
|
return { currentHead: expectedHead, worktreeMissing: false, targetBranch };
|
|
1452
1890
|
}
|
|
1453
1891
|
|
|
1454
|
-
|
|
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 {
|
|
1455
1921
|
const next = { ...stages };
|
|
1456
1922
|
let reset = false;
|
|
1457
1923
|
if (next.validation === 'failed') {
|
|
@@ -1474,6 +1940,28 @@ function resetFailedFinalizationStages(stages: WorkFinalizationStages, wants: {
|
|
|
1474
1940
|
next.branchCleanup = 'pending';
|
|
1475
1941
|
reset = true;
|
|
1476
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
|
+
}
|
|
1477
1965
|
if (reset) delete next.lastError;
|
|
1478
1966
|
return next;
|
|
1479
1967
|
}
|
|
@@ -1516,12 +2004,87 @@ function currentWorkValidationInput(
|
|
|
1516
2004
|
|
|
1517
2005
|
async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1518
2006
|
const session = requireSession(ctx, args);
|
|
1519
|
-
let current = workForSession(ctx, session, 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
|
+
}
|
|
1520
2030
|
const terminalOutcome = terminalCleanupOutcome(ctx, current);
|
|
1521
2031
|
if (terminalOutcome && args.cleanup !== false) {
|
|
1522
2032
|
return await reconcileTerminalCleanup(ctx, session, current, args, terminalOutcome);
|
|
1523
2033
|
}
|
|
1524
|
-
assertWorkControllerOwnership(ctx, session, current, args);
|
|
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
|
+
}
|
|
1525
2088
|
if (current.state === 'cleaned') {
|
|
1526
2089
|
const terminalContract = contractFor(ctx, current);
|
|
1527
2090
|
if (
|
|
@@ -1554,7 +2117,18 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1554
2117
|
if (requestedOutcome === 'completed_no_change' && (!noChangeProof || args.commit === true || args.merge === true)) {
|
|
1555
2118
|
throw new Error('WORK_NO_CHANGE_PROOF_REQUIRED: completed_no_change requires objective-specific evidence and forbids commit/merge');
|
|
1556
2119
|
}
|
|
1557
|
-
const wants =
|
|
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';
|
|
1558
2132
|
|
|
1559
2133
|
const transact = (label: string, update: (fresh: WorkHandleState) => WorkHandleState): WorkHandleState =>
|
|
1560
2134
|
withControllerLock(ctx.controllerHome, { scope: 'worktree', repoId: current.repositoryId, worktreeId: current.checkoutId }, `work-finalize:${current.workId}:${label}`, () => {
|
|
@@ -1567,15 +2141,31 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1567
2141
|
const finalization = { ...fresh.finalization, [stage]: 'failed', lastError: reason } as WorkFinalizationStages;
|
|
1568
2142
|
return markWorkHandleFailed(ctx.controllerHome, { ...fresh, finalization }, reason);
|
|
1569
2143
|
});
|
|
1570
|
-
if (current.workContractId)
|
|
1571
|
-
|
|
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`);
|
|
1572
2161
|
return { work: compactHandle(current), stages: current.finalization, completed: false };
|
|
1573
2162
|
};
|
|
1574
2163
|
|
|
1575
2164
|
const failedCleanupRequested = current.state === 'failed'
|
|
1576
2165
|
&& wants.cleanup
|
|
1577
2166
|
&& !wants.commit
|
|
1578
|
-
&& !wants.merge
|
|
2167
|
+
&& !wants.merge
|
|
2168
|
+
&& retryStage === undefined;
|
|
1579
2169
|
const failedCleanupProof = failedCleanupRequested
|
|
1580
2170
|
? failedCleanupOnlyHead(ctx, current, args)
|
|
1581
2171
|
: undefined;
|
|
@@ -1583,12 +2173,17 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1583
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');
|
|
1584
2174
|
}
|
|
1585
2175
|
|
|
1586
|
-
if (!failedCleanupRequested) {
|
|
2176
|
+
if (!failedCleanupRequested && !noChangeFastPath) {
|
|
1587
2177
|
current = transact('begin', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
1588
2178
|
...fresh,
|
|
1589
|
-
state: fresh.state === 'failed' ?
|
|
2179
|
+
state: fresh.state === 'failed' ? retryHandleStateForFinalization(fresh.finalization) : fresh.state,
|
|
1590
2180
|
failureReason: undefined,
|
|
1591
|
-
finalization:
|
|
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 } : {}),
|
|
1592
2187
|
}));
|
|
1593
2188
|
}
|
|
1594
2189
|
|
|
@@ -1627,7 +2222,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1627
2222
|
validation: 'failed',
|
|
1628
2223
|
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
1629
2224
|
merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
|
|
1630
|
-
branchCleanup:
|
|
2225
|
+
branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
|
|
1631
2226
|
lastError: preservedFailure,
|
|
1632
2227
|
},
|
|
1633
2228
|
}));
|
|
@@ -1659,7 +2254,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1659
2254
|
});
|
|
1660
2255
|
}
|
|
1661
2256
|
|
|
1662
|
-
if (
|
|
2257
|
+
if (deleteBranchRequested && current.finalization.branchCleanup !== 'done') {
|
|
1663
2258
|
const target = selectWorkFinalizationTarget(
|
|
1664
2259
|
getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true }),
|
|
1665
2260
|
current,
|
|
@@ -1713,14 +2308,14 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1713
2308
|
validation: 'failed',
|
|
1714
2309
|
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
1715
2310
|
merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
|
|
1716
|
-
branchCleanup:
|
|
2311
|
+
branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
|
|
1717
2312
|
worktreeCleanup: 'done',
|
|
1718
2313
|
lastError: preservedFailure,
|
|
1719
2314
|
},
|
|
1720
2315
|
}));
|
|
1721
2316
|
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
|
|
1722
2317
|
title: 'failed work cleanup completed',
|
|
1723
|
-
summary: `Controller preserved the failed Work outcome while removing its unchanged clean managed worktree and ${
|
|
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}.`,
|
|
1724
2319
|
detailLevel: 'summary',
|
|
1725
2320
|
});
|
|
1726
2321
|
releasePreparedWorkOwnership(ctx, current);
|
|
@@ -1743,7 +2338,9 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1743
2338
|
&& wants.cleanup
|
|
1744
2339
|
&& !wants.commit
|
|
1745
2340
|
&& !wants.merge;
|
|
1746
|
-
const cleanupReconciliation =
|
|
2341
|
+
const cleanupReconciliation = requestedOutcome === 'completed_no_change'
|
|
2342
|
+
? undefined
|
|
2343
|
+
: cleanupOnlyMergedHead(ctx, current, args);
|
|
1747
2344
|
if (cancelledCleanupRequested && !cleanupReconciliation) {
|
|
1748
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');
|
|
1749
2346
|
}
|
|
@@ -1758,13 +2355,24 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1758
2355
|
validation: cleanupReconciliation.worktreeMissing ? 'done' : fresh.finalization.validation,
|
|
1759
2356
|
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
1760
2357
|
merge: 'done',
|
|
1761
|
-
branchCleanup:
|
|
2358
|
+
branchCleanup: deleteBranchRequested ? 'pending' : 'skipped',
|
|
1762
2359
|
lastError: undefined,
|
|
1763
2360
|
},
|
|
1764
2361
|
}));
|
|
1765
2362
|
}
|
|
1766
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
|
+
|
|
1767
2374
|
const terminalCleanupOnly = Boolean(cleanupReconciliation && wants.cleanup && !wants.commit && !wants.merge);
|
|
2375
|
+
let exactValidationInput: ReturnType<typeof currentWorkValidationInput> | undefined;
|
|
1768
2376
|
if (terminalCleanupOnly) {
|
|
1769
2377
|
current = transact('terminal-cleanup-validation-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
1770
2378
|
...fresh,
|
|
@@ -1780,12 +2388,15 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1780
2388
|
const validationContract = contractFor(ctx, current);
|
|
1781
2389
|
if (!validationContract) throw new Error(`WORK_VALIDATION_CONTRACT_MISSING: ${current.workContractId ?? current.workId}`);
|
|
1782
2390
|
const validationInput = currentWorkValidationInput(validatedRepository, current, validationContract.checks);
|
|
2391
|
+
exactValidationInput = validationInput;
|
|
1783
2392
|
if (validationContract.checks.length === 0) {
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
+
}
|
|
1789
2400
|
projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required.');
|
|
1790
2401
|
} else if (!hasCurrentWorkValidationAuthority({
|
|
1791
2402
|
finalizationValidation: current.finalization.validation,
|
|
@@ -1801,16 +2412,52 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1801
2412
|
markWorkValidationPending(ctx.controllerHome, current);
|
|
1802
2413
|
throw new Error('WORK_VALIDATION_REQUIRED: run work_validate against the exact current workspace before finalization');
|
|
1803
2414
|
}
|
|
1804
|
-
if (
|
|
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) {
|
|
1805
2429
|
throw new Error('WORK_MERGE_UNCOMMITTED_CHANGES: commit the validated workspace before merging');
|
|
1806
2430
|
}
|
|
1807
2431
|
}
|
|
1808
2432
|
|
|
1809
|
-
if (
|
|
1810
|
-
|
|
1811
|
-
if (!repositoryGitStatus(inspected.worktreeRepository).clean) {
|
|
2433
|
+
if (noChangeFastPath) {
|
|
2434
|
+
if (!exactValidationInput?.clean) {
|
|
1812
2435
|
throw new Error('WORK_NO_CHANGE_DIRTY: completed_no_change cannot retain an owned dirty worktree');
|
|
1813
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
|
+
}
|
|
1814
2461
|
}
|
|
1815
2462
|
|
|
1816
2463
|
if (wants.commit && current.finalization.commit === 'pending') {
|
|
@@ -1818,58 +2465,454 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1818
2465
|
const contract = contractFor(ctx, current);
|
|
1819
2466
|
if (contract?.constraints.allowCommit === false) throw new Error('WORK_COMMIT_NOT_ALLOWED: WorkContract disallows commit');
|
|
1820
2467
|
const status = repositoryGitStatus(validated.worktreeRepository);
|
|
1821
|
-
const commitPaths =
|
|
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
|
+
}
|
|
1822
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 });
|
|
1823
2473
|
const pendingAuthorization = [committed.stage, committed.commit].find((execution) => execution?.authorizationDecision?.decision === 'user_confirmation_required')?.authorizationDecision;
|
|
1824
2474
|
if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
|
|
1825
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);
|
|
1826
2487
|
current = transact('commit-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'committed', {
|
|
1827
|
-
expectedHead:
|
|
2488
|
+
expectedHead: committedHead,
|
|
1828
2489
|
finalization: {
|
|
1829
2490
|
...fresh.finalization,
|
|
1830
|
-
validation:
|
|
2491
|
+
validation: checks.length ? (validationPreservedAcrossCommit ? 'done' : 'pending') : 'done',
|
|
1831
2492
|
commit: 'done',
|
|
1832
2493
|
lastError: undefined,
|
|
1833
2494
|
},
|
|
1834
|
-
validatedInputFingerprint:
|
|
2495
|
+
validatedInputFingerprint: checks.length
|
|
2496
|
+
? (validationPreservedAcrossCommit ? postCommitInput.fingerprint : undefined)
|
|
2497
|
+
: postCommitInput.fingerprint,
|
|
1835
2498
|
failureReason: undefined,
|
|
1836
2499
|
}));
|
|
1837
|
-
if (
|
|
2500
|
+
if (checks.length && !validationPreservedAcrossCommit) {
|
|
1838
2501
|
markWorkValidationPending(ctx.controllerHome, current);
|
|
1839
2502
|
return {
|
|
1840
2503
|
work: compactHandle(current),
|
|
1841
2504
|
stages: current.finalization,
|
|
1842
2505
|
completed: false,
|
|
1843
|
-
continuation: 'WORK_COMMITTED_REVALIDATION_REQUIRED: run work_validate on the
|
|
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',
|
|
1844
2507
|
};
|
|
1845
2508
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
+
}
|
|
1853
2550
|
} else if (!wants.commit && current.finalization.commit === 'pending') {
|
|
1854
2551
|
current = transact('commit-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, commit: 'skipped' } }));
|
|
1855
2552
|
}
|
|
1856
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
|
+
|
|
1857
2579
|
if (wants.merge && current.finalization.merge === 'pending') {
|
|
1858
|
-
validateWorkHandle(ctx.controllerHome, current, identity, 'full', 'finalize');
|
|
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
|
+
}
|
|
1859
2588
|
const contract = contractFor(ctx, current);
|
|
1860
2589
|
if (contract?.constraints.allowMerge === false) throw new Error('WORK_MERGE_NOT_ALLOWED: WorkContract disallows merge');
|
|
1861
2590
|
const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
|
|
1862
|
-
const deleteAfterWorktreeCleanup = current.managedWorktree &&
|
|
1863
|
-
const
|
|
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
|
+
}
|
|
1864
2843
|
const pendingAuthorization = merged.steps.find((step) => step.execution.authorizationDecision?.decision === 'user_confirmation_required')?.execution.authorizationDecision;
|
|
1865
2844
|
if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
|
|
1866
2845
|
if (!merged.completed) return { ...failStage('merge', merged.error?.message ?? 'merge failed'), merge: merged };
|
|
1867
2846
|
current = transact('merge-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
|
|
1868
|
-
finalization: { ...fresh.finalization, merge: 'done', branchCleanup:
|
|
2847
|
+
finalization: { ...fresh.finalization, merge: 'done', branchCleanup: !deleteBranchRequested ? 'skipped' : deleteAfterWorktreeCleanup ? 'pending' : 'done', lastError: undefined },
|
|
1869
2848
|
failureReason: undefined,
|
|
1870
2849
|
}));
|
|
1871
2850
|
} else if (!wants.merge && current.finalization.merge === 'pending') {
|
|
1872
|
-
current = transact('merge-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
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
|
+
}
|
|
1873
2916
|
}
|
|
1874
2917
|
|
|
1875
2918
|
if (wants.cleanup && current.finalization.worktreeCleanup === 'pending') {
|
|
@@ -1891,7 +2934,11 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1891
2934
|
current = transact('worktree-cleanup-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'skipped' } }));
|
|
1892
2935
|
}
|
|
1893
2936
|
|
|
1894
|
-
if (
|
|
2937
|
+
if (
|
|
2938
|
+
wants.cleanup
|
|
2939
|
+
&& current.finalization.branchCleanup === 'pending'
|
|
2940
|
+
&& (current.finalization.merge === 'done' || requestedOutcome === 'completed_no_change')
|
|
2941
|
+
) {
|
|
1895
2942
|
const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
|
|
1896
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 });
|
|
1897
2944
|
if (deleted.execution.authorizationDecision?.decision === 'user_confirmation_required') return { authorization: deleted.execution.authorizationDecision, work: compactHandle(current), stages: current.finalization };
|
|
@@ -1929,18 +2976,64 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1929
2976
|
});
|
|
1930
2977
|
}
|
|
1931
2978
|
const receipt = completionReceiptForFinalizedWork(ctx, current, completionContract, args);
|
|
1932
|
-
const
|
|
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(
|
|
1933
2986
|
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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
|
+
),
|
|
1938
3005
|
);
|
|
3006
|
+
if (!fencedCompletion.allowed) {
|
|
3007
|
+
throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fencedCompletion.reason}`);
|
|
3008
|
+
}
|
|
3009
|
+
const recorded = fencedCompletion.value;
|
|
1939
3010
|
if (recorded.requirementId) {
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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
|
+
}
|
|
1944
3037
|
}
|
|
1945
3038
|
}
|
|
1946
3039
|
// Successful WorkContract completion always ends controller ownership.
|
|
@@ -1952,7 +3045,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1952
3045
|
activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
|
|
1953
3046
|
});
|
|
1954
3047
|
}
|
|
1955
|
-
return { work: compactHandle(current), stages: current.finalization, completed: complete, idempotent: !wants.commit && !wants.merge && !wants.cleanup && current.finalization.validation === 'done' };
|
|
3048
|
+
return { work: compactHandle(current), stages: current.finalization, completed: complete, ...(remoteDelivery ? { remoteDelivery } : {}), idempotent: !wants.commit && !wants.merge && !wants.cleanup && current.finalization.validation === 'done' };
|
|
1956
3049
|
}
|
|
1957
3050
|
|
|
1958
3051
|
export async function callExecutionTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>): Promise<CallToolResult | undefined> {
|