@moretea-labs/forge 1.6.0 → 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 +26 -0
- package/CLAUDE.md +2 -0
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- 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/prompt-guard.sh +5 -81
- 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 +1 -1
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/harness-overview.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 +11 -36
- 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/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 +26 -6
- package/docs/operations/controller-performance-and-502.md +124 -0
- package/docs/operations/controller-reliability-runbook.md +1 -1
- package/docs/operations/features.md +2 -2
- 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/wiki/Architecture.md +6 -6
- package/docs/wiki/Implementation.md +2 -2
- package/docs/wiki/Integrations.md +2 -2
- 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-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-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/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/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +1 -1
- 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 +785 -469
- 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/installer/managed-entries.ts +15 -1
- package/src/cli/local-bridge/console-automations.ts +54 -78
- package/src/cli/local-bridge/console-view-models.ts +1 -35
- package/src/cli/local-bridge/facade-api.ts +0 -217
- package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
- package/src/cli/local-bridge/frontend/src/styles.css +1 -1
- package/src/cli/local-bridge/frontend/src/types.ts +5 -8
- package/src/cli/local-bridge/job-store.ts +1 -490
- package/src/cli/local-bridge/server.ts +0 -513
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
- 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/repository-tools.ts +247 -107
- package/src/cli/mcp/server.ts +537 -111
- 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 +101 -14
- package/src/cli/mcp/transports/session-registry.ts +43 -5
- package/src/cli/mcp/types.ts +3 -3
- package/src/cli/repositories/command-classifier.ts +101 -19
- 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/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +61 -3
- package/src/cli/repository/inspector.ts +553 -63
- 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 +47 -8
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- package/src/runtime/control-plane/execution/session-store.ts +18 -0
- 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 +143 -8
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +85 -8
- 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 +132 -1
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +2 -0
- package/src/runtime/control-plane/facade/goal-workloop.ts +686 -129
- package/src/runtime/control-plane/facade/index.ts +1 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +185 -20
- 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 +22 -5
- package/src/runtime/control-plane/facade/work-contract-store.ts +175 -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 +355 -116
- 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/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 +13 -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 +51 -4
- package/src/runtime/execution/process-runtime/check-result.ts +53 -0
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +345 -40
- package/src/runtime/execution/process-runtime/gc.ts +38 -6
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +47 -4
- package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
- package/src/runtime/execution/process-runtime/runtime.ts +43 -3
- package/src/runtime/execution/process-runtime/store.ts +1 -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 +1192 -114
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +184 -82
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2224 -1589
- package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
- package/src/runtime/plugins/browser-adapter.ts +2901 -241
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff.ts +92 -2
- package/src/runtime/plugins/browser-macos-bridge.ts +582 -49
- 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 +8 -2
- package/src/runtime/plugins/external-adapter.ts +324 -3
- package/src/runtime/plugins/external-unix-socket.ts +7 -1
- package/src/runtime/plugins/first-party-registry.ts +2 -0
- package/src/runtime/plugins/github-adapter.ts +125 -0
- 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 +123 -6
- 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 +49 -8
- package/src/runtime/plugins/resend-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +269 -29
- package/src/runtime/plugins/types.ts +35 -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/maintenance-executor.ts +393 -28
- 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 +19 -0
- package/src/runtime/root/release-store.ts +48 -11
- package/src/runtime/root/runtime.ts +23 -5
- package/src/runtime/root/service.ts +100 -29
- package/src/runtime/root/types.ts +2 -0
- package/src/runtime/safe-tooling/ios-development.ts +321 -63
- 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 +700 -92
- 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 +81 -3
- package/src/runtime/workflow/schedules/engine.ts +287 -151
- 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 +2 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
- 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/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 -459
- package/docs/architecture/current/codegraph-context-provider.md +0 -238
- 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 -373
- package/docs/architecture/current/entity-model.md +0 -612
- 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 -169
- 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 -321
- 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 -93
- 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/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,6 +1029,7 @@ 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
|
|
@@ -776,6 +1149,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
|
|
|
776
1149
|
const checks = boundedStringArray(args.checks, 30);
|
|
777
1150
|
const baseRef = typeof args.base_ref === 'string' && args.base_ref.trim() ? args.base_ref.trim() : undefined;
|
|
778
1151
|
const requestedCheckoutId = typeof args.checkout_id === 'string' && args.checkout_id.trim() ? args.checkout_id.trim() : undefined;
|
|
1152
|
+
const needsDependencies = args.needs_dependencies === true;
|
|
779
1153
|
const baseCheckoutId = repository.activeCheckoutId;
|
|
780
1154
|
const baseStatus = repositoryGitStatus(repository);
|
|
781
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');
|
|
@@ -791,6 +1165,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
|
|
|
791
1165
|
allowedPaths,
|
|
792
1166
|
checks,
|
|
793
1167
|
baseRef,
|
|
1168
|
+
needsDependencies,
|
|
794
1169
|
});
|
|
795
1170
|
|
|
796
1171
|
return withWorkPrepareRequest({
|
|
@@ -876,7 +1251,12 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
|
|
|
876
1251
|
});
|
|
877
1252
|
try {
|
|
878
1253
|
const workspace = useWorktree
|
|
879
|
-
? ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
1254
|
+
? ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
1255
|
+
requestId: createdWorkId,
|
|
1256
|
+
title: objective,
|
|
1257
|
+
baseRef,
|
|
1258
|
+
prepareDependencies: needsDependencies,
|
|
1259
|
+
})
|
|
880
1260
|
: { mode: 'current' as const, checkoutId: baseCheckoutId, root: repository.canonicalRoot, branch: baseStatus.branch ?? 'detached', baseRevision: baseStatus.head ?? undefined, managed: false };
|
|
881
1261
|
const refreshed = getRepository(repository.repoId, ctx.controllerHome);
|
|
882
1262
|
const checkout = selectRepositoryCheckout(refreshed, workspace.checkoutId);
|
|
@@ -887,7 +1267,7 @@ function prepareWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, un
|
|
|
887
1267
|
schemaVersion: 1, workId: createdWorkId, sessionId: session.sessionId, principalId: session.principalId,
|
|
888
1268
|
repositoryId: repository.repoId, checkoutId: checkout.activeCheckoutId, worktreePath: checkout.canonicalRoot, branch,
|
|
889
1269
|
sourceCheckoutId: baseCheckoutId, managedWorktree: workspace.managed, workContractId: contract.workId, goalId, delegationVersion: delegation.version,
|
|
890
|
-
baseCommit: workspace.baseRevision ?? head, expectedHead: head, permissionSnapshotVersion: policy.revision,
|
|
1270
|
+
baseCommit: workspace.baseRevision ?? head, deliveryBaseCommit: workspace.baseRevision ?? head, expectedHead: head, permissionSnapshotVersion: policy.revision,
|
|
891
1271
|
state: 'prepared', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), finalization: initialStage(),
|
|
892
1272
|
cleanupResponsibility: { owner: 'work_finalizer', registeredAt: new Date().toISOString() },
|
|
893
1273
|
};
|
|
@@ -1044,15 +1424,16 @@ async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<stri
|
|
|
1044
1424
|
maxOutputBytes: typeof args.max_output_bytes === 'number' ? args.max_output_bytes : undefined,
|
|
1045
1425
|
});
|
|
1046
1426
|
const process = execution.process;
|
|
1047
|
-
const completed = process?.completed === true || execution.route === 'process_direct';
|
|
1048
1427
|
const ok = process ? process.ok === true : execution.ok === true;
|
|
1049
1428
|
const status = process
|
|
1050
1429
|
? (process.completed
|
|
1051
1430
|
? (process.cancelled ? 'cancelled' : process.timedOut ? 'timed_out' : ok ? 'executed' : 'failed')
|
|
1052
1431
|
: 'running')
|
|
1053
|
-
: execution.route === '
|
|
1054
|
-
? '
|
|
1055
|
-
: '
|
|
1432
|
+
: execution.route === 'process_direct'
|
|
1433
|
+
? (ok ? 'executed' : 'failed')
|
|
1434
|
+
: execution.route === 'durable'
|
|
1435
|
+
? 'deferred_durable'
|
|
1436
|
+
: 'rejected';
|
|
1056
1437
|
return {
|
|
1057
1438
|
processId: process?.processId,
|
|
1058
1439
|
commandId,
|
|
@@ -1086,8 +1467,9 @@ async function executeWork(ctx: MultiRepositoryMcpToolContext, args: Record<stri
|
|
|
1086
1467
|
for (const [index, entry] of inputs.entries()) {
|
|
1087
1468
|
const execution = await run(entry, index);
|
|
1088
1469
|
ordered.push(execution);
|
|
1089
|
-
// Do not launch another mutating command while
|
|
1090
|
-
// 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.
|
|
1091
1473
|
if (execution.process && !execution.process.completed) break;
|
|
1092
1474
|
}
|
|
1093
1475
|
return ordered;
|
|
@@ -1134,7 +1516,10 @@ export function selectDefaultWorkValidationChecks(
|
|
|
1134
1516
|
|
|
1135
1517
|
async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1136
1518
|
const session = requireSession(ctx, args);
|
|
1137
|
-
const handle = workForSession(ctx, session, args, {
|
|
1519
|
+
const handle = workForSession(ctx, session, args, {
|
|
1520
|
+
reconcileValidation: false,
|
|
1521
|
+
allowClaimedTerminalCleanup: args.cleanup !== false,
|
|
1522
|
+
});
|
|
1138
1523
|
const terminalOutcome = terminalCleanupOutcome(ctx, handle);
|
|
1139
1524
|
if (terminalOutcome && args.cleanup !== false) {
|
|
1140
1525
|
return await reconcileTerminalCleanup(ctx, session, handle, args, terminalOutcome);
|
|
@@ -1186,6 +1571,24 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1186
1571
|
checks.push({ checkId, ok: false, status: 'missing', summary: `Check not found: ${checkId}` });
|
|
1187
1572
|
break;
|
|
1188
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
|
+
}
|
|
1189
1592
|
const existingBinding = validationRun.processes[checkId];
|
|
1190
1593
|
let process = existingBinding
|
|
1191
1594
|
? getCheckProcessHandle(ctx.controllerHome, handle.repositoryId, existingBinding.processId)
|
|
@@ -1226,11 +1629,16 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1226
1629
|
break;
|
|
1227
1630
|
}
|
|
1228
1631
|
process = executed.process!;
|
|
1632
|
+
const launchedRecord = getProcessRecord(ctx.controllerHome, handle.repositoryId, process.processId);
|
|
1229
1633
|
validationRun = {
|
|
1230
1634
|
...validationRun,
|
|
1231
1635
|
processes: {
|
|
1232
1636
|
...validationRun.processes,
|
|
1233
|
-
[checkId]: {
|
|
1637
|
+
[checkId]: {
|
|
1638
|
+
processId: process.processId,
|
|
1639
|
+
requestId: processRequestId,
|
|
1640
|
+
...(launchedRecord?.checkExecution ? { checkExecution: { ...launchedRecord.checkExecution } } : {}),
|
|
1641
|
+
},
|
|
1234
1642
|
},
|
|
1235
1643
|
};
|
|
1236
1644
|
current = transitionWorkHandle(ctx.controllerHome, current, 'validating', { validationRun });
|
|
@@ -1244,16 +1652,32 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1244
1652
|
checks.push({ checkId, ok: false, status: 'infrastructure_failure', summary: `Validation process record is unavailable: ${process.processId}` });
|
|
1245
1653
|
break;
|
|
1246
1654
|
}
|
|
1655
|
+
const boundCheckExecution = validationRun.processes[checkId]?.checkExecution;
|
|
1247
1656
|
const receipt = processCheckCompletionReceipt(record, {
|
|
1248
1657
|
repoId: handle.repositoryId,
|
|
1249
1658
|
checkoutId: handle.checkoutId,
|
|
1250
1659
|
workId: handle.workId,
|
|
1251
1660
|
checkId,
|
|
1252
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
|
+
} : {}),
|
|
1253
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');
|
|
1254
1678
|
appendVerificationRecord({ controllerHome: ctx.controllerHome, repoId: handle.repositoryId }, handle.workId, {
|
|
1255
1679
|
checkId,
|
|
1256
|
-
outcome:
|
|
1680
|
+
outcome: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'valid_pass' : 'valid_fail',
|
|
1257
1681
|
summary: receipt.summary,
|
|
1258
1682
|
recordedAt: receipt.finishedAt,
|
|
1259
1683
|
sourceRevision: validationHead,
|
|
@@ -1263,7 +1687,6 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1263
1687
|
workspaceFingerprint,
|
|
1264
1688
|
checkId,
|
|
1265
1689
|
requestedChecks,
|
|
1266
|
-
commandId: receipt.commandId,
|
|
1267
1690
|
}),
|
|
1268
1691
|
commandFingerprint: commandFingerprint(checkId, receipt.commandId),
|
|
1269
1692
|
resultArtifactId: receipt.receiptId,
|
|
@@ -1275,9 +1698,10 @@ async function validateWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1275
1698
|
checks.push({
|
|
1276
1699
|
checkId,
|
|
1277
1700
|
ok: receipt.ok,
|
|
1278
|
-
status:
|
|
1701
|
+
status: infrastructureFailed ? 'infrastructure_failure' : receipt.ok ? 'passed' : 'failed',
|
|
1279
1702
|
process,
|
|
1280
1703
|
receipt,
|
|
1704
|
+
...(terminalEvidence.warning ? { warning: terminalEvidence.warning } : {}),
|
|
1281
1705
|
});
|
|
1282
1706
|
if (!receipt.ok) break;
|
|
1283
1707
|
}
|
|
@@ -1405,7 +1829,6 @@ function cleanupOnlyMergedHead(
|
|
|
1405
1829
|
encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000,
|
|
1406
1830
|
});
|
|
1407
1831
|
if (currentBranch.status !== 0 || String(currentBranch.stdout ?? '').trim() !== current.branch) return undefined;
|
|
1408
|
-
if (current.expectedHead && currentHead === current.expectedHead && !cancelledContract) return undefined;
|
|
1409
1832
|
if (current.expectedHead && currentHead !== current.expectedHead && cancelledContract) return undefined;
|
|
1410
1833
|
return { currentHead, cancelledContract, worktreeMissing: false };
|
|
1411
1834
|
}
|
|
@@ -1466,7 +1889,35 @@ function failedCleanupOnlyHead(
|
|
|
1466
1889
|
return { currentHead: expectedHead, worktreeMissing: false, targetBranch };
|
|
1467
1890
|
}
|
|
1468
1891
|
|
|
1469
|
-
|
|
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 {
|
|
1470
1921
|
const next = { ...stages };
|
|
1471
1922
|
let reset = false;
|
|
1472
1923
|
if (next.validation === 'failed') {
|
|
@@ -1489,6 +1940,28 @@ function resetFailedFinalizationStages(stages: WorkFinalizationStages, wants: {
|
|
|
1489
1940
|
next.branchCleanup = 'pending';
|
|
1490
1941
|
reset = true;
|
|
1491
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
|
+
}
|
|
1492
1965
|
if (reset) delete next.lastError;
|
|
1493
1966
|
return next;
|
|
1494
1967
|
}
|
|
@@ -1531,12 +2004,87 @@ function currentWorkValidationInput(
|
|
|
1531
2004
|
|
|
1532
2005
|
async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
1533
2006
|
const session = requireSession(ctx, args);
|
|
1534
|
-
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
|
+
}
|
|
1535
2030
|
const terminalOutcome = terminalCleanupOutcome(ctx, current);
|
|
1536
2031
|
if (terminalOutcome && args.cleanup !== false) {
|
|
1537
2032
|
return await reconcileTerminalCleanup(ctx, session, current, args, terminalOutcome);
|
|
1538
2033
|
}
|
|
1539
|
-
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
|
+
}
|
|
1540
2088
|
if (current.state === 'cleaned') {
|
|
1541
2089
|
const terminalContract = contractFor(ctx, current);
|
|
1542
2090
|
if (
|
|
@@ -1569,7 +2117,18 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1569
2117
|
if (requestedOutcome === 'completed_no_change' && (!noChangeProof || args.commit === true || args.merge === true)) {
|
|
1570
2118
|
throw new Error('WORK_NO_CHANGE_PROOF_REQUIRED: completed_no_change requires objective-specific evidence and forbids commit/merge');
|
|
1571
2119
|
}
|
|
1572
|
-
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';
|
|
1573
2132
|
|
|
1574
2133
|
const transact = (label: string, update: (fresh: WorkHandleState) => WorkHandleState): WorkHandleState =>
|
|
1575
2134
|
withControllerLock(ctx.controllerHome, { scope: 'worktree', repoId: current.repositoryId, worktreeId: current.checkoutId }, `work-finalize:${current.workId}:${label}`, () => {
|
|
@@ -1582,15 +2141,31 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1582
2141
|
const finalization = { ...fresh.finalization, [stage]: 'failed', lastError: reason } as WorkFinalizationStages;
|
|
1583
2142
|
return markWorkHandleFailed(ctx.controllerHome, { ...fresh, finalization }, reason);
|
|
1584
2143
|
});
|
|
1585
|
-
if (current.workContractId)
|
|
1586
|
-
|
|
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`);
|
|
1587
2161
|
return { work: compactHandle(current), stages: current.finalization, completed: false };
|
|
1588
2162
|
};
|
|
1589
2163
|
|
|
1590
2164
|
const failedCleanupRequested = current.state === 'failed'
|
|
1591
2165
|
&& wants.cleanup
|
|
1592
2166
|
&& !wants.commit
|
|
1593
|
-
&& !wants.merge
|
|
2167
|
+
&& !wants.merge
|
|
2168
|
+
&& retryStage === undefined;
|
|
1594
2169
|
const failedCleanupProof = failedCleanupRequested
|
|
1595
2170
|
? failedCleanupOnlyHead(ctx, current, args)
|
|
1596
2171
|
: undefined;
|
|
@@ -1598,12 +2173,17 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1598
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');
|
|
1599
2174
|
}
|
|
1600
2175
|
|
|
1601
|
-
if (!failedCleanupRequested) {
|
|
2176
|
+
if (!failedCleanupRequested && !noChangeFastPath) {
|
|
1602
2177
|
current = transact('begin', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
1603
2178
|
...fresh,
|
|
1604
|
-
state: fresh.state === 'failed' ?
|
|
2179
|
+
state: fresh.state === 'failed' ? retryHandleStateForFinalization(fresh.finalization) : fresh.state,
|
|
1605
2180
|
failureReason: undefined,
|
|
1606
|
-
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 } : {}),
|
|
1607
2187
|
}));
|
|
1608
2188
|
}
|
|
1609
2189
|
|
|
@@ -1642,7 +2222,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1642
2222
|
validation: 'failed',
|
|
1643
2223
|
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
1644
2224
|
merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
|
|
1645
|
-
branchCleanup:
|
|
2225
|
+
branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
|
|
1646
2226
|
lastError: preservedFailure,
|
|
1647
2227
|
},
|
|
1648
2228
|
}));
|
|
@@ -1674,7 +2254,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1674
2254
|
});
|
|
1675
2255
|
}
|
|
1676
2256
|
|
|
1677
|
-
if (
|
|
2257
|
+
if (deleteBranchRequested && current.finalization.branchCleanup !== 'done') {
|
|
1678
2258
|
const target = selectWorkFinalizationTarget(
|
|
1679
2259
|
getRepository(current.repositoryId, ctx.controllerHome, { includeRemoved: true }),
|
|
1680
2260
|
current,
|
|
@@ -1728,14 +2308,14 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1728
2308
|
validation: 'failed',
|
|
1729
2309
|
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
1730
2310
|
merge: fresh.finalization.merge === 'pending' ? 'skipped' : fresh.finalization.merge,
|
|
1731
|
-
branchCleanup:
|
|
2311
|
+
branchCleanup: deleteBranchRequested ? fresh.finalization.branchCleanup : 'skipped',
|
|
1732
2312
|
worktreeCleanup: 'done',
|
|
1733
2313
|
lastError: preservedFailure,
|
|
1734
2314
|
},
|
|
1735
2315
|
}));
|
|
1736
2316
|
appendWorkEvidence({ controllerHome: ctx.controllerHome, repoId: current.repositoryId }, current.workContractId ?? current.workId, {
|
|
1737
2317
|
title: 'failed work cleanup completed',
|
|
1738
|
-
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}.`,
|
|
1739
2319
|
detailLevel: 'summary',
|
|
1740
2320
|
});
|
|
1741
2321
|
releasePreparedWorkOwnership(ctx, current);
|
|
@@ -1758,7 +2338,9 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1758
2338
|
&& wants.cleanup
|
|
1759
2339
|
&& !wants.commit
|
|
1760
2340
|
&& !wants.merge;
|
|
1761
|
-
const cleanupReconciliation =
|
|
2341
|
+
const cleanupReconciliation = requestedOutcome === 'completed_no_change'
|
|
2342
|
+
? undefined
|
|
2343
|
+
: cleanupOnlyMergedHead(ctx, current, args);
|
|
1762
2344
|
if (cancelledCleanupRequested && !cleanupReconciliation) {
|
|
1763
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');
|
|
1764
2346
|
}
|
|
@@ -1773,13 +2355,24 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1773
2355
|
validation: cleanupReconciliation.worktreeMissing ? 'done' : fresh.finalization.validation,
|
|
1774
2356
|
commit: fresh.finalization.commit === 'pending' ? 'skipped' : fresh.finalization.commit,
|
|
1775
2357
|
merge: 'done',
|
|
1776
|
-
branchCleanup:
|
|
2358
|
+
branchCleanup: deleteBranchRequested ? 'pending' : 'skipped',
|
|
1777
2359
|
lastError: undefined,
|
|
1778
2360
|
},
|
|
1779
2361
|
}));
|
|
1780
2362
|
}
|
|
1781
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
|
+
|
|
1782
2374
|
const terminalCleanupOnly = Boolean(cleanupReconciliation && wants.cleanup && !wants.commit && !wants.merge);
|
|
2375
|
+
let exactValidationInput: ReturnType<typeof currentWorkValidationInput> | undefined;
|
|
1783
2376
|
if (terminalCleanupOnly) {
|
|
1784
2377
|
current = transact('terminal-cleanup-validation-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, {
|
|
1785
2378
|
...fresh,
|
|
@@ -1795,12 +2388,15 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1795
2388
|
const validationContract = contractFor(ctx, current);
|
|
1796
2389
|
if (!validationContract) throw new Error(`WORK_VALIDATION_CONTRACT_MISSING: ${current.workContractId ?? current.workId}`);
|
|
1797
2390
|
const validationInput = currentWorkValidationInput(validatedRepository, current, validationContract.checks);
|
|
2391
|
+
exactValidationInput = validationInput;
|
|
1798
2392
|
if (validationContract.checks.length === 0) {
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
+
}
|
|
1804
2400
|
projectWorkValidationOutcome(ctx.controllerHome, current, 'passed', 'No validation checks were required.');
|
|
1805
2401
|
} else if (!hasCurrentWorkValidationAuthority({
|
|
1806
2402
|
finalizationValidation: current.finalization.validation,
|
|
@@ -1816,16 +2412,52 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1816
2412
|
markWorkValidationPending(ctx.controllerHome, current);
|
|
1817
2413
|
throw new Error('WORK_VALIDATION_REQUIRED: run work_validate against the exact current workspace before finalization');
|
|
1818
2414
|
}
|
|
1819
|
-
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) {
|
|
1820
2429
|
throw new Error('WORK_MERGE_UNCOMMITTED_CHANGES: commit the validated workspace before merging');
|
|
1821
2430
|
}
|
|
1822
2431
|
}
|
|
1823
2432
|
|
|
1824
|
-
if (
|
|
1825
|
-
|
|
1826
|
-
if (!repositoryGitStatus(inspected.worktreeRepository).clean) {
|
|
2433
|
+
if (noChangeFastPath) {
|
|
2434
|
+
if (!exactValidationInput?.clean) {
|
|
1827
2435
|
throw new Error('WORK_NO_CHANGE_DIRTY: completed_no_change cannot retain an owned dirty worktree');
|
|
1828
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
|
+
}
|
|
1829
2461
|
}
|
|
1830
2462
|
|
|
1831
2463
|
if (wants.commit && current.finalization.commit === 'pending') {
|
|
@@ -1833,58 +2465,454 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1833
2465
|
const contract = contractFor(ctx, current);
|
|
1834
2466
|
if (contract?.constraints.allowCommit === false) throw new Error('WORK_COMMIT_NOT_ALLOWED: WorkContract disallows commit');
|
|
1835
2467
|
const status = repositoryGitStatus(validated.worktreeRepository);
|
|
1836
|
-
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
|
+
}
|
|
1837
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 });
|
|
1838
2473
|
const pendingAuthorization = [committed.stage, committed.commit].find((execution) => execution?.authorizationDecision?.decision === 'user_confirmation_required')?.authorizationDecision;
|
|
1839
2474
|
if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
|
|
1840
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);
|
|
1841
2487
|
current = transact('commit-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'committed', {
|
|
1842
|
-
expectedHead:
|
|
2488
|
+
expectedHead: committedHead,
|
|
1843
2489
|
finalization: {
|
|
1844
2490
|
...fresh.finalization,
|
|
1845
|
-
validation:
|
|
2491
|
+
validation: checks.length ? (validationPreservedAcrossCommit ? 'done' : 'pending') : 'done',
|
|
1846
2492
|
commit: 'done',
|
|
1847
2493
|
lastError: undefined,
|
|
1848
2494
|
},
|
|
1849
|
-
validatedInputFingerprint:
|
|
2495
|
+
validatedInputFingerprint: checks.length
|
|
2496
|
+
? (validationPreservedAcrossCommit ? postCommitInput.fingerprint : undefined)
|
|
2497
|
+
: postCommitInput.fingerprint,
|
|
1850
2498
|
failureReason: undefined,
|
|
1851
2499
|
}));
|
|
1852
|
-
if (
|
|
2500
|
+
if (checks.length && !validationPreservedAcrossCommit) {
|
|
1853
2501
|
markWorkValidationPending(ctx.controllerHome, current);
|
|
1854
2502
|
return {
|
|
1855
2503
|
work: compactHandle(current),
|
|
1856
2504
|
stages: current.finalization,
|
|
1857
2505
|
completed: false,
|
|
1858
|
-
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',
|
|
1859
2507
|
};
|
|
1860
2508
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
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
|
+
}
|
|
1868
2550
|
} else if (!wants.commit && current.finalization.commit === 'pending') {
|
|
1869
2551
|
current = transact('commit-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, commit: 'skipped' } }));
|
|
1870
2552
|
}
|
|
1871
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
|
+
|
|
1872
2579
|
if (wants.merge && current.finalization.merge === 'pending') {
|
|
1873
|
-
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
|
+
}
|
|
1874
2588
|
const contract = contractFor(ctx, current);
|
|
1875
2589
|
if (contract?.constraints.allowMerge === false) throw new Error('WORK_MERGE_NOT_ALLOWED: WorkContract disallows merge');
|
|
1876
2590
|
const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
|
|
1877
|
-
const deleteAfterWorktreeCleanup = current.managedWorktree &&
|
|
1878
|
-
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
|
+
}
|
|
1879
2843
|
const pendingAuthorization = merged.steps.find((step) => step.execution.authorizationDecision?.decision === 'user_confirmation_required')?.execution.authorizationDecision;
|
|
1880
2844
|
if (pendingAuthorization) return { authorization: pendingAuthorization, work: compactHandle(current), stages: current.finalization };
|
|
1881
2845
|
if (!merged.completed) return { ...failStage('merge', merged.error?.message ?? 'merge failed'), merge: merged };
|
|
1882
2846
|
current = transact('merge-done', (fresh) => transitionWorkHandle(ctx.controllerHome, fresh, 'merged', {
|
|
1883
|
-
finalization: { ...fresh.finalization, merge: 'done', branchCleanup:
|
|
2847
|
+
finalization: { ...fresh.finalization, merge: 'done', branchCleanup: !deleteBranchRequested ? 'skipped' : deleteAfterWorktreeCleanup ? 'pending' : 'done', lastError: undefined },
|
|
1884
2848
|
failureReason: undefined,
|
|
1885
2849
|
}));
|
|
1886
2850
|
} else if (!wants.merge && current.finalization.merge === 'pending') {
|
|
1887
|
-
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
|
+
}
|
|
1888
2916
|
}
|
|
1889
2917
|
|
|
1890
2918
|
if (wants.cleanup && current.finalization.worktreeCleanup === 'pending') {
|
|
@@ -1906,7 +2934,11 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1906
2934
|
current = transact('worktree-cleanup-skipped', (fresh) => writeWorkHandle(ctx.controllerHome, { ...fresh, finalization: { ...fresh.finalization, worktreeCleanup: 'skipped' } }));
|
|
1907
2935
|
}
|
|
1908
2936
|
|
|
1909
|
-
if (
|
|
2937
|
+
if (
|
|
2938
|
+
wants.cleanup
|
|
2939
|
+
&& current.finalization.branchCleanup === 'pending'
|
|
2940
|
+
&& (current.finalization.merge === 'done' || requestedOutcome === 'completed_no_change')
|
|
2941
|
+
) {
|
|
1910
2942
|
const target = selectWorkFinalizationTarget(getRepository(current.repositoryId, ctx.controllerHome), current);
|
|
1911
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 });
|
|
1912
2944
|
if (deleted.execution.authorizationDecision?.decision === 'user_confirmation_required') return { authorization: deleted.execution.authorizationDecision, work: compactHandle(current), stages: current.finalization };
|
|
@@ -1944,18 +2976,64 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1944
2976
|
});
|
|
1945
2977
|
}
|
|
1946
2978
|
const receipt = completionReceiptForFinalizedWork(ctx, current, completionContract, args);
|
|
1947
|
-
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(
|
|
1948
2986
|
{ controllerHome: ctx.controllerHome, repoId: current.repositoryId },
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
+
),
|
|
1953
3005
|
);
|
|
3006
|
+
if (!fencedCompletion.allowed) {
|
|
3007
|
+
throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fencedCompletion.reason}`);
|
|
3008
|
+
}
|
|
3009
|
+
const recorded = fencedCompletion.value;
|
|
1954
3010
|
if (recorded.requirementId) {
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
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
|
+
}
|
|
1959
3037
|
}
|
|
1960
3038
|
}
|
|
1961
3039
|
// Successful WorkContract completion always ends controller ownership.
|
|
@@ -1967,7 +3045,7 @@ async function finalizeWork(ctx: MultiRepositoryMcpToolContext, args: Record<str
|
|
|
1967
3045
|
activeCheckoutId: current.sourceCheckoutId ?? session.activeCheckoutId,
|
|
1968
3046
|
});
|
|
1969
3047
|
}
|
|
1970
|
-
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' };
|
|
1971
3049
|
}
|
|
1972
3050
|
|
|
1973
3051
|
export async function callExecutionTool(ctx: MultiRepositoryMcpToolContext, name: string, args: Record<string, unknown>): Promise<CallToolResult | undefined> {
|