@moretea-labs/forge 1.7.2 → 1.8.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 +58 -62
- package/CHANGELOG.md +26 -0
- package/CLAUDE.md +42 -62
- package/README.md +2 -2
- package/SKILL.md +0 -1
- package/THIRD_PARTY_NOTICES.md +5 -2
- package/adapters/browser/index.ts +2 -0
- package/adapters/chatgpt/browser-delivery-host.ts +71 -0
- package/adapters/chatgpt/browser-delivery-runtime.ts +783 -0
- package/adapters/chatgpt/controller-binding-store.ts +55 -0
- package/adapters/chatgpt/controller-host.ts +119 -0
- package/adapters/chatgpt/controller-round-host.ts +78 -0
- package/adapters/chatgpt/controller-round-settlement-store.ts +45 -0
- package/adapters/chatgpt/provider-delivery.ts +113 -0
- package/adapters/chatgpt/work-conversation-binding-store.ts +192 -0
- package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
- package/adapters/computer/desktop-operator-contract.ts +7 -0
- package/adapters/computer/desktop-operator-discovery.ts +201 -0
- package/adapters/computer/desktop-operator-negotiation.ts +174 -0
- package/adapters/computer/desktop-operator-provider.ts +312 -0
- package/adapters/computer/index.ts +1 -0
- package/adapters/computer/interaction-target-authority.ts +944 -0
- package/adapters/controller-process/binding-store.ts +34 -0
- package/adapters/controller-process/controller-host.ts +26 -0
- package/adapters/mcp/access-mode.ts +132 -0
- package/adapters/mcp/audit.ts +32 -0
- package/adapters/mcp/auth.ts +407 -0
- package/adapters/mcp/controller-round-compatibility.ts +184 -0
- package/adapters/mcp/frozen-client-semantic-compatibility.ts +292 -0
- package/adapters/mcp/instructions.ts +36 -0
- package/adapters/mcp/legacy-context.ts +18 -0
- package/adapters/mcp/multi-repository.ts +437 -0
- package/adapters/mcp/oauth.ts +391 -0
- package/adapters/mcp/paths.ts +140 -0
- package/adapters/mcp/policy.ts +232 -0
- package/adapters/mcp/proxy-env.ts +62 -0
- package/adapters/mcp/readiness-projection.ts +24 -0
- package/adapters/mcp/redaction.ts +21 -0
- package/adapters/mcp/repo.ts +24 -0
- package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
- package/adapters/mcp/runtime-gateway/context-adapter.ts +829 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +191 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +147 -0
- package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +207 -0
- package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +411 -0
- package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
- package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
- package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
- package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +308 -0
- package/adapters/mcp/runtime-gateway/process-tools.ts +289 -0
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +76 -0
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +268 -0
- package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
- package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +110 -0
- package/adapters/mcp/runtime-gateway/result-adapter.ts +108 -0
- package/adapters/mcp/runtime-gateway/router.ts +728 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1251 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +634 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +68 -0
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +186 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +25 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +870 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +3186 -0
- package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +502 -0
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +1 -0
- package/adapters/mcp/server.ts +1048 -0
- package/adapters/mcp/setup.ts +791 -0
- package/adapters/mcp/tool-capability-cache.ts +87 -0
- package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5265 -0
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +150 -0
- package/adapters/mcp/tool-mapping/repository-tools.ts +1691 -0
- package/adapters/mcp/tool-mapping/tools.ts +9 -0
- package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
- package/adapters/mcp/toolset-names.ts +74 -0
- package/adapters/mcp/toolset-selection.ts +41 -0
- package/adapters/mcp/toolset.ts +336 -0
- package/adapters/mcp/transports/http.ts +1456 -0
- package/adapters/mcp/transports/session-registry.ts +513 -0
- package/adapters/mcp/transports/stdio.ts +8 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +139 -0
- package/adapters/mcp/types.ts +41 -0
- package/assets/AGENTS.md +2 -16
- package/assets/CLAUDE.md +2 -16
- package/assets/hooks/AGENTS.md +3 -14
- package/assets/hooks/CLAUDE.md +3 -14
- package/assets/hooks/hook-input.sh +9 -2
- package/assets/hooks/lib/workflow-state.sh +28 -10
- package/assets/hooks/prompt-guard.sh +63 -37
- package/assets/hooks/security-sentinel.sh +2 -1
- package/assets/hooks/session-start-context.sh +7 -5
- package/assets/hooks/stop-orchestrator.sh +13 -3
- package/assets/initializer-question-pack.v4.json +11 -7
- package/assets/partials/05-workflow.partial.md +18 -73
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -2
- package/assets/partials-agents/04-task-protocol.partial.md +14 -59
- package/assets/partials-agents/06-quality-safety.partial.md +11 -16
- package/assets/partials-agents/08-deep-docs.partial.md +1 -2
- package/assets/plan-map.json +11 -11
- package/assets/plugin-registry.v1.json +9 -3
- package/assets/plugin-trust-policy.v1.json +6 -0
- package/assets/recovery/ForgeRecovery.ps1 +100 -0
- package/assets/recovery/forge-wsl-rescue +271 -0
- package/assets/reference-configs/development-protocol.md +11 -15
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/global-working-rules.md +9 -1
- package/assets/reference-configs/harness-overview.md +22 -112
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/architecture-event.ts +3 -34
- package/assets/templates/helpers/architecture-queue.sh +61 -10
- package/assets/templates/helpers/archive-workflow.sh +90 -180
- package/assets/templates/helpers/capability-config.ts +8 -11
- package/assets/templates/helpers/capability-resolver.ts +37 -19
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +1 -9
- package/assets/templates/helpers/check-task-sync.sh +82 -11
- package/assets/templates/helpers/check-task-workflow.sh +1 -3
- package/assets/templates/helpers/context-contract-sync.sh +1 -12
- package/assets/templates/helpers/contract-worktree.sh +26 -24
- package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
- package/assets/templates/helpers/inspect-project-state.ts +1 -1
- package/assets/templates/helpers/migrate-project-template.sh +1 -1
- package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
- package/assets/templates/helpers/new-plan.sh +2 -2
- package/assets/templates/helpers/plan-to-todo.sh +3 -31
- package/assets/templates/helpers/sprint-backlog.sh +1 -1
- package/assets/templates/helpers/switch-plan.sh +1 -1
- package/assets/templates/helpers/workflow-contract.ts +1 -0
- package/assets/workflow-contract.v1.json +63 -171
- package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
- package/docs/architecture/CURRENT.md +148 -13
- package/docs/architecture/EVOLUTION.md +13 -0
- package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
- package/docs/architecture/decisions/20260831-independent-windows-wsl-host-rescue.md +59 -0
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
- package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +35 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +296 -0
- package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
- package/docs/architecture/domains/verification.md +7 -0
- package/docs/architecture/domains/workflow-engine.md +1 -1
- package/docs/architecture/index.md +3 -3
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
- package/docs/forge-chatgpt-mcp-setup.md +5 -3
- package/docs/forge-plugin-management.md +20 -4
- package/docs/forge-runtime-self-healing-loop.md +2 -0
- package/docs/operations/benchmark-cross-version-freeze.md +38 -0
- package/docs/operations/controller-browser-plugin.md +3 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/platform-support.md +16 -8
- package/docs/operations/platform-support.zh-CN.md +2 -2
- package/docs/operations/standalone-disaster-recovery.md +26 -1
- package/docs/operations/uu-remote-rescue.md +41 -0
- package/docs/operations/windows-wsl-host-rescue.md +133 -0
- package/docs/public-usage-guide.md +1 -1
- package/docs/public-usage-guide.zh-CN.md +1 -1
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/02-connect-chatgpt.md +4 -4
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +23 -14
- package/packages/kernel/controller/api/index.ts +2 -0
- package/packages/kernel/controller/application/controller-round-service.ts +29 -0
- package/packages/kernel/controller/application/controller-service.ts +30 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +308 -0
- package/packages/kernel/controller/domain/controller-round.ts +107 -0
- package/packages/kernel/controller/domain/execution-quality.ts +133 -0
- package/packages/kernel/controller/domain/types.ts +56 -0
- package/packages/kernel/controller/index.ts +1 -0
- package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +1384 -0
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +578 -0
- package/packages/kernel/controller/ports/controller-host.ts +16 -0
- package/packages/kernel/goal/api/index.ts +1 -0
- package/packages/kernel/goal/domain/types.ts +40 -0
- package/packages/kernel/goal/index.ts +1 -0
- package/packages/kernel/identity/api/index.ts +5 -0
- package/packages/kernel/identity/application/identity-service.ts +67 -0
- package/packages/kernel/identity/domain/scope.ts +63 -0
- package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
- package/packages/kernel/identity/domain/types.ts +44 -0
- package/packages/kernel/identity/domain/workspace.ts +56 -0
- package/packages/kernel/identity/index.ts +1 -0
- package/packages/kernel/identity/infrastructure/identity-store.ts +75 -0
- package/packages/kernel/memory/api/index.ts +5 -0
- package/packages/kernel/memory/application/experience-service.ts +118 -0
- package/packages/kernel/memory/application/outcome-service.ts +77 -0
- package/packages/kernel/memory/domain/experience.ts +103 -0
- package/packages/kernel/memory/domain/operational-prior.ts +92 -0
- package/packages/kernel/memory/index.ts +1 -0
- package/packages/kernel/progression/api/index.ts +2 -0
- package/packages/kernel/progression/application/projector.ts +211 -0
- package/packages/kernel/progression/domain/types.ts +116 -0
- package/packages/kernel/progression/index.ts +1 -0
- package/packages/kernel/scheduler/api/index.ts +6 -0
- package/packages/kernel/scheduler/application/continuation-service.ts +212 -0
- package/packages/kernel/scheduler/application/eligibility.ts +252 -0
- package/packages/kernel/scheduler/application/schedule-service.ts +24 -0
- package/packages/kernel/scheduler/application/settlement.ts +197 -0
- package/packages/kernel/scheduler/domain/continuation.ts +38 -0
- package/packages/kernel/scheduler/domain/schedule.ts +149 -0
- package/packages/kernel/scheduler/index.ts +1 -0
- package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +38 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +785 -0
- package/packages/kernel/work/api/index.ts +15 -0
- package/packages/kernel/work/application/work-service.ts +148 -0
- package/packages/kernel/work/domain/admission-policy.ts +34 -0
- package/packages/kernel/work/domain/check-receipt.ts +43 -0
- package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
- package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
- package/packages/kernel/work/domain/engineering-design.ts +281 -0
- package/packages/kernel/work/domain/engineering-profile.ts +242 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +254 -0
- package/packages/kernel/work/domain/execution-snapshot.ts +33 -0
- package/packages/kernel/work/domain/implementation-review.ts +510 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +202 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
- package/packages/kernel/work/domain/state-machine.ts +259 -0
- package/packages/kernel/work/domain/types.ts +528 -0
- package/packages/kernel/work/index.ts +1 -0
- package/packages/kernel/work/infrastructure/work-contract-store.ts +1876 -0
- package/packages/kernel/work/ports/work-contract-store.ts +18 -0
- package/packages/plugin-runtime/browser/index.ts +3 -0
- package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
- package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
- package/packages/plugin-runtime/browser/session-authority.ts +61 -0
- package/packages/plugin-runtime/computer/index.ts +6 -0
- package/packages/plugin-runtime/computer/provider-error.ts +26 -0
- package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
- package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
- package/packages/plugin-runtime/computer/provider.ts +19 -0
- package/packages/plugin-runtime/computer/target-authority.ts +190 -0
- package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
- package/packages/plugin-runtime/external/index.ts +1 -0
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +457 -0
- package/packages/plugin-runtime/index.ts +3 -0
- package/packages/protocols/browser/index.ts +1 -0
- package/packages/protocols/browser/session.ts +98 -0
- package/packages/protocols/computer/contract.ts +199 -0
- package/packages/protocols/computer/index.ts +1 -0
- package/packages/protocols/failure/contract.ts +158 -0
- package/packages/protocols/failure/index.ts +1 -0
- package/packages/protocols/handoff/index.ts +1 -0
- package/packages/protocols/handoff/status.ts +22 -0
- package/packages/protocols/mcp/execution-context.ts +11 -0
- package/packages/protocols/mcp/index.ts +2 -0
- package/packages/protocols/mcp/tool-contract.ts +42 -0
- package/packages/workflow-runtime/api/index.ts +2 -0
- package/packages/workflow-runtime/application/interpreter.ts +342 -0
- package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
- package/packages/workflow-runtime/index.ts +1 -0
- package/scripts/assemble-template.ts +1 -1
- package/scripts/benchmark-computer-hotpath.ts +255 -0
- package/scripts/benchmark-controller-round2.ts +54 -2
- package/scripts/benchmark-ios-agent-device.ts +3 -6
- package/scripts/benchmark-mcp-transport.ts +3 -4
- package/scripts/benchmark-operational-memory-activation.ts +235 -0
- package/scripts/benchmark-operational-shadow.ts +28 -0
- package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
- package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
- package/scripts/canary-controller-semantic-wait.ts +195 -0
- package/scripts/canary-global-hook.sh +2 -2
- package/scripts/check-mcp-compatibility.ts +173 -7
- package/scripts/check-platform-support.mjs +28 -2
- package/scripts/check-public-docs.sh +1 -1
- package/scripts/check-release-readiness.sh +1 -1
- package/scripts/check-repository-hygiene.mjs +92 -0
- package/scripts/check-runtime-architecture.mjs +1206 -95
- package/scripts/check-source-duplication.mjs +9 -1
- package/scripts/check-tarball-install-smoke.sh +98 -15
- package/scripts/controller-ngrok-rotation.sh +5 -4
- package/scripts/create-project-dirs.sh +10 -74
- package/scripts/forge-local-recovery-helper.mjs +204 -0
- package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
- package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
- package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
- package/scripts/hook-shim.sh +2 -2
- package/scripts/init-project.sh +12 -70
- package/scripts/install-desktop-operator-registration.ts +4 -2
- package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
- package/scripts/install-standalone-recovery.ts +122 -10
- package/scripts/install-uu-remote-rescue-registration.ts +74 -0
- package/scripts/install-windows-host-recovery-registration.ts +22 -0
- package/scripts/lib/controller-home-env.sh +6 -3
- package/scripts/lib/project-init-lib.sh +39 -582
- package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
- package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
- package/scripts/open-source-audit-allowlist.txt +12 -1
- package/scripts/portability-bindings.mjs +70 -0
- package/scripts/run-bounded-command.mjs +55 -0
- package/scripts/run-bun-test-file.ts +22 -6
- package/scripts/run-governed-gate.ts +4 -1
- package/scripts/smoke-mcp-http-runtime.ts +1 -1
- package/scripts/smoke-runtime-recovery.ts +34 -1
- package/scripts/smoke-schedule-engine.ts +1 -1
- package/scripts/stage-runtime-release.ts +6 -1
- package/scripts/test-governance.ts +51 -2
- package/src/cli/agent-jobs/job-manager.ts +10 -1
- package/src/cli/chatgpt-browser/bind-server.ts +2 -1
- package/src/cli/chatgpt-browser/binding.ts +20 -10
- package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
- package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
- package/src/cli/chatgpt-browser/engine.ts +10 -11
- package/src/cli/chatgpt-browser/native-provider.ts +24 -83
- package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
- package/src/cli/chatgpt-browser/types.ts +1 -0
- package/src/cli/commands/bootstrap-control.ts +334 -0
- package/src/cli/commands/brain-assistant.ts +38 -0
- package/src/cli/commands/brain.ts +193 -40
- package/src/cli/commands/chatgpt.ts +127 -0
- package/src/cli/commands/computer.ts +437 -0
- package/src/cli/commands/doctor.ts +18 -1
- package/src/cli/commands/init-hook.ts +71 -61
- package/src/cli/commands/init.ts +3 -1
- package/src/cli/commands/mcp.ts +16 -10
- package/src/cli/commands/plugin.ts +38 -12
- package/src/cli/commands/recovery.ts +493 -80
- package/src/cli/commands/runtime.ts +9 -4
- package/src/cli/commands/setup-profile.ts +89 -39
- package/src/cli/commands/status.ts +2 -1
- package/src/cli/controller/check-runner.ts +487 -98
- package/src/cli/controller/context/types.ts +7 -1
- package/src/cli/controller/context-pack.ts +23 -3
- package/src/cli/controller/issue-store.ts +1 -1
- package/src/cli/controller/systemd-user.ts +110 -0
- package/src/cli/controller/types.ts +10 -70
- package/src/cli/controller/worklog.ts +67 -13
- package/src/cli/editing/edit-operation-contract.ts +92 -0
- package/src/cli/editing/edit-session.ts +289 -13
- package/src/cli/github/plugin.ts +17 -12
- package/src/cli/hook/route-registry.ts +3 -5
- package/src/cli/hook/runtime.ts +49 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/local-bridge/connector-freshness.ts +2 -2
- package/src/cli/local-bridge/console-automations.ts +2 -2
- package/src/cli/local-bridge/facade-api.ts +41 -14
- package/src/cli/local-bridge/job-store.ts +1 -1
- package/src/cli/local-bridge/server.ts +24 -27
- package/src/cli/mcp/access-mode.ts +2 -132
- package/src/cli/mcp/access-tools.ts +2 -387
- package/src/cli/mcp/audit.ts +2 -32
- package/src/cli/mcp/auth.ts +2 -404
- package/src/cli/mcp/instructions.ts +2 -34
- package/src/cli/mcp/legacy-context.ts +2 -16
- package/src/cli/mcp/legacy-tool-service.ts +2 -5308
- package/src/cli/mcp/multi-repository.ts +2 -436
- package/src/cli/mcp/oauth.ts +2 -375
- package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
- package/src/cli/mcp/paths.ts +2 -140
- package/src/cli/mcp/policy.ts +2 -232
- package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
- package/src/cli/mcp/proxy-env.ts +2 -62
- package/src/cli/mcp/readiness-projection.ts +2 -24
- package/src/cli/mcp/redaction.ts +2 -21
- package/src/cli/mcp/repo.ts +2 -24
- package/src/cli/mcp/repository-tools.ts +2 -1613
- package/src/cli/mcp/server.ts +2 -951
- package/src/cli/mcp/setup.ts +2 -737
- package/src/cli/mcp/tool-capability-cache.ts +2 -87
- package/src/cli/mcp/tool-contract.ts +2 -14
- package/src/cli/mcp/tools.ts +2 -9
- package/src/cli/mcp/toolset-names.ts +2 -70
- package/src/cli/mcp/toolset-selection.ts +2 -41
- package/src/cli/mcp/toolset.ts +2 -336
- package/src/cli/mcp/transports/AGENTS.md +1 -10
- package/src/cli/mcp/transports/CLAUDE.md +1 -10
- package/src/cli/mcp/transports/http.ts +2 -1414
- package/src/cli/mcp/transports/session-registry.ts +2 -490
- package/src/cli/mcp/transports/stdio.ts +2 -8
- package/src/cli/mcp/types.ts +2 -41
- package/src/cli/mcp/umbrella-tools.ts +2 -117
- package/src/cli/repositories/command-executor.ts +64 -18
- package/src/cli/repositories/command-scope.ts +117 -1
- package/src/cli/repositories/controller-home.ts +119 -1
- package/src/cli/repositories/identity.ts +13 -0
- package/src/cli/repositories/registry.ts +68 -49
- package/src/cli/repositories/repository-snapshot.ts +53 -7
- package/src/cli/repositories/runtime-storage.ts +27 -2
- package/src/cli/repositories/safe-patch.ts +34 -38
- package/src/cli/repositories/selected-path-actions.ts +22 -2
- package/src/cli/repositories/structured-git.ts +105 -25
- package/src/cli/runtime/helper-runner.ts +7 -0
- package/src/cli/runtime-invocation.ts +8 -1
- package/src/cli/tools/codegraph.ts +44 -25
- package/src/core/adoption/manifest-templates.ts +1 -1
- package/src/core/adoption/plan.ts +3 -26
- package/src/core/adoption/workflow-contract-plan.ts +17 -9
- package/src/effects/process-runner.ts +8 -14
- package/src/runtime/context/assistant-context.ts +154 -0
- package/src/runtime/context/assistant-work-context.ts +147 -0
- package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
- package/src/runtime/context/codegraph-read-provider.ts +55 -22
- package/src/runtime/context/codegraph-sidecar.cjs +8 -1
- package/src/runtime/context/context-closure.ts +268 -0
- package/src/runtime/context/context-plane.ts +211 -0
- package/src/runtime/context/project-engineering-contract.ts +60 -0
- package/src/runtime/control-plane/bootstrap/capability-provider.ts +74 -0
- package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
- package/src/runtime/control-plane/bootstrap/index.ts +5 -0
- package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
- package/src/runtime/control-plane/bootstrap/store.ts +21 -0
- package/src/runtime/control-plane/bootstrap/types.ts +87 -0
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +328 -0
- package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +320 -0
- package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +169 -0
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +350 -0
- package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +85 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +369 -99
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +103 -15
- package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +2 -2
- package/src/runtime/control-plane/execution/retained-work-resume.ts +336 -6
- package/src/runtime/control-plane/execution/validation.ts +28 -3
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +116 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +44 -2
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +34 -3
- package/src/runtime/control-plane/execution/work-execution-support.ts +42 -22
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1103 -166
- package/src/runtime/control-plane/execution/work-handle-authority.ts +219 -22
- package/src/runtime/control-plane/execution/work-handle-store.ts +77 -0
- package/src/runtime/control-plane/execution/work-head-settlement.ts +75 -0
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +145 -23
- package/src/runtime/control-plane/execution/work-operation-service.ts +49 -13
- package/src/runtime/control-plane/execution/work-preparation-service.ts +31 -15
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +1 -1
- package/src/runtime/control-plane/execution/work-task-receipt.ts +20 -1
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +407 -76
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +88 -18
- package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
- package/src/runtime/control-plane/execution/work-verification-service.ts +258 -4
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +200 -15
- package/src/runtime/control-plane/facade/capability-registry.ts +21 -10
- package/src/runtime/control-plane/facade/controller-round-relay.ts +3 -1118
- package/src/runtime/control-plane/facade/controller-session-store.ts +2 -488
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -3
- package/src/runtime/control-plane/facade/goal-workloop.ts +972 -137
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +158 -0
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +10 -0
- package/src/runtime/control-plane/facade/index.ts +4 -2
- package/src/runtime/control-plane/facade/operation-digest.ts +22 -58
- package/src/runtime/control-plane/facade/plan-contract-store.ts +1004 -38
- package/src/runtime/control-plane/facade/repository-work-admission.ts +19 -1
- package/src/runtime/control-plane/facade/requirement-authority.ts +218 -0
- package/src/runtime/control-plane/facade/requirement-board.ts +6 -1
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +48 -0
- package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
- package/src/runtime/control-plane/facade/suggested-actions.ts +4 -1
- package/src/runtime/control-plane/facade/types.ts +189 -532
- package/src/runtime/control-plane/facade/work-admission-policy.ts +84 -35
- package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
- package/src/runtime/control-plane/facade/work-implementation-review.ts +2 -0
- package/src/runtime/control-plane/facade/work-state-machine.ts +2 -250
- package/src/runtime/control-plane/generated-cache-retention.ts +239 -18
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +12 -1
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +76 -13
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -1
- package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +173 -81
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +397 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +301 -560
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +20 -4
- package/src/runtime/control-plane/launcher/thin-launcher.ts +50 -4
- package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +169 -0
- package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +30 -22
- package/src/runtime/control-plane/persistence/sqlite-store.ts +545 -76
- package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
- package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
- package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
- package/src/runtime/control-plane/release-retention.ts +56 -16
- package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
- package/src/runtime/control-plane/routing/route-policy.ts +19 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +574 -37
- package/src/runtime/control-plane/runtime-generation.ts +6 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +465 -0
- package/src/runtime/control-plane/workspace/workspace-store.ts +80 -0
- package/src/runtime/diagnostics/incident-repair.ts +406 -0
- package/src/runtime/diagnostics/mcp-timing.ts +125 -6
- package/src/runtime/diagnostics/performance.ts +6 -14
- package/src/runtime/diagnostics/read-only-tool.ts +1 -1
- package/src/runtime/evidence/artifact-store.ts +65 -2
- package/src/runtime/evidence/operational-shadow.ts +38 -0
- package/src/runtime/evidence/process-check-receipt.ts +5 -43
- package/src/runtime/evidence/sensitive-output.ts +4 -4
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
- package/src/runtime/execution/jobs/store.ts +121 -2
- package/src/runtime/execution/managed-workspace.ts +55 -5
- package/src/runtime/execution/process-runtime/canary.ts +19 -5
- package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +23 -9
- package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
- package/src/runtime/execution/process-runtime/check-result.ts +73 -2
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -3
- package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +38 -87
- package/src/runtime/execution/process-runtime/index.ts +1 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +32 -1
- package/src/runtime/execution/process-runtime/persisted-check.ts +128 -21
- package/src/runtime/execution/process-runtime/resource-claims.ts +60 -3
- package/src/runtime/execution/process-runtime/runtime.ts +132 -15
- package/src/runtime/execution/process-runtime/types.ts +22 -0
- package/src/runtime/execution/workers/executor.ts +16 -13
- package/src/runtime/execution/workers/worker-entry.ts +1 -1
- package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
- package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
- package/src/runtime/gateway/mcp/process-tools.ts +2 -289
- package/src/runtime/gateway/mcp/router.ts +2 -1125
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
- package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6655
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
- package/src/runtime/maintenance/cleanup.ts +38 -24
- package/src/runtime/platform/browser-product-discovery.ts +174 -0
- package/src/runtime/platform/computer-platform.ts +14 -0
- package/src/runtime/platform/executable-discovery.ts +54 -0
- package/src/runtime/platform/fetch-http-server.ts +90 -0
- package/src/runtime/platform/service-manager.ts +138 -0
- package/src/runtime/platform/windows-host-discovery.ts +118 -0
- package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
- package/src/runtime/plugins/action-application.ts +135 -0
- package/src/runtime/plugins/app-store-connect-adapter.ts +9 -21
- package/src/runtime/plugins/browser-adapter.ts +140 -100
- package/src/runtime/plugins/browser-automation-service.ts +15 -36
- package/src/runtime/plugins/browser-macos-bridge.ts +69 -21
- package/src/runtime/plugins/browser-manifest-surface.ts +30 -6
- package/src/runtime/plugins/browser-native-session-service.ts +1 -1
- package/src/runtime/plugins/browser-provider-registry.ts +2 -109
- package/src/runtime/plugins/browser-registration.ts +16 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
- package/src/runtime/plugins/browser-runtime.ts +41 -9
- package/src/runtime/plugins/browser-session-authority.ts +101 -291
- package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
- package/src/runtime/plugins/browser-session-store.ts +229 -36
- package/src/runtime/plugins/browser-session-types.ts +2 -98
- package/src/runtime/plugins/capability-authorization-grants.ts +17 -0
- package/src/runtime/plugins/catalog-trust.ts +82 -0
- package/src/runtime/plugins/computer-registration.ts +839 -0
- package/src/runtime/plugins/config-store.ts +119 -0
- package/src/runtime/plugins/desktop-operator-external-policy.ts +932 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +111 -8
- package/src/runtime/plugins/errors.ts +8 -1
- package/src/runtime/plugins/external-adapter.ts +45 -322
- package/src/runtime/plugins/external-provider-policy.ts +48 -0
- package/src/runtime/plugins/external-registration.ts +9 -1
- package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
- package/src/runtime/plugins/external-unix-socket.ts +64 -193
- package/src/runtime/plugins/first-party-external-registration.ts +163 -0
- package/src/runtime/plugins/first-party-registry.ts +3 -1
- package/src/runtime/plugins/github-adapter.ts +1 -2
- package/src/runtime/plugins/gmail-adapter.ts +6 -6
- package/src/runtime/plugins/google-calendar-adapter.ts +6 -6
- package/src/runtime/plugins/google-shared.ts +31 -46
- package/src/runtime/plugins/google-tasks-adapter.ts +6 -6
- package/src/runtime/plugins/ios-adapter.ts +1 -2
- package/src/runtime/plugins/local-system-adapter.ts +180 -0
- package/src/runtime/plugins/macos-capability-broker.ts +15 -105
- package/src/runtime/plugins/resend-adapter.ts +15 -24
- package/src/runtime/plugins/store.ts +493 -51
- package/src/runtime/plugins/types.ts +8 -1
- package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
- package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
- package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +86 -430
- package/src/runtime/projections/controller-context.ts +12 -6
- package/src/runtime/projections/git-status-sampler.ts +73 -22
- package/src/runtime/projections/materialized-view.ts +14 -8
- package/src/runtime/recovery/capability-action-application.ts +228 -0
- package/src/runtime/recovery/index.ts +3 -0
- package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
- package/src/runtime/recovery/local-jobs-repair.ts +14 -14
- package/src/runtime/recovery/maintenance-executor.ts +358 -76
- package/src/runtime/recovery/quarantine-retention.ts +256 -0
- package/src/runtime/release/identity.ts +6 -0
- package/src/runtime/resources/leases/store.ts +1 -1
- package/src/runtime/root/active-execution-power-assertion.ts +139 -0
- package/src/runtime/root/assistant-learning-loop.ts +45 -0
- package/src/runtime/root/browser-session-composition.ts +59 -0
- package/src/runtime/root/computer-composition.ts +191 -0
- package/src/runtime/root/computer-target-composition.ts +16 -0
- package/src/runtime/root/computer-target-persistence.ts +39 -0
- package/src/runtime/root/controller-round-composition.ts +107 -0
- package/src/runtime/root/controller-services.ts +2 -0
- package/src/runtime/root/entry.ts +4 -3
- package/src/runtime/root/gateway-adapter.ts +3 -3
- package/src/runtime/root/local-bridge.ts +5 -2
- package/src/runtime/root/mcp-transport.ts +5 -5
- package/src/runtime/root/package-connector-service.ts +108 -56
- package/src/runtime/root/package-runtime-release.ts +85 -18
- package/src/runtime/root/package-runtime-service.ts +60 -97
- package/src/runtime/root/release-execution-canary.ts +71 -0
- package/src/runtime/root/release-loader.ts +68 -0
- package/src/runtime/root/release-manifest.ts +106 -2
- package/src/runtime/root/release-materialize.ts +103 -4
- package/src/runtime/root/release-store.ts +31 -2
- package/src/runtime/root/runtime.ts +240 -25
- package/src/runtime/root/scheduled-controller-composition.ts +51 -0
- package/src/runtime/root/scheduler.ts +12 -4
- package/src/runtime/root/service-runner.ts +1 -1
- package/src/runtime/root/service.ts +41 -16
- package/src/runtime/root/status.ts +1 -0
- package/src/runtime/root/types.ts +24 -1
- package/src/runtime/shared/local-bridge-surface.ts +1 -1
- package/src/runtime/shared/process-environment.ts +19 -4
- package/src/runtime/shared/storage-capacity.ts +68 -0
- package/src/runtime/standalone-recovery/controller-home-migration.ts +1116 -0
- package/src/runtime/standalone-recovery/core.ts +984 -230
- package/src/runtime/standalone-recovery/entry.ts +182 -51
- package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
- package/src/runtime/standalone-recovery/installer.ts +212 -104
- package/src/runtime/standalone-recovery/mcp-server.ts +243 -0
- package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
- package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
- package/src/runtime/standalone-recovery/performance.ts +135 -0
- package/src/runtime/standalone-recovery/release.ts +5 -0
- package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +191 -343
- package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
- package/src/runtime/workflow/schedules/settlement.ts +2 -191
- package/src/runtime/workflow/schedules/store.ts +2 -607
- package/src/runtime/workflow/schedules/types.ts +2 -127
- package/src/runtime/workflow/schedules/work-continuation.ts +180 -15
- package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
- package/src/runtime/workflows/runtime.ts +486 -0
- package/src/testing/test-governance.ts +90 -29
- package/assets/templates/helpers/archive-architecture-request.sh +0 -244
- package/docs/architecture/history/README.md +0 -7
- package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
- package/docs/architecture/history/global-hook-runtime.md +0 -166
- package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
- package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
- package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
- package/docs/architecture/requests/archive/2026/root.md +0 -68
- package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
- package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
- package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
- package/scripts/archive-architecture-request.sh +0 -4
- package/scripts/run-skill-evals.ts +0 -1022
|
@@ -0,0 +1,3186 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { collectRuntimePerformanceDiagnostics } from "../../../src/runtime/diagnostics/performance";
|
|
5
|
+
import { mintEngineeringAdmissionEvidence } from "./engineering-preconditions";
|
|
6
|
+
import type { CallToolResult } from "../../../packages/protocols/mcp/tool-contract";
|
|
7
|
+
import type { MultiRepositoryMcpToolContext } from "../multi-repository";
|
|
8
|
+
import { result } from "./result-adapter";
|
|
9
|
+
import { selected } from "./shared-adapter";
|
|
10
|
+
import { controllerReadinessEvidence, invalidFacadeOperation, repositoryRevisionContains } from "./status-inbox-adapter";
|
|
11
|
+
import { freshGitIdentity } from "../../../src/cli/repository/inspector";
|
|
12
|
+
import { repositoryCheckoutLifecycle, selectRepositoryCheckout } from "../../../src/cli/repositories/registry";
|
|
13
|
+
import { repositoryGitStatus } from "../../../src/cli/repositories/structured-git";
|
|
14
|
+
import { DEFAULT_WORK_CHECK_LEASE_WAIT_MS, getProcessRecord, isManagedProcessActive, listProcessRecords, processCheckCompletionReceipt, processRuntimeResourceDiagnostics } from "../../../src/runtime/execution/process-runtime";
|
|
15
|
+
import { classifyPersistedCheckTerminalEvidence } from "../../../src/runtime/execution/process-runtime/check-result";
|
|
16
|
+
import { listWorkBoundRepositoryProcessEvidence, listWorkBoundRepositoryRemoteEffectProcessEvidence } from "../../../src/runtime/control-plane/execution/work-process-evidence";
|
|
17
|
+
import { completeRemoteEffectWorkFromProcessReceipt } from "../../../packages/kernel/work/api/index";
|
|
18
|
+
import { executionIdentityForRepository } from "../../../src/runtime/control-plane/execution/execution-identity";
|
|
19
|
+
import { executeRegisteredWorkflow, observeAndReconcileRegisteredWorkflow } from "../../../src/runtime/workflows/runtime";
|
|
20
|
+
import { readWorkflowRun } from "../../../src/runtime/control-plane/persistence/workflow-run-store";
|
|
21
|
+
import { schedulePublicationOutcomeCollection } from "../../../src/runtime/root/assistant-learning-loop";
|
|
22
|
+
import { recordControllerExperience, recordControllerOutcome, type ControllerExperienceDraft, type ControllerOutcomeObservationDraft } from "../../../src/runtime/context/assistant-work-context";
|
|
23
|
+
import { ensureXiaohongshuWorkflowInstalled, XIAOHONGSHU_WORKFLOW_IDS } from "../../../src/runtime/workflows/first-party/xiaohongshu";
|
|
24
|
+
import { readWorkHandle, resolveWorkDeliveryTargetBranch, workDeliveryBaseRevision, type WorkHandleState } from "../../../src/runtime/control-plane/execution/work-handle-store";
|
|
25
|
+
import { ensureRepositoryWorkHandle, rebindRepositoryWorkHandleControllerIdentity, reconcileRepositoryWorkHandlePlacement } from "../../../src/runtime/control-plane/execution/work-handle-authority";
|
|
26
|
+
import { recoverControllerAuthority } from "../../../src/runtime/control-plane/execution/controller-authority-recovery";
|
|
27
|
+
import { reconcileSingleTerminalWorkCleanup, recoverTerminalWorkHandle } from "../../../src/runtime/control-plane/execution/work-terminal-cleanup";
|
|
28
|
+
import { commandFingerprint, verificationInputFingerprint, workspaceValidationFingerprint } from "../../../src/runtime/control-plane/execution/verification-evidence";
|
|
29
|
+
import { resolveWorkVerificationContext } from "../../../src/runtime/control-plane/execution/work-verification-context";
|
|
30
|
+
import { executeWorkVerification, executeWorkVerificationBatch } from "../../../src/runtime/control-plane/execution/work-verification-service";
|
|
31
|
+
import { implementationReviewContentFingerprint } from "../../../src/runtime/control-plane/execution/implementation-review-content";
|
|
32
|
+
import { implementationReviewCommittedBaseRevision, reconcileDirectCanonicalTargetAdvanceCommand } from "../../../src/runtime/control-plane/execution/work-finalization-service";
|
|
33
|
+
import { acceptReviewedDirectEditWorkReconciliation } from "../../../src/runtime/control-plane/execution/direct-edit-work-completion";
|
|
34
|
+
import { readForgeRuntimeStatus } from "../../../src/runtime/control-plane/runtime-status-client";
|
|
35
|
+
import { ensureControllerDispositionContinuation, repositoryCleanContinuationEventName, triggerWorkContinuationRepositoryEvent } from "../../../src/runtime/workflow/schedules/work-continuation";
|
|
36
|
+
import { callRhWorkScheduleAdapter, isRhWorkScheduleOperation } from "./scheduler-adapter";
|
|
37
|
+
import { assertAutomatedOperationAllowed } from "../../../src/runtime/control-plane/governance/external-effects";
|
|
38
|
+
import { listControllerChecks, readLatestControllerCheckEvidence } from "../../../src/cli/controller/check-runner";
|
|
39
|
+
import { finalizeRemoteEffectWorkFromActionReceipt } from "../../../src/runtime/plugins/store";
|
|
40
|
+
import { gitSnapshot } from "../../../src/cli/repository/inspector";
|
|
41
|
+
import { buildWorkflowWatchdogReport } from "../../../src/runtime/watchdog/workflow-watchdog";
|
|
42
|
+
import { applyRuntimeMaintenance, buildRecoveryAuditRecord, buildRuntimeMaintenanceStatus, writeRecoveryAuditRecord, type RecoveryActionDescriptor } from "../../../src/runtime/recovery";
|
|
43
|
+
import { callStandaloneRecoveryTool } from "./recovery-client-adapter";
|
|
44
|
+
import { allowedFacadeOperations, buildFacadeResult, classifyVerificationOutcome, getHandoffItem, normalizeCheckIds, runGoalWorkloop, runSelfHealingLoop, delegateToCodexCerebellum, buildWorkContinuationSnapshot, acceptPlanStepEvidence, admitPlanContractAsync, approvePlanContractAsync, getPlanContract, listPlanContracts, resolvePlanAdmission, withPrimaryWorkAdmissionLockAsync, repairDanglingPlanStepWorkBinding, repairPlanStepForTechnicalRetry, replanActivePlanBoundWorkScope, repairDraftPlanContractAsync, completePlanStepForWork, summarizePlanContract, summarizeWorkContract, supersedePlanContract, verifyGoalWorkloop } from "../../../src/runtime/control-plane/facade";
|
|
45
|
+
import { getWorkContract, listWorkContracts, type WorkContract } from "../../../packages/kernel/work/api/index";
|
|
46
|
+
import { readExecutionSession, startExecutionSession, updateExecutionSession } from "../../../src/runtime/control-plane/execution/session-store";
|
|
47
|
+
import { changedPaths as workChangedPaths, changedPathsFromUnbornBase as workChangedPathsFromUnbornBase } from "../../../src/runtime/control-plane/execution/work-task-receipt";
|
|
48
|
+
import { readRequirement } from "../../../src/runtime/control-plane/persistence/requirement-store";
|
|
49
|
+
import { admitRequirement, completeRequirementGoal, continueRequirement } from "../../../src/runtime/control-plane/facade/requirement-authority";
|
|
50
|
+
import { ensureManagedWorkspace } from "../../../src/runtime/execution/managed-workspace";
|
|
51
|
+
import { materializeRepositoryWorkPlacement } from "../../../src/runtime/control-plane/facade/repository-work-admission";
|
|
52
|
+
import { ensureRunningRepositoryWorkCheckout, reauthorizeRetainedCancelledRepositoryWork } from "../../../src/runtime/control-plane/execution/retained-work-resume";
|
|
53
|
+
import { currentPermissionSnapshotVersion } from "../../../src/runtime/control-plane/execution/validation";
|
|
54
|
+
import { callExecutionTool } from "./execution-tools";
|
|
55
|
+
import { launchSuperController } from "../../../src/runtime/control-plane/launcher/thin-launcher";
|
|
56
|
+
import { getExternalControllerLaunchReservation } from "../../../src/runtime/control-plane/launcher/launch-reservation-store";
|
|
57
|
+
import { providerMcpReservationIdentity } from "../../../src/runtime/control-plane/launcher/provider-mcp-bootstrap";
|
|
58
|
+
import { runStandaloneChatgptPrompt, runWorkChatgptContinuation, settleWorkChatgptAutomationTab } from "../../../src/runtime/control-plane/launcher/chatgpt-work-continuation";
|
|
59
|
+
import { chatgptControllerRoundBinding, recordChatgptControllerRoundTabSettlement, renderChatgptControllerRoundPrompt, prepareControllerAssistantContext, prepareControllerAssistantContextBundle } from "../../../src/runtime/root/controller-round-composition";
|
|
60
|
+
import { assertControllerOwnershipAuthority, bindControllerSessionToCurrentRuntime, controllerRoundBlockerClass, controllerSessionAuthorityDigest, controllerSessionAuthorityMatches, controllerSessionPrincipalId, getControllerSession, getRetainedControllerSession, mintControllerSessionAuthority, releaseControllerSessionWithAuthority, releaseObservedControllerSession, resumeControllerSession, withControllerSessionTerminalizationFence, type ControllerTerminalizationAuthority, acknowledgeControllerRoundClaim, beginControllerRoundRelayAfterRelease, beginInitialControllerRoundDispatch, bindControllerRoundSuccessorWork, reconcileControllerRoundAfterAbandonedRelease, reconcileControllerRoundAfterTerminalWork, finishControllerRoundRelayDispatch, getControllerRoundRelay, rearmControllerRoundAfterProviderRecovery, resolveRequirementControllerRoundRelayForWork, submitControllerRoundDisposition, type ControllerRoundRelayRecord, type ControllerRoundDisposition } from "../../../packages/kernel/controller/api/index";
|
|
61
|
+
import { parseControllerDispositionCompatibilityCapability, parseControllerRoundCompatibilityCapability, parsePlanObligationCompatibilityCapability } from "../controller-round-compatibility";
|
|
62
|
+
import { parseFrozenSemanticCompatibilityCapability } from "../frozen-client-semantic-compatibility";
|
|
63
|
+
import {
|
|
64
|
+
assertFacadeControllerRoundAuthority,
|
|
65
|
+
assertSessionlessFacadeControllerAuthority,
|
|
66
|
+
authenticatedFacadeControllerIdentity,
|
|
67
|
+
bindFacadeControllerOwnership,
|
|
68
|
+
currentFacadeTerminalizationAuthority,
|
|
69
|
+
currentTerminalCleanupAuthority,
|
|
70
|
+
dispatchedChatgptRelayAuthorizesStaleControllerRecovery,
|
|
71
|
+
runtimeIdentitySnapshot,
|
|
72
|
+
sessionlessFacadeControllerAuthorityMatches,
|
|
73
|
+
} from './controller-authority-adapter';
|
|
74
|
+
// Bounded internal compatibility export while remaining runtime-gateway callers migrate to the dedicated owner.
|
|
75
|
+
export { runtimeIdentitySnapshot } from './controller-authority-adapter';
|
|
76
|
+
|
|
77
|
+
export const RH_WORK_VERIFY_LEASE_WAIT_MS = DEFAULT_WORK_CHECK_LEASE_WAIT_MS;
|
|
78
|
+
|
|
79
|
+
const CONTROLLER_PROVIDER_RECOVERY_CAPABILITY_PREFIX = 'controller.provider.recover:';
|
|
80
|
+
const CONTROLLER_PROVIDER_RECOVERY_AUDIT_ACTION: RecoveryActionDescriptor = {
|
|
81
|
+
id: 'recovery.controller_provider_probe',
|
|
82
|
+
title: 'Verify Controller provider recovery',
|
|
83
|
+
description: 'Verify the current ChatGPT provider with a non-semantic probe before rearming one exact blocked ControllerRound.',
|
|
84
|
+
class: 'unknown',
|
|
85
|
+
risk: 'medium',
|
|
86
|
+
confirmation: 'authorization',
|
|
87
|
+
localOnly: false,
|
|
88
|
+
boundedTo: ['controller_round', 'chatgpt_provider'],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export interface ControllerProviderRecoveryInput {
|
|
92
|
+
controllerHome: string;
|
|
93
|
+
repoId: string;
|
|
94
|
+
repoRoot: string;
|
|
95
|
+
workId: string;
|
|
96
|
+
relayScopeId: string;
|
|
97
|
+
authorityId: string;
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
now?: () => string;
|
|
100
|
+
probe?: typeof runStandaloneChatgptPrompt;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function recoverControllerRoundAfterVerifiedProviderRepair(input: ControllerProviderRecoveryInput) {
|
|
104
|
+
const store = { controllerHome: input.controllerHome, repoId: input.repoId };
|
|
105
|
+
const workId = input.workId.trim();
|
|
106
|
+
const relayScopeId = input.relayScopeId.trim();
|
|
107
|
+
const authorityId = input.authorityId.trim();
|
|
108
|
+
const blocked = getControllerRoundRelay(store, workId);
|
|
109
|
+
if (!blocked) throw new Error(`CONTROLLER_PROVIDER_RECOVERY_RELAY_REQUIRED: ${workId}`);
|
|
110
|
+
if (blocked.relayScopeId !== relayScopeId) throw new Error(`CONTROLLER_PROVIDER_RECOVERY_SCOPE_MISMATCH: ${workId}`);
|
|
111
|
+
if ((blocked.authorityId?.trim() || '') !== authorityId) throw new Error(`CONTROLLER_PROVIDER_RECOVERY_AUTHORITY_MISMATCH: ${workId}`);
|
|
112
|
+
if (controllerRoundBlockerClass(blocked) !== 'consecutive_failures') throw new Error(`CONTROLLER_PROVIDER_RECOVERY_BLOCKER_MISMATCH: ${workId}`);
|
|
113
|
+
if (getControllerSession(store, workId)) throw new Error(`CONTROLLER_PROVIDER_RECOVERY_ACTIVE_CLAIM: ${workId}`);
|
|
114
|
+
|
|
115
|
+
const probe = input.probe ?? runStandaloneChatgptPrompt;
|
|
116
|
+
const nonce = randomUUID();
|
|
117
|
+
const result = await probe({
|
|
118
|
+
controllerHome: input.controllerHome,
|
|
119
|
+
repoId: input.repoId,
|
|
120
|
+
repoRoot: input.repoRoot,
|
|
121
|
+
scopeId: `controller-provider-recovery:${workId}:${authorityId}`,
|
|
122
|
+
prompt: `Forge provider recovery probe ${nonce}. Reply with ACK only. Do not invoke tools or modify external state.`,
|
|
123
|
+
tabPolicy: 'new',
|
|
124
|
+
timeoutMs: input.timeoutMs ?? 60_000,
|
|
125
|
+
});
|
|
126
|
+
if (result.status !== 'dispatched' || result.providerDeliveryStatus !== 'dispatch_confirmed') {
|
|
127
|
+
const code = result.error?.code ?? `CHATGPT_PROVIDER_${result.providerDeliveryStatus?.toUpperCase() ?? 'PROBE_FAILED'}`;
|
|
128
|
+
throw new Error(`CONTROLLER_PROVIDER_RECOVERY_PROBE_FAILED: ${code}: ${result.error?.message ?? result.status}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const verifiedAt = input.now?.() ?? new Date().toISOString();
|
|
132
|
+
if (Date.parse(verifiedAt) <= Date.parse(blocked.updatedAt)) {
|
|
133
|
+
throw new Error(`CONTROLLER_PROVIDER_RECOVERY_EVIDENCE_NOT_FRESH: ${workId}`);
|
|
134
|
+
}
|
|
135
|
+
const audit = writeRecoveryAuditRecord(
|
|
136
|
+
input.controllerHome,
|
|
137
|
+
input.repoId,
|
|
138
|
+
buildRecoveryAuditRecord({
|
|
139
|
+
actor: 'rh_work.controller_provider_recovery',
|
|
140
|
+
action: CONTROLLER_PROVIDER_RECOVERY_AUDIT_ACTION,
|
|
141
|
+
result: 'succeeded',
|
|
142
|
+
reason: `Verified provider recovery for exact ControllerRound ${workId}.`,
|
|
143
|
+
evidence: [{
|
|
144
|
+
source: 'chatgpt_provider_recovery_probe',
|
|
145
|
+
message: 'ChatGPT provider dispatch was confirmed by a non-semantic recovery probe.',
|
|
146
|
+
at: verifiedAt,
|
|
147
|
+
details: {
|
|
148
|
+
workId,
|
|
149
|
+
relayScopeId,
|
|
150
|
+
controllerAuthorityId: authorityId,
|
|
151
|
+
blockedUpdatedAt: blocked.updatedAt,
|
|
152
|
+
provider: result.provider,
|
|
153
|
+
providerDeliveryStatus: result.providerDeliveryStatus,
|
|
154
|
+
browserSessionId: result.browserSessionId,
|
|
155
|
+
executionPreferenceVerified: result.executionPreferenceVerified,
|
|
156
|
+
},
|
|
157
|
+
}],
|
|
158
|
+
at: verifiedAt,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
const relay = rearmControllerRoundAfterProviderRecovery(store, {
|
|
162
|
+
workId,
|
|
163
|
+
relayScopeId,
|
|
164
|
+
authorityId,
|
|
165
|
+
expectedUpdatedAt: blocked.updatedAt,
|
|
166
|
+
evidenceId: audit.id,
|
|
167
|
+
});
|
|
168
|
+
return { relay, audit, probe: result };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function contextRecord(value: unknown): Record<string, unknown> {
|
|
172
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function contextText(value: unknown, maxChars: number): string | undefined {
|
|
176
|
+
if (typeof value !== 'string') return undefined;
|
|
177
|
+
return value.length <= maxChars ? value : `${value.slice(0, Math.max(0, maxChars - 3))}...`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function ensureFacadeWorkHandle(
|
|
181
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
182
|
+
repository: ReturnType<typeof selected>,
|
|
183
|
+
workId: string,
|
|
184
|
+
args: Record<string, unknown>,
|
|
185
|
+
): WorkHandleState | undefined {
|
|
186
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
187
|
+
return ensureRepositoryWorkHandle({
|
|
188
|
+
controllerHome: ctx.controllerHome,
|
|
189
|
+
repository,
|
|
190
|
+
workId,
|
|
191
|
+
identity: { sessionId: identity.sessionId, principalId: identity.principalId },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function materializeFacadeWorkPlacement(
|
|
196
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
197
|
+
repository: ReturnType<typeof selected>,
|
|
198
|
+
workId: string,
|
|
199
|
+
args: Record<string, unknown>,
|
|
200
|
+
) {
|
|
201
|
+
return materializeRepositoryWorkPlacement(
|
|
202
|
+
{ controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
203
|
+
workId,
|
|
204
|
+
(contract) => {
|
|
205
|
+
const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
206
|
+
requestId: workId,
|
|
207
|
+
title: contract.objective,
|
|
208
|
+
baseRef: contract.baseRevision,
|
|
209
|
+
prepareDependencies: args.needs_dependencies === true,
|
|
210
|
+
});
|
|
211
|
+
if (workspace.checkoutId === repository.activeCheckoutId) {
|
|
212
|
+
throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
|
|
213
|
+
}
|
|
214
|
+
return workspace;
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function claimNewFacadeWork(
|
|
220
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
221
|
+
repository: ReturnType<typeof selected>,
|
|
222
|
+
workId: string,
|
|
223
|
+
args: Record<string, unknown>,
|
|
224
|
+
) {
|
|
225
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
226
|
+
const authority = mintControllerSessionAuthority();
|
|
227
|
+
const session = resumeControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
|
|
228
|
+
workId,
|
|
229
|
+
controllerId: identity.controllerId,
|
|
230
|
+
controllerType: identity.controllerType,
|
|
231
|
+
sessionId: identity.sessionId,
|
|
232
|
+
authorityDigest: authority.authorityDigest,
|
|
233
|
+
principalId: identity.principalId,
|
|
234
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
235
|
+
leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
236
|
+
});
|
|
237
|
+
return { session, controllerAuthorityId: authority.authorityId };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function bindFacadeExecutionSession(
|
|
241
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
242
|
+
repository: ReturnType<typeof selected>,
|
|
243
|
+
handle: WorkHandleState,
|
|
244
|
+
args: Record<string, unknown>,
|
|
245
|
+
) {
|
|
246
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
247
|
+
const session = startExecutionSession(ctx.controllerHome, {
|
|
248
|
+
sessionId: identity.sessionId,
|
|
249
|
+
principalId: identity.principalId,
|
|
250
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
251
|
+
permissionSnapshotVersion: handle.permissionSnapshotVersion,
|
|
252
|
+
});
|
|
253
|
+
return updateExecutionSession(ctx.controllerHome, {
|
|
254
|
+
sessionId: session.sessionId,
|
|
255
|
+
principalId: session.principalId,
|
|
256
|
+
controllerInstanceId: session.controllerInstanceId,
|
|
257
|
+
}, {
|
|
258
|
+
activeRepositoryId: repository.repoId,
|
|
259
|
+
activeCheckoutId: handle.checkoutId,
|
|
260
|
+
activeWorkId: handle.workId,
|
|
261
|
+
permissionSnapshotVersion: handle.permissionSnapshotVersion,
|
|
262
|
+
lastValidatedAt: new Date().toISOString(),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function finalizeFacadeWorkHandle(
|
|
267
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
268
|
+
repository: ReturnType<typeof selected>,
|
|
269
|
+
args: Record<string, unknown>,
|
|
270
|
+
operation: 'finalize' | 'stop',
|
|
271
|
+
): Promise<CallToolResult | undefined> {
|
|
272
|
+
const workId = String(args.work_id ?? '').trim();
|
|
273
|
+
if (!workId) return undefined;
|
|
274
|
+
let handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId)
|
|
275
|
+
?? recoverTerminalWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
276
|
+
if (!handle) return undefined;
|
|
277
|
+
const session = bindFacadeExecutionSession(ctx, repository, handle, args);
|
|
278
|
+
const explicitTargetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
|
|
279
|
+
? args.target_branch.trim()
|
|
280
|
+
: undefined;
|
|
281
|
+
const targetBranch = resolveWorkDeliveryTargetBranch(handle, repository.defaultBranch, explicitTargetBranch);
|
|
282
|
+
const common = {
|
|
283
|
+
session_id: session.sessionId,
|
|
284
|
+
repo_id: repository.repoId,
|
|
285
|
+
work_id: workId,
|
|
286
|
+
target_branch: targetBranch,
|
|
287
|
+
delete_branch: args.delete_branch !== false,
|
|
288
|
+
cleanup: args.cleanup !== false,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (operation === 'stop') {
|
|
292
|
+
return callExecutionTool(ctx, 'work_finalize', {
|
|
293
|
+
...common,
|
|
294
|
+
commit: false,
|
|
295
|
+
merge: false,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const handleCheckoutId = handle.checkoutId;
|
|
300
|
+
const registeredCheckout = repository.checkouts.find((candidate) => candidate.checkoutId === handleCheckoutId);
|
|
301
|
+
const registeredLifecycle = registeredCheckout ? repositoryCheckoutLifecycle(registeredCheckout) : undefined;
|
|
302
|
+
const checkoutUnavailable = !registeredCheckout || (registeredLifecycle !== 'active' && registeredLifecycle !== 'archived');
|
|
303
|
+
if (checkoutUnavailable) {
|
|
304
|
+
const exactChangedHead = Boolean(handle.managedWorktree
|
|
305
|
+
&& handle.expectedHead
|
|
306
|
+
&& handle.baseCommit
|
|
307
|
+
&& handle.expectedHead !== handle.baseCommit);
|
|
308
|
+
const retainedNoChangeRecovery = args.completion_outcome === 'completed_no_change'
|
|
309
|
+
&& handle.managedWorktree
|
|
310
|
+
&& handle.finalization.validation === 'done'
|
|
311
|
+
&& handle.finalization.worktreeCleanup === 'done'
|
|
312
|
+
&& Boolean(handle.validatedInputFingerprint)
|
|
313
|
+
&& (handle.state === 'merged' || handle.state === 'cleaned');
|
|
314
|
+
if ((!exactChangedHead && !retainedNoChangeRecovery) || args.cleanup === false) {
|
|
315
|
+
throw new Error(`WORK_FINALIZATION_CHECKOUT_UNAVAILABLE: ${repository.repoId}/${handle.checkoutId ?? 'unknown'} is ${registeredLifecycle ?? 'unregistered'}`);
|
|
316
|
+
}
|
|
317
|
+
if (retainedNoChangeRecovery) {
|
|
318
|
+
const noChangeEvidence = typeof args.no_change_evidence === 'string' && args.no_change_evidence.trim()
|
|
319
|
+
? args.no_change_evidence.trim()
|
|
320
|
+
: `Retained exact validation proves Work ${workId} has no repository delta after managed-worktree cleanup.`;
|
|
321
|
+
return callExecutionTool(ctx, 'work_finalize', {
|
|
322
|
+
...common,
|
|
323
|
+
commit: false,
|
|
324
|
+
merge: false,
|
|
325
|
+
completion_outcome: 'completed_no_change',
|
|
326
|
+
no_change_evidence: noChangeEvidence,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
// Physical cleanup can succeed before the Work completion receipt is
|
|
330
|
+
// persisted. Re-enter the canonical finalizer only after structural registry
|
|
331
|
+
// evidence proves this exact checkout is no longer selectable. No human-
|
|
332
|
+
// readable error message participates in lifecycle authority.
|
|
333
|
+
return callExecutionTool(ctx, 'work_finalize', {
|
|
334
|
+
...common,
|
|
335
|
+
commit: false,
|
|
336
|
+
merge: false,
|
|
337
|
+
completion_outcome: 'completed_changed',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const worktree: ReturnType<typeof selectRepositoryCheckout> = selectRepositoryCheckout(repository, handle.checkoutId, { allowArchived: true });
|
|
341
|
+
const status = repositoryGitStatus(worktree);
|
|
342
|
+
const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
|
|
343
|
+
const committedDelta = Boolean(head && handle.baseCommit && head !== handle.baseCommit);
|
|
344
|
+
const requestedCommit = typeof args.commit === 'boolean' ? args.commit : !status.clean;
|
|
345
|
+
// A clean checkout whose HEAD already differs from base is already committed.
|
|
346
|
+
// Treat commit=true as satisfied by that exact candidate instead of trying to
|
|
347
|
+
// manufacture an empty commit and recording GIT_NOTHING_STAGED as a delivery
|
|
348
|
+
// failure. The candidate is still adopted/revalidated and merge-fenced below.
|
|
349
|
+
const commit = status.clean && committedDelta ? false : requestedCommit;
|
|
350
|
+
if (
|
|
351
|
+
status.clean
|
|
352
|
+
&& committedDelta
|
|
353
|
+
&& commit === false
|
|
354
|
+
&& head
|
|
355
|
+
&& handle.expectedHead
|
|
356
|
+
&& head !== handle.expectedHead
|
|
357
|
+
&& (handle.state === 'prepared' || handle.state === 'editing')
|
|
358
|
+
) {
|
|
359
|
+
// A clean committed successor must enter through the single audited adoption
|
|
360
|
+
// authority before finalization, even when allowedPaths/forbiddenPaths are
|
|
361
|
+
// empty. Empty allowedPaths is repository-scoped authority; work_prepare still
|
|
362
|
+
// fences ancestry, checkout, branch, ownership, cleanliness, and any declared
|
|
363
|
+
// path restrictions before advancing expectedHead.
|
|
364
|
+
const adopted = await callExecutionTool(ctx, 'work_prepare', {
|
|
365
|
+
session_id: session.sessionId,
|
|
366
|
+
repo_id: repository.repoId,
|
|
367
|
+
checkout_id: handle.checkoutId,
|
|
368
|
+
work_id: workId,
|
|
369
|
+
expected_previous_head: handle.expectedHead,
|
|
370
|
+
adopt_candidate_head: head,
|
|
371
|
+
});
|
|
372
|
+
if (!adopted || adopted.isError === true) return adopted;
|
|
373
|
+
handle = readWorkHandle(ctx.controllerHome, repository.repoId, workId) ?? handle;
|
|
374
|
+
}
|
|
375
|
+
const requestedOutcome = args.completion_outcome === 'completed_no_change' || args.completion_outcome === 'completed_changed'
|
|
376
|
+
? args.completion_outcome
|
|
377
|
+
: undefined;
|
|
378
|
+
const completionOutcome = requestedOutcome ?? (!commit && !committedDelta && status.clean ? 'completed_no_change' : 'completed_changed');
|
|
379
|
+
// A proven no-change completion has nothing to merge. Defaulting merge=true
|
|
380
|
+
// forced the generic Git delivery path to do unnecessary work and conflicted
|
|
381
|
+
// with work_finalize's explicit no-change contract.
|
|
382
|
+
const merge = typeof args.merge === 'boolean' ? args.merge : completionOutcome !== 'completed_no_change';
|
|
383
|
+
const explicitNoChangeEvidence = typeof args.no_change_evidence === 'string' ? args.no_change_evidence.trim() : '';
|
|
384
|
+
const noChangeEvidence = completionOutcome === 'completed_no_change'
|
|
385
|
+
? explicitNoChangeEvidence || `Validated Work ${workId} has no repository delta from base ${handle.baseCommit ?? 'unknown'} at clean HEAD ${head ?? 'unknown'}.`
|
|
386
|
+
: undefined;
|
|
387
|
+
const finalizeArgs = {
|
|
388
|
+
...common,
|
|
389
|
+
commit,
|
|
390
|
+
merge,
|
|
391
|
+
no_ff: args.no_ff === true,
|
|
392
|
+
remote_write: args.remote_write === true,
|
|
393
|
+
completion_outcome: completionOutcome,
|
|
394
|
+
...(noChangeEvidence ? { no_change_evidence: noChangeEvidence } : {}),
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const validateExactWorkspace = async (): Promise<CallToolResult | undefined> => {
|
|
398
|
+
const contract = getWorkContract({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, workId);
|
|
399
|
+
const validation = await callExecutionTool(ctx, 'work_validate', {
|
|
400
|
+
session_id: session.sessionId,
|
|
401
|
+
repo_id: repository.repoId,
|
|
402
|
+
work_id: workId,
|
|
403
|
+
check_ids: contract?.checks ?? [],
|
|
404
|
+
});
|
|
405
|
+
if (!validation || validation.isError === true) return validation;
|
|
406
|
+
const validationPayload = contextRecord(validation.structuredContent);
|
|
407
|
+
return contextRecord(validationPayload.validation).passed === true ? undefined : validation;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
let physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
|
|
411
|
+
let payload = contextRecord(physical?.structuredContent);
|
|
412
|
+
if (physical?.isError === true && contextRecord(payload.error).code === 'WORK_VALIDATION_REQUIRED') {
|
|
413
|
+
const validationFailure = await validateExactWorkspace();
|
|
414
|
+
if (validationFailure) return validationFailure;
|
|
415
|
+
physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
|
|
416
|
+
payload = contextRecord(physical?.structuredContent);
|
|
417
|
+
}
|
|
418
|
+
if (
|
|
419
|
+
physical
|
|
420
|
+
&& physical.isError !== true
|
|
421
|
+
&& typeof payload.continuation === 'string'
|
|
422
|
+
&& payload.continuation.startsWith('WORK_COMMITTED_REVALIDATION_REQUIRED')
|
|
423
|
+
) {
|
|
424
|
+
const validationFailure = await validateExactWorkspace();
|
|
425
|
+
if (validationFailure) return validationFailure;
|
|
426
|
+
physical = await callExecutionTool(ctx, 'work_finalize', finalizeArgs);
|
|
427
|
+
}
|
|
428
|
+
return physical;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function planObligationDispositionsFromArgs(value: unknown) {
|
|
432
|
+
if (!Array.isArray(value)) return undefined;
|
|
433
|
+
return value
|
|
434
|
+
.filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry))
|
|
435
|
+
.map((entry) => ({
|
|
436
|
+
predecessorPlanId: String(entry.predecessor_plan_id ?? ''),
|
|
437
|
+
obligationId: String(entry.obligation_id ?? ''),
|
|
438
|
+
disposition: String(entry.disposition ?? '') as 'keep' | 'change' | 'defer' | 'drop',
|
|
439
|
+
successorRefs: Array.isArray(entry.successor_refs) ? entry.successor_refs.map(String) : [],
|
|
440
|
+
rationale: typeof entry.rationale === 'string' ? entry.rationale : undefined,
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export async function runFacadeRepair(
|
|
445
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
446
|
+
repository: ReturnType<typeof selected>,
|
|
447
|
+
args: Record<string, unknown>,
|
|
448
|
+
): Promise<CallToolResult> {
|
|
449
|
+
const store = {
|
|
450
|
+
controllerHome: ctx.controllerHome,
|
|
451
|
+
repoId: repository.repoId,
|
|
452
|
+
revisionContains: (ancestorRevision: string, descendantRevision: string) =>
|
|
453
|
+
repositoryRevisionContains(repository.canonicalRoot, ancestorRevision, descendantRevision),
|
|
454
|
+
};
|
|
455
|
+
let maintenanceSnapshot: ReturnType<typeof buildRuntimeMaintenanceStatus> | undefined;
|
|
456
|
+
let maintenanceStatus: {
|
|
457
|
+
readyForExecution?: boolean;
|
|
458
|
+
recommendedActions?: string[];
|
|
459
|
+
candidates?: Array<{ kind?: string; reason?: string; suggestedAction?: string; safe?: boolean }>;
|
|
460
|
+
warnings?: string[];
|
|
461
|
+
} | undefined;
|
|
462
|
+
try {
|
|
463
|
+
const status = buildRuntimeMaintenanceStatus(repository, ctx.controllerHome, {
|
|
464
|
+
minAgeMinutes: typeof args.min_age_minutes === 'number' ? args.min_age_minutes : undefined,
|
|
465
|
+
maxCandidates: typeof args.max_candidates === 'number' ? args.max_candidates : 20,
|
|
466
|
+
});
|
|
467
|
+
maintenanceSnapshot = status;
|
|
468
|
+
maintenanceStatus = {
|
|
469
|
+
readyForExecution: status.readyForExecution,
|
|
470
|
+
recommendedActions: status.recommendedActions,
|
|
471
|
+
candidates: status.candidates.map((candidate) => ({
|
|
472
|
+
kind: candidate.kind,
|
|
473
|
+
reason: candidate.reason,
|
|
474
|
+
suggestedAction: candidate.suggestedAction,
|
|
475
|
+
safe: candidate.safe,
|
|
476
|
+
})),
|
|
477
|
+
warnings: status.warnings,
|
|
478
|
+
};
|
|
479
|
+
} catch {
|
|
480
|
+
maintenanceStatus = {
|
|
481
|
+
readyForExecution: false,
|
|
482
|
+
recommendedActions: [],
|
|
483
|
+
candidates: [],
|
|
484
|
+
warnings: ['runtime_maintenance_status inspection failed; treating as infrastructure issue, not acceptance failure'],
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const repairOperation = args.repair_operation === 'repair' || args.repair_operation === 'verify' || args.repair_operation === 'handoff'
|
|
489
|
+
? args.repair_operation
|
|
490
|
+
: 'diagnose';
|
|
491
|
+
const dryRun = args.dry_run === undefined ? true : args.dry_run === true;
|
|
492
|
+
const elevatedRepair = args.destructive === true || args.remote_write === true || args.remote_effect === true;
|
|
493
|
+
const planId = typeof args.plan_id === 'string' ? args.plan_id.trim() : '';
|
|
494
|
+
const planStepId = typeof args.plan_step_id === 'string' ? args.plan_step_id.trim() : '';
|
|
495
|
+
const exactWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
496
|
+
|
|
497
|
+
if (planId && !planStepId) {
|
|
498
|
+
const plan = getPlanContract(store, planId);
|
|
499
|
+
if (!plan) {
|
|
500
|
+
const facade = buildFacadeResult({ status: 'not_found', summary: `PlanContract ${planId} not found.`, data: { operation: repairOperation, dryRun, planId, repaired: false } });
|
|
501
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
502
|
+
}
|
|
503
|
+
const pendingRevision = plan.status === 'replanning' ? plan.pendingRevision : undefined;
|
|
504
|
+
if (plan.status !== 'draft' && !pendingRevision) {
|
|
505
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: `PLAN_DRAFT_REPAIR_STATUS_INVALID: ${plan.planId}:${plan.status}`, data: { operation: repairOperation, dryRun, planId, repaired: false } });
|
|
506
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
507
|
+
}
|
|
508
|
+
const repairBase = pendingRevision ?? plan;
|
|
509
|
+
if (repairOperation !== 'repair' || dryRun) {
|
|
510
|
+
const facade = buildFacadeResult({
|
|
511
|
+
summary: pendingRevision
|
|
512
|
+
? `PlanContract ${plan.planId} has staged revision r${pendingRevision.revision}. Exact in-place revision repair is available; stable Plan identity and committed revision remain authoritative until approval.`
|
|
513
|
+
: `PlanContract ${plan.planId} is a draft. Exact in-place repair is available; the Plan identity and Requirement authority are preserved and only a fully valid draft may be persisted.`,
|
|
514
|
+
data: { operation: repairOperation, dryRun, plan: summarizePlanContract(plan), repaired: false, repairRequired: true },
|
|
515
|
+
suggestedNextActions: [{ label: 'Repair this exact draft Plan', tool: 'rh_work', operation: 'repair', payload: { plan_id: plan.planId, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
|
|
516
|
+
});
|
|
517
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
518
|
+
}
|
|
519
|
+
const rawSteps = Array.isArray(args.plan_steps)
|
|
520
|
+
? args.plan_steps
|
|
521
|
+
: repairBase.steps.map((step) => ({
|
|
522
|
+
id: step.id, objective: step.objective, dependencies: step.dependencies, authoritative_files: step.authoritativeFiles,
|
|
523
|
+
allowed_paths: step.allowedPaths, forbidden_paths: step.forbiddenPaths, check_ids: step.checks, acceptance_criteria: step.acceptanceCriteria,
|
|
524
|
+
}));
|
|
525
|
+
const steps = rawSteps
|
|
526
|
+
.filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step))
|
|
527
|
+
.map((step) => ({
|
|
528
|
+
id: String(step.id ?? ''),
|
|
529
|
+
objective: String(step.objective ?? ''),
|
|
530
|
+
dependencies: Array.isArray(step.dependencies) ? step.dependencies.map(String) : [],
|
|
531
|
+
authoritativeFiles: Array.isArray(step.authoritative_files) ? step.authoritative_files.map(String) : [],
|
|
532
|
+
allowedPaths: Array.isArray(step.allowed_paths) ? step.allowed_paths.map(String) : [],
|
|
533
|
+
forbiddenPaths: Array.isArray(step.forbidden_paths) ? step.forbidden_paths.map(String) : [],
|
|
534
|
+
checks: Array.isArray(step.check_ids) ? step.check_ids.map(String) : [],
|
|
535
|
+
acceptanceCriteria: Array.isArray(step.acceptance_criteria) ? step.acceptance_criteria.map(String) : [],
|
|
536
|
+
}));
|
|
537
|
+
const availableChecks = listControllerChecks(repository.canonicalRoot);
|
|
538
|
+
const normalizedPlanChecks = normalizeCheckIds(steps.flatMap((step) => step.checks), availableChecks);
|
|
539
|
+
if (normalizedPlanChecks.invalidCheckIds.length > 0) {
|
|
540
|
+
const facade = buildFacadeResult({
|
|
541
|
+
status: 'failed',
|
|
542
|
+
summary: `PLAN_CHECKS_INVALID: ${normalizedPlanChecks.invalidCheckIds.join(', ')}. Draft repair was not persisted.`,
|
|
543
|
+
data: { operation: repairOperation, dryRun: false, planId, repaired: false, normalizedChecks: normalizedPlanChecks, registeredCheckIds: availableChecks.map((check) => check.id).slice(0, 80) },
|
|
544
|
+
suggestedNextActions: [],
|
|
545
|
+
});
|
|
546
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const repaired = await repairDraftPlanContractAsync(store, planId, {
|
|
550
|
+
expectedSourceRevision: repairBase.sourceRevision,
|
|
551
|
+
scopeKey: typeof args.scope_key === 'string' ? args.scope_key : plan.scopeKey,
|
|
552
|
+
sourceRevision: typeof args.source_revision === 'string' ? args.source_revision : repairBase.sourceRevision,
|
|
553
|
+
goal: typeof args.objective === 'string' ? args.objective : repairBase.goal,
|
|
554
|
+
nonGoals: Array.isArray(args.non_goals) ? args.non_goals.map(String) : repairBase.nonGoals,
|
|
555
|
+
assumptions: Array.isArray(args.assumptions) ? args.assumptions.map(String) : repairBase.assumptions,
|
|
556
|
+
resolvedDecisions: Array.isArray(args.resolved_decisions) ? args.resolved_decisions.map(String) : repairBase.resolvedDecisions,
|
|
557
|
+
stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : repairBase.stopConditions,
|
|
558
|
+
replanConditions: Array.isArray(args.replan_conditions) ? args.replan_conditions.map(String) : repairBase.replanConditions,
|
|
559
|
+
integrationStrategy: typeof args.integration_strategy === 'string' ? args.integration_strategy : repairBase.integrationStrategy,
|
|
560
|
+
obligationDispositions: planObligationDispositionsFromArgs(args.obligation_dispositions) ?? repairBase.obligationDispositions,
|
|
561
|
+
steps,
|
|
562
|
+
});
|
|
563
|
+
const facade = buildFacadeResult({
|
|
564
|
+
summary: pendingRevision ? `PlanContract ${repaired.planId} staged revision repaired in place; stable Plan identity and committed authority were preserved.` : `PlanContract ${repaired.planId} draft repaired in place; identity and Requirement authority were preserved.`,
|
|
565
|
+
data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(repaired), repaired: true, replacementPlanCreated: false },
|
|
566
|
+
suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: repaired.planId }, risk: 'workspace_write', confidence: 'medium' }],
|
|
567
|
+
});
|
|
568
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_DRAFT_REPAIR_FAILED', data: { operation: repairOperation, dryRun: false, planId, repaired: false } });
|
|
571
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (planStepId && !planId) {
|
|
576
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: 'PLAN_STEP_REPAIR_CONTEXT_REQUIRED: plan_id and plan_step_id are both required.', data: { operation: repairOperation, dryRun, repaired: false } });
|
|
577
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (planId && planStepId) {
|
|
581
|
+
const plan = getPlanContract(store, planId);
|
|
582
|
+
const step = plan?.steps.find((candidate) => candidate.id === planStepId);
|
|
583
|
+
if (!plan || !step) {
|
|
584
|
+
const facade = buildFacadeResult({ status: 'not_found', summary: !plan ? `PlanContract ${planId} not found.` : `PLAN_STEP_NOT_FOUND: ${planStepId}`, data: { operation: repairOperation, dryRun, planId, planStepId, repaired: false } });
|
|
585
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
586
|
+
}
|
|
587
|
+
if (!step.workId) {
|
|
588
|
+
const facade = buildFacadeResult({ summary: `Plan step ${planId}/${planStepId} has no Work binding to repair.`, data: { operation: repairOperation, dryRun, planId, planStepId, repaired: false, repairRequired: false } });
|
|
589
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
590
|
+
}
|
|
591
|
+
const boundWork = getWorkContract(store, step.workId);
|
|
592
|
+
if (boundWork) {
|
|
593
|
+
if (['completed', 'failed', 'cancelled'].includes(boundWork.status)) {
|
|
594
|
+
if (repairOperation !== 'repair' || dryRun) {
|
|
595
|
+
const facade = buildFacadeResult({
|
|
596
|
+
summary: `PLAN_STEP_TERMINAL_WORK_RECONCILIABLE: ${planId}/${planStepId} is bound to existing terminal Work ${boundWork.workId}. Explicit repair can project that exact Work without creating a replacement.`,
|
|
597
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, repairRequired: true, reusedExistingWork: true },
|
|
598
|
+
suggestedNextActions: [{ label: 'Project existing terminal Work', tool: 'rh_work', operation: 'repair', payload: { plan_id: planId, plan_step_id: planStepId, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
|
|
599
|
+
});
|
|
600
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
const reconciledPlan = completePlanStepForWork(store, { planId, stepId: planStepId, work: boundWork });
|
|
604
|
+
const facade = buildFacadeResult({
|
|
605
|
+
summary: `Reconciled Plan step ${planId}/${planStepId} from its existing terminal Work ${boundWork.workId}; no replacement Work was created.`,
|
|
606
|
+
data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(reconciledPlan), boundWorkId: boundWork.workId, repaired: true, reusedExistingWork: true },
|
|
607
|
+
});
|
|
608
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_STEP_TERMINAL_WORK_RECONCILIATION_FAILED', data: { operation: repairOperation, dryRun: false, planId, planStepId, boundWorkId: boundWork.workId, repaired: false } });
|
|
611
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const requestedRevisionLabel = typeof args.superseded_by === 'string' ? args.superseded_by.trim() : '';
|
|
615
|
+
const requestedAllowedPaths = Array.isArray(args.allowed_paths)
|
|
616
|
+
? [...new Set([...step.allowedPaths, ...args.allowed_paths.map(String).map((value) => value.trim()).filter(Boolean)])]
|
|
617
|
+
: step.allowedPaths;
|
|
618
|
+
const scopeReplanRequested = Boolean(requestedRevisionLabel) || requestedAllowedPaths.length > step.allowedPaths.length;
|
|
619
|
+
if (scopeReplanRequested) {
|
|
620
|
+
const requestedSourceRevision = typeof args.source_revision === 'string' ? args.source_revision.trim() : '';
|
|
621
|
+
if (!requestedRevisionLabel || !requestedSourceRevision || requestedAllowedPaths.length === step.allowedPaths.length) {
|
|
622
|
+
const facade = buildFacadeResult({
|
|
623
|
+
status: 'blocked',
|
|
624
|
+
summary: 'PLAN_WORK_SCOPE_REPLAN_INPUT_REQUIRED: superseded_by, source_revision, and at least one new allowed_paths entry are required for an active Plan-bound Work scope replan.',
|
|
625
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, requestedRevisionLabel: requestedRevisionLabel || undefined, requestedSourceRevision: requestedSourceRevision || undefined, requestedAllowedPaths },
|
|
626
|
+
});
|
|
627
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
628
|
+
}
|
|
629
|
+
if (repairOperation !== 'repair' || dryRun) {
|
|
630
|
+
const facade = buildFacadeResult({
|
|
631
|
+
summary: `PLAN_WORK_SCOPE_REPLAN_AVAILABLE: ${planId}/${planStepId} can atomically move exact Work ${boundWork.workId} to stable Plan ${planId} revision label ${requestedRevisionLabel} while widening only allowed-path authority.`,
|
|
632
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, requestedRevisionLabel, requestedSourceRevision, requestedAllowedPaths, repaired: false, repairRequired: true, reusedExistingWork: true },
|
|
633
|
+
suggestedNextActions: [{ label: 'Replan exact active Work scope', tool: 'rh_work', operation: 'repair', payload: { plan_id: planId, plan_step_id: planStepId, superseded_by: requestedRevisionLabel, source_revision: requestedSourceRevision, allowed_paths: requestedAllowedPaths, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
|
|
634
|
+
});
|
|
635
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
const replanned = replanActivePlanBoundWorkScope(store, {
|
|
639
|
+
planId,
|
|
640
|
+
stepId: planStepId,
|
|
641
|
+
workId: boundWork.workId,
|
|
642
|
+
requestedRevisionLabel,
|
|
643
|
+
sourceRevision: requestedSourceRevision,
|
|
644
|
+
allowedPaths: requestedAllowedPaths,
|
|
645
|
+
reason: typeof args.reason === 'string' && args.reason.trim()
|
|
646
|
+
? args.reason.trim()
|
|
647
|
+
: 'Explicit Controller repair widened a frozen Plan path fence after current-source evidence proved the existing Plan contract omitted a path required by its own acceptance scope.',
|
|
648
|
+
});
|
|
649
|
+
const facade = buildFacadeResult({
|
|
650
|
+
summary: `Replanned ${planId}/${planStepId} as ${replanned.currentPlan.planId} r${replanned.currentPlan.revision ?? 1} and retained the same active Work ${replanned.work.workId} atomically; semantic acceptance and checks were not widened.`,
|
|
651
|
+
data: { operation: repairOperation, dryRun: false, priorPlan: summarizePlanContract(replanned.priorPlan), currentPlan: summarizePlanContract(replanned.currentPlan), work: summarizeWorkContract(replanned.work), repaired: true, replacementWorkCreated: false, reusedExistingWork: true },
|
|
652
|
+
});
|
|
653
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_WORK_SCOPE_REPLAN_FAILED', data: { operation: repairOperation, dryRun: false, planId, planStepId, boundWorkId: boundWork.workId, requestedRevisionLabel, repaired: false } });
|
|
656
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const facade = buildFacadeResult({
|
|
660
|
+
status: 'blocked',
|
|
661
|
+
summary: `PLAN_STEP_BOUND_WORK_STILL_EXISTS: ${planId}/${planStepId} is bound to active Work ${boundWork.workId}; continue that exact Work, or explicitly request a scope-only stable Plan revision instead of replacing the Work.`,
|
|
662
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: boundWork.workId, repaired: false, repairRequired: false },
|
|
663
|
+
suggestedNextActions: [{ label: 'Continue existing Work', tool: 'rh_work', operation: 'continue', payload: { work_id: boundWork.workId }, risk: 'readonly', confidence: 'high' }],
|
|
664
|
+
});
|
|
665
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
666
|
+
}
|
|
667
|
+
const conflicting = listWorkContracts({ ...store, status: 'active', limit: 200 })
|
|
668
|
+
.filter((candidate) => candidate.planId === planId && candidate.planStepId === planStepId && candidate.workId !== step.workId);
|
|
669
|
+
if (conflicting.length > 0) {
|
|
670
|
+
const facade = buildFacadeResult({
|
|
671
|
+
status: 'blocked',
|
|
672
|
+
summary: `PLAN_STEP_REPAIR_CONFLICT: ${planId}/${planStepId} is bound to missing Work ${step.workId}, but ${conflicting.length} other active Work record(s) claim the same step. Resolve the conflicting authority before changing the Plan binding.`,
|
|
673
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: step.workId, conflictingWorkIds: conflicting.map((candidate) => candidate.workId), repaired: false },
|
|
674
|
+
});
|
|
675
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
676
|
+
}
|
|
677
|
+
if (repairOperation !== 'repair' || dryRun) {
|
|
678
|
+
const facade = buildFacadeResult({
|
|
679
|
+
summary: `PLAN_STEP_DANGLING_WORK_BINDING: ${planId}/${planStepId} points to missing Work ${step.workId}. Exact repair is available and will clear only this unchanged ghost binding.`,
|
|
680
|
+
data: { operation: repairOperation, dryRun, planId, planStepId, boundWorkId: step.workId, repaired: false, repairRequired: true },
|
|
681
|
+
suggestedNextActions: [{ label: 'Repair exact dangling binding', tool: 'rh_work', operation: 'repair', payload: { plan_id: planId, plan_step_id: planStepId, repair_operation: 'repair', dry_run: false }, risk: 'workspace_write', confidence: 'high' }],
|
|
682
|
+
});
|
|
683
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const repairedPlan = repairDanglingPlanStepWorkBinding(store, {
|
|
687
|
+
planId,
|
|
688
|
+
stepId: planStepId,
|
|
689
|
+
expectedWorkId: step.workId,
|
|
690
|
+
reason: 'Explicit Controller repair confirmed that the exact bound Work record is absent and no other active primary Work claims this Plan step.',
|
|
691
|
+
});
|
|
692
|
+
const facade = buildFacadeResult({
|
|
693
|
+
summary: `Repaired dangling Plan step binding ${planId}/${planStepId}; ${step.workId} was cleared without creating a replacement Work.`,
|
|
694
|
+
data: { operation: repairOperation, dryRun: false, plan: summarizePlanContract(repairedPlan), boundWorkId: step.workId, repaired: true, replacementWorkCreated: false },
|
|
695
|
+
});
|
|
696
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PLAN_STEP_DANGLING_WORK_REPAIR_FAILED', data: { operation: repairOperation, dryRun: false, planId, planStepId, boundWorkId: step.workId, repaired: false } });
|
|
699
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// The self-healing facade is a policy/planning surface; the authoritative
|
|
704
|
+
// maintenance executor owns mutations. Execute it here only for an explicit,
|
|
705
|
+
// non-dry-run repair whose entire observed candidate set is already classified
|
|
706
|
+
// safe. Unsafe/destructive/remote repair continues through the approval path.
|
|
707
|
+
if (
|
|
708
|
+
repairOperation === 'repair'
|
|
709
|
+
&& !dryRun
|
|
710
|
+
&& !elevatedRepair
|
|
711
|
+
&& !exactWorkId
|
|
712
|
+
&& maintenanceSnapshot
|
|
713
|
+
&& maintenanceSnapshot.candidates.length > 0
|
|
714
|
+
) {
|
|
715
|
+
const applied = applyRuntimeMaintenance(repository, ctx.controllerHome, {
|
|
716
|
+
actionId: 'full_maintenance_pass',
|
|
717
|
+
confirmMaintenance: true,
|
|
718
|
+
minAgeMinutes: typeof args.min_age_minutes === 'number' ? args.min_age_minutes : undefined,
|
|
719
|
+
maxCandidates: typeof args.max_candidates === 'number' ? args.max_candidates : 20,
|
|
720
|
+
});
|
|
721
|
+
const actions = applied.applied.slice(0, 20).map((entry) => ({
|
|
722
|
+
kind: entry.kind,
|
|
723
|
+
id: entry.id,
|
|
724
|
+
applied: entry.applied,
|
|
725
|
+
result: entry.result,
|
|
726
|
+
...(entry.error ? { error: entry.error.slice(0, 300) } : {}),
|
|
727
|
+
}));
|
|
728
|
+
const appliedCount = applied.applied.filter((entry) => entry.applied).length;
|
|
729
|
+
// Protected stale runtime temp entries are intentionally non-blocking maintenance
|
|
730
|
+
// diagnostics. Keep repair completion semantics aligned with rh_status readiness so
|
|
731
|
+
// their presence does not falsely report a blocked repair after safe debt is cleared.
|
|
732
|
+
const remainingCandidateCount = applied.candidates.filter((candidate) => candidate.kind !== 'stale_runtime_temp_entry').length;
|
|
733
|
+
const blocked = remainingCandidateCount > 0;
|
|
734
|
+
const facade = buildFacadeResult({
|
|
735
|
+
status: blocked ? 'blocked' : 'ok',
|
|
736
|
+
summary: blocked
|
|
737
|
+
? `Runtime maintenance applied ${appliedCount} candidate(s); ${remainingCandidateCount} candidate(s) remain after the authoritative executor pass.`
|
|
738
|
+
: `Runtime maintenance applied ${appliedCount} candidate(s); no maintenance candidates remain.`,
|
|
739
|
+
data: {
|
|
740
|
+
operation: 'repair',
|
|
741
|
+
dryRun: false,
|
|
742
|
+
applied: appliedCount > 0,
|
|
743
|
+
actionId: 'full_maintenance_pass',
|
|
744
|
+
appliedCount,
|
|
745
|
+
remainingCandidateCount,
|
|
746
|
+
actions,
|
|
747
|
+
classification: 'infrastructure_recovery',
|
|
748
|
+
isAcceptanceFailure: false,
|
|
749
|
+
},
|
|
750
|
+
warnings: applied.warnings.slice(0, 5),
|
|
751
|
+
suggestedNextActions: [{
|
|
752
|
+
label: 'Verify controller status after repair',
|
|
753
|
+
tool: 'rh_status',
|
|
754
|
+
operation: 'get',
|
|
755
|
+
risk: 'readonly',
|
|
756
|
+
confidence: 'high',
|
|
757
|
+
}],
|
|
758
|
+
rawAvailable: false,
|
|
759
|
+
});
|
|
760
|
+
return result(facade as unknown as Record<string, unknown>, blocked);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
let watchdogSummary: string | undefined;
|
|
764
|
+
let performanceSummary: string | undefined;
|
|
765
|
+
try {
|
|
766
|
+
const watchdog = buildWorkflowWatchdogReport(ctx.controllerHome, repository, { includeProcesses: false });
|
|
767
|
+
watchdogSummary = `status=${watchdog.status}; findings=${watchdog.findings.length}; stale=${watchdog.staleWork.length}`.slice(0, 240);
|
|
768
|
+
} catch {
|
|
769
|
+
watchdogSummary = undefined;
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
const perf = collectRuntimePerformanceDiagnostics({
|
|
773
|
+
repoId: repository.repoId,
|
|
774
|
+
repoRoot: repository.canonicalRoot,
|
|
775
|
+
includeProcesses: false,
|
|
776
|
+
includeTempDirs: false,
|
|
777
|
+
});
|
|
778
|
+
performanceSummary = perf.summary.slice(0, 240);
|
|
779
|
+
} catch {
|
|
780
|
+
performanceSummary = undefined;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const daemon = readForgeRuntimeStatus(ctx.controllerHome);
|
|
784
|
+
const readiness = await controllerReadinessEvidence(ctx, repository);
|
|
785
|
+
const facade = runSelfHealingLoop(
|
|
786
|
+
{ repoId: repository.repoId, handoffStore: store },
|
|
787
|
+
{
|
|
788
|
+
operation: repairOperation,
|
|
789
|
+
dryRun,
|
|
790
|
+
approvalConfirmed: args.approval_confirmed === true,
|
|
791
|
+
workId: typeof args.work_id === 'string' ? args.work_id : undefined,
|
|
792
|
+
chatgptPullFailed: args.chatgpt_pull_failed === true,
|
|
793
|
+
destructive: args.destructive === true,
|
|
794
|
+
remoteEffect: args.remote_write === true || args.remote_effect === true,
|
|
795
|
+
maintenanceStatus,
|
|
796
|
+
diagnostics: {
|
|
797
|
+
watchdogSummary,
|
|
798
|
+
performanceSummary,
|
|
799
|
+
controllerDaemonUnhealthy: daemon.status !== 'ready',
|
|
800
|
+
schedulerUnhealthy: readiness.durableScheduler.status !== 'ready',
|
|
801
|
+
codexUnavailable: args.codex_available === false,
|
|
802
|
+
grokUnavailable: args.grok_available === false || args.target === 'grok',
|
|
803
|
+
pluginUnavailable: args.plugin_unavailable === true,
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
);
|
|
807
|
+
return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked' || facade.status === 'approval_required' || facade.status === 'failed');
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export function repositoryWorkHandleHasSourceDelta(
|
|
811
|
+
repository: ReturnType<typeof selected>,
|
|
812
|
+
handle: WorkHandleState,
|
|
813
|
+
): boolean {
|
|
814
|
+
try {
|
|
815
|
+
const checkout = selectRepositoryCheckout(repository, handle.checkoutId, { allowArchived: true });
|
|
816
|
+
const status = repositoryGitStatus(checkout);
|
|
817
|
+
const head = status.head ?? handle.expectedHead ?? handle.baseCommit;
|
|
818
|
+
return !status.clean || Boolean(head && handle.baseCommit && head !== handle.baseCommit);
|
|
819
|
+
} catch (_error) {
|
|
820
|
+
// A checkout may already have been removed by an earlier bounded cleanup
|
|
821
|
+
// attempt. The WorkHandle's fenced expectedHead remains authoritative for
|
|
822
|
+
// whether repository delivery existed and must not be downgraded to an
|
|
823
|
+
// effect-only semantic completion.
|
|
824
|
+
return Boolean(handle.expectedHead && handle.baseCommit && handle.expectedHead !== handle.baseCommit);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export function finalizeRemoteEffectWorkFromRepositoryProcessReceipt(
|
|
829
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
830
|
+
repository: ReturnType<typeof selected>,
|
|
831
|
+
workId: string,
|
|
832
|
+
checkoutId: string,
|
|
833
|
+
) {
|
|
834
|
+
const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
835
|
+
const evidence = listWorkBoundRepositoryRemoteEffectProcessEvidence({
|
|
836
|
+
controllerHome: ctx.controllerHome,
|
|
837
|
+
repoId: repository.repoId,
|
|
838
|
+
checkoutId,
|
|
839
|
+
workId,
|
|
840
|
+
})[0];
|
|
841
|
+
if (!evidence) return undefined;
|
|
842
|
+
return completeRemoteEffectWorkFromProcessReceipt(store, workId, {
|
|
843
|
+
processId: evidence.processId,
|
|
844
|
+
actionId: evidence.actionId,
|
|
845
|
+
requestId: evidence.requestId,
|
|
846
|
+
semanticKey: evidence.semanticKey,
|
|
847
|
+
resultDigest: evidence.resultDigest,
|
|
848
|
+
recordedAt: evidence.finishedAt ?? new Date().toISOString(),
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function reconcileTerminalFacadeWorkVerifications(
|
|
853
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
854
|
+
repository: ReturnType<typeof selected>,
|
|
855
|
+
workId: string,
|
|
856
|
+
): { sourceRevision?: string; workspaceFingerprint?: string; implementationReviewWorkspaceFingerprint?: string; workspaceChangedPaths?: string[]; reconciledProcessIds: string[]; workBoundProcessEvidenceIds: string[] } {
|
|
857
|
+
const resolvedVerification = resolveWorkVerificationContext({ controllerHome: ctx.controllerHome, repository, workId });
|
|
858
|
+
if (!resolvedVerification.ok) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
|
|
859
|
+
const { store, workContract, repository: verificationRepository, checks: availableChecks } = resolvedVerification.context;
|
|
860
|
+
if (!workContract || workContract.completionReceipt) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
|
|
861
|
+
const verificationStatus = repositoryGitStatus(verificationRepository);
|
|
862
|
+
const sourceRevision = verificationStatus.head ?? undefined;
|
|
863
|
+
if (!sourceRevision) return { reconciledProcessIds: [], workBoundProcessEvidenceIds: [] };
|
|
864
|
+
let verificationHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
865
|
+
const workspaceFingerprint = workspaceValidationFingerprint(verificationRepository.canonicalRoot, verificationStatus);
|
|
866
|
+
// Work-bound repository Process evidence remains semantic/result evidence,
|
|
867
|
+
// never a typed check receipt. repository_change may use it only when no
|
|
868
|
+
// checks are declared. local_effect may bind the same exact durable Process
|
|
869
|
+
// ids for Controller semantic review even when checks are declared; the
|
|
870
|
+
// normal missing-check gate below still requires typed verification receipts.
|
|
871
|
+
const workBoundProcessEvidenceIds = (
|
|
872
|
+
workContract.workKind === 'local_effect'
|
|
873
|
+
|| (workContract.workKind === 'repository_change' && workContract.checks.length === 0)
|
|
874
|
+
)
|
|
875
|
+
? listWorkBoundRepositoryProcessEvidence({
|
|
876
|
+
controllerHome: ctx.controllerHome,
|
|
877
|
+
repoId: repository.repoId,
|
|
878
|
+
checkoutId: verificationRepository.activeCheckoutId,
|
|
879
|
+
workId,
|
|
880
|
+
}).map((evidence) => evidence.processId)
|
|
881
|
+
: [];
|
|
882
|
+
const workloopCtx = {
|
|
883
|
+
workStore: store,
|
|
884
|
+
handoffStore: store,
|
|
885
|
+
repoId: repository.repoId,
|
|
886
|
+
availableChecks,
|
|
887
|
+
};
|
|
888
|
+
const seenChecks = new Set<string>();
|
|
889
|
+
const reconciledProcessIds: string[] = [];
|
|
890
|
+
const candidates = listProcessRecords(ctx.controllerHome, repository.repoId, 500)
|
|
891
|
+
.filter((record) => (
|
|
892
|
+
record.workId === workId
|
|
893
|
+
&& record.checkoutId === verificationRepository.activeCheckoutId
|
|
894
|
+
&& !isManagedProcessActive(record)
|
|
895
|
+
&& record.origin?.workVerificationSnapshot === true
|
|
896
|
+
&& typeof record.origin?.checkId === 'string'
|
|
897
|
+
&& typeof record.origin?.requestSemanticFingerprint === 'string'
|
|
898
|
+
));
|
|
899
|
+
|
|
900
|
+
for (const record of candidates) {
|
|
901
|
+
const checkId = record.origin?.checkId?.trim() ?? '';
|
|
902
|
+
if (!checkId || seenChecks.has(checkId)) continue;
|
|
903
|
+
seenChecks.add(checkId);
|
|
904
|
+
const classified = classifyVerificationOutcome({ checkId, available: availableChecks });
|
|
905
|
+
if (classified.outcome === 'invalid_check_id' || !classified.normalizedCheckId) continue;
|
|
906
|
+
const normalizedCheckId = classified.normalizedCheckId;
|
|
907
|
+
const requestedChecks = workContract.checks.length ? workContract.checks : [normalizedCheckId];
|
|
908
|
+
const currentFingerprint = verificationInputFingerprint({
|
|
909
|
+
sourceRevision,
|
|
910
|
+
workspaceFingerprint,
|
|
911
|
+
checkId: normalizedCheckId,
|
|
912
|
+
requestedChecks,
|
|
913
|
+
});
|
|
914
|
+
if (record.origin?.requestSemanticFingerprint !== currentFingerprint || !record.checkExecution) continue;
|
|
915
|
+
|
|
916
|
+
try {
|
|
917
|
+
const receipt = processCheckCompletionReceipt(record, {
|
|
918
|
+
repoId: verificationRepository.repoId,
|
|
919
|
+
checkoutId: verificationRepository.activeCheckoutId,
|
|
920
|
+
workId,
|
|
921
|
+
checkId: normalizedCheckId,
|
|
922
|
+
processId: record.processId,
|
|
923
|
+
requestId: record.origin?.requestId,
|
|
924
|
+
checkExecution: {
|
|
925
|
+
cacheKey: record.checkExecution.cacheKey,
|
|
926
|
+
revision: record.checkExecution.revision,
|
|
927
|
+
definitionDigest: record.checkExecution.definitionDigest,
|
|
928
|
+
environmentFingerprint: record.checkExecution.environmentFingerprint,
|
|
929
|
+
timeoutMs: record.checkExecution.timeoutMs,
|
|
930
|
+
scopeKey: record.checkExecution.scopeKey,
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
const latestContract = getWorkContract(store, workId);
|
|
934
|
+
if (latestContract?.checkRefs.some((entry) => entry.receipt?.receiptId === receipt.receiptId)) continue;
|
|
935
|
+
|
|
936
|
+
const legacyEvidence = record.origin?.checkResultReceiptPath
|
|
937
|
+
? undefined
|
|
938
|
+
: readLatestControllerCheckEvidence(verificationRepository.canonicalRoot, normalizedCheckId);
|
|
939
|
+
const evidenceState = classifyPersistedCheckTerminalEvidence(record, normalizedCheckId, { legacyEvidence });
|
|
940
|
+
const failureClass = evidenceState.failureClass;
|
|
941
|
+
const infrastructureFailed = receipt.timedOut
|
|
942
|
+
|| receipt.cancelled
|
|
943
|
+
|| evidenceState.state !== 'matched'
|
|
944
|
+
|| (!receipt.ok && failureClass !== 'acceptance_failure');
|
|
945
|
+
const checkFailed = !receipt.ok && !infrastructureFailed;
|
|
946
|
+
verifyGoalWorkloop(workloopCtx, {
|
|
947
|
+
workId,
|
|
948
|
+
checkId: normalizedCheckId,
|
|
949
|
+
sourceRevision,
|
|
950
|
+
workspaceFingerprint,
|
|
951
|
+
verificationInputFingerprint: currentFingerprint,
|
|
952
|
+
commandFingerprint: commandFingerprint(normalizedCheckId, receipt.commandId),
|
|
953
|
+
receipt,
|
|
954
|
+
infrastructureFailed,
|
|
955
|
+
checkFailed,
|
|
956
|
+
});
|
|
957
|
+
reconciledProcessIds.push(record.processId);
|
|
958
|
+
} catch {
|
|
959
|
+
// Exact receipt/process identity is mandatory. Any malformed, stale, or
|
|
960
|
+
// mismatched terminal Process remains non-authoritative and is ignored.
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
let deliveryBaseRevision = verificationHandle
|
|
965
|
+
? workDeliveryBaseRevision(verificationHandle)
|
|
966
|
+
: workContract.baseRevision;
|
|
967
|
+
const latestContract = getWorkContract(store, workId) ?? workContract;
|
|
968
|
+
if (latestContract.phase === 'review' && verificationHandle && !verificationHandle.managedWorktree && verificationHandle.expectedHead) {
|
|
969
|
+
const targetBranch = resolveWorkDeliveryTargetBranch(verificationHandle, verificationRepository.defaultBranch);
|
|
970
|
+
const reconciliation = reconcileDirectCanonicalTargetAdvanceCommand({
|
|
971
|
+
controllerHome: ctx.controllerHome,
|
|
972
|
+
repoId: repository.repoId,
|
|
973
|
+
workId,
|
|
974
|
+
handle: verificationHandle,
|
|
975
|
+
root: verificationRepository.canonicalRoot,
|
|
976
|
+
targetBranch,
|
|
977
|
+
status: verificationStatus,
|
|
978
|
+
scope: { allowedPaths: latestContract.allowedPaths, forbiddenPaths: latestContract.forbiddenPaths },
|
|
979
|
+
checkIds: latestContract.checks,
|
|
980
|
+
checkRefs: latestContract.checkRefs,
|
|
981
|
+
evidenceTitle: 'review target advancement reconciled',
|
|
982
|
+
});
|
|
983
|
+
if (reconciliation.reconciled) {
|
|
984
|
+
verificationHandle = reconciliation.handle;
|
|
985
|
+
deliveryBaseRevision = workDeliveryBaseRevision(verificationHandle);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
deliveryBaseRevision = verificationHandle
|
|
990
|
+
? implementationReviewCommittedBaseRevision(
|
|
991
|
+
verificationRepository,
|
|
992
|
+
verificationHandle,
|
|
993
|
+
workContract.baseRevision,
|
|
994
|
+
sourceRevision,
|
|
995
|
+
)
|
|
996
|
+
: workContract.baseRevision;
|
|
997
|
+
const committedPaths = deliveryBaseRevision
|
|
998
|
+
? workChangedPaths(verificationRepository.canonicalRoot, deliveryBaseRevision, sourceRevision)
|
|
999
|
+
: workContract.repositoryBaseState === 'unborn'
|
|
1000
|
+
? workChangedPathsFromUnbornBase(verificationRepository.canonicalRoot, sourceRevision)
|
|
1001
|
+
: [];
|
|
1002
|
+
const workspaceChangedPaths = [...new Set([
|
|
1003
|
+
...committedPaths,
|
|
1004
|
+
...verificationStatus.staged,
|
|
1005
|
+
...verificationStatus.unstaged,
|
|
1006
|
+
...verificationStatus.untracked,
|
|
1007
|
+
])].sort();
|
|
1008
|
+
const implementationReviewWorkspaceFingerprint = implementationReviewContentFingerprint(
|
|
1009
|
+
verificationRepository.canonicalRoot,
|
|
1010
|
+
workspaceChangedPaths,
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
return { sourceRevision, workspaceFingerprint, implementationReviewWorkspaceFingerprint, workspaceChangedPaths, reconciledProcessIds, workBoundProcessEvidenceIds };
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export async function runFacadeVerify(
|
|
1017
|
+
ctx: MultiRepositoryMcpToolContext,
|
|
1018
|
+
repository: ReturnType<typeof selected>,
|
|
1019
|
+
args: Record<string, unknown>,
|
|
1020
|
+
): Promise<CallToolResult> {
|
|
1021
|
+
const workId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
1022
|
+
const checkId = String(args.check_id ?? args.checkId ?? '').trim();
|
|
1023
|
+
const hasBatchInput = Object.prototype.hasOwnProperty.call(args, 'check_ids');
|
|
1024
|
+
if (checkId && hasBatchInput) {
|
|
1025
|
+
const blocked = buildFacadeResult({
|
|
1026
|
+
status: 'blocked',
|
|
1027
|
+
summary: 'rh_work verify accepts either check_id or check_ids, not both.',
|
|
1028
|
+
data: { workId: workId || undefined, verificationStarted: false },
|
|
1029
|
+
warnings: ['WORK_VERIFY_CHECK_INPUT_CONFLICT'],
|
|
1030
|
+
});
|
|
1031
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
1032
|
+
}
|
|
1033
|
+
if (hasBatchInput && !Array.isArray(args.check_ids)) {
|
|
1034
|
+
const blocked = buildFacadeResult({
|
|
1035
|
+
status: 'blocked',
|
|
1036
|
+
summary: 'rh_work verify check_ids must be an array.',
|
|
1037
|
+
data: { workId: workId || undefined, verificationStarted: false },
|
|
1038
|
+
warnings: ['WORK_VERIFY_CHECK_IDS_INVALID'],
|
|
1039
|
+
});
|
|
1040
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
1041
|
+
}
|
|
1042
|
+
const commonVerificationInput = {
|
|
1043
|
+
controllerHome: ctx.controllerHome,
|
|
1044
|
+
repository,
|
|
1045
|
+
workId: workId || undefined,
|
|
1046
|
+
requestId: typeof args.request_id === 'string' && args.request_id.trim() ? args.request_id.trim() : undefined,
|
|
1047
|
+
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
1048
|
+
interactiveWaitMs: 0,
|
|
1049
|
+
leaseWaitMs: RH_WORK_VERIFY_LEASE_WAIT_MS,
|
|
1050
|
+
simulate: args.simulate_check === true || args.infrastructure_failed === true || args.check_failed === true || args.skipped === true
|
|
1051
|
+
? {
|
|
1052
|
+
infrastructureFailed: args.infrastructure_failed === true,
|
|
1053
|
+
checkFailed: args.check_failed === true,
|
|
1054
|
+
skipped: args.skipped === true,
|
|
1055
|
+
}
|
|
1056
|
+
: undefined,
|
|
1057
|
+
allowDurableCheckExecution: workId ? ({ work }: { work: WorkContract }) => {
|
|
1058
|
+
try {
|
|
1059
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
1060
|
+
const owner = getControllerSession({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, work.workId);
|
|
1061
|
+
if (!sessionlessFacadeControllerAuthorityMatches(owner, identity)) return false;
|
|
1062
|
+
return Boolean(
|
|
1063
|
+
owner
|
|
1064
|
+
&& owner.controllerId === identity.controllerId
|
|
1065
|
+
&& controllerSessionPrincipalId(owner) === identity.principalId
|
|
1066
|
+
&& owner.controllerInstanceId === identity.controllerInstanceId,
|
|
1067
|
+
);
|
|
1068
|
+
} catch {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
} : undefined,
|
|
1072
|
+
};
|
|
1073
|
+
const verification = hasBatchInput
|
|
1074
|
+
? await executeWorkVerificationBatch({
|
|
1075
|
+
...commonVerificationInput,
|
|
1076
|
+
checkIds: (args.check_ids as unknown[]).map(String),
|
|
1077
|
+
})
|
|
1078
|
+
: await executeWorkVerification({ ...commonVerificationInput, checkId });
|
|
1079
|
+
return result(verification.facade as unknown as Record<string, unknown>, verification.isError);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** MCP rh_work transport adapter. Canonical lifecycle semantics remain in Kernel/application services; this layer normalizes ABI input and orchestrates those services. */
|
|
1083
|
+
export async function callWorkAdapter(ctx: MultiRepositoryMcpToolContext, args: Record<string, unknown>): Promise<CallToolResult> {
|
|
1084
|
+
{
|
|
1085
|
+
const repository = selected(ctx, args);
|
|
1086
|
+
const store = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
1087
|
+
const requestedOperation = String(args.operation ?? 'start');
|
|
1088
|
+
const frozenScheduleDeleteId = requestedOperation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith('schedule.delete:')
|
|
1089
|
+
? args.capability_id.slice('schedule.delete:'.length).trim()
|
|
1090
|
+
: '';
|
|
1091
|
+
let frozenImplementationReview: { decision: 'approved' | 'changes_required' | 'blocked'; workId: string } | undefined;
|
|
1092
|
+
let frozenControllerDisposition: ReturnType<typeof parseControllerDispositionCompatibilityCapability>;
|
|
1093
|
+
let frozenControllerRoundOperation: ReturnType<typeof parseControllerRoundCompatibilityCapability>;
|
|
1094
|
+
let frozenPlanObligationDispositions: ReturnType<typeof parsePlanObligationCompatibilityCapability>;
|
|
1095
|
+
let frozenSemanticOperation: ReturnType<typeof parseFrozenSemanticCompatibilityCapability>;
|
|
1096
|
+
try {
|
|
1097
|
+
if (requestedOperation === 'repair' && typeof args.capability_id === 'string') {
|
|
1098
|
+
const capability = args.capability_id.trim();
|
|
1099
|
+
const prefix = 'work.review:';
|
|
1100
|
+
if (capability.startsWith(prefix)) {
|
|
1101
|
+
const remainder = capability.slice(prefix.length);
|
|
1102
|
+
const separator = remainder.indexOf(':');
|
|
1103
|
+
if (separator <= 0 || separator === remainder.length - 1) throw new Error('WORK_IMPLEMENTATION_REVIEW_COMPATIBILITY_INVALID');
|
|
1104
|
+
const decision = remainder.slice(0, separator);
|
|
1105
|
+
const workId = remainder.slice(separator + 1).trim();
|
|
1106
|
+
if (!['approved', 'changes_required', 'blocked'].includes(decision) || !workId) throw new Error('WORK_IMPLEMENTATION_REVIEW_COMPATIBILITY_INVALID');
|
|
1107
|
+
frozenImplementationReview = { decision: decision as 'approved' | 'changes_required' | 'blocked', workId };
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
frozenControllerDisposition = parseControllerDispositionCompatibilityCapability(requestedOperation, args.capability_id);
|
|
1111
|
+
frozenControllerRoundOperation = parseControllerRoundCompatibilityCapability(requestedOperation, args.capability_id);
|
|
1112
|
+
frozenPlanObligationDispositions = parsePlanObligationCompatibilityCapability(requestedOperation, args.capability_id);
|
|
1113
|
+
frozenSemanticOperation = parseFrozenSemanticCompatibilityCapability(requestedOperation, args.capability_id);
|
|
1114
|
+
if (frozenControllerDisposition
|
|
1115
|
+
&& (args.relay_scope_id !== undefined || (frozenControllerDisposition.authorityId && args.controller_authority_id !== undefined))) {
|
|
1116
|
+
throw new Error('CONTROLLER_RELAY_DISPOSITION_COMPATIBILITY_CONFLICT');
|
|
1117
|
+
}
|
|
1118
|
+
if (frozenPlanObligationDispositions && Array.isArray(args.obligation_dispositions)) {
|
|
1119
|
+
throw new Error('PLAN_OBLIGATION_COMPATIBILITY_CONFLICT');
|
|
1120
|
+
}
|
|
1121
|
+
if (frozenSemanticOperation) {
|
|
1122
|
+
const requirementScoped = frozenSemanticOperation.operation !== 'work_review';
|
|
1123
|
+
if (requirementScoped) {
|
|
1124
|
+
const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id.trim() : '';
|
|
1125
|
+
if (!requirementId) throw new Error('FROZEN_SEMANTIC_COMPATIBILITY_SCOPE_REQUIRED: requirement_id must remain explicit outside the compatibility envelope');
|
|
1126
|
+
}
|
|
1127
|
+
for (const key of Object.keys(frozenSemanticOperation.args)) {
|
|
1128
|
+
if (args[key] !== undefined) throw new Error(`FROZEN_SEMANTIC_COMPATIBILITY_CONFLICT: native field ${key} is also present`);
|
|
1129
|
+
}
|
|
1130
|
+
if (frozenSemanticOperation.operation === 'work_review') {
|
|
1131
|
+
const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
1132
|
+
if (!explicitWorkId) throw new Error('FROZEN_SEMANTIC_COMPATIBILITY_SCOPE_REQUIRED: work_id must remain explicit outside the work_review envelope');
|
|
1133
|
+
if (frozenImplementationReview) throw new Error('WORK_IMPLEMENTATION_REVIEW_COMPATIBILITY_CONFLICT');
|
|
1134
|
+
if (args.review_decision !== undefined || args.review_rationale !== undefined) {
|
|
1135
|
+
throw new Error('WORK_IMPLEMENTATION_REVIEW_COMPATIBILITY_CONFLICT');
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
return result(buildFacadeResult({
|
|
1141
|
+
status: 'blocked',
|
|
1142
|
+
summary: error instanceof Error ? error.message : 'Controller round compatibility input is invalid.',
|
|
1143
|
+
data: {},
|
|
1144
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1145
|
+
}
|
|
1146
|
+
if (frozenControllerRoundOperation) {
|
|
1147
|
+
args.relay_scope_id = frozenControllerRoundOperation.relayScopeId;
|
|
1148
|
+
args.controller_authority_id = frozenControllerRoundOperation.authorityId;
|
|
1149
|
+
if (frozenControllerRoundOperation.operation === 'review') {
|
|
1150
|
+
if (args.review_decision !== undefined || args.review_rationale !== undefined) {
|
|
1151
|
+
return result(buildFacadeResult({
|
|
1152
|
+
status: 'blocked',
|
|
1153
|
+
summary: 'CONTROLLER_ROUND_REVIEW_COMPATIBILITY_CONFLICT: native review fields cannot be combined with the frozen review carrier.',
|
|
1154
|
+
data: {},
|
|
1155
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1156
|
+
}
|
|
1157
|
+
args.review_decision = frozenControllerRoundOperation.reviewDecision;
|
|
1158
|
+
args.review_rationale = typeof args.reason === 'string' ? args.reason : '';
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (frozenControllerDisposition) {
|
|
1162
|
+
args.relay_scope_id = frozenControllerDisposition.relayScopeId;
|
|
1163
|
+
if (frozenControllerDisposition.authorityId) args.controller_authority_id = frozenControllerDisposition.authorityId;
|
|
1164
|
+
}
|
|
1165
|
+
if (frozenPlanObligationDispositions) {
|
|
1166
|
+
args.obligation_dispositions = frozenPlanObligationDispositions;
|
|
1167
|
+
}
|
|
1168
|
+
if (frozenSemanticOperation?.operation === 'plan_create') {
|
|
1169
|
+
// Frozen semantic compatibility fills only the field absent from the
|
|
1170
|
+
// older schema. Plan identity/scope/source/steps/relation remain native
|
|
1171
|
+
// arguments and canonical Plan admission/continuity stays authoritative.
|
|
1172
|
+
args.obligation_dispositions = frozenSemanticOperation.args.obligation_dispositions;
|
|
1173
|
+
}
|
|
1174
|
+
if (frozenSemanticOperation?.operation === 'start') {
|
|
1175
|
+
// Older rh_work schemas may lack the technical fields required to admit a
|
|
1176
|
+
// terminal Plan successor. The bounded envelope fills only those closed
|
|
1177
|
+
// fields; objective, Requirement/Plan binding, route policy, engineering
|
|
1178
|
+
// verification, ControllerRound authority and lifecycle remain canonical.
|
|
1179
|
+
Object.assign(args, frozenSemanticOperation.args);
|
|
1180
|
+
}
|
|
1181
|
+
if (frozenSemanticOperation?.operation === 'work_review') {
|
|
1182
|
+
args.review_decision = frozenSemanticOperation.args.decision;
|
|
1183
|
+
args.review_rationale = typeof args.reason === 'string' ? args.reason : '';
|
|
1184
|
+
}
|
|
1185
|
+
if (frozenImplementationReview) {
|
|
1186
|
+
const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
1187
|
+
if (!explicitWorkId || explicitWorkId !== frozenImplementationReview.workId) {
|
|
1188
|
+
return result(buildFacadeResult({
|
|
1189
|
+
status: 'blocked',
|
|
1190
|
+
summary: `WORK_IMPLEMENTATION_REVIEW_SCOPE_MISMATCH: capability targets ${frozenImplementationReview.workId}; exact work_id is required.`,
|
|
1191
|
+
data: { workId: frozenImplementationReview.workId, implementationReviewRecorded: false },
|
|
1192
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1193
|
+
}
|
|
1194
|
+
args.review_decision = frozenImplementationReview.decision;
|
|
1195
|
+
args.review_rationale = typeof args.reason === 'string' ? args.reason : '';
|
|
1196
|
+
}
|
|
1197
|
+
const frozenSemanticFacadeOperation = frozenSemanticOperation?.operation === 'work_review'
|
|
1198
|
+
? 'review'
|
|
1199
|
+
: frozenSemanticOperation?.operation;
|
|
1200
|
+
const operation = frozenSemanticFacadeOperation ?? frozenControllerRoundOperation?.operation ?? (frozenControllerDisposition
|
|
1201
|
+
? 'controller_disposition'
|
|
1202
|
+
: frozenImplementationReview ? 'review'
|
|
1203
|
+
: frozenScheduleDeleteId ? 'schedule_delete' : requestedOperation);
|
|
1204
|
+
if (!allowedFacadeOperations('rh_work').includes(operation)) {
|
|
1205
|
+
return invalidFacadeOperation('rh_work', operation);
|
|
1206
|
+
}
|
|
1207
|
+
if (isRhWorkScheduleOperation(operation)) {
|
|
1208
|
+
return await callRhWorkScheduleAdapter(ctx, repository, operation, args, {
|
|
1209
|
+
scheduleIdOverride: frozenScheduleDeleteId || undefined,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
const technicalRetryWorkId = operation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith('plan.step.retry:')
|
|
1213
|
+
? args.capability_id.slice('plan.step.retry:'.length).trim()
|
|
1214
|
+
: '';
|
|
1215
|
+
if (technicalRetryWorkId) {
|
|
1216
|
+
const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
1217
|
+
if (!explicitWorkId || explicitWorkId !== technicalRetryWorkId) {
|
|
1218
|
+
return result(buildFacadeResult({
|
|
1219
|
+
status: 'blocked',
|
|
1220
|
+
summary: `PLAN_STEP_TECHNICAL_RETRY_SCOPE_MISMATCH: capability targets ${technicalRetryWorkId}; exact work_id is required.`,
|
|
1221
|
+
data: { workId: technicalRetryWorkId, repaired: false },
|
|
1222
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1223
|
+
}
|
|
1224
|
+
try {
|
|
1225
|
+
const work = getWorkContract(store, technicalRetryWorkId);
|
|
1226
|
+
if (!work) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_WORK_NOT_FOUND: ${technicalRetryWorkId}`);
|
|
1227
|
+
const reason = typeof args.reason === 'string' ? args.reason.trim() : '';
|
|
1228
|
+
if (!reason) throw new Error('PLAN_STEP_TECHNICAL_RETRY_REASON_REQUIRED');
|
|
1229
|
+
const handle = readWorkHandle(ctx.controllerHome, repository.repoId, technicalRetryWorkId);
|
|
1230
|
+
const cleanupComplete = Boolean(handle
|
|
1231
|
+
&& handle.managedWorktree
|
|
1232
|
+
&& handle.state === 'cleaned'
|
|
1233
|
+
&& handle.finalization.branchCleanup === 'done'
|
|
1234
|
+
&& handle.finalization.worktreeCleanup === 'done'
|
|
1235
|
+
&& handle.cleanupReceipt?.complete === true
|
|
1236
|
+
&& handle.baseCommit
|
|
1237
|
+
&& handle.expectedHead === handle.baseCommit
|
|
1238
|
+
&& !existsSync(handle.worktreePath));
|
|
1239
|
+
if (!cleanupComplete) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_CLEANUP_INCOMPLETE: ${technicalRetryWorkId}`);
|
|
1240
|
+
const conflicting = work.planId && work.planStepId
|
|
1241
|
+
? listWorkContracts({ ...store, status: 'active', limit: 200 }).filter((candidate) => candidate.planId === work.planId && candidate.planStepId === work.planStepId)
|
|
1242
|
+
: [];
|
|
1243
|
+
if (conflicting.length > 0) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_ACTIVE_WORK_CONFLICT: ${conflicting.map((candidate) => candidate.workId).join(',')}`);
|
|
1244
|
+
const repairedPlan = repairPlanStepForTechnicalRetry(store, { work, cleanupComplete, reason });
|
|
1245
|
+
return result(buildFacadeResult({
|
|
1246
|
+
summary: `Plan step ${work.planId}/${work.planStepId} was restored for an explicit technical retry after terminal Work ${work.workId}; no Work was revived or created.`,
|
|
1247
|
+
data: { workId: work.workId, plan: summarizePlanContract(repairedPlan), repaired: true, replacementWorkCreated: false },
|
|
1248
|
+
}) as unknown as Record<string, unknown>);
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
return result(buildFacadeResult({
|
|
1251
|
+
status: 'blocked',
|
|
1252
|
+
summary: error instanceof Error ? error.message : 'PLAN_STEP_TECHNICAL_RETRY_FAILED',
|
|
1253
|
+
data: { workId: technicalRetryWorkId, repaired: false },
|
|
1254
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const providerRecoveryWorkId = operation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith(CONTROLLER_PROVIDER_RECOVERY_CAPABILITY_PREFIX)
|
|
1259
|
+
? args.capability_id.slice(CONTROLLER_PROVIDER_RECOVERY_CAPABILITY_PREFIX.length).trim()
|
|
1260
|
+
: '';
|
|
1261
|
+
if (providerRecoveryWorkId) {
|
|
1262
|
+
const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
1263
|
+
const relayScopeId = typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : '';
|
|
1264
|
+
const authorityId = typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '';
|
|
1265
|
+
if (!explicitWorkId || explicitWorkId !== providerRecoveryWorkId) {
|
|
1266
|
+
return result(buildFacadeResult({
|
|
1267
|
+
status: 'blocked',
|
|
1268
|
+
summary: `CONTROLLER_PROVIDER_RECOVERY_SCOPE_MISMATCH: capability targets ${providerRecoveryWorkId}; exact work_id is required.`,
|
|
1269
|
+
data: { workId: providerRecoveryWorkId, providerRecovered: false },
|
|
1270
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1271
|
+
}
|
|
1272
|
+
if (!relayScopeId || !authorityId) {
|
|
1273
|
+
return result(buildFacadeResult({
|
|
1274
|
+
status: 'blocked',
|
|
1275
|
+
summary: 'CONTROLLER_PROVIDER_RECOVERY_AUTHORITY_REQUIRED: exact controller_authority_id and relay_scope_id are required.',
|
|
1276
|
+
data: { workId: providerRecoveryWorkId, providerRecovered: false },
|
|
1277
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1278
|
+
}
|
|
1279
|
+
try {
|
|
1280
|
+
const recovered = await recoverControllerRoundAfterVerifiedProviderRepair({
|
|
1281
|
+
controllerHome: ctx.controllerHome,
|
|
1282
|
+
repoId: repository.repoId,
|
|
1283
|
+
repoRoot: repository.canonicalRoot,
|
|
1284
|
+
workId: providerRecoveryWorkId,
|
|
1285
|
+
relayScopeId,
|
|
1286
|
+
authorityId,
|
|
1287
|
+
timeoutMs: typeof args.probe_timeout_ms === 'number' ? args.probe_timeout_ms : undefined,
|
|
1288
|
+
});
|
|
1289
|
+
return result(buildFacadeResult({
|
|
1290
|
+
summary: `Verified ChatGPT provider recovery and rearmed exact ControllerRound for Work ${providerRecoveryWorkId} without changing semantic round identity or recovery budgets.`,
|
|
1291
|
+
data: {
|
|
1292
|
+
workId: providerRecoveryWorkId,
|
|
1293
|
+
providerRecovered: true,
|
|
1294
|
+
recoveryAuditId: recovered.audit.id,
|
|
1295
|
+
provider: recovered.probe.provider,
|
|
1296
|
+
relay: recovered.relay,
|
|
1297
|
+
},
|
|
1298
|
+
}) as unknown as Record<string, unknown>);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
return result(buildFacadeResult({
|
|
1301
|
+
status: 'blocked',
|
|
1302
|
+
summary: error instanceof Error ? error.message : 'Controller provider recovery failed.',
|
|
1303
|
+
data: { workId: providerRecoveryWorkId, providerRecovered: false },
|
|
1304
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const authorityRecoveryWorkId = operation === 'repair' && typeof args.capability_id === 'string' && args.capability_id.startsWith('controller.authority.recover:')
|
|
1309
|
+
? args.capability_id.slice('controller.authority.recover:'.length).trim()
|
|
1310
|
+
: '';
|
|
1311
|
+
if (authorityRecoveryWorkId) {
|
|
1312
|
+
const explicitWorkId = typeof args.work_id === 'string' ? args.work_id.trim() : '';
|
|
1313
|
+
if (!explicitWorkId || explicitWorkId !== authorityRecoveryWorkId) {
|
|
1314
|
+
return result(buildFacadeResult({
|
|
1315
|
+
status: 'blocked',
|
|
1316
|
+
summary: `WORK_CONTROLLER_AUTHORITY_RECOVERY_SCOPE_MISMATCH: capability targets ${authorityRecoveryWorkId}; exact work_id is required.`,
|
|
1317
|
+
data: { workId: authorityRecoveryWorkId, authorityRecovered: false },
|
|
1318
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1319
|
+
}
|
|
1320
|
+
try {
|
|
1321
|
+
const recovered = recoverControllerAuthority({
|
|
1322
|
+
controllerHome: ctx.controllerHome,
|
|
1323
|
+
repoId: repository.repoId,
|
|
1324
|
+
repositoryActiveCheckoutId: repository.activeCheckoutId,
|
|
1325
|
+
workId: authorityRecoveryWorkId,
|
|
1326
|
+
requestedBy: typeof args.requested_by === 'string' ? args.requested_by : undefined,
|
|
1327
|
+
identity: authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true }),
|
|
1328
|
+
runtime: runtimeIdentitySnapshot(ctx),
|
|
1329
|
+
leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
1330
|
+
});
|
|
1331
|
+
return result(buildFacadeResult({
|
|
1332
|
+
summary: `Controller authority for exact Work ${authorityRecoveryWorkId} was recovered without changing semantic Work identity, relay scope, or recovery budgets.`,
|
|
1333
|
+
data: recovered,
|
|
1334
|
+
}) as unknown as Record<string, unknown>);
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
return result(buildFacadeResult({
|
|
1337
|
+
status: 'blocked',
|
|
1338
|
+
summary: error instanceof Error ? error.message : 'Controller authority recovery failed.',
|
|
1339
|
+
data: { workId: authorityRecoveryWorkId, authorityRecovered: false },
|
|
1340
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (operation === 'workflow_execute' || operation === 'workflow_reconcile') {
|
|
1345
|
+
try {
|
|
1346
|
+
const workId = String(args.work_id ?? '').trim();
|
|
1347
|
+
const workflowId = String(args.workflow_id ?? '').trim();
|
|
1348
|
+
const runId = String(args.workflow_run_id ?? '').trim();
|
|
1349
|
+
if (!workId || !workflowId || !runId) throw new Error('WORKFLOW_FACADE_IDENTITY_REQUIRED');
|
|
1350
|
+
const work = getWorkContract(store, workId);
|
|
1351
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
1352
|
+
assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
1353
|
+
const owner = getControllerSession(store, workId);
|
|
1354
|
+
const relay = getControllerRoundRelay(store, workId);
|
|
1355
|
+
if (!owner) throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${workId}`);
|
|
1356
|
+
const authorityId = relay?.authorityId?.trim() || (typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '');
|
|
1357
|
+
if (!authorityId) throw new Error('WORKFLOW_CONTROLLER_AUTHORITY_REQUIRED');
|
|
1358
|
+
const workRepository = selectRepositoryCheckout(repository, work.checkoutId);
|
|
1359
|
+
const executionIdentity = executionIdentityForRepository(workRepository, { workId });
|
|
1360
|
+
if (Object.values(XIAOHONGSHU_WORKFLOW_IDS).includes(workflowId as never)) {
|
|
1361
|
+
ensureXiaohongshuWorkflowInstalled(ctx.controllerHome, workflowId);
|
|
1362
|
+
}
|
|
1363
|
+
const projectId = typeof args.workflow_scope_project_id === 'string' ? args.workflow_scope_project_id.trim() : '';
|
|
1364
|
+
const registryScope = projectId ? { kind: 'project' as const, projectId } : { kind: 'controller' as const };
|
|
1365
|
+
const workflowInputs = args.workflow_inputs && typeof args.workflow_inputs === 'object' && !Array.isArray(args.workflow_inputs)
|
|
1366
|
+
? args.workflow_inputs as Record<string, import('../../../packages/workflow-runtime/api/index').WorkflowJsonValue>
|
|
1367
|
+
: {};
|
|
1368
|
+
const base = {
|
|
1369
|
+
controllerHome: ctx.controllerHome,
|
|
1370
|
+
repository: workRepository,
|
|
1371
|
+
executionIdentity,
|
|
1372
|
+
workId,
|
|
1373
|
+
controller: { controllerId: owner.controllerId, authorityId },
|
|
1374
|
+
runId,
|
|
1375
|
+
registryScope,
|
|
1376
|
+
workflowId,
|
|
1377
|
+
inputs: workflowInputs,
|
|
1378
|
+
timeoutMs: typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
1379
|
+
};
|
|
1380
|
+
const learningMetadata = (workflow: Awaited<ReturnType<typeof executeRegisteredWorkflow>>) => {
|
|
1381
|
+
const persisted = readWorkflowRun(ctx.controllerHome, workId, runId)?.value;
|
|
1382
|
+
let outcomeCollectionSchedule;
|
|
1383
|
+
let outcomeCollectionError: string | undefined;
|
|
1384
|
+
if (workflow.status === 'succeeded' && workflow.publicationReceipt) {
|
|
1385
|
+
try {
|
|
1386
|
+
outcomeCollectionSchedule = schedulePublicationOutcomeCollection({ controllerHome: ctx.controllerHome, repoId: repository.repoId, workId, publication: workflow.publicationReceipt, workflowInputs });
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
outcomeCollectionError = error instanceof Error ? error.message : 'OUTCOME_COLLECTION_SCHEDULE_FAILED';
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return {
|
|
1392
|
+
...(persisted?.evidenceRef ? { workflowEvidenceRef: persisted.evidenceRef } : {}),
|
|
1393
|
+
...(outcomeCollectionSchedule ? { outcomeCollectionSchedule } : {}),
|
|
1394
|
+
...(outcomeCollectionError ? { outcomeCollectionError } : {}),
|
|
1395
|
+
};
|
|
1396
|
+
};
|
|
1397
|
+
if (operation === 'workflow_reconcile') {
|
|
1398
|
+
const reconciliationRequestId = String(args.workflow_reconciliation_request_id ?? '').trim();
|
|
1399
|
+
if (!reconciliationRequestId) throw new Error('WORKFLOW_RECONCILIATION_REQUEST_ID_REQUIRED');
|
|
1400
|
+
const reconciled = await observeAndReconcileRegisteredWorkflow({ ...base, reconciliationRequestId });
|
|
1401
|
+
if (reconciled.status === 'running') {
|
|
1402
|
+
const resumed = await executeRegisteredWorkflow(base);
|
|
1403
|
+
return result(buildFacadeResult({
|
|
1404
|
+
summary: `Workflow ${workflowId}/${runId} reconciled from canonical observation and resumed without replaying the uncertain effect.`,
|
|
1405
|
+
data: { workflow: resumed, ...learningMetadata(resumed) },
|
|
1406
|
+
}) as unknown as Record<string, unknown>);
|
|
1407
|
+
}
|
|
1408
|
+
return result(buildFacadeResult({
|
|
1409
|
+
status: reconciled.status === 'failed' ? 'blocked' : 'ok',
|
|
1410
|
+
summary: `Workflow ${workflowId}/${runId} reconciliation settled as ${reconciled.status}.`,
|
|
1411
|
+
data: { workflow: reconciled },
|
|
1412
|
+
}) as unknown as Record<string, unknown>, reconciled.status === 'failed');
|
|
1413
|
+
}
|
|
1414
|
+
const executed = await executeRegisteredWorkflow(base);
|
|
1415
|
+
return result(buildFacadeResult({
|
|
1416
|
+
status: executed.status === 'failed' || executed.status === 'reconcile_required' ? 'blocked' : 'ok',
|
|
1417
|
+
summary: `Workflow ${workflowId}/${runId} is ${executed.status}.`,
|
|
1418
|
+
data: { workflow: executed, ...learningMetadata(executed) },
|
|
1419
|
+
}) as unknown as Record<string, unknown>, executed.status === 'failed' || executed.status === 'reconcile_required');
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
return result(buildFacadeResult({
|
|
1422
|
+
status: 'blocked',
|
|
1423
|
+
summary: error instanceof Error ? error.message : 'Workflow execution failed.',
|
|
1424
|
+
data: { workflowExecuted: false },
|
|
1425
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
if (operation === 'outcome_record' || operation === 'experience_record') {
|
|
1430
|
+
try {
|
|
1431
|
+
const workId = String(args.work_id ?? '').trim();
|
|
1432
|
+
if (!workId) throw new Error('LEARNING_LOOP_WORK_ID_REQUIRED');
|
|
1433
|
+
const work = getWorkContract(store, workId);
|
|
1434
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
1435
|
+
assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
1436
|
+
const owner = getControllerSession(store, workId);
|
|
1437
|
+
const relay = getControllerRoundRelay(store, workId);
|
|
1438
|
+
if (!owner) throw new Error(`WORK_CONTROLLER_OWNER_REQUIRED: ${workId}`);
|
|
1439
|
+
const authorityId = relay?.authorityId?.trim() || (typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '');
|
|
1440
|
+
if (!authorityId) throw new Error('LEARNING_LOOP_CONTROLLER_AUTHORITY_REQUIRED');
|
|
1441
|
+
const identity = { workId, controllerId: owner.controllerId, authorityId };
|
|
1442
|
+
if (operation === 'outcome_record') {
|
|
1443
|
+
if (!args.outcome_observation || typeof args.outcome_observation !== 'object' || Array.isArray(args.outcome_observation)) {
|
|
1444
|
+
throw new Error('OUTCOME_OBSERVATION_REQUIRED');
|
|
1445
|
+
}
|
|
1446
|
+
const outcome = recordControllerOutcome({
|
|
1447
|
+
controllerHome: ctx.controllerHome,
|
|
1448
|
+
repoId: repository.repoId,
|
|
1449
|
+
identity,
|
|
1450
|
+
draft: args.outcome_observation as ControllerOutcomeObservationDraft,
|
|
1451
|
+
});
|
|
1452
|
+
return result(buildFacadeResult({
|
|
1453
|
+
summary: `OutcomeObservation ${outcome.id} recorded from canonical Work/ControllerRound evidence.`,
|
|
1454
|
+
data: { outcome },
|
|
1455
|
+
}) as unknown as Record<string, unknown>);
|
|
1456
|
+
}
|
|
1457
|
+
if (!args.experience_draft || typeof args.experience_draft !== 'object' || Array.isArray(args.experience_draft)) {
|
|
1458
|
+
throw new Error('EXPERIENCE_DRAFT_REQUIRED');
|
|
1459
|
+
}
|
|
1460
|
+
const experience = recordControllerExperience({
|
|
1461
|
+
controllerHome: ctx.controllerHome,
|
|
1462
|
+
repoId: repository.repoId,
|
|
1463
|
+
identity,
|
|
1464
|
+
draft: args.experience_draft as ControllerExperienceDraft,
|
|
1465
|
+
qualityAdjustmentFingerprint: typeof args.quality_adjustment_fingerprint === 'string' ? args.quality_adjustment_fingerprint.trim() || undefined : undefined,
|
|
1466
|
+
});
|
|
1467
|
+
return result(buildFacadeResult({
|
|
1468
|
+
summary: `Experience ${experience.id} recorded from canonical evidence for reuse by the next ControllerRound.`,
|
|
1469
|
+
data: { experience },
|
|
1470
|
+
}) as unknown as Record<string, unknown>);
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
return result(buildFacadeResult({
|
|
1473
|
+
status: 'blocked',
|
|
1474
|
+
summary: error instanceof Error ? error.message : 'Learning-loop record failed.',
|
|
1475
|
+
data: { recorded: false },
|
|
1476
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (operation === 'controller_get_owner') {
|
|
1481
|
+
const owner = getControllerSession(store, String(args.work_id ?? '').trim());
|
|
1482
|
+
return result(buildFacadeResult({ summary: owner ? `Work is claimed by ${owner.controllerId}.` : 'Work has no active controller owner.', data: { owner } }) as unknown as Record<string, unknown>);
|
|
1483
|
+
}
|
|
1484
|
+
if (operation === 'controller_claim') {
|
|
1485
|
+
try {
|
|
1486
|
+
const workId = String(args.work_id ?? '').trim();
|
|
1487
|
+
const work = getWorkContract(store, workId);
|
|
1488
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
1489
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
1490
|
+
const activeLaunchReservation = identity.controllerType === 'codex'
|
|
1491
|
+
? getExternalControllerLaunchReservation(store, workId)
|
|
1492
|
+
: undefined;
|
|
1493
|
+
if (activeLaunchReservation?.controllerType === 'codex') {
|
|
1494
|
+
const expectedLaunchIdentity = providerMcpReservationIdentity('codex', activeLaunchReservation.reservationId);
|
|
1495
|
+
if (identity.principalId !== expectedLaunchIdentity.principalId || identity.sessionId !== expectedLaunchIdentity.sessionId) {
|
|
1496
|
+
throw new Error(`WORK_CONTROLLER_LAUNCH_IDENTITY_MISMATCH: ${workId}; active Codex launch reservation requires its exact reservation-scoped MCP identity.`);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
let authorizedRelay = assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
1500
|
+
const observedOwner = getControllerSession(store, workId);
|
|
1501
|
+
const dispatchedRelay = getControllerRoundRelay(store, workId);
|
|
1502
|
+
const requestedRelayScopeId = typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : '';
|
|
1503
|
+
if (!authorizedRelay && identity.controllerAuthorityId && requestedRelayScopeId) {
|
|
1504
|
+
authorizedRelay = resolveRequirementControllerRoundRelayForWork(store, {
|
|
1505
|
+
workId,
|
|
1506
|
+
authorityId: identity.controllerAuthorityId,
|
|
1507
|
+
relayScopeId: requestedRelayScopeId,
|
|
1508
|
+
});
|
|
1509
|
+
if (!authorizedRelay) {
|
|
1510
|
+
throw new Error(`WORK_CONTROLLER_ROUND_AUTHORITY_UNBOUND: ${workId}:${requestedRelayScopeId}`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (observedOwner && observedOwner.authorityDigest
|
|
1514
|
+
&& (observedOwner.sessionId !== identity.sessionId || (observedOwner.controllerInstanceId?.trim() || '') !== identity.controllerInstanceId)
|
|
1515
|
+
&& !dispatchedRelay?.authorityId?.trim()
|
|
1516
|
+
&& !controllerSessionAuthorityMatches(observedOwner, identity.controllerAuthorityId)) {
|
|
1517
|
+
throw new Error(`WORK_CONTROLLER_SCOPE_MISMATCH: ${workId}; controller_claim requires the existing Work-bound controller authority after transport rotation.`);
|
|
1518
|
+
}
|
|
1519
|
+
const inheritedRequirementAuthority = !dispatchedRelay?.authorityId?.trim() && authorizedRelay?.authorityId?.trim()
|
|
1520
|
+
? {
|
|
1521
|
+
authorityId: authorizedRelay.authorityId!.trim(),
|
|
1522
|
+
authorityDigest: controllerSessionAuthorityDigest(authorizedRelay.authorityId!),
|
|
1523
|
+
}
|
|
1524
|
+
: undefined;
|
|
1525
|
+
const directAuthority = dispatchedRelay?.authorityId?.trim() || inheritedRequirementAuthority
|
|
1526
|
+
? undefined
|
|
1527
|
+
: mintControllerSessionAuthority();
|
|
1528
|
+
const sessionAuthority = inheritedRequirementAuthority ?? directAuthority;
|
|
1529
|
+
const crossOwnerRecovery = Boolean(observedOwner)
|
|
1530
|
+
&& (
|
|
1531
|
+
observedOwner!.controllerId !== identity.controllerId
|
|
1532
|
+
|| controllerSessionPrincipalId(observedOwner!) !== identity.principalId
|
|
1533
|
+
)
|
|
1534
|
+
&& dispatchedChatgptRelayAuthorizesStaleControllerRecovery(store, workId, dispatchedRelay, identity.controllerType);
|
|
1535
|
+
const session = resumeControllerSession(store, {
|
|
1536
|
+
workId,
|
|
1537
|
+
controllerId: identity.controllerId,
|
|
1538
|
+
controllerType: identity.controllerType,
|
|
1539
|
+
sessionId: identity.sessionId,
|
|
1540
|
+
...(sessionAuthority ? { authorityDigest: sessionAuthority.authorityDigest } : {}),
|
|
1541
|
+
principalId: identity.principalId,
|
|
1542
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
1543
|
+
...(crossOwnerRecovery
|
|
1544
|
+
? {
|
|
1545
|
+
expectedClaimGeneration: observedOwner!.claimGeneration,
|
|
1546
|
+
allowStaleRecovery: true,
|
|
1547
|
+
}
|
|
1548
|
+
: {}),
|
|
1549
|
+
leaseMs: typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
1550
|
+
});
|
|
1551
|
+
const permissionSnapshotVersion = currentPermissionSnapshotVersion(ctx.controllerHome, repository.repoId);
|
|
1552
|
+
const executionSession = startExecutionSession(ctx.controllerHome, {
|
|
1553
|
+
sessionId: identity.sessionId,
|
|
1554
|
+
principalId: identity.principalId,
|
|
1555
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
1556
|
+
permissionSnapshotVersion,
|
|
1557
|
+
});
|
|
1558
|
+
updateExecutionSession(ctx.controllerHome, {
|
|
1559
|
+
sessionId: executionSession.sessionId,
|
|
1560
|
+
principalId: executionSession.principalId,
|
|
1561
|
+
controllerInstanceId: executionSession.controllerInstanceId,
|
|
1562
|
+
}, {
|
|
1563
|
+
activeRepositoryId: repository.repoId,
|
|
1564
|
+
activeCheckoutId: work.checkoutId || repository.activeCheckoutId,
|
|
1565
|
+
activeWorkId: work.workId,
|
|
1566
|
+
permissionSnapshotVersion,
|
|
1567
|
+
lastValidatedAt: new Date().toISOString(),
|
|
1568
|
+
});
|
|
1569
|
+
const assistantContextBundle = prepareControllerAssistantContextBundle(store, workId);
|
|
1570
|
+
const relay = acknowledgeControllerRoundClaim(
|
|
1571
|
+
{ controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
1572
|
+
{ workId, session, assistantContextSnapshot: assistantContextBundle?.snapshot ?? null },
|
|
1573
|
+
);
|
|
1574
|
+
return result(buildFacadeResult({
|
|
1575
|
+
summary: relay?.status === 'claimed'
|
|
1576
|
+
? `Controller ${session.controllerId} claimed ${session.workId}; the dispatched ChatGPT round is mechanically acknowledged and still requires an explicit semantic disposition.`
|
|
1577
|
+
: `Controller ${session.controllerId} claimed ${session.workId}.`,
|
|
1578
|
+
data: {
|
|
1579
|
+
session,
|
|
1580
|
+
relay,
|
|
1581
|
+
assistantContext: assistantContextBundle?.rendered ?? prepareControllerAssistantContext(store, workId),
|
|
1582
|
+
assistantContextSnapshot: assistantContextBundle?.snapshot,
|
|
1583
|
+
controllerAuthorityId: dispatchedRelay?.authorityId?.trim() || inheritedRequirementAuthority?.authorityId || directAuthority?.authorityId,
|
|
1584
|
+
controllerAuthorityCarrier: dispatchedRelay?.authorityId?.trim() || inheritedRequirementAuthority
|
|
1585
|
+
? 'controller_authority_id'
|
|
1586
|
+
: 'controller_authority_id_or_session_id_compat',
|
|
1587
|
+
},
|
|
1588
|
+
}) as unknown as Record<string, unknown>);
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller claim failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (operation === 'controller_disposition') {
|
|
1594
|
+
try {
|
|
1595
|
+
const workId = String(args.work_id ?? '').trim();
|
|
1596
|
+
const disposition = frozenControllerDisposition?.disposition ?? String(args.disposition ?? '').trim();
|
|
1597
|
+
if (!['continue_immediately', 'wait', 'wait_for_user', 'goal_complete'].includes(disposition)) {
|
|
1598
|
+
throw new Error('CONTROLLER_RELAY_DISPOSITION_INVALID');
|
|
1599
|
+
}
|
|
1600
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
|
|
1601
|
+
const work = getWorkContract(store, workId);
|
|
1602
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
1603
|
+
const currentRelay = getControllerRoundRelay(store, workId);
|
|
1604
|
+
const terminalGoalComplete = work.status === 'completed' && disposition === 'goal_complete';
|
|
1605
|
+
const terminalSuccessorContinuation = work.status === 'completed'
|
|
1606
|
+
&& disposition === 'continue_immediately'
|
|
1607
|
+
&& Boolean(currentRelay?.successorWorkId);
|
|
1608
|
+
const terminalRoundClosure = terminalGoalComplete || terminalSuccessorContinuation;
|
|
1609
|
+
const currentOwner = getControllerSession(store, workId);
|
|
1610
|
+
if (!currentOwner && terminalRoundClosure) {
|
|
1611
|
+
// Live ownership remains the compatibility fence for ordinary rounds.
|
|
1612
|
+
// Once a terminal predecessor lease is gone, only the exact opaque
|
|
1613
|
+
// ControllerRound capability may authorize the semantic close.
|
|
1614
|
+
assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
1615
|
+
}
|
|
1616
|
+
if (!currentOwner && !terminalRoundClosure) {
|
|
1617
|
+
if (work.status === 'failed' || work.status === 'cancelled' || work.status === 'completed') {
|
|
1618
|
+
throw new Error(`CONTROLLER_RELAY_WORK_TERMINAL: ${work.status}`);
|
|
1619
|
+
}
|
|
1620
|
+
throw new Error(`CONTROLLER_RELAY_ACTIVE_CLAIM_REQUIRED: ${workId}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (currentOwner) {
|
|
1623
|
+
if (currentOwner.controllerType !== 'chatgpt') throw new Error(`CONTROLLER_RELAY_CHATGPT_ONLY: ${workId}`);
|
|
1624
|
+
if (currentOwner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
|
|
1625
|
+
if ((controllerSessionPrincipalId(currentOwner)) !== identity.principalId) {
|
|
1626
|
+
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
1627
|
+
}
|
|
1628
|
+
// Finalization may terminalize the Work before the prompt-required
|
|
1629
|
+
// goal_complete disposition is submitted. A frozen MCP client may also
|
|
1630
|
+
// rotate transport sessions between those two calls. Never resume or
|
|
1631
|
+
// rewrite a terminal Work lease here; terminal authorization is fenced
|
|
1632
|
+
// by the already-claimed relay lineage in submitControllerRoundDisposition.
|
|
1633
|
+
if (!terminalRoundClosure) {
|
|
1634
|
+
bindFacadeControllerOwnership(ctx, store, workId, { ...identity, controllerType: 'chatgpt' }, {
|
|
1635
|
+
relayScopeId: typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : undefined,
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
const explicitRequirementId = typeof args.requirement_id === 'string' ? args.requirement_id.trim() : '';
|
|
1640
|
+
if (explicitRequirementId && work.requirementId && explicitRequirementId !== work.requirementId) {
|
|
1641
|
+
throw new Error(`CONTROLLER_RELAY_REQUIREMENT_MISMATCH: Work ${workId} belongs to ${work.requirementId}, not ${explicitRequirementId}`);
|
|
1642
|
+
}
|
|
1643
|
+
const rationale = typeof args.reason === 'string' ? args.reason.trim() : '';
|
|
1644
|
+
const chatgptBinding = chatgptControllerRoundBinding(store, workId);
|
|
1645
|
+
let relay: ControllerRoundRelayRecord;
|
|
1646
|
+
let requirementAcceptance;
|
|
1647
|
+
if (disposition === 'goal_complete' && work.requirementId) {
|
|
1648
|
+
if (!rationale) throw new Error('REQUIREMENT_ACCEPTANCE_METADATA_REQUIRED: goal_complete for a Requirement-bound Work requires reason');
|
|
1649
|
+
const completedGoal = completeRequirementGoal({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
|
|
1650
|
+
workId,
|
|
1651
|
+
identity,
|
|
1652
|
+
requirementId: work.requirementId,
|
|
1653
|
+
rationale,
|
|
1654
|
+
relayScopeId: frozenControllerDisposition?.relayScopeId ?? (typeof args.relay_scope_id === 'string' ? args.relay_scope_id : undefined),
|
|
1655
|
+
handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
|
|
1656
|
+
stateFingerprint: typeof args.state_fingerprint === 'string' ? args.state_fingerprint : undefined,
|
|
1657
|
+
bindingId: chatgptBinding?.bindingId,
|
|
1658
|
+
maxRounds: typeof args.max_rounds === 'number' ? args.max_rounds : undefined,
|
|
1659
|
+
maxRepeatedState: typeof args.max_repeated_state === 'number' ? args.max_repeated_state : undefined,
|
|
1660
|
+
maxFailures: typeof args.max_failures === 'number' ? args.max_failures : undefined,
|
|
1661
|
+
});
|
|
1662
|
+
relay = completedGoal.relay;
|
|
1663
|
+
requirementAcceptance = completedGoal.requirementAcceptance;
|
|
1664
|
+
} else {
|
|
1665
|
+
const existingRelay = disposition === 'goal_complete' ? getControllerRoundRelay(store, workId) : undefined;
|
|
1666
|
+
if (existingRelay?.status === 'goal_complete' && existingRelay.disposition === 'goal_complete') {
|
|
1667
|
+
relay = existingRelay;
|
|
1668
|
+
} else {
|
|
1669
|
+
try {
|
|
1670
|
+
relay = submitControllerRoundDisposition({ controllerHome: ctx.controllerHome, repoId: repository.repoId }, {
|
|
1671
|
+
workId,
|
|
1672
|
+
identity,
|
|
1673
|
+
disposition: disposition as ControllerRoundDisposition,
|
|
1674
|
+
executionQualityDecisions: args.execution_quality_decisions as Parameters<typeof submitControllerRoundDisposition>[1]['executionQualityDecisions'],
|
|
1675
|
+
executionQualityAdjustmentResults: args.execution_quality_adjustment_results as Parameters<typeof submitControllerRoundDisposition>[1]['executionQualityAdjustmentResults'],
|
|
1676
|
+
assistantContextDigest: typeof args.assistant_context_digest === 'string' ? args.assistant_context_digest : undefined,
|
|
1677
|
+
assistantContextUsage: args.assistant_context_usage as Parameters<typeof submitControllerRoundDisposition>[1]['assistantContextUsage'],
|
|
1678
|
+
relayScopeId: frozenControllerDisposition?.relayScopeId ?? (typeof args.relay_scope_id === 'string' ? args.relay_scope_id : undefined),
|
|
1679
|
+
requirementId: typeof args.requirement_id === 'string' ? args.requirement_id : undefined,
|
|
1680
|
+
handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
|
|
1681
|
+
stateFingerprint: typeof args.state_fingerprint === 'string' ? args.state_fingerprint : undefined,
|
|
1682
|
+
reason: typeof args.reason === 'string' ? args.reason : undefined,
|
|
1683
|
+
bindingId: chatgptBinding?.bindingId,
|
|
1684
|
+
maxRounds: typeof args.max_rounds === 'number' ? args.max_rounds : undefined,
|
|
1685
|
+
maxRepeatedState: typeof args.max_repeated_state === 'number' ? args.max_repeated_state : undefined,
|
|
1686
|
+
maxFailures: typeof args.max_failures === 'number' ? args.max_failures : undefined,
|
|
1687
|
+
});
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
const raced = disposition === 'goal_complete' ? getControllerRoundRelay(store, workId) : undefined;
|
|
1690
|
+
if (!raced || raced.status !== 'goal_complete' || raced.disposition !== 'goal_complete') throw error;
|
|
1691
|
+
relay = raced;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const continuationSchedule = ensureControllerDispositionContinuation(
|
|
1696
|
+
ctx.controllerHome,
|
|
1697
|
+
repository.repoId,
|
|
1698
|
+
relay,
|
|
1699
|
+
);
|
|
1700
|
+
return result(buildFacadeResult({
|
|
1701
|
+
status: relay.status === 'blocked' ? 'blocked' : 'ok',
|
|
1702
|
+
summary: relay.status === 'pending_release'
|
|
1703
|
+
? `Controller disposition ${relay.disposition} recorded; relay will dispatch only after the current lease is released.`
|
|
1704
|
+
: continuationSchedule
|
|
1705
|
+
? `Controller disposition ${relay.disposition} recorded with status ${relay.status}; exact-Work continuation is now event-driven by ${continuationSchedule.trigger.eventName}.`
|
|
1706
|
+
: `Controller disposition ${relay.disposition} recorded with status ${relay.status}.`,
|
|
1707
|
+
data: { relay, ...(requirementAcceptance ? { requirementAcceptance } : {}), ...(continuationSchedule ? { continuationSchedule } : {}) },
|
|
1708
|
+
}) as unknown as Record<string, unknown>, relay.status === 'blocked');
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller disposition failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
if (operation === 'controller_release') {
|
|
1714
|
+
try {
|
|
1715
|
+
const workId = String(args.work_id ?? '').trim();
|
|
1716
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
|
|
1717
|
+
try {
|
|
1718
|
+
assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
const observed = getControllerSession(store, workId);
|
|
1721
|
+
const runtime = runtimeIdentitySnapshot(ctx);
|
|
1722
|
+
const samePrincipalCanonicalRuntimeMigration = Boolean(
|
|
1723
|
+
observed
|
|
1724
|
+
&& observed.controllerId === identity.controllerId
|
|
1725
|
+
&& observed.controllerType === identity.controllerType
|
|
1726
|
+
&& controllerSessionPrincipalId(observed) === identity.principalId
|
|
1727
|
+
&& (observed.controllerInstanceId?.trim() || '') !== identity.controllerInstanceId
|
|
1728
|
+
&& runtime.running
|
|
1729
|
+
&& runtime.runtimeInstanceId === identity.controllerInstanceId,
|
|
1730
|
+
);
|
|
1731
|
+
if (!samePrincipalCanonicalRuntimeMigration) throw error;
|
|
1732
|
+
}
|
|
1733
|
+
const observedOwner = getControllerSession(store, workId);
|
|
1734
|
+
const work = getWorkContract(store, workId);
|
|
1735
|
+
const terminalWork = work ? ['completed', 'failed', 'cancelled'].includes(work.status) : false;
|
|
1736
|
+
const pendingTerminalRelay = !observedOwner && work?.status === 'completed'
|
|
1737
|
+
? getControllerRoundRelay(store, workId)
|
|
1738
|
+
: undefined;
|
|
1739
|
+
const retainedReleaseWitness = pendingTerminalRelay?.status === 'pending_release'
|
|
1740
|
+
? getRetainedControllerSession(store, workId)
|
|
1741
|
+
: undefined;
|
|
1742
|
+
if (retainedReleaseWitness && pendingTerminalRelay) {
|
|
1743
|
+
const retainedPrincipal = controllerSessionPrincipalId(retainedReleaseWitness);
|
|
1744
|
+
if (retainedReleaseWitness.controllerId !== identity.controllerId
|
|
1745
|
+
|| retainedReleaseWitness.controllerType !== identity.controllerType
|
|
1746
|
+
|| retainedPrincipal !== identity.principalId
|
|
1747
|
+
|| retainedReleaseWitness.claimGeneration !== pendingTerminalRelay.claimGeneration
|
|
1748
|
+
|| pendingTerminalRelay.controllerId !== identity.controllerId
|
|
1749
|
+
|| pendingTerminalRelay.principalId !== identity.principalId) {
|
|
1750
|
+
throw new Error(`CONTROLLER_RELAY_RELEASE_FENCE_MISMATCH: ${workId}`);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
const owner = observedOwner
|
|
1754
|
+
? terminalWork
|
|
1755
|
+
? (() => {
|
|
1756
|
+
// A terminal Work cannot be rebound to a replacement MCP transport.
|
|
1757
|
+
// Authenticate the caller against the observed durable controller epoch
|
|
1758
|
+
// without mutating it; releaseControllerSessionWithAuthority remains the
|
|
1759
|
+
// exact claim-generation CAS authority for physical lease release.
|
|
1760
|
+
const ownerPrincipal = observedOwner.principalId?.trim() || observedOwner.controllerId;
|
|
1761
|
+
const ownerInstanceId = observedOwner.controllerInstanceId?.trim() || '';
|
|
1762
|
+
if (observedOwner.controllerId !== identity.controllerId) {
|
|
1763
|
+
throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${workId}`);
|
|
1764
|
+
}
|
|
1765
|
+
if (observedOwner.controllerType !== identity.controllerType) {
|
|
1766
|
+
throw new Error(`WORK_CONTROLLER_TYPE_MISMATCH: ${workId}`);
|
|
1767
|
+
}
|
|
1768
|
+
if (ownerPrincipal !== identity.principalId) {
|
|
1769
|
+
throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${workId}`);
|
|
1770
|
+
}
|
|
1771
|
+
if (!ownerInstanceId || ownerInstanceId !== identity.controllerInstanceId) {
|
|
1772
|
+
throw new Error(`WORK_CONTROLLER_INSTANCE_MISMATCH: ${workId}`);
|
|
1773
|
+
}
|
|
1774
|
+
if (typeof observedOwner.claimGeneration !== 'number' || observedOwner.claimGeneration < 1) {
|
|
1775
|
+
throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
1776
|
+
}
|
|
1777
|
+
return observedOwner;
|
|
1778
|
+
})()
|
|
1779
|
+
: bindFacadeControllerOwnership(ctx, store, workId, identity, {
|
|
1780
|
+
relayScopeId: typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : undefined,
|
|
1781
|
+
})
|
|
1782
|
+
: undefined;
|
|
1783
|
+
if (owner) {
|
|
1784
|
+
const ownerPrincipal = controllerSessionPrincipalId(owner);
|
|
1785
|
+
const ownerInstanceId = owner.controllerInstanceId?.trim() || '';
|
|
1786
|
+
if (typeof owner.claimGeneration !== 'number' || owner.claimGeneration < 1) {
|
|
1787
|
+
throw new Error(`WORK_CONTROLLER_CLAIM_GENERATION_REQUIRED: ${workId}`);
|
|
1788
|
+
}
|
|
1789
|
+
const released = releaseControllerSessionWithAuthority(store, {
|
|
1790
|
+
workId,
|
|
1791
|
+
actor: `controller-release:${identity.controllerId}:${identity.controllerInstanceId}`,
|
|
1792
|
+
authority: {
|
|
1793
|
+
controllerId: owner.controllerId,
|
|
1794
|
+
controllerType: owner.controllerType,
|
|
1795
|
+
principalId: ownerPrincipal,
|
|
1796
|
+
controllerInstanceId: ownerInstanceId,
|
|
1797
|
+
claimGeneration: owner.claimGeneration,
|
|
1798
|
+
},
|
|
1799
|
+
});
|
|
1800
|
+
if (!released.allowed) {
|
|
1801
|
+
throw new Error(`WORK_CONTROLLER_RELEASE_FENCED: ${workId}:${released.reason}`);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
const executionSession = readExecutionSession(ctx.controllerHome, identity);
|
|
1805
|
+
if (executionSession?.activeWorkId === workId) {
|
|
1806
|
+
updateExecutionSession(ctx.controllerHome, identity, { activeWorkId: undefined, lastValidatedAt: new Date().toISOString() });
|
|
1807
|
+
}
|
|
1808
|
+
const relayStore = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
1809
|
+
const releasedSession = owner ?? retainedReleaseWitness;
|
|
1810
|
+
const relay = releasedSession ? beginControllerRoundRelayAfterRelease(
|
|
1811
|
+
relayStore,
|
|
1812
|
+
{ workId, releasedSession },
|
|
1813
|
+
) : undefined;
|
|
1814
|
+
const abandonedRelay = owner && !relay
|
|
1815
|
+
? reconcileControllerRoundAfterAbandonedRelease(relayStore, { workId, releasedSession: owner })
|
|
1816
|
+
: undefined;
|
|
1817
|
+
if (relay?.status === 'dispatching') {
|
|
1818
|
+
const relayWorkId = relay.originWorkId;
|
|
1819
|
+
try {
|
|
1820
|
+
assertAutomatedOperationAllowed('external_controller_wake', {
|
|
1821
|
+
controller_type: 'chatgpt',
|
|
1822
|
+
relay_scope_id: relay.relayScopeId,
|
|
1823
|
+
requirement_id: relay.requirementId,
|
|
1824
|
+
});
|
|
1825
|
+
const prompt = renderChatgptControllerRoundPrompt(relayStore, relay);
|
|
1826
|
+
const relayBinding = chatgptControllerRoundBinding(relayStore, relayWorkId);
|
|
1827
|
+
const predecessorBinding = relayWorkId !== workId ? chatgptControllerRoundBinding(relayStore, workId) : undefined;
|
|
1828
|
+
const dispatched = await runWorkChatgptContinuation({
|
|
1829
|
+
controllerHome: ctx.controllerHome,
|
|
1830
|
+
repoId: repository.repoId,
|
|
1831
|
+
repoRoot: repository.canonicalRoot,
|
|
1832
|
+
workId: relayWorkId,
|
|
1833
|
+
prompt,
|
|
1834
|
+
controllerAuthorityId: relay.authorityId,
|
|
1835
|
+
relayScopeId: relay.relayScopeId,
|
|
1836
|
+
browserSessionId: relayBinding?.browserSessionId ?? predecessorBinding?.browserSessionId,
|
|
1837
|
+
conversationUrl: relayBinding?.conversationUrl ?? predecessorBinding?.conversationUrl,
|
|
1838
|
+
tabPolicy: 'reuse',
|
|
1839
|
+
});
|
|
1840
|
+
if (dispatched.status === 'failed') throw new Error(`${dispatched.error?.code ?? 'CONTROLLER_RELAY_DISPATCH_FAILED'}:${dispatched.error?.message ?? 'Controller relay dispatch failed'}`);
|
|
1841
|
+
// The next prompt is externally committed before this transition is
|
|
1842
|
+
// recorded. Provider conversation context may be reused across the
|
|
1843
|
+
// handoff, but the successor Work receives its own binding/authority.
|
|
1844
|
+
recordChatgptControllerRoundTabSettlement(relayStore, {
|
|
1845
|
+
workId: relayWorkId,
|
|
1846
|
+
relayScopeId: relay.relayScopeId,
|
|
1847
|
+
status: 'retained_for_immediate_continuation',
|
|
1848
|
+
});
|
|
1849
|
+
const updatedBinding = chatgptControllerRoundBinding(relayStore, relayWorkId);
|
|
1850
|
+
const completed = finishControllerRoundRelayDispatch(
|
|
1851
|
+
relayStore,
|
|
1852
|
+
{ workId: relayWorkId, ok: true, bindingId: updatedBinding?.bindingId },
|
|
1853
|
+
);
|
|
1854
|
+
return result(buildFacadeResult({
|
|
1855
|
+
summary: relayWorkId === workId
|
|
1856
|
+
? `Controller lease released and immediate relay ${relay.relayScopeId} dispatched through the canonical ChatGPT launcher.`
|
|
1857
|
+
: `Controller lease released; relay ${relay.relayScopeId} handed off from completed ${workId} to successor ${relayWorkId} and dispatched through the canonical ChatGPT launcher.`,
|
|
1858
|
+
data: { relay: completed, dispatch: dispatched, ...(relayWorkId !== workId ? { predecessorWorkId: workId, successorWorkId: relayWorkId } : {}) },
|
|
1859
|
+
}) as unknown as Record<string, unknown>);
|
|
1860
|
+
} catch (relayError) {
|
|
1861
|
+
const relayFailure = relayError instanceof Error ? relayError.message : String(relayError);
|
|
1862
|
+
const failed = finishControllerRoundRelayDispatch(
|
|
1863
|
+
relayStore,
|
|
1864
|
+
{ workId: relayWorkId, ok: false, error: relayFailure, outcomeUnknown: /OUTCOME_UNKNOWN/i.test(relayFailure) },
|
|
1865
|
+
);
|
|
1866
|
+
return result(buildFacadeResult({
|
|
1867
|
+
status: 'blocked',
|
|
1868
|
+
summary: `Controller lease released, but immediate relay dispatch failed: ${relayError instanceof Error ? relayError.message : String(relayError)}`,
|
|
1869
|
+
data: { relay: failed },
|
|
1870
|
+
}) as unknown as Record<string, unknown>, true);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
const settledRelay = getControllerRoundRelay(relayStore, workId);
|
|
1874
|
+
let tabSettlement;
|
|
1875
|
+
if (
|
|
1876
|
+
owner?.controllerType === 'chatgpt'
|
|
1877
|
+
&& settledRelay
|
|
1878
|
+
&& ['waiting', 'waiting_for_user', 'goal_complete', 'blocked', 'failed'].includes(settledRelay.status)
|
|
1879
|
+
) {
|
|
1880
|
+
const binding = chatgptControllerRoundBinding(store, workId);
|
|
1881
|
+
const browserSessionId = binding?.browserSessionId;
|
|
1882
|
+
if (browserSessionId) {
|
|
1883
|
+
tabSettlement = await settleWorkChatgptAutomationTab({
|
|
1884
|
+
controllerHome: ctx.controllerHome,
|
|
1885
|
+
workId,
|
|
1886
|
+
browserSessionId,
|
|
1887
|
+
});
|
|
1888
|
+
recordChatgptControllerRoundTabSettlement(relayStore, {
|
|
1889
|
+
workId,
|
|
1890
|
+
relayScopeId: settledRelay.relayScopeId,
|
|
1891
|
+
status: tabSettlement.status,
|
|
1892
|
+
error: tabSettlement.error?.message,
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
return result(buildFacadeResult({
|
|
1897
|
+
summary: abandonedRelay
|
|
1898
|
+
? 'Controller lease released; the claimed round was mechanically marked abandoned without semantic completion and can be relaunched through the bounded launcher path.'
|
|
1899
|
+
: 'Controller lease released.',
|
|
1900
|
+
data: { relay: getControllerRoundRelay(relayStore, workId) ?? abandonedRelay ?? relay, tabSettlement },
|
|
1901
|
+
}) as unknown as Record<string, unknown>);
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller release failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
if (operation === 'launcher_start') {
|
|
1907
|
+
try {
|
|
1908
|
+
const controllerType = String(args.controller_type ?? 'codex');
|
|
1909
|
+
if (!['chatgpt', 'codex', 'grok', 'claude'].includes(controllerType)) throw new Error('CONTROLLER_TYPE_INVALID');
|
|
1910
|
+
const workId = String(args.work_id ?? '').trim();
|
|
1911
|
+
const launchArgs = Array.isArray(args.launch_args) ? args.launch_args.map(String) : [];
|
|
1912
|
+
if (controllerType === 'chatgpt') {
|
|
1913
|
+
const work = getWorkContract(store, workId);
|
|
1914
|
+
if (!work) throw new Error(`WORK_NOT_FOUND: ${workId}`);
|
|
1915
|
+
const handoffId = typeof args.handoff_id === 'string' ? args.handoff_id.trim() : '';
|
|
1916
|
+
const handoff = handoffId ? getHandoffItem(store, handoffId) : undefined;
|
|
1917
|
+
const valueForFlag = (flag: string): string | undefined => {
|
|
1918
|
+
const index = launchArgs.indexOf(flag);
|
|
1919
|
+
if (index < 0) return undefined;
|
|
1920
|
+
const value = launchArgs[index + 1];
|
|
1921
|
+
if (!value || value.startsWith('--')) throw new Error(`CHATGPT_LAUNCH_ARG_VALUE_REQUIRED: ${flag}`);
|
|
1922
|
+
return value;
|
|
1923
|
+
};
|
|
1924
|
+
const supportedFlags = new Set(['--model', '--reasoning', '--tab-policy', '--timeout-ms']);
|
|
1925
|
+
for (let index = 0; index < launchArgs.length; index += 2) {
|
|
1926
|
+
const flag = launchArgs[index];
|
|
1927
|
+
if (!flag || !supportedFlags.has(flag)) throw new Error(`CHATGPT_LAUNCH_ARG_UNSUPPORTED: ${flag ?? ''}`);
|
|
1928
|
+
if (!launchArgs[index + 1] || launchArgs[index + 1]!.startsWith('--')) throw new Error(`CHATGPT_LAUNCH_ARG_VALUE_REQUIRED: ${flag}`);
|
|
1929
|
+
}
|
|
1930
|
+
const reasoning = valueForFlag('--reasoning') ?? 'high';
|
|
1931
|
+
if (!['medium', 'high', 'xhigh'].includes(reasoning)) throw new Error(`CHATGPT_LAUNCH_REASONING_INVALID: ${reasoning}`);
|
|
1932
|
+
const tabPolicy = valueForFlag('--tab-policy') ?? 'auto';
|
|
1933
|
+
if (!['auto', 'reuse', 'new'].includes(tabPolicy)) throw new Error(`CHATGPT_LAUNCH_TAB_POLICY_INVALID: ${tabPolicy}`);
|
|
1934
|
+
const timeoutValue = valueForFlag('--timeout-ms');
|
|
1935
|
+
const timeoutMs = timeoutValue === undefined ? undefined : Number(timeoutValue);
|
|
1936
|
+
if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) throw new Error(`CHATGPT_LAUNCH_TIMEOUT_INVALID: ${timeoutValue}`);
|
|
1937
|
+
const continuationPrompt = typeof args.continuation_prompt === 'string' ? args.continuation_prompt.trim() : '';
|
|
1938
|
+
const relayStore = { controllerHome: ctx.controllerHome, repoId: repository.repoId };
|
|
1939
|
+
const existingBinding = chatgptControllerRoundBinding(relayStore, workId);
|
|
1940
|
+
const relay = beginInitialControllerRoundDispatch(
|
|
1941
|
+
relayStore,
|
|
1942
|
+
{
|
|
1943
|
+
workId,
|
|
1944
|
+
identity: authenticatedFacadeControllerIdentity(ctx, args),
|
|
1945
|
+
requirementId: work.requirementId,
|
|
1946
|
+
bindingId: existingBinding?.bindingId,
|
|
1947
|
+
},
|
|
1948
|
+
);
|
|
1949
|
+
if (relay.status === 'blocked') {
|
|
1950
|
+
throw new Error(`CONTROLLER_RELAY_LAUNCH_BLOCKED: ${relay.blockedReason ?? relay.relayScopeId}`);
|
|
1951
|
+
}
|
|
1952
|
+
const prompt = [
|
|
1953
|
+
renderChatgptControllerRoundPrompt(store, relay, { exactOriginWork: true }),
|
|
1954
|
+
`Controller round: ${relay.relayScopeId}. launcher_start opened this durable round; dispatch success is not semantic completion.`,
|
|
1955
|
+
handoff ? `Handoff: ${handoff.summary}\nNext: ${handoff.recommendedContinuationPrompt ?? handoff.recommendedPrompt}` : '',
|
|
1956
|
+
continuationPrompt ? `Continuation: ${continuationPrompt}` : '',
|
|
1957
|
+
].filter(Boolean).join('\n');
|
|
1958
|
+
let dispatched: Awaited<ReturnType<typeof runWorkChatgptContinuation>>;
|
|
1959
|
+
try {
|
|
1960
|
+
dispatched = await runWorkChatgptContinuation({
|
|
1961
|
+
controllerHome: ctx.controllerHome,
|
|
1962
|
+
repoId: repository.repoId,
|
|
1963
|
+
repoRoot: repository.canonicalRoot,
|
|
1964
|
+
workId,
|
|
1965
|
+
prompt,
|
|
1966
|
+
controllerAuthorityId: relay.authorityId,
|
|
1967
|
+
relayScopeId: relay.relayScopeId,
|
|
1968
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
1969
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
1970
|
+
model: valueForFlag('--model') ?? 'gpt-5.6',
|
|
1971
|
+
reasoning: reasoning as 'medium' | 'high' | 'xhigh',
|
|
1972
|
+
tabPolicy: tabPolicy as 'auto' | 'reuse' | 'new',
|
|
1973
|
+
timeoutMs,
|
|
1974
|
+
});
|
|
1975
|
+
if (dispatched.status === 'failed') throw new Error(`${dispatched.error?.code ?? 'CHATGPT_WORK_CONTINUATION_FAILED'}:${dispatched.error?.message ?? 'ChatGPT Work continuation failed'}`);
|
|
1976
|
+
} catch (launchError) {
|
|
1977
|
+
const launchFailure = launchError instanceof Error ? launchError.message : String(launchError);
|
|
1978
|
+
finishControllerRoundRelayDispatch(
|
|
1979
|
+
{ controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
1980
|
+
{ workId, ok: false, error: launchFailure, outcomeUnknown: /OUTCOME_UNKNOWN/i.test(launchFailure) },
|
|
1981
|
+
);
|
|
1982
|
+
throw launchError;
|
|
1983
|
+
}
|
|
1984
|
+
const updatedBinding = chatgptControllerRoundBinding(relayStore, workId);
|
|
1985
|
+
const completedRelay = finishControllerRoundRelayDispatch(
|
|
1986
|
+
relayStore,
|
|
1987
|
+
{ workId, ok: true, bindingId: updatedBinding?.bindingId },
|
|
1988
|
+
);
|
|
1989
|
+
return result(buildFacadeResult({
|
|
1990
|
+
summary: 'ChatGPT continuation dispatched; wake completion remains pending until the new ChatGPT Controller claims the Work, and semantic closure still requires an explicit disposition.',
|
|
1991
|
+
data: {
|
|
1992
|
+
workId,
|
|
1993
|
+
relay: completedRelay,
|
|
1994
|
+
browserSessionId: dispatched.browserSessionId,
|
|
1995
|
+
conversationUrl: dispatched.conversationUrl,
|
|
1996
|
+
executionPreferenceVerified: dispatched.executionPreferenceVerified,
|
|
1997
|
+
},
|
|
1998
|
+
}) as unknown as Record<string, unknown>);
|
|
1999
|
+
}
|
|
2000
|
+
const launched = await launchSuperController({ work: store, handoff: store }, {
|
|
2001
|
+
controllerType: controllerType as 'codex' | 'grok' | 'claude',
|
|
2002
|
+
executable: typeof args.executable === 'string' && args.executable.trim() ? args.executable.trim() : undefined,
|
|
2003
|
+
args: launchArgs,
|
|
2004
|
+
workId,
|
|
2005
|
+
launchReservationMs: typeof args.launch_reservation_ms === 'number' ? args.launch_reservation_ms : typeof args.lease_ms === 'number' ? args.lease_ms : undefined,
|
|
2006
|
+
handoffId: typeof args.handoff_id === 'string' ? args.handoff_id : undefined,
|
|
2007
|
+
browserSessionId: typeof args.browser_session_id === 'string' ? args.browser_session_id : undefined,
|
|
2008
|
+
conversationUrl: typeof args.conversation_url === 'string' ? args.conversation_url : undefined,
|
|
2009
|
+
continuationPrompt: typeof args.continuation_prompt === 'string' ? args.continuation_prompt : undefined,
|
|
2010
|
+
cwd: repository.canonicalRoot,
|
|
2011
|
+
});
|
|
2012
|
+
return result(buildFacadeResult({ summary: `Thin Launcher started ${launched.controllerType}.`, data: { pid: launched.pid, executable: launched.executable, workId, reservationId: launched.reservationId } }) as unknown as Record<string, unknown>);
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
return result(buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Launcher failed.', data: {} }) as unknown as Record<string, unknown>, true);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (operation === 'plan_list') {
|
|
2018
|
+
const plans = listPlanContracts({ ...store, status: 'active', limit: typeof args.limit === 'number' ? args.limit : 20 });
|
|
2019
|
+
const facade = buildFacadeResult({
|
|
2020
|
+
summary: `${plans.length} active PlanContract(s) in this repository.`,
|
|
2021
|
+
data: { plans: plans.map(summarizePlanContract), bounded: true },
|
|
2022
|
+
});
|
|
2023
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2024
|
+
}
|
|
2025
|
+
if (operation === 'plan_get') {
|
|
2026
|
+
const plan = getPlanContract(store, String(args.plan_id ?? ''));
|
|
2027
|
+
const facade = plan
|
|
2028
|
+
? buildFacadeResult({ summary: `PlanContract ${plan.planId} retrieved.`, data: { plan: args.detail_level === 'detail' ? plan : summarizePlanContract(plan) }, detailLevel: args.detail_level === 'detail' ? 'detail' : 'summary' })
|
|
2029
|
+
: buildFacadeResult({ status: 'not_found', summary: `PlanContract ${String(args.plan_id ?? '')} not found.`, data: { planId: String(args.plan_id ?? '') } });
|
|
2030
|
+
return result(facade as unknown as Record<string, unknown>, !plan);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
const checks = listControllerChecks(repository.canonicalRoot);
|
|
2034
|
+
const workloopSource = freshGitIdentity(repository.canonicalRoot);
|
|
2035
|
+
const workloopStatus = repositoryGitStatus(repository);
|
|
2036
|
+
const workloopChangedPaths = [...new Set([
|
|
2037
|
+
...workloopStatus.staged,
|
|
2038
|
+
...workloopStatus.unstaged,
|
|
2039
|
+
...workloopStatus.untracked,
|
|
2040
|
+
])].sort();
|
|
2041
|
+
const workloopCtx = {
|
|
2042
|
+
workStore: store,
|
|
2043
|
+
handoffStore: store,
|
|
2044
|
+
planStore: store,
|
|
2045
|
+
repoId: repository.repoId,
|
|
2046
|
+
checkoutId: repository.activeCheckoutId,
|
|
2047
|
+
principalId: ctx.principalId,
|
|
2048
|
+
controllerInstanceId: ctx.controllerInstanceId,
|
|
2049
|
+
availableChecks: checks,
|
|
2050
|
+
sourceRevision: workloopSource.head ?? undefined,
|
|
2051
|
+
sourceBaseState: workloopSource.head ? 'revision' as const : 'unborn' as const,
|
|
2052
|
+
workspaceDirty: !workloopStatus.clean,
|
|
2053
|
+
workspaceChangedPaths: workloopChangedPaths,
|
|
2054
|
+
materializeIsolatedWorkspace: ({ workId, title, baseRef, needsDependencies }: { workId: string; title: string; baseRef?: string; needsDependencies?: boolean }) => {
|
|
2055
|
+
const workspace = ensureManagedWorkspace(ctx.controllerHome, repository, {
|
|
2056
|
+
requestId: workId,
|
|
2057
|
+
title,
|
|
2058
|
+
baseRef,
|
|
2059
|
+
prepareDependencies: needsDependencies === true,
|
|
2060
|
+
});
|
|
2061
|
+
if (!workspace.managed || !workspace.checkoutId || !workspace.root) throw new Error('MANAGED_WORKSPACE_NOT_MATERIALIZED');
|
|
2062
|
+
return { checkoutId: workspace.checkoutId, root: workspace.root, baseRevision: workspace.baseRevision, managed: true as const };
|
|
2063
|
+
},
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
if (operation === 'requirement_continue') {
|
|
2067
|
+
const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id.trim() : '';
|
|
2068
|
+
if (!requirementId) {
|
|
2069
|
+
return result(buildFacadeResult({
|
|
2070
|
+
status: 'blocked',
|
|
2071
|
+
summary: 'REQUIREMENT_CONTINUE_INPUT_REQUIRED: requirement_id is required.',
|
|
2072
|
+
data: { requirementResumed: false },
|
|
2073
|
+
}) as unknown as Record<string, unknown>, true);
|
|
2074
|
+
}
|
|
2075
|
+
try {
|
|
2076
|
+
const continued = continueRequirement({ controllerHome: ctx.controllerHome }, requirementId);
|
|
2077
|
+
return result(buildFacadeResult({
|
|
2078
|
+
summary: continued.resumed
|
|
2079
|
+
? `Requirement ${continued.requirement.requirementId} resumed from waiting_for_user to active by explicit semantic continue.`
|
|
2080
|
+
: `REQUIREMENT_ALREADY_ACTIVE: ${continued.requirement.requirementId}. Explicit continue is idempotent.`,
|
|
2081
|
+
data: {
|
|
2082
|
+
requirement: continued.requirement,
|
|
2083
|
+
requirementResumed: continued.resumed,
|
|
2084
|
+
semanticDecision: 'continue',
|
|
2085
|
+
},
|
|
2086
|
+
suggestedNextActions: [],
|
|
2087
|
+
}) as unknown as Record<string, unknown>);
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
return result(buildFacadeResult({
|
|
2090
|
+
status: 'blocked',
|
|
2091
|
+
summary: error instanceof Error ? error.message : String(error),
|
|
2092
|
+
data: { requirementResumed: false },
|
|
2093
|
+
suggestedNextActions: [],
|
|
2094
|
+
}) as unknown as Record<string, unknown>, true);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
if (operation === 'requirement_create') {
|
|
2099
|
+
const compatibilityArgs = frozenSemanticOperation?.operation === 'requirement_create' ? frozenSemanticOperation.args : undefined;
|
|
2100
|
+
const requirementId = typeof args.requirement_id === 'string' ? args.requirement_id : '';
|
|
2101
|
+
const title = compatibilityArgs?.requirement_title ?? (typeof args.requirement_title === 'string' ? args.requirement_title : '');
|
|
2102
|
+
const outcomeStatement = compatibilityArgs?.requirement_outcome ?? (typeof args.requirement_outcome === 'string' ? args.requirement_outcome : '');
|
|
2103
|
+
if (!requirementId.trim() || !title.trim() || !outcomeStatement.trim()) {
|
|
2104
|
+
return result(buildFacadeResult({
|
|
2105
|
+
status: 'blocked',
|
|
2106
|
+
summary: 'REQUIREMENT_CREATE_INPUT_REQUIRED: requirement_id, requirement_title, and requirement_outcome are required.',
|
|
2107
|
+
data: { requirementCreated: false },
|
|
2108
|
+
}) as unknown as Record<string, unknown>, true);
|
|
2109
|
+
}
|
|
2110
|
+
let admission;
|
|
2111
|
+
try {
|
|
2112
|
+
admission = admitRequirement({ controllerHome: ctx.controllerHome }, {
|
|
2113
|
+
requirementId,
|
|
2114
|
+
title,
|
|
2115
|
+
outcomeStatement,
|
|
2116
|
+
acceptanceCriteria: compatibilityArgs?.requirement_acceptance_criteria
|
|
2117
|
+
?? (Array.isArray(args.requirement_acceptance_criteria) ? args.requirement_acceptance_criteria.map(String) : []),
|
|
2118
|
+
requiredDeliveryReferences: compatibilityArgs?.requirement_delivery_references
|
|
2119
|
+
?? (Array.isArray(args.requirement_delivery_references) ? args.requirement_delivery_references.map(String) : []),
|
|
2120
|
+
legacyAliases: compatibilityArgs?.requirement_legacy_aliases
|
|
2121
|
+
?? (Array.isArray(args.requirement_legacy_aliases) ? args.requirement_legacy_aliases.map(String) : []),
|
|
2122
|
+
});
|
|
2123
|
+
} catch (error) {
|
|
2124
|
+
return result(buildFacadeResult({
|
|
2125
|
+
status: 'blocked',
|
|
2126
|
+
summary: error instanceof Error ? error.message : String(error),
|
|
2127
|
+
data: { requirementCreated: false },
|
|
2128
|
+
}) as unknown as Record<string, unknown>, true);
|
|
2129
|
+
}
|
|
2130
|
+
if (admission.decision === 'existing_conflict') {
|
|
2131
|
+
return result(buildFacadeResult({
|
|
2132
|
+
status: 'blocked',
|
|
2133
|
+
summary: `REQUIREMENT_ALREADY_EXISTS_CONFLICT: ${admission.requirement.requirementId}. Existing Requirement authority was not changed.`,
|
|
2134
|
+
data: { requirement: admission.requirement, requirementCreated: false, admissionDecision: admission.decision },
|
|
2135
|
+
suggestedNextActions: [],
|
|
2136
|
+
}) as unknown as Record<string, unknown>, true);
|
|
2137
|
+
}
|
|
2138
|
+
return result(buildFacadeResult({
|
|
2139
|
+
summary: admission.decision === 'created'
|
|
2140
|
+
? `Requirement ${admission.requirement.requirementId} created. Requirement authority does not imply a Plan; Controller chooses the next action.`
|
|
2141
|
+
: `REQUIREMENT_AUTHORITY_REUSED: ${admission.requirement.requirementId}. Requirement authority does not imply a Plan; Controller chooses the next action.`,
|
|
2142
|
+
data: { requirement: admission.requirement, requirementCreated: admission.created, admissionDecision: admission.decision },
|
|
2143
|
+
suggestedNextActions: [],
|
|
2144
|
+
}) as unknown as Record<string, unknown>);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
if (operation.startsWith('plan_')) {
|
|
2148
|
+
try {
|
|
2149
|
+
if (operation === 'plan_create') {
|
|
2150
|
+
const rawSteps = Array.isArray(args.plan_steps) ? args.plan_steps : [];
|
|
2151
|
+
const requestedPlanId = String(args.plan_id ?? '').trim();
|
|
2152
|
+
const requestedRequirementId = typeof args.requirement_id === 'string' && args.requirement_id.trim() ? args.requirement_id.trim() : undefined;
|
|
2153
|
+
const requestedPlanRelation: 'extend' | 'parallel' | undefined = args.plan_relation === 'extend' || args.plan_relation === 'parallel'
|
|
2154
|
+
? args.plan_relation
|
|
2155
|
+
: undefined;
|
|
2156
|
+
const relatedPlanId = typeof args.related_plan_id === 'string' && args.related_plan_id.trim() ? args.related_plan_id.trim() : undefined;
|
|
2157
|
+
if (requestedRequirementId && !readRequirement({ controllerHome: ctx.controllerHome }, requestedRequirementId)) {
|
|
2158
|
+
const facade = buildFacadeResult({
|
|
2159
|
+
status: 'failed',
|
|
2160
|
+
summary: `PLAN_REQUIREMENT_NOT_FOUND: ${requestedRequirementId}. Plan was not persisted; create or reconcile the Requirement authority first.`,
|
|
2161
|
+
data: { executionStarted: false, planContractCreated: false, admissionDecision: 'missing_requirement', requirementId: requestedRequirementId },
|
|
2162
|
+
});
|
|
2163
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2164
|
+
}
|
|
2165
|
+
const admissionInput = {
|
|
2166
|
+
requirementId: requestedRequirementId,
|
|
2167
|
+
scopeKey: String(args.scope_key ?? ''),
|
|
2168
|
+
planRelation: requestedPlanRelation,
|
|
2169
|
+
relatedPlanId,
|
|
2170
|
+
};
|
|
2171
|
+
const renderPlanAdmission = (admission: ReturnType<typeof resolvePlanAdmission>): CallToolResult | undefined => {
|
|
2172
|
+
if (admission.admissionDecision === 'create_new') return undefined;
|
|
2173
|
+
if (admission.reason === 'exact_scope_authority' && admission.plan) {
|
|
2174
|
+
const exactDraftRepair = admission.plan.status === 'draft' && requestedPlanId === admission.plan.planId;
|
|
2175
|
+
const facade = buildFacadeResult({
|
|
2176
|
+
summary: exactDraftRepair
|
|
2177
|
+
? `PLAN_DRAFT_REPAIR_REQUIRED: draft Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; preserve that authority and amend it through rh_work repair.`
|
|
2178
|
+
: `PLAN_AUTHORITY_REUSED: active Plan ${admission.plan.planId} already owns scope ${admission.normalizedScopeKey}; no duplicate draft was created.`,
|
|
2179
|
+
data: {
|
|
2180
|
+
plan: summarizePlanContract(admission.plan),
|
|
2181
|
+
executionStarted: false,
|
|
2182
|
+
planContractCreated: false,
|
|
2183
|
+
admissionDecision: 'reuse_existing',
|
|
2184
|
+
resolutionRequired: false,
|
|
2185
|
+
...(exactDraftRepair ? { repairRequired: true } : {}),
|
|
2186
|
+
},
|
|
2187
|
+
suggestedNextActions: exactDraftRepair
|
|
2188
|
+
? [{
|
|
2189
|
+
label: 'Repair this exact draft Plan',
|
|
2190
|
+
tool: 'rh_work',
|
|
2191
|
+
operation: 'repair',
|
|
2192
|
+
payload: {
|
|
2193
|
+
plan_id: admission.plan.planId,
|
|
2194
|
+
repair_operation: 'repair',
|
|
2195
|
+
dry_run: false,
|
|
2196
|
+
scope_key: args.scope_key,
|
|
2197
|
+
source_revision: args.source_revision,
|
|
2198
|
+
objective: args.objective,
|
|
2199
|
+
plan_steps: args.plan_steps,
|
|
2200
|
+
non_goals: args.non_goals,
|
|
2201
|
+
assumptions: args.assumptions,
|
|
2202
|
+
resolved_decisions: args.resolved_decisions,
|
|
2203
|
+
stop_conditions: args.stop_conditions,
|
|
2204
|
+
replan_conditions: args.replan_conditions,
|
|
2205
|
+
integration_strategy: args.integration_strategy,
|
|
2206
|
+
},
|
|
2207
|
+
risk: 'workspace_write',
|
|
2208
|
+
confidence: 'high',
|
|
2209
|
+
}]
|
|
2210
|
+
: [{ label: 'Read active Plan', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: admission.plan.planId }, risk: 'readonly', confidence: 'high' }],
|
|
2211
|
+
});
|
|
2212
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2213
|
+
}
|
|
2214
|
+
if (admission.reason === 'extension_target_required') {
|
|
2215
|
+
const facade = buildFacadeResult({
|
|
2216
|
+
summary: `PLAN_EXTENSION_TARGET_REQUIRED: select related_plan_id from the active Plan slices for Requirement ${requestedRequirementId}.`,
|
|
2217
|
+
data: { executionStarted: false, planContractCreated: false, admissionDecision: 'resolution_required', resolutionRequired: true, candidates: admission.candidates.map(summarizePlanContract) },
|
|
2218
|
+
});
|
|
2219
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2220
|
+
}
|
|
2221
|
+
if (admission.reason === 'extend_existing' && admission.plan) {
|
|
2222
|
+
// plan_create + plan_relation=extend is a compatibility transport
|
|
2223
|
+
// for revising the explicitly related stable Plan identity. Preflight
|
|
2224
|
+
// must continue into atomic admission; no successor Plan is minted.
|
|
2225
|
+
return undefined;
|
|
2226
|
+
}
|
|
2227
|
+
throw new Error(`PLAN_ADMISSION_RESULT_INVALID: ${admission.admissionDecision}:${admission.reason}`);
|
|
2228
|
+
};
|
|
2229
|
+
const plans = listPlanContracts({ ...store, status: 'all', limit: 100 });
|
|
2230
|
+
const preflightAdmission = resolvePlanAdmission(plans, admissionInput);
|
|
2231
|
+
const preflightResult = renderPlanAdmission(preflightAdmission);
|
|
2232
|
+
if (preflightResult) return preflightResult;
|
|
2233
|
+
const requestedPlanCheckIds = rawSteps
|
|
2234
|
+
.filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step))
|
|
2235
|
+
.flatMap((step) => Array.isArray(step.check_ids) ? step.check_ids.map(String) : []);
|
|
2236
|
+
const normalizedPlanChecks = normalizeCheckIds(requestedPlanCheckIds, checks);
|
|
2237
|
+
if (normalizedPlanChecks.invalidCheckIds.length > 0) {
|
|
2238
|
+
const facade = buildFacadeResult({
|
|
2239
|
+
status: 'failed',
|
|
2240
|
+
summary: `PLAN_CHECKS_INVALID: ${normalizedPlanChecks.invalidCheckIds.join(', ')}. Plan was not persisted; select replacement IDs from registeredCheckIds in this response, then request readiness only for the checks you choose.`,
|
|
2241
|
+
data: {
|
|
2242
|
+
executionStarted: false,
|
|
2243
|
+
planContractCreated: false,
|
|
2244
|
+
admissionDecision: 'invalid_checks',
|
|
2245
|
+
normalizedChecks: normalizedPlanChecks,
|
|
2246
|
+
registeredCheckIds: checks.map((check) => check.id).slice(0, 80),
|
|
2247
|
+
},
|
|
2248
|
+
suggestedNextActions: [],
|
|
2249
|
+
});
|
|
2250
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2251
|
+
}
|
|
2252
|
+
const admitted = await admitPlanContractAsync(store, {
|
|
2253
|
+
planId: String(args.plan_id ?? ''),
|
|
2254
|
+
repoId: repository.repoId,
|
|
2255
|
+
requirementId: requestedRequirementId,
|
|
2256
|
+
scopeKey: String(args.scope_key ?? ''),
|
|
2257
|
+
planRelation: requestedPlanRelation,
|
|
2258
|
+
relatedPlanId,
|
|
2259
|
+
sourceRevision: String(args.source_revision ?? ''),
|
|
2260
|
+
goal: String(args.objective ?? ''),
|
|
2261
|
+
nonGoals: Array.isArray(args.non_goals) ? args.non_goals.map(String) : undefined,
|
|
2262
|
+
assumptions: Array.isArray(args.assumptions) ? args.assumptions.map(String) : undefined,
|
|
2263
|
+
resolvedDecisions: Array.isArray(args.resolved_decisions) ? args.resolved_decisions.map(String) : undefined,
|
|
2264
|
+
stopConditions: Array.isArray(args.stop_conditions) ? args.stop_conditions.map(String) : undefined,
|
|
2265
|
+
replanConditions: Array.isArray(args.replan_conditions) ? args.replan_conditions.map(String) : undefined,
|
|
2266
|
+
integrationStrategy: typeof args.integration_strategy === 'string' ? args.integration_strategy : undefined,
|
|
2267
|
+
obligationDispositions: planObligationDispositionsFromArgs(args.obligation_dispositions),
|
|
2268
|
+
steps: rawSteps.filter((step): step is Record<string, unknown> => Boolean(step) && typeof step === 'object' && !Array.isArray(step)).map((step) => ({
|
|
2269
|
+
id: String(step.id ?? ''),
|
|
2270
|
+
objective: String(step.objective ?? ''),
|
|
2271
|
+
dependencies: Array.isArray(step.dependencies) ? step.dependencies.map(String) : [],
|
|
2272
|
+
authoritativeFiles: Array.isArray(step.authoritative_files) ? step.authoritative_files.map(String) : [],
|
|
2273
|
+
allowedPaths: Array.isArray(step.allowed_paths) ? step.allowed_paths.map(String) : [],
|
|
2274
|
+
forbiddenPaths: Array.isArray(step.forbidden_paths) ? step.forbidden_paths.map(String) : [],
|
|
2275
|
+
checks: Array.isArray(step.check_ids) ? step.check_ids.map(String) : [],
|
|
2276
|
+
acceptanceCriteria: Array.isArray(step.acceptance_criteria) ? step.acceptance_criteria.map(String) : [],
|
|
2277
|
+
})),
|
|
2278
|
+
});
|
|
2279
|
+
if (admitted.reason === 'extend_existing' && admitted.plan) {
|
|
2280
|
+
const plan = admitted.plan;
|
|
2281
|
+
const requestedLabel = requestedPlanId && requestedPlanId !== plan.planId ? ` Requested compatibility plan_id ${requestedPlanId} was retained only as revision audit metadata.` : '';
|
|
2282
|
+
const facade = buildFacadeResult({
|
|
2283
|
+
summary: `PLAN_REVISION_REUSED_AUTHORITY: Plan ${plan.planId} was revised in place; no successor PlanContract was created.${requestedLabel}`,
|
|
2284
|
+
data: {
|
|
2285
|
+
plan: summarizePlanContract(plan),
|
|
2286
|
+
executionStarted: false,
|
|
2287
|
+
planContractCreated: false,
|
|
2288
|
+
admissionDecision: 'reuse_existing',
|
|
2289
|
+
resolutionRequired: false,
|
|
2290
|
+
},
|
|
2291
|
+
suggestedNextActions: [{ label: 'Approve revised Plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: plan.planId }, risk: 'workspace_write', confidence: 'high' }],
|
|
2292
|
+
});
|
|
2293
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2294
|
+
}
|
|
2295
|
+
const racedAdmissionResult = renderPlanAdmission(admitted);
|
|
2296
|
+
if (racedAdmissionResult) return racedAdmissionResult;
|
|
2297
|
+
if (!admitted.plan) throw new Error('PLAN_ADMISSION_CREATE_MISSING_PLAN');
|
|
2298
|
+
const plan = admitted.plan;
|
|
2299
|
+
const facade = buildFacadeResult({
|
|
2300
|
+
summary: `PlanContract ${plan.planId} created as draft after atomic authority admission; no execution was started.`,
|
|
2301
|
+
data: { plan: summarizePlanContract(plan), executionStarted: false, planContractCreated: true, admissionDecision: 'create_new' },
|
|
2302
|
+
suggestedNextActions: [{ label: 'Approve reviewed plan', tool: 'rh_work', operation: 'plan_approve', payload: { plan_id: plan.planId }, risk: 'workspace_write', confidence: 'medium' }],
|
|
2303
|
+
});
|
|
2304
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2305
|
+
}
|
|
2306
|
+
if (operation === 'plan_approve') {
|
|
2307
|
+
const plan = await approvePlanContractAsync(store, String(args.plan_id ?? ''));
|
|
2308
|
+
const facade = buildFacadeResult({
|
|
2309
|
+
summary: `PlanContract ${plan.planId} approved at source revision ${plan.sourceRevision}; execution remains explicit.`,
|
|
2310
|
+
data: { plan: summarizePlanContract(plan), executionStarted: false },
|
|
2311
|
+
});
|
|
2312
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2313
|
+
}
|
|
2314
|
+
if (operation === 'plan_accept_step') {
|
|
2315
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
2316
|
+
const planId = String(args.plan_id ?? '').trim();
|
|
2317
|
+
const stepId = String(args.plan_step_id ?? '').trim();
|
|
2318
|
+
const rationale = String(args.acceptance_rationale ?? '').trim();
|
|
2319
|
+
const before = getPlanContract(store, planId);
|
|
2320
|
+
const beforeStep = before?.steps.find((candidate) => candidate.id === stepId);
|
|
2321
|
+
const predecessorWorkId = beforeStep?.workId?.trim();
|
|
2322
|
+
const predecessorWork = predecessorWorkId ? getWorkContract(store, predecessorWorkId) : undefined;
|
|
2323
|
+
const claimedRelay = predecessorWorkId ? getControllerRoundRelay(store, predecessorWorkId) : undefined;
|
|
2324
|
+
const currentOwner = predecessorWorkId ? getControllerSession(store, predecessorWorkId) : undefined;
|
|
2325
|
+
const claimedTerminalRound = Boolean(
|
|
2326
|
+
predecessorWork
|
|
2327
|
+
&& predecessorWork.status === 'completed'
|
|
2328
|
+
&& claimedRelay?.status === 'claimed'
|
|
2329
|
+
);
|
|
2330
|
+
if (claimedTerminalRound && predecessorWorkId) {
|
|
2331
|
+
// Semantic acceptance may occur after MCP/Runtime replacement or
|
|
2332
|
+
// physical Work-lease release, but a still-open terminal round is
|
|
2333
|
+
// fenced by its exact opaque capability. Never reclaim the Work.
|
|
2334
|
+
assertFacadeControllerRoundAuthority(ctx, store, predecessorWorkId, args);
|
|
2335
|
+
if (currentOwner) {
|
|
2336
|
+
if (currentOwner.controllerType !== identity.controllerType) throw new Error(`CONTROLLER_RELAY_CONTROLLER_TYPE_MISMATCH: ${predecessorWorkId}`);
|
|
2337
|
+
if (currentOwner.controllerId !== identity.controllerId) throw new Error(`WORK_CONTROLLER_OWNER_MISMATCH: ${predecessorWorkId}`);
|
|
2338
|
+
if (controllerSessionPrincipalId(currentOwner) !== identity.principalId) throw new Error(`WORK_CONTROLLER_PRINCIPAL_MISMATCH: ${predecessorWorkId}`);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
const plan = acceptPlanStepEvidence(store, {
|
|
2342
|
+
planId,
|
|
2343
|
+
stepId,
|
|
2344
|
+
reviewer: identity.principalId,
|
|
2345
|
+
rationale,
|
|
2346
|
+
acceptedSourceRevision: workloopCtx.sourceRevision,
|
|
2347
|
+
});
|
|
2348
|
+
const facade = buildFacadeResult({
|
|
2349
|
+
summary: `Plan step ${stepId} semantically accepted by the current Controller. Successor execution remains an explicit Controller start.`,
|
|
2350
|
+
data: {
|
|
2351
|
+
plan: summarizePlanContract(plan),
|
|
2352
|
+
semanticAcceptanceRecorded: true,
|
|
2353
|
+
reviewer: identity.principalId,
|
|
2354
|
+
...(predecessorWorkId ? { predecessorWorkId } : {}),
|
|
2355
|
+
successorAdmissionRequired: plan.status !== 'finalized',
|
|
2356
|
+
},
|
|
2357
|
+
suggestedNextActions: plan.status === 'finalized'
|
|
2358
|
+
? []
|
|
2359
|
+
: [{ label: 'Read the next approved Plan step', tool: 'rh_work', operation: 'plan_get', payload: { plan_id: plan.planId }, risk: 'readonly', confidence: 'high' }],
|
|
2360
|
+
});
|
|
2361
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2362
|
+
}
|
|
2363
|
+
const plan = supersedePlanContract(store, String(args.plan_id ?? ''), String(args.superseded_by ?? ''));
|
|
2364
|
+
const facade = buildFacadeResult({ summary: `PlanContract ${plan.planId} superseded by ${plan.supersededBy}.`, data: { plan: summarizePlanContract(plan) } });
|
|
2365
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2366
|
+
} catch (error) {
|
|
2367
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'PlanContract operation failed.', data: { operation, executionStarted: false } });
|
|
2368
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
if (operation === 'repair' && args.capability_id === 'recovery.migrate_controller_home') {
|
|
2373
|
+
const workId = String(args.work_id ?? '').trim();
|
|
2374
|
+
if (!workId) {
|
|
2375
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: 'RECOVERY_CONTROLLER_HOME_MIGRATION_WORK_REQUIRED', data: { executionStarted: false } });
|
|
2376
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2377
|
+
}
|
|
2378
|
+
try {
|
|
2379
|
+
const work = getWorkContract(store, workId);
|
|
2380
|
+
if (!work || ['completed', 'failed', 'cancelled'].includes(work.status)) {
|
|
2381
|
+
throw new Error(`RECOVERY_CONTROLLER_HOME_MIGRATION_ACTIVE_WORK_REQUIRED: ${workId}`);
|
|
2382
|
+
}
|
|
2383
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
2384
|
+
const owner = getControllerSession(store, workId);
|
|
2385
|
+
if (!owner || controllerSessionPrincipalId(owner) !== identity.principalId || owner.sessionId !== identity.sessionId) {
|
|
2386
|
+
throw new Error(`RECOVERY_CONTROLLER_HOME_MIGRATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
|
|
2387
|
+
}
|
|
2388
|
+
const liveGit = gitSnapshot(repository.canonicalRoot);
|
|
2389
|
+
if (!liveGit.head) throw new Error('RECOVERY_CONTROLLER_HOME_MIGRATION_SOURCE_REVISION_REQUIRED');
|
|
2390
|
+
const migrationRequestId = typeof args.request_id === 'string' && args.request_id.trim()
|
|
2391
|
+
? args.request_id.trim()
|
|
2392
|
+
: `controller-home-migration:${workId}`;
|
|
2393
|
+
const scheduled = await callStandaloneRecoveryTool(ctx.controllerHome, 'migrate_controller_home', {
|
|
2394
|
+
request_id: migrationRequestId,
|
|
2395
|
+
canonical_source_root: repository.canonicalRoot,
|
|
2396
|
+
expected_source_revision: liveGit.head,
|
|
2397
|
+
});
|
|
2398
|
+
const facade = buildFacadeResult({
|
|
2399
|
+
summary: `Standalone Recovery accepted the Controller Home migration transaction for Work ${workId}.`,
|
|
2400
|
+
data: { workId, migration: scheduled, executionStarted: true },
|
|
2401
|
+
});
|
|
2402
|
+
return result(facade as unknown as Record<string, unknown>);
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
const facade = buildFacadeResult({
|
|
2405
|
+
status: 'blocked',
|
|
2406
|
+
summary: error instanceof Error ? error.message : 'Controller Home migration scheduling failed.',
|
|
2407
|
+
data: { workId, executionStarted: false },
|
|
2408
|
+
});
|
|
2409
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (operation === 'repair') {
|
|
2414
|
+
return await runFacadeRepair(ctx, repository, args);
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
if (operation === 'verify') {
|
|
2418
|
+
const workId = String(args.work_id ?? '').trim();
|
|
2419
|
+
try {
|
|
2420
|
+
if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
const blocked = buildFacadeResult({
|
|
2423
|
+
status: 'blocked',
|
|
2424
|
+
summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`,
|
|
2425
|
+
data: { workId, verificationStarted: false },
|
|
2426
|
+
});
|
|
2427
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2428
|
+
}
|
|
2429
|
+
return await runFacadeVerify(ctx, repository, args);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
if (operation === 'review') {
|
|
2433
|
+
const workId = String(args.work_id ?? '').trim();
|
|
2434
|
+
try {
|
|
2435
|
+
if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
2436
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
2437
|
+
const reconciled = workId ? reconcileTerminalFacadeWorkVerifications(ctx, repository, workId) : undefined;
|
|
2438
|
+
if (!workId || !reconciled?.sourceRevision || !reconciled.workspaceFingerprint || !reconciled.implementationReviewWorkspaceFingerprint) {
|
|
2439
|
+
throw new Error(`WORK_IMPLEMENTATION_REVIEW_SOURCE_IDENTITY_REQUIRED: ${workId || 'work_id_missing'}`);
|
|
2440
|
+
}
|
|
2441
|
+
const facade = runGoalWorkloop({
|
|
2442
|
+
...workloopCtx,
|
|
2443
|
+
principalId: identity.principalId,
|
|
2444
|
+
controllerInstanceId: identity.controllerInstanceId,
|
|
2445
|
+
sourceRevision: reconciled.sourceRevision,
|
|
2446
|
+
workspaceFingerprint: reconciled.workspaceFingerprint,
|
|
2447
|
+
implementationReviewWorkspaceFingerprint: reconciled.implementationReviewWorkspaceFingerprint,
|
|
2448
|
+
workspaceChangedPaths: reconciled.workspaceChangedPaths,
|
|
2449
|
+
workBoundProcessEvidenceIds: reconciled.workBoundProcessEvidenceIds,
|
|
2450
|
+
}, 'review', args);
|
|
2451
|
+
return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked' || facade.status === 'failed' || facade.status === 'not_found');
|
|
2452
|
+
} catch (error) {
|
|
2453
|
+
const blocked = buildFacadeResult({
|
|
2454
|
+
status: 'blocked',
|
|
2455
|
+
summary: error instanceof Error ? error.message : `Work ${workId} implementation review failed.`,
|
|
2456
|
+
data: { workId, implementationReviewRecorded: false },
|
|
2457
|
+
});
|
|
2458
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
if (operation === 'delegate') {
|
|
2463
|
+
const facade = delegateToCodexCerebellum(
|
|
2464
|
+
{ repoId: repository.repoId },
|
|
2465
|
+
{
|
|
2466
|
+
workId: typeof args.work_id === 'string' ? args.work_id : undefined,
|
|
2467
|
+
target: args.target === 'grok' || args.target === 'claude' || args.target === 'codex' ? args.target : 'codex',
|
|
2468
|
+
objective: typeof args.objective === 'string' ? args.objective : 'Delegated cerebellum work',
|
|
2469
|
+
acceptanceCriteria: Array.isArray(args.acceptance_criteria) ? args.acceptance_criteria.map(String) : undefined,
|
|
2470
|
+
allowedPaths: Array.isArray(args.allowed_paths) ? args.allowed_paths.map(String) : undefined,
|
|
2471
|
+
forbiddenPaths: Array.isArray(args.forbidden_paths) ? args.forbidden_paths.map(String) : undefined,
|
|
2472
|
+
available: typeof args.available === 'boolean' ? args.available : undefined,
|
|
2473
|
+
codexAvailable: args.codex_available !== false,
|
|
2474
|
+
workerOutput: args.worker_output && typeof args.worker_output === 'object' && !Array.isArray(args.worker_output)
|
|
2475
|
+
? args.worker_output as { uncertain?: boolean; summary?: string; patchProposal?: string; evidenceSummary?: string }
|
|
2476
|
+
: undefined,
|
|
2477
|
+
},
|
|
2478
|
+
);
|
|
2479
|
+
return result(facade as unknown as Record<string, unknown>, facade.status === 'blocked');
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (operation === 'stop') {
|
|
2483
|
+
const workId = String(args.work_id ?? '').trim();
|
|
2484
|
+
const existingWork = workId ? getWorkContract(store, workId) : undefined;
|
|
2485
|
+
const terminalCleanupOnly = Boolean(
|
|
2486
|
+
existingWork
|
|
2487
|
+
&& ['completed', 'failed', 'cancelled'].includes(existingWork.status)
|
|
2488
|
+
&& args.cleanup !== false,
|
|
2489
|
+
);
|
|
2490
|
+
if (terminalCleanupOnly && existingWork) {
|
|
2491
|
+
// Terminal resource cleanup is not semantic terminalization. Never
|
|
2492
|
+
// reacquire/reopen Controller ownership merely to settle an outcome
|
|
2493
|
+
// that is already durable. Active ownership/rounds still fence the
|
|
2494
|
+
// cleanup path, while the lower terminal cleanup authority preserves
|
|
2495
|
+
// dirty/unique source before removing Work-owned resources.
|
|
2496
|
+
const owner = getControllerSession(store, workId);
|
|
2497
|
+
let cleanupAuthority: ControllerTerminalizationAuthority | undefined;
|
|
2498
|
+
if (owner) {
|
|
2499
|
+
try {
|
|
2500
|
+
cleanupAuthority = currentTerminalCleanupAuthority(ctx, store, workId, args);
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
return result(buildFacadeResult({
|
|
2503
|
+
status: 'blocked',
|
|
2504
|
+
summary: error instanceof Error ? error.message : `Work ${workId} terminal cleanup authority check failed.`,
|
|
2505
|
+
data: { workId, terminalizationApplied: false, cleanupOnly: true },
|
|
2506
|
+
}) as unknown as Record<string, unknown>, true);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
const relay = getControllerRoundRelay(store, workId);
|
|
2510
|
+
if (relay && !['goal_complete', 'handed_off', 'failed'].includes(relay.status)) {
|
|
2511
|
+
return result(buildFacadeResult({
|
|
2512
|
+
status: 'blocked',
|
|
2513
|
+
summary: `WORK_TERMINAL_CLEANUP_ACTIVE_ROUND: ${workId}:${relay.status}.`,
|
|
2514
|
+
data: { workId, terminalizationApplied: false, cleanupOnly: true, relayStatus: relay.status },
|
|
2515
|
+
}) as unknown as Record<string, unknown>, true);
|
|
2516
|
+
}
|
|
2517
|
+
try {
|
|
2518
|
+
const cleanup = await reconcileSingleTerminalWorkCleanup(
|
|
2519
|
+
ctx.controllerHome,
|
|
2520
|
+
repository.repoId,
|
|
2521
|
+
workId,
|
|
2522
|
+
{
|
|
2523
|
+
targetBranch: typeof args.target_branch === 'string' ? args.target_branch : undefined,
|
|
2524
|
+
deleteBranch: args.delete_branch !== false,
|
|
2525
|
+
controllerAuthority: cleanupAuthority,
|
|
2526
|
+
},
|
|
2527
|
+
);
|
|
2528
|
+
const cleanupCompleted = cleanup.status === 'cleaned';
|
|
2529
|
+
const cleanupRetained = cleanup.status === 'retained';
|
|
2530
|
+
const cleanupSettled = cleanupCompleted || cleanupRetained || cleanup.status === 'no_handle';
|
|
2531
|
+
return result(buildFacadeResult({
|
|
2532
|
+
status: cleanupSettled ? 'ok' : 'blocked',
|
|
2533
|
+
summary: cleanupCompleted
|
|
2534
|
+
? `Terminal Work ${workId} outcome was preserved; managed repository cleanup completed without reopening Controller ownership.`
|
|
2535
|
+
: cleanupRetained
|
|
2536
|
+
? `Terminal Work ${workId} outcome was preserved; managed repository retention was recorded durably.`
|
|
2537
|
+
: cleanup.status === 'no_handle'
|
|
2538
|
+
? `Terminal Work ${workId} has no managed repository resources requiring cleanup.`
|
|
2539
|
+
: `Terminal Work ${workId} outcome was preserved; managed repository cleanup remains incomplete and visible for retry.`,
|
|
2540
|
+
data: {
|
|
2541
|
+
work: summarizeWorkContract(existingWork),
|
|
2542
|
+
finalStatus: existingWork.status,
|
|
2543
|
+
terminalizationApplied: false,
|
|
2544
|
+
cleanupOnly: true,
|
|
2545
|
+
worktreeDeleted: cleanupCompleted,
|
|
2546
|
+
cleanupPending: !cleanupSettled,
|
|
2547
|
+
cleanupRetained,
|
|
2548
|
+
lifecycleCleanup: cleanup,
|
|
2549
|
+
},
|
|
2550
|
+
}) as unknown as Record<string, unknown>, !cleanupSettled);
|
|
2551
|
+
} catch (error) {
|
|
2552
|
+
return result(buildFacadeResult({
|
|
2553
|
+
status: 'blocked',
|
|
2554
|
+
summary: `Terminal Work ${workId} cleanup-only reconciliation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2555
|
+
data: {
|
|
2556
|
+
work: summarizeWorkContract(existingWork),
|
|
2557
|
+
finalStatus: existingWork.status,
|
|
2558
|
+
terminalizationApplied: false,
|
|
2559
|
+
cleanupOnly: true,
|
|
2560
|
+
worktreeDeleted: false,
|
|
2561
|
+
cleanupPending: true,
|
|
2562
|
+
},
|
|
2563
|
+
}) as unknown as Record<string, unknown>, true);
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
const observedOwner = workId ? getControllerSession(store, workId) : undefined;
|
|
2567
|
+
const observedRelay = workId ? getControllerRoundRelay(store, workId) : undefined;
|
|
2568
|
+
const activeWorkProcess = workId
|
|
2569
|
+
? processRuntimeResourceDiagnostics().activeProcessIds
|
|
2570
|
+
.map((processId) => getProcessRecord(ctx.controllerHome, repository.repoId, processId))
|
|
2571
|
+
.some((process) => Boolean(process && process.workId === workId && isManagedProcessActive(process)))
|
|
2572
|
+
: false;
|
|
2573
|
+
// Maintenance stop is intentionally narrower than ControllerRound recovery.
|
|
2574
|
+
// Once no Controller owns the Work, no Process is active, and the relay is
|
|
2575
|
+
// absent or terminally failed/handed-off, the obsolete round capability must
|
|
2576
|
+
// not make the durable Work immortal. The ControllerSession task lock below
|
|
2577
|
+
// still fences a concurrent fresh claim before Work terminalization.
|
|
2578
|
+
const ownerlessMaintenanceStop = Boolean(
|
|
2579
|
+
workId
|
|
2580
|
+
&& !observedOwner
|
|
2581
|
+
&& !activeWorkProcess
|
|
2582
|
+
&& Boolean(observedRelay)
|
|
2583
|
+
&& ['failed', 'handed_off'].includes(observedRelay!.status),
|
|
2584
|
+
);
|
|
2585
|
+
if (!ownerlessMaintenanceStop) {
|
|
2586
|
+
try {
|
|
2587
|
+
if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
2588
|
+
} catch (error) {
|
|
2589
|
+
const blocked = buildFacadeResult({
|
|
2590
|
+
status: 'blocked',
|
|
2591
|
+
summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`,
|
|
2592
|
+
data: { workId, terminalizationApplied: false },
|
|
2593
|
+
});
|
|
2594
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
let terminalizationAuthority: ControllerTerminalizationAuthority | undefined;
|
|
2598
|
+
if (!ownerlessMaintenanceStop) {
|
|
2599
|
+
try {
|
|
2600
|
+
terminalizationAuthority = currentFacadeTerminalizationAuthority(ctx, store, workId, args);
|
|
2601
|
+
} catch (error) {
|
|
2602
|
+
const blocked = buildFacadeResult({
|
|
2603
|
+
status: 'blocked',
|
|
2604
|
+
summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`,
|
|
2605
|
+
data: { workId, terminalizationApplied: false },
|
|
2606
|
+
});
|
|
2607
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
const fenced = withControllerSessionTerminalizationFence(
|
|
2612
|
+
store,
|
|
2613
|
+
{
|
|
2614
|
+
workId,
|
|
2615
|
+
actor: `rh-work-stop:${terminalizationAuthority?.controllerId ?? String(args.requested_by ?? 'explicit')}`,
|
|
2616
|
+
authority: terminalizationAuthority,
|
|
2617
|
+
},
|
|
2618
|
+
() => runGoalWorkloop({ ...workloopCtx, sourceRevision: workloopCtx.sourceRevision ?? undefined }, 'stop', args),
|
|
2619
|
+
);
|
|
2620
|
+
if (!fenced.allowed) {
|
|
2621
|
+
const blocked = buildFacadeResult({
|
|
2622
|
+
status: 'blocked',
|
|
2623
|
+
summary: `WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`,
|
|
2624
|
+
data: {
|
|
2625
|
+
workId,
|
|
2626
|
+
terminalizationApplied: false,
|
|
2627
|
+
currentClaimGeneration: fenced.owner?.claimGeneration,
|
|
2628
|
+
},
|
|
2629
|
+
});
|
|
2630
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2631
|
+
}
|
|
2632
|
+
const facade = fenced.value;
|
|
2633
|
+
if (facade.status !== 'ok') {
|
|
2634
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
2635
|
+
}
|
|
2636
|
+
const reconcileStoppedRound = () => {
|
|
2637
|
+
const stopped = getWorkContract(store, workId);
|
|
2638
|
+
if (!stopped || !['failed', 'cancelled'].includes(stopped.status) || getControllerSession(store, workId)) return undefined;
|
|
2639
|
+
const terminalRelay = reconcileControllerRoundAfterTerminalWork(store, { workId, actor: 'rh-work-stop-terminal-reconcile' });
|
|
2640
|
+
if (terminalRelay) return terminalRelay;
|
|
2641
|
+
const retained = getRetainedControllerSession(store, workId);
|
|
2642
|
+
if (!retained) return undefined;
|
|
2643
|
+
return reconcileControllerRoundAfterAbandonedRelease(
|
|
2644
|
+
{ controllerHome: ctx.controllerHome, repoId: repository.repoId },
|
|
2645
|
+
{ workId, releasedSession: retained },
|
|
2646
|
+
);
|
|
2647
|
+
};
|
|
2648
|
+
try {
|
|
2649
|
+
const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'stop');
|
|
2650
|
+
if (!physical) {
|
|
2651
|
+
const relay = reconcileStoppedRound();
|
|
2652
|
+
return result({
|
|
2653
|
+
...facade,
|
|
2654
|
+
data: { ...(facade.data && typeof facade.data === 'object' ? facade.data : {}), ...(relay ? { relay } : {}) },
|
|
2655
|
+
} as unknown as Record<string, unknown>);
|
|
2656
|
+
}
|
|
2657
|
+
const cleanup = contextRecord(physical.structuredContent);
|
|
2658
|
+
const cleanupCompleted = cleanup.cleanupCompleted === true || contextRecord(cleanup.work).state === 'cleaned';
|
|
2659
|
+
const cleanupRetained = cleanup.cleanupRetained === true;
|
|
2660
|
+
const cleanupSettled = cleanupCompleted || cleanupRetained;
|
|
2661
|
+
const stoppedRelay = reconcileStoppedRound();
|
|
2662
|
+
const response = {
|
|
2663
|
+
...facade,
|
|
2664
|
+
status: cleanupSettled ? 'ok' : 'blocked',
|
|
2665
|
+
summary: cleanupCompleted
|
|
2666
|
+
? `${facade.summary} Managed worktree and branch cleanup completed automatically.`
|
|
2667
|
+
: cleanupRetained
|
|
2668
|
+
? `${facade.summary} Managed worktree and branch retention was recorded durably; automatic cleanup is disabled for this terminal Work.`
|
|
2669
|
+
: `${facade.summary} Automatic managed-resource cleanup is incomplete and remains visible for retry.`,
|
|
2670
|
+
data: {
|
|
2671
|
+
...(facade.data && typeof facade.data === 'object' ? facade.data : {}),
|
|
2672
|
+
worktreeDeleted: cleanupCompleted,
|
|
2673
|
+
cleanupPending: !cleanupSettled,
|
|
2674
|
+
cleanupRetained,
|
|
2675
|
+
lifecycleCleanup: cleanup,
|
|
2676
|
+
...(stoppedRelay ? { relay: stoppedRelay } : {}),
|
|
2677
|
+
},
|
|
2678
|
+
};
|
|
2679
|
+
return result(response as unknown as Record<string, unknown>, !cleanupSettled || physical.isError === true);
|
|
2680
|
+
} catch (error) {
|
|
2681
|
+
const response = {
|
|
2682
|
+
...facade,
|
|
2683
|
+
status: 'blocked',
|
|
2684
|
+
summary: `${facade.summary} Automatic managed-resource cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2685
|
+
data: {
|
|
2686
|
+
...(facade.data && typeof facade.data === 'object' ? facade.data : {}),
|
|
2687
|
+
worktreeDeleted: false,
|
|
2688
|
+
cleanupPending: true,
|
|
2689
|
+
},
|
|
2690
|
+
};
|
|
2691
|
+
return result(response as unknown as Record<string, unknown>, true);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
if (operation === 'finalize') {
|
|
2696
|
+
const workId = String(args.work_id ?? '').trim();
|
|
2697
|
+
try {
|
|
2698
|
+
if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
2699
|
+
} catch (error) {
|
|
2700
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : `Work ${workId} controller-round authority check failed.`, data: { workId, lifecycleClosed: false } });
|
|
2701
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2702
|
+
}
|
|
2703
|
+
const finalizeReconciliation = workId
|
|
2704
|
+
? reconcileTerminalFacadeWorkVerifications(ctx, repository, workId)
|
|
2705
|
+
: undefined;
|
|
2706
|
+
const semanticFinalizeContext = {
|
|
2707
|
+
...workloopCtx,
|
|
2708
|
+
sourceRevision: finalizeReconciliation?.sourceRevision ?? workloopCtx.sourceRevision ?? undefined,
|
|
2709
|
+
workspaceFingerprint: finalizeReconciliation?.workspaceFingerprint,
|
|
2710
|
+
implementationReviewWorkspaceFingerprint: finalizeReconciliation?.implementationReviewWorkspaceFingerprint,
|
|
2711
|
+
workspaceChangedPaths: finalizeReconciliation?.workspaceChangedPaths,
|
|
2712
|
+
workBoundProcessEvidenceIds: finalizeReconciliation?.workBoundProcessEvidenceIds,
|
|
2713
|
+
};
|
|
2714
|
+
let before = workId ? getWorkContract(store, workId) : undefined;
|
|
2715
|
+
let terminalizationAuthority: ControllerTerminalizationAuthority | undefined;
|
|
2716
|
+
// Finalize may commit, merge, clean resources, and complete the Work.
|
|
2717
|
+
// It therefore shares the same exact-claim authority as stop. Transport
|
|
2718
|
+
// or Runtime recovery remains an explicit controller_claim operation;
|
|
2719
|
+
// terminalization itself must never rebind an unrelated controller scope.
|
|
2720
|
+
if (before && !['completed', 'failed', 'cancelled'].includes(before.status)) {
|
|
2721
|
+
try {
|
|
2722
|
+
terminalizationAuthority = currentFacadeTerminalizationAuthority(ctx, store, workId, args);
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : `Work ${workId} terminalization authority check failed.`, data: { workId, terminalizationApplied: false, lifecycleClosed: false } });
|
|
2725
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
if (before && !before.completionReceipt && args.reconcile_historical_delivery === true) {
|
|
2729
|
+
try {
|
|
2730
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
2731
|
+
const owner = getControllerSession(store, workId);
|
|
2732
|
+
if (!owner || (controllerSessionPrincipalId(owner)) !== identity.principalId) {
|
|
2733
|
+
throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_CLAIM_REQUIRED: ${workId}`);
|
|
2734
|
+
}
|
|
2735
|
+
const historicalHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
2736
|
+
const explicitTargetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
|
|
2737
|
+
? args.target_branch.trim()
|
|
2738
|
+
: undefined;
|
|
2739
|
+
const reconciliationTargetBranch = historicalHandle
|
|
2740
|
+
? resolveWorkDeliveryTargetBranch(historicalHandle, repository.defaultBranch, explicitTargetBranch)
|
|
2741
|
+
: explicitTargetBranch || repository.defaultBranch || 'main';
|
|
2742
|
+
if (historicalHandle?.managedWorktree) {
|
|
2743
|
+
const managedCleanupComplete = historicalHandle.finalization.merge === 'done'
|
|
2744
|
+
&& historicalHandle.finalization.branchCleanup === 'done'
|
|
2745
|
+
&& historicalHandle.finalization.worktreeCleanup === 'done'
|
|
2746
|
+
&& !existsSync(historicalHandle.worktreePath);
|
|
2747
|
+
if (!managedCleanupComplete) {
|
|
2748
|
+
let managedCheckout: ReturnType<typeof selectRepositoryCheckout>;
|
|
2749
|
+
try {
|
|
2750
|
+
managedCheckout = selectRepositoryCheckout(repository, historicalHandle.checkoutId, { allowArchived: true });
|
|
2751
|
+
} catch {
|
|
2752
|
+
throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_MANAGED_CLEANUP_REQUIRED: ${workId}`);
|
|
2753
|
+
}
|
|
2754
|
+
const managedStatus = repositoryGitStatus(managedCheckout);
|
|
2755
|
+
const targetRevision = String(args.reconcile_target_revision ?? '').trim();
|
|
2756
|
+
const exactCleanCandidate = existsSync(historicalHandle.worktreePath)
|
|
2757
|
+
&& managedStatus.clean
|
|
2758
|
+
&& managedStatus.head === targetRevision
|
|
2759
|
+
&& historicalHandle.expectedHead?.trim() === targetRevision;
|
|
2760
|
+
if (!exactCleanCandidate) throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_MANAGED_CLEANUP_REQUIRED: ${workId}`);
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
acceptReviewedDirectEditWorkReconciliation({
|
|
2764
|
+
controllerHome: ctx.controllerHome,
|
|
2765
|
+
repoId: repository.repoId,
|
|
2766
|
+
checkoutId: before.checkoutId ?? repository.activeCheckoutId,
|
|
2767
|
+
repoRoot: repository.canonicalRoot,
|
|
2768
|
+
workId,
|
|
2769
|
+
targetBranch: reconciliationTargetBranch,
|
|
2770
|
+
targetRevision: String(args.reconcile_target_revision ?? ''),
|
|
2771
|
+
comparedPaths: Array.isArray(args.reconcile_compared_paths) ? args.reconcile_compared_paths.map(String) : [],
|
|
2772
|
+
reviewer: identity.principalId,
|
|
2773
|
+
rationale: String(args.reconcile_rationale ?? ''),
|
|
2774
|
+
cleanupOwnershipProof: String(args.reconcile_cleanup_proof ?? ''),
|
|
2775
|
+
});
|
|
2776
|
+
const released = releaseObservedControllerSession(store, {
|
|
2777
|
+
workId,
|
|
2778
|
+
actor: `direct-edit-reconciliation:${identity.principalId}`,
|
|
2779
|
+
owner,
|
|
2780
|
+
});
|
|
2781
|
+
if (!released.allowed) {
|
|
2782
|
+
throw new Error(`DIRECT_EDIT_WORK_RECONCILIATION_CONTROLLER_RELEASE_FENCED: ${workId}:${released.reason}`);
|
|
2783
|
+
}
|
|
2784
|
+
before = getWorkContract(store, workId);
|
|
2785
|
+
} catch (error) {
|
|
2786
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Historical Work delivery reconciliation failed.', data: { workId, lifecycleClosed: false } });
|
|
2787
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
const repositoryDeliveryHandle = workId
|
|
2791
|
+
? readWorkHandle(ctx.controllerHome, repository.repoId, workId)
|
|
2792
|
+
: undefined;
|
|
2793
|
+
const effectHasRepositoryDelta = Boolean(
|
|
2794
|
+
before
|
|
2795
|
+
&& ['local_effect', 'remote_effect'].includes(before.workKind)
|
|
2796
|
+
&& (
|
|
2797
|
+
(repositoryDeliveryHandle && repositoryWorkHandleHasSourceDelta(repository, repositoryDeliveryHandle))
|
|
2798
|
+
|| (finalizeReconciliation?.workspaceChangedPaths?.length ?? 0) > 0
|
|
2799
|
+
)
|
|
2800
|
+
);
|
|
2801
|
+
if (effectHasRepositoryDelta && !repositoryDeliveryHandle) {
|
|
2802
|
+
const blocked = buildFacadeResult({
|
|
2803
|
+
status: 'blocked',
|
|
2804
|
+
summary: `WORK_EFFECT_REPOSITORY_DELIVERY_HANDLE_REQUIRED: ${workId} has repository source delta and cannot use effect-only terminalization without a physical WorkHandle.`,
|
|
2805
|
+
data: { workId, lifecycleClosed: false },
|
|
2806
|
+
});
|
|
2807
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2808
|
+
}
|
|
2809
|
+
// Pure remote effects may complete from either the canonical plugin
|
|
2810
|
+
// action receipt or the deliberately narrow trusted repository
|
|
2811
|
+
// Process authority (`git push` + exact remote lease). A remote_effect
|
|
2812
|
+
// that acquired source delta is no longer pure and must use physical
|
|
2813
|
+
// repository delivery instead.
|
|
2814
|
+
if (before?.workKind === 'remote_effect' && !before.completionReceipt && !effectHasRepositoryDelta) {
|
|
2815
|
+
try {
|
|
2816
|
+
before = finalizeRemoteEffectWorkFromActionReceipt(ctx.controllerHome, repository.repoId, workId);
|
|
2817
|
+
} catch (pluginError) {
|
|
2818
|
+
const checkoutId = before.checkoutId ?? repositoryDeliveryHandle?.checkoutId ?? repository.activeCheckoutId;
|
|
2819
|
+
const processCompleted = finalizeRemoteEffectWorkFromRepositoryProcessReceipt(ctx, repository, workId, checkoutId);
|
|
2820
|
+
if (processCompleted) {
|
|
2821
|
+
before = processCompleted;
|
|
2822
|
+
} else {
|
|
2823
|
+
const blocked = buildFacadeResult({
|
|
2824
|
+
status: 'blocked',
|
|
2825
|
+
summary: pluginError instanceof Error ? pluginError.message : 'Remote-effect semantic finalization failed.',
|
|
2826
|
+
data: { workId, lifecycleClosed: false },
|
|
2827
|
+
});
|
|
2828
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
const completedCleanupPending = Boolean(
|
|
2833
|
+
before?.completionReceipt
|
|
2834
|
+
&& args.cleanup !== false
|
|
2835
|
+
&& before.worktreeRef?.trim()
|
|
2836
|
+
&& existsSync(before.worktreeRef),
|
|
2837
|
+
);
|
|
2838
|
+
const repositoryDeliveryRequired = Boolean(
|
|
2839
|
+
before
|
|
2840
|
+
&& before.workKind !== 'read_only_review'
|
|
2841
|
+
&& (
|
|
2842
|
+
!['local_effect', 'remote_effect'].includes(before.workKind)
|
|
2843
|
+
|| effectHasRepositoryDelta
|
|
2844
|
+
)
|
|
2845
|
+
);
|
|
2846
|
+
if (before && ((repositoryDeliveryRequired && !before.completionReceipt) || completedCleanupPending)) {
|
|
2847
|
+
try {
|
|
2848
|
+
const physical = await finalizeFacadeWorkHandle(ctx, repository, args, 'finalize');
|
|
2849
|
+
if (physical?.isError === true) return physical;
|
|
2850
|
+
const refreshed = getWorkContract(store, workId);
|
|
2851
|
+
if (physical && !refreshed?.completionReceipt) return physical;
|
|
2852
|
+
} catch (error) {
|
|
2853
|
+
const blocked = buildFacadeResult({
|
|
2854
|
+
status: 'blocked',
|
|
2855
|
+
summary: error instanceof Error ? error.message : 'Work delivery/finalization failed.',
|
|
2856
|
+
data: { workId, lifecycleClosed: false },
|
|
2857
|
+
});
|
|
2858
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
const semanticWork = getWorkContract(store, workId);
|
|
2862
|
+
let facade;
|
|
2863
|
+
if (semanticWork && !['completed', 'failed', 'cancelled'].includes(semanticWork.status)) {
|
|
2864
|
+
try {
|
|
2865
|
+
const authority = terminalizationAuthority ?? currentFacadeTerminalizationAuthority(ctx, store, workId, args);
|
|
2866
|
+
const fenced = withControllerSessionTerminalizationFence(
|
|
2867
|
+
store,
|
|
2868
|
+
{
|
|
2869
|
+
workId,
|
|
2870
|
+
actor: `rh-work-finalize:${authority.controllerId}:${authority.controllerInstanceId}`,
|
|
2871
|
+
authority,
|
|
2872
|
+
},
|
|
2873
|
+
() => runGoalWorkloop(semanticFinalizeContext, 'finalize', args),
|
|
2874
|
+
);
|
|
2875
|
+
if (!fenced.allowed) {
|
|
2876
|
+
throw new Error(`WORK_TERMINALIZATION_AUTHORITY_FENCED: ${workId}:${fenced.reason}`);
|
|
2877
|
+
}
|
|
2878
|
+
facade = fenced.value;
|
|
2879
|
+
} catch (error) {
|
|
2880
|
+
const blocked = buildFacadeResult({
|
|
2881
|
+
status: 'blocked',
|
|
2882
|
+
summary: error instanceof Error ? error.message : `Work ${workId} semantic finalization authority check failed.`,
|
|
2883
|
+
data: { workId, terminalizationApplied: false, lifecycleClosed: false },
|
|
2884
|
+
});
|
|
2885
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2886
|
+
}
|
|
2887
|
+
} else {
|
|
2888
|
+
facade = runGoalWorkloop(semanticFinalizeContext, 'finalize', args);
|
|
2889
|
+
}
|
|
2890
|
+
let completed = getWorkContract(store, workId);
|
|
2891
|
+
const postSemanticCleanupPending = Boolean(
|
|
2892
|
+
completed?.completionReceipt
|
|
2893
|
+
&& args.cleanup !== false
|
|
2894
|
+
&& completed.worktreeRef?.trim()
|
|
2895
|
+
&& existsSync(completed.worktreeRef),
|
|
2896
|
+
);
|
|
2897
|
+
if (postSemanticCleanupPending) {
|
|
2898
|
+
try {
|
|
2899
|
+
const physical = await finalizeFacadeWorkHandle(ctx, repository, { ...args, commit: false, merge: false }, 'finalize');
|
|
2900
|
+
if (physical?.isError === true) return physical;
|
|
2901
|
+
completed = getWorkContract(store, workId);
|
|
2902
|
+
} catch (error) {
|
|
2903
|
+
const blocked = buildFacadeResult({
|
|
2904
|
+
status: 'blocked',
|
|
2905
|
+
summary: error instanceof Error ? error.message : `Work ${workId} terminal cleanup reconciliation failed.`,
|
|
2906
|
+
data: { workId, terminalizationApplied: true, lifecycleClosed: false },
|
|
2907
|
+
});
|
|
2908
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
// Finalizing a Work proves the Work lifecycle only. A Plan step may aggregate
|
|
2912
|
+
// acceptance criteria that are broader than this Work (for example, a canary
|
|
2913
|
+
// plus a later stabilization soak), so finalize must never synthesize semantic
|
|
2914
|
+
// Plan acceptance. Only the explicit plan_accept_step operation may promote a
|
|
2915
|
+
// validating step to completed after the Controller reviews all criteria.
|
|
2916
|
+
const completedHandle = readWorkHandle(ctx.controllerHome, repository.repoId, workId);
|
|
2917
|
+
const lifecycleClosed = Boolean(completed?.completionReceipt)
|
|
2918
|
+
&& (!completedHandle || completedHandle.finalization.worktreeCleanup !== 'pending');
|
|
2919
|
+
let blockerResolutionOccurrences: Array<{ scheduleId: string; occurrenceId?: string; status?: string }> = [];
|
|
2920
|
+
if (lifecycleClosed && facade.status === 'ok') {
|
|
2921
|
+
const explicitTargetBranch = typeof args.target_branch === 'string' && args.target_branch.trim()
|
|
2922
|
+
? args.target_branch.trim()
|
|
2923
|
+
: undefined;
|
|
2924
|
+
const targetBranch = completedHandle
|
|
2925
|
+
? resolveWorkDeliveryTargetBranch(completedHandle, repository.defaultBranch, explicitTargetBranch)
|
|
2926
|
+
: explicitTargetBranch || repository.defaultBranch || 'main';
|
|
2927
|
+
const targetStatus = repositoryGitStatus(repository);
|
|
2928
|
+
if (targetStatus.clean && targetStatus.branch === targetBranch && targetStatus.head) {
|
|
2929
|
+
blockerResolutionOccurrences = await triggerWorkContinuationRepositoryEvent(
|
|
2930
|
+
ctx.controllerHome,
|
|
2931
|
+
repository.repoId,
|
|
2932
|
+
repositoryCleanContinuationEventName(targetBranch),
|
|
2933
|
+
`repository-clean:${targetBranch}:${targetStatus.head}`,
|
|
2934
|
+
{ data: { targetBranch, targetRevision: targetStatus.head, sourceWorkId: workId } },
|
|
2935
|
+
);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
const response = {
|
|
2939
|
+
...facade,
|
|
2940
|
+
data: {
|
|
2941
|
+
...(facade.data && typeof facade.data === 'object' ? facade.data : {}),
|
|
2942
|
+
lifecycleClosed,
|
|
2943
|
+
...(blockerResolutionOccurrences.length > 0 ? { blockerResolutionOccurrences } : {}),
|
|
2944
|
+
},
|
|
2945
|
+
};
|
|
2946
|
+
return result(response as unknown as Record<string, unknown>, response.status === 'blocked' || response.status === 'failed' || response.status === 'not_found');
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
let resumedControllerSession: ReturnType<typeof resumeControllerSession> | undefined;
|
|
2950
|
+
let cancelledWorkReauthorized = false;
|
|
2951
|
+
let reconstructedCancelledCheckout = false;
|
|
2952
|
+
let reconstructedRunningCheckout = false;
|
|
2953
|
+
let continuationSourceRevision = workloopCtx.sourceRevision;
|
|
2954
|
+
let continuationWorkspaceFingerprint: string | undefined;
|
|
2955
|
+
let continuationImplementationReviewWorkspaceFingerprint: string | undefined;
|
|
2956
|
+
let continuationWorkspaceChangedPaths: string[] | undefined;
|
|
2957
|
+
let continuationWorkBoundProcessEvidenceIds: string[] | undefined;
|
|
2958
|
+
if (operation === 'continue') {
|
|
2959
|
+
try {
|
|
2960
|
+
const workId = String(args.work_id ?? '').trim();
|
|
2961
|
+
if (workId) assertFacadeControllerRoundAuthority(ctx, store, workId, args);
|
|
2962
|
+
let work = getWorkContract(store, workId);
|
|
2963
|
+
if (work?.status === 'cancelled') {
|
|
2964
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
2965
|
+
const resumed = reauthorizeRetainedCancelledRepositoryWork({
|
|
2966
|
+
controllerHome: ctx.controllerHome,
|
|
2967
|
+
repository,
|
|
2968
|
+
workId,
|
|
2969
|
+
identity,
|
|
2970
|
+
requestedBy: typeof args.requested_by === 'string' ? args.requested_by : undefined,
|
|
2971
|
+
approvalConfirmed: args.approval_confirmed === true,
|
|
2972
|
+
prepareDependencies: args.needs_dependencies === true,
|
|
2973
|
+
});
|
|
2974
|
+
cancelledWorkReauthorized = true;
|
|
2975
|
+
reconstructedCancelledCheckout = resumed.reconstructedCheckout;
|
|
2976
|
+
work = getWorkContract(store, workId);
|
|
2977
|
+
}
|
|
2978
|
+
if (work && !['cancelled', 'completed', 'failed'].includes(work.status)) {
|
|
2979
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args);
|
|
2980
|
+
// Continue uses the same Kernel rebind authority as terminalization.
|
|
2981
|
+
// MCP transport identity is replaceable; principal/controller and
|
|
2982
|
+
// canonical Runtime ownership remain fenced by the ControllerSession.
|
|
2983
|
+
resumedControllerSession = bindFacadeControllerOwnership(ctx, store, workId, identity, {
|
|
2984
|
+
allowClaimIfMissing: true,
|
|
2985
|
+
leaseMs: 3_600_000,
|
|
2986
|
+
relayScopeId: typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : undefined,
|
|
2987
|
+
});
|
|
2988
|
+
rebindRepositoryWorkHandleControllerIdentity({
|
|
2989
|
+
controllerHome: ctx.controllerHome,
|
|
2990
|
+
repositoryId: repository.repoId,
|
|
2991
|
+
workId,
|
|
2992
|
+
identity: { sessionId: identity.sessionId, principalId: identity.principalId },
|
|
2993
|
+
});
|
|
2994
|
+
reconcileRepositoryWorkHandlePlacement({
|
|
2995
|
+
controllerHome: ctx.controllerHome,
|
|
2996
|
+
repositoryId: repository.repoId,
|
|
2997
|
+
workId,
|
|
2998
|
+
});
|
|
2999
|
+
const recovered = ensureRunningRepositoryWorkCheckout({
|
|
3000
|
+
controllerHome: ctx.controllerHome,
|
|
3001
|
+
repository,
|
|
3002
|
+
workId,
|
|
3003
|
+
identity,
|
|
3004
|
+
prepareDependencies: args.needs_dependencies === true,
|
|
3005
|
+
});
|
|
3006
|
+
reconstructedRunningCheckout = recovered.reconstructedCheckout;
|
|
3007
|
+
if (reconstructedRunningCheckout) {
|
|
3008
|
+
work = getWorkContract(store, workId);
|
|
3009
|
+
if (!work) throw new Error(`WORK_CONTINUE_RECONSTRUCTION_CONTRACT_MISSING: ${workId}`);
|
|
3010
|
+
}
|
|
3011
|
+
if (work.worktreePolicy.required === true && !work.worktreeRef) {
|
|
3012
|
+
materializeFacadeWorkPlacement(ctx, repository, workId, args);
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
if (workId) {
|
|
3016
|
+
const reconciled = reconcileTerminalFacadeWorkVerifications(ctx, repository, workId);
|
|
3017
|
+
continuationSourceRevision = reconciled.sourceRevision ?? continuationSourceRevision;
|
|
3018
|
+
continuationWorkspaceFingerprint = reconciled.workspaceFingerprint ?? continuationWorkspaceFingerprint;
|
|
3019
|
+
continuationImplementationReviewWorkspaceFingerprint = reconciled.implementationReviewWorkspaceFingerprint ?? continuationImplementationReviewWorkspaceFingerprint;
|
|
3020
|
+
continuationWorkspaceChangedPaths = reconciled.workspaceChangedPaths ?? continuationWorkspaceChangedPaths;
|
|
3021
|
+
continuationWorkBoundProcessEvidenceIds = reconciled.workBoundProcessEvidenceIds;
|
|
3022
|
+
}
|
|
3023
|
+
} catch (error) {
|
|
3024
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Controller resume failed.', data: { operation, executionStarted: false, ownershipResumed: false } });
|
|
3025
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
let terminalSuccessorAdmission: {
|
|
3030
|
+
predecessorWorkId: string;
|
|
3031
|
+
relay: ControllerRoundRelayRecord;
|
|
3032
|
+
identity: ReturnType<typeof authenticatedFacadeControllerIdentity>;
|
|
3033
|
+
} | undefined;
|
|
3034
|
+
if (operation === 'start' && String(args.work_relation ?? '').trim() === 'continue') {
|
|
3035
|
+
const predecessorWorkId = String(args.related_work_id ?? '').trim();
|
|
3036
|
+
const predecessorWork = predecessorWorkId ? getWorkContract(store, predecessorWorkId) : undefined;
|
|
3037
|
+
if (predecessorWork?.status === 'completed' && predecessorWork.planId) {
|
|
3038
|
+
const relay = getControllerRoundRelay(store, predecessorWorkId);
|
|
3039
|
+
if (!relay || !['claimed', 'pending_release'].includes(relay.status)) {
|
|
3040
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: `TERMINAL_SUCCESSOR_CONTROLLER_ROUND_REQUIRED: ${predecessorWorkId}:${relay?.status ?? 'missing'}`, data: { operation, executionStarted: false, predecessorWorkId } });
|
|
3041
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
3042
|
+
}
|
|
3043
|
+
const workKind = typeof args.work_kind === 'string' ? args.work_kind.trim() : '';
|
|
3044
|
+
if (!workKind) {
|
|
3045
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: `TERMINAL_SUCCESSOR_WORK_KIND_REQUIRED: ${predecessorWorkId}`, data: { operation, executionStarted: false, predecessorWorkId } });
|
|
3046
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
3047
|
+
}
|
|
3048
|
+
const explicitAuthorityId = typeof args.controller_authority_id === 'string' ? args.controller_authority_id.trim() : '';
|
|
3049
|
+
const explicitRelayScopeId = typeof args.relay_scope_id === 'string' ? args.relay_scope_id.trim() : '';
|
|
3050
|
+
if (!explicitAuthorityId || !explicitRelayScopeId) {
|
|
3051
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: `TERMINAL_SUCCESSOR_CONTROLLER_ROUND_AUTHORITY_REQUIRED: ${predecessorWorkId}`, data: { operation, executionStarted: false, predecessorWorkId } });
|
|
3052
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
3053
|
+
}
|
|
3054
|
+
try {
|
|
3055
|
+
assertFacadeControllerRoundAuthority(ctx, store, predecessorWorkId, args);
|
|
3056
|
+
const identity = authenticatedFacadeControllerIdentity(ctx, args, { allowTransportSessionRollover: true });
|
|
3057
|
+
if (relay.controllerId !== identity.controllerId || relay.principalId !== identity.principalId || relay.controllerType !== identity.controllerType) {
|
|
3058
|
+
throw new Error(`TERMINAL_SUCCESSOR_CONTROLLER_ROUND_IDENTITY_MISMATCH: ${predecessorWorkId}`);
|
|
3059
|
+
}
|
|
3060
|
+
terminalSuccessorAdmission = { predecessorWorkId, relay, identity };
|
|
3061
|
+
} catch (error) {
|
|
3062
|
+
const facade = buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? error.message : 'Terminal successor ControllerRound authority validation failed.', data: { operation, executionStarted: false, predecessorWorkId } });
|
|
3063
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
const semanticAdmissionRequired = operation === 'start' && Boolean(
|
|
3069
|
+
(typeof args.plan_id === 'string' && args.plan_id.trim())
|
|
3070
|
+
|| (typeof args.plan_step_id === 'string' && args.plan_step_id.trim())
|
|
3071
|
+
|| (typeof args.requirement_id === 'string' && args.requirement_id.trim())
|
|
3072
|
+
|| (typeof args.related_work_id === 'string' && args.related_work_id.trim())
|
|
3073
|
+
|| (typeof args.work_relation === 'string' && args.work_relation.trim()),
|
|
3074
|
+
);
|
|
3075
|
+
const startContext = {
|
|
3076
|
+
...workloopCtx,
|
|
3077
|
+
sourceRevision: continuationSourceRevision ?? undefined,
|
|
3078
|
+
workspaceFingerprint: continuationWorkspaceFingerprint,
|
|
3079
|
+
implementationReviewWorkspaceFingerprint: continuationImplementationReviewWorkspaceFingerprint,
|
|
3080
|
+
workspaceChangedPaths: continuationWorkspaceChangedPaths,
|
|
3081
|
+
workBoundProcessEvidenceIds: continuationWorkBoundProcessEvidenceIds,
|
|
3082
|
+
semanticAdmissionLocked: semanticAdmissionRequired,
|
|
3083
|
+
};
|
|
3084
|
+
let trustedEngineeringEvidence: ReturnType<typeof mintEngineeringAdmissionEvidence> | undefined;
|
|
3085
|
+
if ((operation === 'start' || operation === 'continue') && args.engineering_preconditions !== undefined) {
|
|
3086
|
+
try {
|
|
3087
|
+
const sourceRevision = startContext.sourceRevision?.trim();
|
|
3088
|
+
if (!sourceRevision) throw new Error('ENGINEERING_PRECONDITIONS_SOURCE_REQUIRED');
|
|
3089
|
+
const existingWork = operation === 'continue'
|
|
3090
|
+
? getWorkContract(store, String(args.work_id ?? '').trim())
|
|
3091
|
+
: undefined;
|
|
3092
|
+
const requirementContext = existingWork
|
|
3093
|
+
? { objective: existingWork.objective, acceptanceCriteria: existingWork.acceptanceCriteria }
|
|
3094
|
+
: {
|
|
3095
|
+
objective: String(args.objective ?? ''),
|
|
3096
|
+
acceptanceCriteria: Array.isArray(args.acceptance_criteria) ? args.acceptance_criteria.map(String) : [],
|
|
3097
|
+
};
|
|
3098
|
+
const verified = mintEngineeringAdmissionEvidence({
|
|
3099
|
+
repoRoot: repository.canonicalRoot,
|
|
3100
|
+
sourceRevision,
|
|
3101
|
+
draft: args.engineering_preconditions,
|
|
3102
|
+
requirementContext,
|
|
3103
|
+
existingProjectContractReceipt: existingWork?.engineeringContext?.projectContractReceipt,
|
|
3104
|
+
});
|
|
3105
|
+
trustedEngineeringEvidence = verified;
|
|
3106
|
+
} catch (error) {
|
|
3107
|
+
const facade = buildFacadeResult({
|
|
3108
|
+
status: 'blocked',
|
|
3109
|
+
summary: error instanceof Error ? error.message : 'ENGINEERING_PRECONDITIONS_INVALID',
|
|
3110
|
+
data: { operation, executionStarted: false, engineeringPreconditionsAccepted: false },
|
|
3111
|
+
});
|
|
3112
|
+
return result(facade as unknown as Record<string, unknown>, true);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
const facade = semanticAdmissionRequired
|
|
3116
|
+
? await withPrimaryWorkAdmissionLockAsync(store, () => runGoalWorkloop(startContext, 'start', args, { verifiedEngineeringEvidence: trustedEngineeringEvidence }))
|
|
3117
|
+
: runGoalWorkloop(startContext, operation as 'start' | 'continue', args, { verifiedEngineeringEvidence: trustedEngineeringEvidence });
|
|
3118
|
+
const facadeData = facade.data && typeof facade.data === 'object' ? facade.data as Record<string, unknown> : {};
|
|
3119
|
+
const facadeWorkId = contextText(contextRecord(facadeData.work).workId, 200);
|
|
3120
|
+
if (facade.status === 'ok' && facadeWorkId) {
|
|
3121
|
+
try {
|
|
3122
|
+
if (facadeData.workContractCreated === true) {
|
|
3123
|
+
materializeFacadeWorkPlacement(ctx, repository, facadeWorkId, args);
|
|
3124
|
+
}
|
|
3125
|
+
const handle = ensureFacadeWorkHandle(ctx, repository, facadeWorkId, args);
|
|
3126
|
+
if (handle) facadeData.executionHandle = { workId: handle.workId, checkoutId: handle.checkoutId, managedWorktree: handle.managedWorktree, state: handle.state };
|
|
3127
|
+
if (facadeData.workContractCreated === true && !terminalSuccessorAdmission) {
|
|
3128
|
+
const claimed = claimNewFacadeWork(ctx, repository, facadeWorkId, args);
|
|
3129
|
+
facadeData.controllerSession = claimed.session;
|
|
3130
|
+
facadeData.controllerAuthorityId = claimed.controllerAuthorityId;
|
|
3131
|
+
facadeData.controllerAuthorityCarrier = 'controller_authority_id_or_session_id_compat';
|
|
3132
|
+
facadeData.ownershipClaimed = true;
|
|
3133
|
+
}
|
|
3134
|
+
if (terminalSuccessorAdmission) {
|
|
3135
|
+
const currentRelay = getControllerRoundRelay(store, terminalSuccessorAdmission.predecessorWorkId);
|
|
3136
|
+
let boundRelay: ControllerRoundRelayRecord;
|
|
3137
|
+
if (currentRelay?.status === 'pending_release') {
|
|
3138
|
+
// Idempotent retry after the Controller already submitted its
|
|
3139
|
+
// disposition. Never create/rebind another successor.
|
|
3140
|
+
if (currentRelay.successorWorkId !== facadeWorkId) {
|
|
3141
|
+
throw new Error(`CONTROLLER_RELAY_SUCCESSOR_ALREADY_BOUND: ${terminalSuccessorAdmission.predecessorWorkId}:${currentRelay.successorWorkId ?? 'none'}`);
|
|
3142
|
+
}
|
|
3143
|
+
boundRelay = currentRelay;
|
|
3144
|
+
} else {
|
|
3145
|
+
boundRelay = bindControllerRoundSuccessorWork(store, {
|
|
3146
|
+
workId: terminalSuccessorAdmission.predecessorWorkId,
|
|
3147
|
+
successorWorkId: facadeWorkId,
|
|
3148
|
+
identity: terminalSuccessorAdmission.identity,
|
|
3149
|
+
controllerAuthorityId: terminalSuccessorAdmission.relay.authorityId,
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
facadeData.predecessorWorkId = terminalSuccessorAdmission.predecessorWorkId;
|
|
3153
|
+
facadeData.successorWorkId = facadeWorkId;
|
|
3154
|
+
facadeData.terminalSuccessorRelay = boundRelay;
|
|
3155
|
+
facadeData.ownershipClaimed = false;
|
|
3156
|
+
}
|
|
3157
|
+
} catch (error) {
|
|
3158
|
+
const blocked = buildFacadeResult({ status: 'blocked', summary: `WORK_HANDLE_MATERIALIZATION_FAILED: ${error instanceof Error ? error.message : String(error)}`, data: { ...facadeData, workId: facadeWorkId, executionStarted: false, canonicalWorkRetained: true } });
|
|
3159
|
+
return result(blocked as unknown as Record<string, unknown>, true);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
const response = resumedControllerSession
|
|
3163
|
+
? {
|
|
3164
|
+
...facade,
|
|
3165
|
+
...(cancelledWorkReauthorized
|
|
3166
|
+
? {
|
|
3167
|
+
status: 'ok' as const,
|
|
3168
|
+
summary: `Explicit current-user reauthorization resumed ${resumedControllerSession.workId}; implementation may continue on the exact Work identity.`,
|
|
3169
|
+
}
|
|
3170
|
+
: { summary: `Controller ownership resumed for ${resumedControllerSession.workId}. ${facade.summary}` }),
|
|
3171
|
+
data: {
|
|
3172
|
+
...facadeData,
|
|
3173
|
+
ownershipResumed: true,
|
|
3174
|
+
controllerSession: resumedControllerSession,
|
|
3175
|
+
...(reconstructedRunningCheckout ? { reconstructedRunningCheckout: true } : {}),
|
|
3176
|
+
...(cancelledWorkReauthorized ? {
|
|
3177
|
+
cancelledWorkReauthorized: true,
|
|
3178
|
+
reconstructedCancelledCheckout,
|
|
3179
|
+
nextStep: 'execute',
|
|
3180
|
+
} : {}),
|
|
3181
|
+
},
|
|
3182
|
+
}
|
|
3183
|
+
: facade;
|
|
3184
|
+
return result(response as unknown as Record<string, unknown>, response.status === 'blocked' || response.status === 'failed' || response.status === 'not_found');
|
|
3185
|
+
}
|
|
3186
|
+
}
|