@moretea-labs/forge 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +70 -7
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +17 -2
- package/CONTRIBUTING.md +3 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/adapters/chatgpt/browser-delivery-host.ts +7 -1
- package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
- package/adapters/chatgpt/controller-binding-store.ts +101 -22
- package/adapters/chatgpt/controller-round-host.ts +15 -25
- package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
- package/adapters/chatgpt/provider-delivery.ts +228 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
- package/adapters/computer/chatgpt-extension-target.ts +463 -0
- package/adapters/computer/chatgpt-target-router.ts +103 -0
- package/adapters/computer/chrome-extension/background.js +385 -0
- package/adapters/computer/chrome-extension/content.js +378 -0
- package/adapters/computer/chrome-extension/core.js +24 -0
- package/adapters/computer/chrome-extension/manifest.json +30 -0
- package/adapters/computer/desktop-operator-discovery.ts +11 -31
- package/adapters/computer/interaction-target-authority.ts +142 -18
- package/adapters/mcp/auth.ts +0 -1
- package/adapters/mcp/instructions.ts +5 -4
- package/adapters/mcp/multi-repository.ts +3 -19
- package/adapters/mcp/oauth.ts +73 -0
- package/adapters/mcp/paths.ts +4 -1
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
- package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
- package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
- package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
- package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
- package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
- package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
- package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
- package/adapters/mcp/runtime-gateway/router.ts +18 -504
- package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
- package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
- package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
- package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
- package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
- package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
- package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
- package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
- package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
- package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
- package/adapters/mcp/server.ts +67 -23
- package/adapters/mcp/setup.ts +1 -3
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
- package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
- package/adapters/mcp/toolset-names.ts +5 -0
- package/adapters/mcp/toolset.ts +6 -5
- package/adapters/mcp/transports/http-observation.ts +365 -0
- package/adapters/mcp/transports/http.ts +154 -677
- package/adapters/mcp/transports/oauth-http.ts +322 -0
- package/adapters/mcp/transports/session-registry.ts +6 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
- package/assets/plugin-registry.v1.json +6 -4
- package/assets/reference-configs/agentic-development-flow.md +25 -0
- package/assets/reference-configs/global-working-rules.md +19 -0
- package/assets/skill-version.json +2 -2
- package/docs/architecture/CURRENT.md +111 -41
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
- package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
- package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
- package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
- package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
- package/docs/architecture/domains/verification.md +1 -0
- package/docs/architecture/domains/workflow-engine.md +6 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/forge-plugin-management.md +2 -2
- package/docs/operations/controller-performance-and-502.md +17 -0
- package/docs/operations/controller-reliability-runbook.md +2 -2
- package/docs/operations/features.md +1 -1
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +2 -0
- package/docs/operations/releasing.md +2 -0
- package/docs/operations/standalone-disaster-recovery.md +14 -6
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +25 -14
- package/packages/kernel/cognition/api/index.ts +6 -0
- package/packages/kernel/cognition/application/activation-service.ts +451 -0
- package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
- package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
- package/packages/kernel/cognition/application/memory-service.ts +64 -0
- package/packages/kernel/cognition/domain/learning.ts +106 -0
- package/packages/kernel/cognition/domain/memory.ts +254 -0
- package/packages/kernel/controller/api/index.ts +1 -0
- package/packages/kernel/controller/application/continuation-service.ts +281 -0
- package/packages/kernel/controller/application/controller-round-service.ts +12 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
- package/packages/kernel/controller/domain/controller-round.ts +4 -1
- package/packages/kernel/controller/domain/execution-quality.ts +3 -1
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
- package/packages/kernel/controller/ports/controller-host.ts +2 -0
- package/packages/kernel/identity/api/index.ts +10 -0
- package/packages/kernel/identity/domain/activity-projection.ts +43 -0
- package/packages/kernel/identity/domain/capability-broker.ts +60 -0
- package/packages/kernel/identity/domain/grant.ts +72 -0
- package/packages/kernel/identity/domain/owned-resource.ts +116 -0
- package/packages/kernel/identity/domain/user-request.ts +93 -0
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
- package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
- package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
- package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
- package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
- package/packages/kernel/scheduler/api/index.ts +0 -2
- package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
- package/packages/kernel/scheduler/application/settlement.ts +0 -15
- package/packages/kernel/scheduler/domain/schedule.ts +87 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
- package/packages/kernel/work/api/index.ts +3 -2
- package/packages/kernel/work/application/work-service.ts +17 -42
- package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
- package/packages/kernel/work/domain/engineering-design.ts +10 -5
- package/packages/kernel/work/domain/engineering-profile.ts +8 -12
- package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
- package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
- package/packages/kernel/work/domain/implementation-review.ts +14 -331
- package/packages/kernel/work/domain/objective-graph.ts +119 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +25 -63
- package/packages/kernel/work/domain/types.ts +57 -51
- package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
- package/packages/kernel/work/ports/work-contract-store.ts +5 -2
- package/packages/plugin-runtime/browser/session-authority.ts +2 -1
- package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
- package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
- package/packages/plugin-runtime/computer/index.ts +2 -0
- package/packages/plugin-runtime/computer/target-authority.ts +26 -1
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
- package/packages/protocols/computer/contract.ts +27 -1
- package/scripts/activate-source-baseline.ts +7 -9
- package/scripts/canary-controller-semantic-wait.ts +12 -24
- package/scripts/check-mcp-compatibility.ts +48 -136
- package/scripts/check-runtime-architecture.mjs +622 -217
- package/scripts/forge-local-recovery-helper.mjs +93 -15
- package/scripts/lib/project-init-lib.sh +3 -0
- package/scripts/open-source-audit-allowlist.txt +1 -2
- package/scripts/run-bounded-command.mjs +5 -0
- package/scripts/run-governed-gate.ts +0 -1
- package/scripts/run-mobile-monitor-bridge.ts +0 -1
- package/scripts/smoke-control-plane-failure-domain.ts +94 -0
- package/scripts/smoke-mcp-http-runtime.ts +5 -0
- package/scripts/smoke-runtime-recovery.ts +12 -43
- package/scripts/smoke-schedule-engine.ts +23 -15
- package/scripts/smoke-unattended-transport-recovery.ts +223 -0
- package/scripts/smoke-user-request-continuation-restart.ts +232 -0
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
- package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
- package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
- package/scripts/smoke-workflow-supervisor.ts +103 -0
- package/scripts/verify-forge-runtime.sh +2 -1
- package/src/cli/agent-jobs/executor-health.ts +11 -34
- package/src/cli/agent-jobs/integration.ts +18 -49
- package/src/cli/agent-jobs/job-manager.ts +7 -3
- package/src/cli/commands/brain-assistant.ts +27 -13
- package/src/cli/commands/brain.ts +13 -15
- package/src/cli/commands/chatgpt.ts +1 -163
- package/src/cli/commands/recovery.ts +135 -121
- package/src/cli/commands/runtime.ts +3 -3
- package/src/cli/commands/setup-profile.ts +8 -0
- package/src/cli/controller/check-runner.ts +13 -2
- package/src/cli/controller/completion-backlog.ts +3 -3
- package/src/cli/controller/completion-orchestrator.ts +4 -43
- package/src/cli/controller/completion-target.ts +3 -72
- package/src/cli/controller/context/source-materializer.ts +81 -100
- package/src/cli/controller/context-pack.ts +10 -8
- package/src/cli/controller/execution-completion.ts +4 -7
- package/src/cli/controller/execution-policy.ts +36 -153
- package/src/cli/controller/governance.ts +2 -3
- package/src/cli/controller/issue-store.ts +4 -99
- package/src/cli/controller/launch-agents.ts +59 -18
- package/src/cli/controller/operational-plan.ts +24 -51
- package/src/cli/controller/runtime-config.ts +5 -1
- package/src/cli/controller/stuck-state-migration.ts +2 -3
- package/src/cli/controller/systemd-user.ts +39 -1
- package/src/cli/editing/edit-session.ts +1 -7
- package/src/cli/local-bridge/console-automations.ts +18 -13
- package/src/cli/local-bridge/console-view-models.ts +3 -12
- package/src/cli/local-bridge/facade-api.ts +64 -191
- package/src/cli/local-bridge/server.ts +24 -147
- package/src/cli/local-bridge/types.ts +0 -1
- package/src/cli/mcp/oauth-client-retirement.ts +74 -0
- package/src/cli/repositories/command-executor.ts +29 -23
- package/src/cli/repositories/command-scope.ts +35 -16
- package/src/cli/repositories/controller-home.ts +48 -0
- package/src/cli/repositories/ephemeral-workspace.ts +20 -30
- package/src/cli/repositories/git-command-executor.ts +2 -2
- package/src/cli/repositories/registry.ts +77 -11
- package/src/cli/repositories/structured-git.ts +62 -2
- package/src/cli/repository/session-cache.ts +92 -6
- package/src/runtime/context/assistant-context.ts +20 -7
- package/src/runtime/context/assistant-work-context.ts +89 -27
- package/src/runtime/context/automatic-learning.ts +565 -0
- package/src/runtime/context/cognitive-attention.ts +173 -0
- package/src/runtime/context/cognitive-skill-canary.ts +158 -0
- package/src/runtime/context/context-closure.ts +46 -9
- package/src/runtime/context/context-pack-process.ts +103 -0
- package/src/runtime/context/context-plane.ts +1 -9
- package/src/runtime/context/direct-controller-learning.ts +298 -0
- package/src/runtime/context/semantic-navigation-contract.ts +9 -0
- package/src/runtime/context/semantic-navigation.ts +6 -34
- package/src/runtime/context/typescript-navigation-process.ts +145 -0
- package/src/runtime/context/typescript-navigation.ts +89 -3
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
- package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
- package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
- package/src/runtime/control-plane/execution/session-store.ts +31 -1
- package/src/runtime/control-plane/execution/validation.ts +6 -8
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
- package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
- package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
- package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
- package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
- package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
- package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
- package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
- package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
- package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
- package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
- package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
- package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
- package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
- package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
- package/src/runtime/control-plane/facade/index.ts +0 -1
- package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
- package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
- package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
- package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
- package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
- package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
- package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
- package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
- package/src/runtime/control-plane/facade/types.ts +49 -108
- package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
- package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
- package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
- package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
- package/src/runtime/control-plane/governance/access-policy.ts +19 -2
- package/src/runtime/control-plane/governance/authorization.ts +20 -3
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
- package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
- package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
- package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
- package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
- package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
- package/src/runtime/control-plane/release-retention.ts +135 -47
- package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
- package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
- package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
- package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
- package/src/runtime/diagnostics/incident-repair.ts +313 -146
- package/src/runtime/diagnostics/mcp-timing.ts +114 -0
- package/src/runtime/evidence/artifact-store.ts +158 -40
- package/src/runtime/evidence/evidence-store.ts +112 -8
- package/src/runtime/evidence/output-handle-index.ts +225 -0
- package/src/runtime/evidence/result-store.ts +158 -44
- package/src/runtime/execution/jobs/store.ts +3 -4
- package/src/runtime/execution/managed-workspace.ts +57 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
- package/src/runtime/execution/process-runtime/check-result.ts +105 -1
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
- package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
- package/src/runtime/execution/process-runtime/gc.ts +15 -2
- package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
- package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
- package/src/runtime/execution/process-runtime/index.ts +3 -0
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
- package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
- package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
- package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
- package/src/runtime/execution/process-runtime/runtime.ts +35 -2
- package/src/runtime/execution/process-runtime/store.ts +30 -2
- package/src/runtime/execution/process-runtime/types.ts +6 -1
- package/src/runtime/execution/workers/executor.ts +2 -6
- package/src/runtime/platform/service-inventory.ts +130 -0
- package/src/runtime/platform/service-manager.ts +8 -0
- package/src/runtime/plugins/action-application.ts +85 -28
- package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
- package/src/runtime/plugins/browser-adapter.ts +757 -34
- package/src/runtime/plugins/browser-automation-service.ts +23 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
- package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
- package/src/runtime/plugins/browser-native-session-service.ts +63 -13
- package/src/runtime/plugins/browser-registration.ts +16 -0
- package/src/runtime/plugins/browser-runtime.ts +14 -1
- package/src/runtime/plugins/browser-session-authority.ts +2 -2
- package/src/runtime/plugins/browser-session-store.ts +46 -76
- package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
- package/src/runtime/plugins/computer-registration.ts +222 -11
- package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
- package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
- package/src/runtime/plugins/external-adapter.ts +42 -6
- package/src/runtime/plugins/external-registration.ts +21 -1
- package/src/runtime/plugins/gmail-adapter.ts +23 -2
- package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
- package/src/runtime/plugins/google-shared.ts +14 -7
- package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
- package/src/runtime/plugins/ios-adapter.ts +1 -1
- package/src/runtime/plugins/lightweight-action.ts +208 -2
- package/src/runtime/plugins/local-system-adapter.ts +12 -2
- package/src/runtime/plugins/managed-process-adapter.ts +12 -5
- package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
- package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +602 -428
- package/src/runtime/plugins/types.ts +20 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
- package/src/runtime/projections/invalidation.ts +7 -3
- package/src/runtime/projections/materialized-view.ts +80 -36
- package/src/runtime/recovery/actions.ts +3 -3
- package/src/runtime/recovery/capability-action-application.ts +46 -45
- package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
- package/src/runtime/recovery/maintenance-executor.ts +374 -123
- package/src/runtime/recovery/store.ts +29 -11
- package/src/runtime/release/release-coordinator.ts +296 -0
- package/src/runtime/release/release-session.ts +716 -0
- package/src/runtime/release/release-state-migration.ts +25 -0
- package/src/runtime/resources/leases/store.ts +56 -25
- package/src/runtime/root/computer-composition.ts +191 -6
- package/src/runtime/root/controller-progression-composition.ts +192 -0
- package/src/runtime/root/controller-round-composition.ts +20 -1
- package/src/runtime/root/deployment-topology.ts +106 -0
- package/src/runtime/root/entry.ts +4 -3
- package/src/runtime/root/known-good-recovery.ts +184 -0
- package/src/runtime/root/local-bridge.ts +0 -1
- package/src/runtime/root/mcp-transport.ts +111 -18
- package/src/runtime/root/ownership.ts +261 -9
- package/src/runtime/root/package-connector-service.ts +105 -19
- package/src/runtime/root/package-runtime-release.ts +117 -18
- package/src/runtime/root/package-runtime-service.ts +17 -11
- package/src/runtime/root/release-execution-canary.ts +44 -12
- package/src/runtime/root/release-loader.ts +90 -8
- package/src/runtime/root/release-manifest.ts +80 -6
- package/src/runtime/root/release-materialize.ts +372 -28
- package/src/runtime/root/release-store.ts +395 -36
- package/src/runtime/root/runtime-lane.ts +191 -0
- package/src/runtime/root/runtime.ts +159 -27
- package/src/runtime/root/scheduled-controller-composition.ts +28 -3
- package/src/runtime/root/service-runner.ts +2 -2
- package/src/runtime/root/service.ts +36 -33
- package/src/runtime/root/status.ts +48 -0
- package/src/runtime/root/types.ts +32 -3
- package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
- package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
- package/src/runtime/root/write-fence.ts +50 -2
- package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
- package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
- package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
- package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
- package/src/runtime/shared/process-environment.ts +20 -9
- package/src/runtime/shared/response-budget.ts +1 -3
- package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
- package/src/runtime/standalone-recovery/core.ts +3702 -342
- package/src/runtime/standalone-recovery/entry.ts +469 -39
- package/src/runtime/standalone-recovery/installer.ts +262 -148
- package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
- package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
- package/src/runtime/standalone-recovery/performance.ts +54 -16
- package/src/runtime/standalone-recovery/release.ts +53 -5
- package/src/runtime/standalone-recovery/service-labels.ts +5 -2
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
- package/src/runtime/workflow/schedules/engine.ts +138 -172
- package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
- package/src/runtime/workflows/runtime.ts +15 -19
- package/src/testing/test-governance.ts +5 -1
- package/supervisor/chatgpt-conversation.ts +23 -0
- package/supervisor/client.ts +201 -0
- package/supervisor/control-plane.ts +868 -0
- package/supervisor/entry.ts +15 -0
- package/supervisor/forge-validators.ts +80 -0
- package/supervisor/index.ts +5 -0
- package/supervisor/native-browser-adapter.ts +786 -0
- package/supervisor/native-messaging/host.ts +39 -0
- package/supervisor/native-messaging/manifest.ts +26 -0
- package/supervisor/native-messaging/protocol.ts +27 -0
- package/supervisor/paths.ts +18 -0
- package/supervisor/protocol.ts +202 -0
- package/supervisor/server.ts +404 -0
- package/supervisor/store.ts +1451 -0
- package/supervisor/tsconfig.json +8 -0
- package/supervisor/types.ts +255 -0
- package/adapters/chatgpt/controller-host.ts +0 -119
- package/adapters/mcp/controller-round-compatibility.ts +0 -184
- package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
- package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
- package/packages/kernel/progression/api/index.ts +0 -2
- package/packages/kernel/progression/application/projector.ts +0 -211
- package/packages/kernel/progression/domain/types.ts +0 -116
- package/packages/kernel/progression/index.ts +0 -1
- package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
- package/packages/kernel/scheduler/domain/continuation.ts +0 -38
- package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
- package/packages/kernel/work/domain/admission-policy.ts +0 -34
- package/scripts/benchmark-background-check-overlap.ts +0 -228
- package/scripts/benchmark-computer-hotpath.ts +0 -255
- package/scripts/benchmark-controller-hotpath.ts +0 -212
- package/scripts/benchmark-controller-round2.ts +0 -191
- package/scripts/benchmark-ios-agent-device.ts +0 -511
- package/scripts/benchmark-mcp-host.sh +0 -117
- package/scripts/benchmark-mcp-transport.ts +0 -169
- package/scripts/benchmark-operational-memory-activation.ts +0 -235
- package/scripts/benchmark-operational-shadow.ts +0 -28
- package/scripts/benchmark-quality-harness.ts +0 -146
- package/scripts/benchmark-route-session-concurrency.ts +0 -915
- package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
- package/scripts/benchmark-thin-harness.ts +0 -251
- package/scripts/benchmark-typescript-navigation.ts +0 -186
- package/scripts/benchmark-work-efficiency.ts +0 -334
- package/scripts/build-controller-ui.ts +0 -28
- package/scripts/check-npm-release.sh +0 -32
- package/scripts/check-public-release-surface.sh +0 -19
- package/scripts/check-release-published.sh +0 -57
- package/scripts/check-release-readiness.sh +0 -37
- package/scripts/check-release-version.mjs +0 -74
- package/scripts/check-tarball-install-smoke.sh +0 -150
- package/scripts/package-source-archive.sh +0 -147
- package/scripts/public-release-files.txt +0 -39
- package/scripts/public-release.ts +0 -174
- package/scripts/publish-release-tarball.sh +0 -27
- package/scripts/route-nl-vs-ts-eval.ts +0 -679
- package/scripts/stage-runtime-release.ts +0 -44
- package/src/cli/controller/progress.ts +0 -656
- package/src/cli/controller/work-mode.ts +0 -181
- package/src/cli/local-bridge/console-assets.ts +0 -17
- package/src/cli/local-bridge/dashboard.ts +0 -17
- package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
- package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
- package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
- package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
- package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/styles.css +0 -28
- package/src/cli/local-bridge/frontend/src/types.ts +0 -15
- package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
- package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
- package/src/cli/local-bridge/ui-dist/app.css +0 -1
- package/src/cli/local-bridge/ui-dist/app.js +0 -11228
- package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
- package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
- package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
- package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
- package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
- package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
- package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
- package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
- package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
- package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
- package/src/runtime/control-plane/routing/index.ts +0 -1
- package/src/runtime/control-plane/routing/route-policy.ts +0 -401
- package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { mkdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { scopedOperationRoot } from '../../../cli/repositories/controller-home';
|
|
5
5
|
import { withControllerLock } from '../../../cli/repositories/locks';
|
|
6
6
|
import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../shared/json-files';
|
|
7
7
|
import {
|
|
@@ -15,23 +15,13 @@ import {
|
|
|
15
15
|
} from './semantic-admission';
|
|
16
16
|
import {
|
|
17
17
|
listControlPlaneRecords,
|
|
18
|
-
|
|
18
|
+
readControlPlaneRecord,
|
|
19
19
|
readControlPlaneRecordWithinTransaction,
|
|
20
20
|
withControlPlaneTransaction,
|
|
21
21
|
writeControlPlaneRecordWithinTransaction,
|
|
22
22
|
} from '../persistence/sqlite-store';
|
|
23
23
|
import { readRequirement } from '../persistence/requirement-store';
|
|
24
|
-
import {
|
|
25
|
-
getWorkContract,
|
|
26
|
-
canonicalizeWorkContractForAuthority,
|
|
27
|
-
isDirectEditWorkCompletionReceipt,
|
|
28
|
-
isRepositoryCompletionReceipt,
|
|
29
|
-
rebindPlanBoundWorkContract,
|
|
30
|
-
refreshPlanBoundWorkRevision,
|
|
31
|
-
retirePlanBoundWorkContract,
|
|
32
|
-
isTerminalWorkContractStatus,
|
|
33
|
-
type WorkContract,
|
|
34
|
-
} from '../../../../packages/kernel/work/api/index';
|
|
24
|
+
import type { ScopeRef } from '../../../../packages/kernel/identity/api/index';
|
|
35
25
|
import {
|
|
36
26
|
isTerminalPlanContractStatus,
|
|
37
27
|
type EvidenceRef,
|
|
@@ -42,12 +32,13 @@ import {
|
|
|
42
32
|
type PlanRevisionDraft,
|
|
43
33
|
type PlanRevisionRecord,
|
|
44
34
|
type PlanStep,
|
|
45
|
-
type PlanStepDeliveryCarry,
|
|
46
35
|
} from './types';
|
|
47
36
|
|
|
48
37
|
export interface PlanContractStoreLocation {
|
|
49
38
|
controllerHome?: string;
|
|
50
39
|
repoId?: string;
|
|
40
|
+
/** Portable semantic scope key (for example the ForgeInstance semantic scope). Takes precedence over repoId. */
|
|
41
|
+
scopeKey?: string;
|
|
51
42
|
root?: string;
|
|
52
43
|
}
|
|
53
44
|
|
|
@@ -59,7 +50,8 @@ export interface PlanContractStoreOptions extends PlanContractStoreLocation {
|
|
|
59
50
|
|
|
60
51
|
export interface CreatePlanContractInput {
|
|
61
52
|
planId: string;
|
|
62
|
-
|
|
53
|
+
/** Optional node-local placement provenance; absent for a ForgeInstance-scoped Plan. */
|
|
54
|
+
repoId?: string;
|
|
63
55
|
requirementId?: string;
|
|
64
56
|
scopeKey: string;
|
|
65
57
|
sourceRevision: string;
|
|
@@ -90,7 +82,8 @@ export type AdmitPlanContractResult = PlanAdmissionResolution;
|
|
|
90
82
|
export interface PlanContractSummary {
|
|
91
83
|
planId: string;
|
|
92
84
|
revision: number;
|
|
93
|
-
|
|
85
|
+
/** Node-local placement provenance; absent for a ForgeInstance-scoped Plan. */
|
|
86
|
+
repoId?: string;
|
|
94
87
|
requirementId?: string;
|
|
95
88
|
scopeKey: string;
|
|
96
89
|
sourceRevision: string;
|
|
@@ -109,6 +102,18 @@ function bounded(values: readonly string[] | undefined, limit: number, maxLength
|
|
|
109
102
|
return (values ?? []).map((value) => String(value).trim()).filter(Boolean).slice(0, limit).map((value) => value.slice(0, maxLength));
|
|
110
103
|
}
|
|
111
104
|
|
|
105
|
+
function normalizePlanSemanticItems(items: readonly { id: string; objective: string; dependencies?: string[] }[] | undefined) {
|
|
106
|
+
const normalized = (items ?? []).slice(0, 100).map((item) => ({
|
|
107
|
+
id: String(item.id).trim().slice(0, 200),
|
|
108
|
+
objective: String(item.objective).trim().slice(0, 2_000),
|
|
109
|
+
dependencies: [...new Set((item.dependencies ?? []).map(String).map((value) => value.trim()).filter(Boolean))].slice(0, 100),
|
|
110
|
+
}));
|
|
111
|
+
if (normalized.some((item) => !item.id || !item.objective) || new Set(normalized.map((item) => item.id)).size !== normalized.length) {
|
|
112
|
+
throw new Error('PLAN_SEMANTIC_ITEMS_INVALID');
|
|
113
|
+
}
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
|
|
112
117
|
function normalizeScopeKey(value: string): string {
|
|
113
118
|
return normalizePlanScopeKey(value) || 'unknown';
|
|
114
119
|
}
|
|
@@ -118,23 +123,128 @@ export function currentPlanRevision(plan: PlanContract): number {
|
|
|
118
123
|
return Number.isInteger(revision) && revision > 0 ? revision : 1;
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
export function currentPlanSemanticRevision(plan: PlanContract): number {
|
|
127
|
+
const revision = Number(plan.semanticRevision);
|
|
128
|
+
return Number.isInteger(revision) && revision > 0 ? revision : 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface PlanSemanticView {
|
|
132
|
+
planId: string;
|
|
133
|
+
revision: number;
|
|
134
|
+
semanticScope: ScopeRef;
|
|
135
|
+
requirementId?: string;
|
|
136
|
+
/** One-way lineage fact: this Plan is historical and its obligations continue in the named successor. */
|
|
137
|
+
supersededBy?: string;
|
|
138
|
+
requirementBasisRevision?: number;
|
|
139
|
+
sourceBasisRevision: string;
|
|
140
|
+
goal: string;
|
|
141
|
+
nonGoals: string[];
|
|
142
|
+
assumptions: string[];
|
|
143
|
+
resolvedDecisions: string[];
|
|
144
|
+
stopConditions: string[];
|
|
145
|
+
replanConditions: string[];
|
|
146
|
+
integrationStrategy?: string;
|
|
147
|
+
items: Array<{ id: string; objective: string; dependencies: string[] }>;
|
|
148
|
+
createdAt: string;
|
|
149
|
+
updatedAt: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface PlanSemanticRevisionRecord extends PlanSemanticView {
|
|
153
|
+
schemaVersion: 1;
|
|
154
|
+
recordedAt: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface RevisePlanSemanticInput {
|
|
158
|
+
expectedRevision: number;
|
|
159
|
+
/** Optional one-way successor relation. Once set it cannot be cleared or changed. */
|
|
160
|
+
supersededBy?: string;
|
|
161
|
+
requirementBasisRevision?: number;
|
|
162
|
+
sourceBasisRevision?: string;
|
|
163
|
+
goal?: string;
|
|
164
|
+
nonGoals?: string[];
|
|
165
|
+
assumptions?: string[];
|
|
166
|
+
resolvedDecisions?: string[];
|
|
167
|
+
stopConditions?: string[];
|
|
168
|
+
replanConditions?: string[];
|
|
169
|
+
integrationStrategy?: string | null;
|
|
170
|
+
items?: Array<{ id: string; objective: string; dependencies?: string[] }>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface CreatePlanSemanticInput {
|
|
174
|
+
planId: string;
|
|
175
|
+
/** Optional node-local placement provenance; absent for a ForgeInstance-scoped Plan. */
|
|
176
|
+
repoId?: string;
|
|
177
|
+
requirementId?: string;
|
|
178
|
+
requirementBasisRevision?: number;
|
|
179
|
+
scopeKey?: string;
|
|
180
|
+
sourceBasisRevision?: string;
|
|
181
|
+
goal: string;
|
|
182
|
+
nonGoals?: string[];
|
|
183
|
+
assumptions?: string[];
|
|
184
|
+
resolvedDecisions?: string[];
|
|
185
|
+
stopConditions?: string[];
|
|
186
|
+
replanConditions?: string[];
|
|
187
|
+
integrationStrategy?: string;
|
|
188
|
+
items?: Array<{ id: string; objective: string; dependencies?: string[] }>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function legacyPlanSemanticContext(plan: PlanContract) {
|
|
192
|
+
return {
|
|
193
|
+
requirementBasisRevision: undefined,
|
|
194
|
+
sourceBasisRevision: plan.sourceRevision,
|
|
195
|
+
goal: plan.goal,
|
|
196
|
+
nonGoals: [...plan.nonGoals],
|
|
197
|
+
assumptions: [...plan.assumptions],
|
|
198
|
+
resolvedDecisions: [...plan.resolvedDecisions],
|
|
199
|
+
stopConditions: [...plan.stopConditions],
|
|
200
|
+
replanConditions: [...plan.replanConditions],
|
|
201
|
+
integrationStrategy: plan.integrationStrategy,
|
|
202
|
+
items: plan.steps.map((step) => ({ id: step.id, objective: step.objective, dependencies: [...step.dependencies] })),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function planSemanticView(plan: PlanContract): PlanSemanticView {
|
|
207
|
+
const semantic = plan.semanticContext ?? legacyPlanSemanticContext(plan);
|
|
208
|
+
const semanticScope: ScopeRef = plan.requirementId?.trim()
|
|
209
|
+
? { schemaVersion: 1, kind: 'requirement', id: plan.requirementId.trim() }
|
|
210
|
+
: { schemaVersion: 1, kind: 'plan', id: plan.planId };
|
|
211
|
+
return {
|
|
212
|
+
planId: plan.planId,
|
|
213
|
+
revision: currentPlanSemanticRevision(plan),
|
|
214
|
+
semanticScope,
|
|
215
|
+
requirementId: plan.requirementId,
|
|
216
|
+
...(plan.supersededBy?.trim() ? { supersededBy: plan.supersededBy.trim() } : {}),
|
|
217
|
+
requirementBasisRevision: semantic.requirementBasisRevision,
|
|
218
|
+
sourceBasisRevision: semantic.sourceBasisRevision,
|
|
219
|
+
goal: semantic.goal,
|
|
220
|
+
nonGoals: [...semantic.nonGoals],
|
|
221
|
+
assumptions: [...semantic.assumptions],
|
|
222
|
+
resolvedDecisions: [...semantic.resolvedDecisions],
|
|
223
|
+
stopConditions: [...semantic.stopConditions],
|
|
224
|
+
replanConditions: [...semantic.replanConditions],
|
|
225
|
+
integrationStrategy: semantic.integrationStrategy,
|
|
226
|
+
items: semantic.items.map((item) => ({ id: item.id, objective: item.objective, dependencies: [...item.dependencies] })),
|
|
227
|
+
createdAt: plan.createdAt,
|
|
228
|
+
updatedAt: plan.semanticUpdatedAt ?? plan.createdAt,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
121
232
|
interface PlanRevisionRecordStore {
|
|
122
233
|
schemaVersion: 1;
|
|
123
234
|
updatedAt: string;
|
|
124
235
|
revisions: PlanRevisionRecord[];
|
|
125
236
|
}
|
|
126
237
|
|
|
238
|
+
interface PlanSemanticRevisionStore {
|
|
239
|
+
schemaVersion: 1;
|
|
240
|
+
updatedAt: string;
|
|
241
|
+
revisions: PlanSemanticRevisionRecord[];
|
|
242
|
+
}
|
|
243
|
+
|
|
127
244
|
function planRevisionRecordKey(planId: string, revision: number): string {
|
|
128
245
|
return `${sanitizeFileComponent(planId)}:r${revision}`;
|
|
129
246
|
}
|
|
130
247
|
|
|
131
|
-
function projectDependencyReadySteps(steps: readonly PlanStep[]): PlanStep[] {
|
|
132
|
-
const completed = new Set(steps.filter((step) => step.status === 'completed').map((step) => step.id));
|
|
133
|
-
return steps.map((step) => step.status === 'pending'
|
|
134
|
-
&& step.dependencies.every((dependencyId) => completed.has(dependencyId))
|
|
135
|
-
? { ...step, status: 'ready' as const }
|
|
136
|
-
: step);
|
|
137
|
-
}
|
|
138
248
|
|
|
139
249
|
function normalizeStep(step: CreatePlanContractInput['steps'][number]): PlanStep {
|
|
140
250
|
return {
|
|
@@ -206,81 +316,8 @@ export function listUnresolvedPlanObligations(plan: PlanContract): PlanObligatio
|
|
|
206
316
|
return listPlanObligations(plan, 'unresolved');
|
|
207
317
|
}
|
|
208
318
|
|
|
209
|
-
function retireResolvedPredecessorObligationDispositions(
|
|
210
|
-
plan: PlanContract,
|
|
211
|
-
allPlans: readonly PlanContract[],
|
|
212
|
-
): PlanContract {
|
|
213
|
-
const dispositions = plan.obligationDispositions ?? [];
|
|
214
|
-
if (dispositions.length === 0 || (plan.supersedes?.length ?? 0) === 0) return plan;
|
|
215
|
-
const resolvedByPredecessor = new Map<string, Set<string>>();
|
|
216
|
-
for (const predecessorId of plan.supersedes ?? []) {
|
|
217
|
-
const predecessor = allPlans.find((candidate) => candidate.planId === predecessorId);
|
|
218
|
-
if (!predecessor) continue;
|
|
219
|
-
resolvedByPredecessor.set(
|
|
220
|
-
predecessorId,
|
|
221
|
-
new Set(listPlanObligations(predecessor, 'completed_steps').map((obligation) => obligation.obligationId)),
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
const retained = dispositions.filter((disposition) =>
|
|
225
|
-
!resolvedByPredecessor.get(disposition.predecessorPlanId)?.has(disposition.obligationId));
|
|
226
|
-
return retained.length === dispositions.length ? plan : { ...plan, obligationDispositions: retained };
|
|
227
|
-
}
|
|
228
319
|
|
|
229
|
-
function successorObligationRefs(plan: PlanContract): Set<string> {
|
|
230
|
-
const refs = new Set<string>(['goal']);
|
|
231
|
-
for (const step of plan.steps) {
|
|
232
|
-
refs.add(`step:${step.id}`);
|
|
233
|
-
step.acceptanceCriteria.forEach((_criterion, index) => refs.add(`step:${step.id}:acceptance:${index}`));
|
|
234
|
-
}
|
|
235
|
-
plan.nonGoals.forEach((_value, index) => refs.add(`non_goal:${index}`));
|
|
236
|
-
plan.resolvedDecisions.forEach((_value, index) => refs.add(`resolved_decision:${index}`));
|
|
237
|
-
plan.stopConditions.forEach((_value, index) => refs.add(`stop_condition:${index}`));
|
|
238
|
-
plan.replanConditions.forEach((_value, index) => refs.add(`replan_condition:${index}`));
|
|
239
|
-
if (plan.integrationStrategy?.trim()) refs.add('integration_strategy');
|
|
240
|
-
return refs;
|
|
241
|
-
}
|
|
242
320
|
|
|
243
|
-
function obligationContinuityErrors(plan: PlanContract, allPlans: readonly PlanContract[]): string[] {
|
|
244
|
-
const errors: string[] = [];
|
|
245
|
-
const dispositions = plan.obligationDispositions ?? [];
|
|
246
|
-
const successorRefs = successorObligationRefs(plan);
|
|
247
|
-
const predecessorObligations = new Map<string, Set<string>>();
|
|
248
|
-
for (const predecessorId of plan.supersedes ?? []) {
|
|
249
|
-
const predecessor = allPlans.find((candidate) => candidate.planId === predecessorId);
|
|
250
|
-
if (!predecessor) {
|
|
251
|
-
errors.push(`superseded predecessor not found: ${predecessorId}`);
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
predecessorObligations.set(predecessor.planId, new Set(listUnresolvedPlanObligations(predecessor).map((entry) => entry.obligationId)));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const seen = new Set<string>();
|
|
258
|
-
for (const disposition of dispositions) {
|
|
259
|
-
const key = `${disposition.predecessorPlanId}:${disposition.obligationId}`;
|
|
260
|
-
if (seen.has(key)) errors.push(`duplicate obligation disposition ${key}`);
|
|
261
|
-
seen.add(key);
|
|
262
|
-
const known = predecessorObligations.get(disposition.predecessorPlanId);
|
|
263
|
-
if ((plan.supersedes ?? []).includes(disposition.predecessorPlanId) && !known?.has(disposition.obligationId)) {
|
|
264
|
-
errors.push(`unknown predecessor obligation ${key}`);
|
|
265
|
-
}
|
|
266
|
-
if ((disposition.disposition === 'keep' || disposition.disposition === 'change') && disposition.successorRefs.length === 0) {
|
|
267
|
-
errors.push(`obligation ${key} ${disposition.disposition} requires successor_refs`);
|
|
268
|
-
}
|
|
269
|
-
for (const ref of disposition.successorRefs) {
|
|
270
|
-
if (!successorRefs.has(ref)) errors.push(`obligation ${key} references unknown successor_ref ${ref}`);
|
|
271
|
-
}
|
|
272
|
-
if (disposition.disposition !== 'keep' && !disposition.rationale?.trim()) {
|
|
273
|
-
errors.push(`obligation ${key} ${disposition.disposition} requires rationale`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
for (const [predecessorId, obligations] of predecessorObligations) {
|
|
277
|
-
const covered = new Set(dispositions.filter((entry) => entry.predecessorPlanId === predecessorId).map((entry) => entry.obligationId));
|
|
278
|
-
for (const obligationId of obligations) {
|
|
279
|
-
if (!covered.has(obligationId)) errors.push(`uncovered predecessor obligation ${predecessorId}:${obligationId}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
return [...new Set(errors)];
|
|
283
|
-
}
|
|
284
321
|
|
|
285
322
|
function updatePlanContract(
|
|
286
323
|
options: PlanContractStoreOptions,
|
|
@@ -297,16 +334,17 @@ function updatePlanContract(
|
|
|
297
334
|
writePlanContractStore(options, { schemaVersion: 1, updatedAt: next.updatedAt, contracts });
|
|
298
335
|
return next;
|
|
299
336
|
};
|
|
300
|
-
if (!options.controllerHome || !options
|
|
337
|
+
if (!options.controllerHome || !planContractStoreScopeKey(options)) return apply();
|
|
301
338
|
return withControllerLock(
|
|
302
339
|
options.controllerHome,
|
|
303
|
-
{ scope: 'task', repoId: options
|
|
340
|
+
{ scope: 'task', repoId: requirePlanContractStoreScopeKey(options), taskId: `plan-${sanitizeFileComponent(planId)}` },
|
|
304
341
|
'plan-contract-store',
|
|
305
342
|
apply,
|
|
306
343
|
15_000,
|
|
307
344
|
);
|
|
308
345
|
}
|
|
309
346
|
|
|
347
|
+
|
|
310
348
|
function assertRequirementReference(options: PlanContractStoreOptions, requirementId: string | undefined): void {
|
|
311
349
|
const normalized = requirementId?.trim();
|
|
312
350
|
if (!normalized || !options.controllerHome) return;
|
|
@@ -322,10 +360,10 @@ export function planContractRoot(location: PlanContractStoreLocation): string {
|
|
|
322
360
|
mkdirSync(location.root, { recursive: true });
|
|
323
361
|
return location.root;
|
|
324
362
|
}
|
|
325
|
-
if (!location.controllerHome || !location
|
|
326
|
-
throw new Error('plan contract store requires either root or controllerHome + repoId');
|
|
363
|
+
if (!location.controllerHome || !planContractStoreScopeKey(location)) {
|
|
364
|
+
throw new Error('plan contract store requires either root or controllerHome + repoId/scopeKey');
|
|
327
365
|
}
|
|
328
|
-
const root = join(
|
|
366
|
+
const root = join(scopedOperationRoot(location.controllerHome, requirePlanContractStoreScopeKey(location)), 'plan-contracts');
|
|
329
367
|
mkdirSync(root, { recursive: true });
|
|
330
368
|
return root;
|
|
331
369
|
}
|
|
@@ -334,10 +372,18 @@ export function planContractStorePath(location: PlanContractStoreLocation): stri
|
|
|
334
372
|
return join(planContractRoot(location), 'index.json');
|
|
335
373
|
}
|
|
336
374
|
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
|
|
337
379
|
export function planRevisionStorePath(location: PlanContractStoreLocation): string {
|
|
338
380
|
return join(planContractRoot(location), 'revisions.json');
|
|
339
381
|
}
|
|
340
382
|
|
|
383
|
+
function planSemanticRevisionStorePath(location: PlanContractStoreLocation): string {
|
|
384
|
+
return join(planContractRoot(location), 'semantic-revisions.json');
|
|
385
|
+
}
|
|
386
|
+
|
|
341
387
|
export function listPlanRevisionRecords(
|
|
342
388
|
options: PlanContractStoreOptions,
|
|
343
389
|
planId?: string,
|
|
@@ -350,7 +396,7 @@ export function listPlanRevisionRecords(
|
|
|
350
396
|
.sort((left, right) => right.revision - left.revision);
|
|
351
397
|
}
|
|
352
398
|
return listControlPlaneRecords<PlanRevisionRecord>(options.controllerHome, {
|
|
353
|
-
namespace: 'plan_revision', scope: options
|
|
399
|
+
namespace: 'plan_revision', scope: requirePlanContractStoreScopeKey(options), limit: 5_000,
|
|
354
400
|
}).map((record) => record.value)
|
|
355
401
|
.filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
|
|
356
402
|
.sort((left, right) => right.revision - left.revision);
|
|
@@ -365,6 +411,62 @@ function appendJsonPlanRevisionRecord(options: PlanContractStoreOptions, record:
|
|
|
365
411
|
writeJsonAtomic(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [record, ...store.revisions] });
|
|
366
412
|
}
|
|
367
413
|
|
|
414
|
+
/** Exact historical lookup; revision-list limits are not version-existence gates. */
|
|
415
|
+
export function getPlanSemanticRevisionRecord(
|
|
416
|
+
options: PlanContractStoreOptions,
|
|
417
|
+
planId: string,
|
|
418
|
+
revision: number,
|
|
419
|
+
): PlanSemanticRevisionRecord | undefined {
|
|
420
|
+
if (!Number.isInteger(revision) || revision < 1) return undefined;
|
|
421
|
+
const normalizedPlanId = sanitizeFileComponent(planId);
|
|
422
|
+
const record = sqliteBacked(options)
|
|
423
|
+
? readControlPlaneRecord<PlanSemanticRevisionRecord>(
|
|
424
|
+
options.controllerHome, 'plan_semantic_revision', requirePlanContractStoreScopeKey(options),
|
|
425
|
+
planRevisionRecordKey(normalizedPlanId, revision),
|
|
426
|
+
)?.value
|
|
427
|
+
: readJsonFile<PlanSemanticRevisionStore>(
|
|
428
|
+
planSemanticRevisionStorePath(options), { schemaVersion: 1, updatedAt: nowIso(options), revisions: [] },
|
|
429
|
+
).revisions.find((entry) => entry.planId === normalizedPlanId && entry.revision === revision);
|
|
430
|
+
return record ? {
|
|
431
|
+
...record,
|
|
432
|
+
semanticScope: record.semanticScope ?? (record.requirementId
|
|
433
|
+
? { schemaVersion: 1, kind: 'requirement', id: record.requirementId }
|
|
434
|
+
: { schemaVersion: 1, kind: 'plan', id: record.planId }),
|
|
435
|
+
} : undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function listPlanSemanticRevisionRecords(options: PlanContractStoreOptions, planId?: string): PlanSemanticRevisionRecord[] {
|
|
439
|
+
const normalizedPlanId = planId ? sanitizeFileComponent(planId) : undefined;
|
|
440
|
+
if (!sqliteBacked(options)) {
|
|
441
|
+
const store = readJsonFile<PlanSemanticRevisionStore>(planSemanticRevisionStorePath(options), { schemaVersion: 1, updatedAt: nowIso(options), revisions: [] });
|
|
442
|
+
return store.revisions.filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
|
|
443
|
+
.map((record) => ({
|
|
444
|
+
...record,
|
|
445
|
+
semanticScope: record.semanticScope ?? (record.requirementId
|
|
446
|
+
? { schemaVersion: 1, kind: 'requirement', id: record.requirementId }
|
|
447
|
+
: { schemaVersion: 1, kind: 'plan', id: record.planId }),
|
|
448
|
+
} as PlanSemanticRevisionRecord))
|
|
449
|
+
.sort((left, right) => right.revision - left.revision);
|
|
450
|
+
}
|
|
451
|
+
return listControlPlaneRecords<PlanSemanticRevisionRecord>(options.controllerHome, {
|
|
452
|
+
namespace: 'plan_semantic_revision', scope: requirePlanContractStoreScopeKey(options), limit: 5_000,
|
|
453
|
+
}).map((record) => record.value)
|
|
454
|
+
.filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
|
|
455
|
+
.map((record) => ({
|
|
456
|
+
...record,
|
|
457
|
+
semanticScope: record.semanticScope ?? (record.requirementId
|
|
458
|
+
? { schemaVersion: 1, kind: 'requirement', id: record.requirementId }
|
|
459
|
+
: { schemaVersion: 1, kind: 'plan', id: record.planId }),
|
|
460
|
+
} as PlanSemanticRevisionRecord))
|
|
461
|
+
.sort((left, right) => right.revision - left.revision);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function appendJsonPlanSemanticRevisionRecord(options: PlanContractStoreOptions, record: PlanSemanticRevisionRecord): void {
|
|
465
|
+
const path = planSemanticRevisionStorePath(options);
|
|
466
|
+
const store = readJsonFile<PlanSemanticRevisionStore>(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [] });
|
|
467
|
+
if (store.revisions.some((existing) => existing.planId === record.planId && existing.revision === record.revision)) return;
|
|
468
|
+
writeJsonAtomic(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [record, ...store.revisions].slice(0, 5_000) });
|
|
469
|
+
}
|
|
368
470
|
function planRevisionRecord(plan: PlanContract, input: { recordedAt: string; reason: string; requestedRevisionLabel?: string }): PlanRevisionRecord {
|
|
369
471
|
return {
|
|
370
472
|
schemaVersion: 1, repoId: plan.repoId, planId: plan.planId, revision: currentPlanRevision(plan),
|
|
@@ -389,30 +491,6 @@ function revisionDraftFromCandidate(candidate: PlanContract, input: { requestedR
|
|
|
389
491
|
};
|
|
390
492
|
}
|
|
391
493
|
|
|
392
|
-
function planFromPendingRevision(current: PlanContract, pending: PlanRevisionDraft, at: string): PlanContract {
|
|
393
|
-
return {
|
|
394
|
-
...current, revision: pending.revision, sourceRevision: pending.sourceRevision, goal: pending.goal, nonGoals: [...pending.nonGoals],
|
|
395
|
-
assumptions: [...pending.assumptions], resolvedDecisions: [...pending.resolvedDecisions], stopConditions: [...pending.stopConditions],
|
|
396
|
-
replanConditions: [...pending.replanConditions], integrationStrategy: pending.integrationStrategy, steps: structuredClone(pending.steps),
|
|
397
|
-
obligationDispositions: pending.obligationDispositions ? structuredClone(pending.obligationDispositions) : undefined,
|
|
398
|
-
deliveryCarries: pending.deliveryCarries ? structuredClone(pending.deliveryCarries) : undefined,
|
|
399
|
-
pendingRevision: undefined, updatedAt: at,
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function revisionContinuityErrors(current: PlanContract, candidate: PlanContract, allPlans: readonly PlanContract[]): string[] {
|
|
404
|
-
const validationCandidate: PlanContract = { ...candidate, supersedes: [current.planId] };
|
|
405
|
-
return obligationContinuityErrors(validationCandidate, allPlans);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function retireResolvedRevisionObligationDispositions(current: PlanContract, candidate: PlanContract): PlanContract {
|
|
409
|
-
const dispositions = candidate.obligationDispositions ?? [];
|
|
410
|
-
if (dispositions.length === 0) return candidate;
|
|
411
|
-
const completed = new Set(listPlanObligations(current, 'completed_steps').map((obligation) => obligation.obligationId));
|
|
412
|
-
if (completed.size === 0) return candidate;
|
|
413
|
-
const retained = dispositions.filter((disposition) => !(disposition.predecessorPlanId === current.planId && completed.has(disposition.obligationId)));
|
|
414
|
-
return retained.length === dispositions.length ? candidate : { ...candidate, obligationDispositions: retained };
|
|
415
|
-
}
|
|
416
494
|
|
|
417
495
|
function assertRevisionScopeAuthority(current: PlanContract, allPlans: readonly PlanContract[]): void {
|
|
418
496
|
const conflicting = allPlans.find((plan) => plan.planId !== current.planId
|
|
@@ -420,35 +498,25 @@ function assertRevisionScopeAuthority(current: PlanContract, allPlans: readonly
|
|
|
420
498
|
if (conflicting) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${current.scopeKey}:${conflicting.planId}`);
|
|
421
499
|
}
|
|
422
500
|
|
|
423
|
-
function deriveRequiredRevisionDeliveryCarries(
|
|
424
|
-
options: PlanContractStoreOptions,
|
|
425
|
-
current: PlanContract,
|
|
426
|
-
candidate: PlanContract,
|
|
427
|
-
at: string,
|
|
428
|
-
): PlanStepDeliveryCarry[] {
|
|
429
|
-
const carries = derivePlanStepDeliveryCarries(options, current, candidate, at);
|
|
430
|
-
// A validating projection backed by a still-completed Work but missing its
|
|
431
|
-
// immutable completion receipt is corrupt and cannot be silently downgraded.
|
|
432
|
-
// A changed execution contract, failed/cancelled Work, or a successor source
|
|
433
|
-
// that does not contain the delivered revision simply means "do not carry".
|
|
434
|
-
for (const previousStep of current.steps) {
|
|
435
|
-
if (previousStep.status !== 'validating' || !previousStep.workId) continue;
|
|
436
|
-
const nextStep = candidate.steps.find((step) => step.id === previousStep.id);
|
|
437
|
-
if (!nextStep || !samePlanStepExecutionContract(previousStep, nextStep)) continue;
|
|
438
|
-
if (!acceptanceChangesExplicitlyReconciled(current, previousStep, candidate, nextStep)) continue;
|
|
439
|
-
const work = getWorkContract(options, previousStep.workId);
|
|
440
|
-
if (work?.status === 'completed' && !work.completionReceipt) {
|
|
441
|
-
throw new Error(`PLAN_DELIVERY_CARRY_RECEIPT_REQUIRED: ${current.planId}/${previousStep.id}`);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return carries;
|
|
445
|
-
}
|
|
446
501
|
|
|
447
502
|
export function emptyPlanContractStore(updatedAt: string): PlanContractStore {
|
|
448
503
|
return { schemaVersion: 1, updatedAt, contracts: [] };
|
|
449
504
|
}
|
|
450
|
-
|
|
451
|
-
|
|
505
|
+
/** Canonical Plan store scope key: an explicit portable scope, else repository placement. */
|
|
506
|
+
export function planContractStoreScopeKey(options: PlanContractStoreLocation): string | undefined {
|
|
507
|
+
return options.scopeKey?.trim() || options.repoId?.trim() || undefined;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function requirePlanContractStoreScopeKey(options: PlanContractStoreLocation): string {
|
|
511
|
+
const key = planContractStoreScopeKey(options);
|
|
512
|
+
if (!key) throw new Error('PLAN_STORE_SCOPE_REQUIRED: controllerHome requires repoId or scopeKey');
|
|
513
|
+
return key;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
type ControlPlanePlanStoreOptions = PlanContractStoreOptions & { controllerHome: string };
|
|
517
|
+
|
|
518
|
+
function sqliteBacked(options: PlanContractStoreOptions): options is ControlPlanePlanStoreOptions {
|
|
519
|
+
return Boolean(!options.root && options.controllerHome?.trim() && planContractStoreScopeKey(options));
|
|
452
520
|
}
|
|
453
521
|
|
|
454
522
|
export function readPlanContractStore(options: PlanContractStoreOptions): PlanContractStore {
|
|
@@ -457,7 +525,7 @@ export function readPlanContractStore(options: PlanContractStoreOptions): PlanCo
|
|
|
457
525
|
}
|
|
458
526
|
const records = listControlPlaneRecords<PlanContract>(options.controllerHome, {
|
|
459
527
|
namespace: 'plan_contract',
|
|
460
|
-
scope: options
|
|
528
|
+
scope: requirePlanContractStoreScopeKey(options),
|
|
461
529
|
limit: 5_000,
|
|
462
530
|
});
|
|
463
531
|
if (records.length > 0) {
|
|
@@ -474,10 +542,10 @@ export function readPlanContractStore(options: PlanContractStoreOptions): PlanCo
|
|
|
474
542
|
if (legacy.contracts.length > 0) {
|
|
475
543
|
withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
476
544
|
for (const contract of legacy.contracts) {
|
|
477
|
-
if (readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', options
|
|
545
|
+
if (readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', requirePlanContractStoreScopeKey(options), contract.planId)) continue;
|
|
478
546
|
writeControlPlaneRecordWithinTransaction(database, {
|
|
479
547
|
namespace: 'plan_contract',
|
|
480
|
-
scope: options
|
|
548
|
+
scope: requirePlanContractStoreScopeKey(options),
|
|
481
549
|
key: contract.planId,
|
|
482
550
|
schemaVersion: 1,
|
|
483
551
|
value: contract,
|
|
@@ -500,7 +568,7 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
|
|
|
500
568
|
const current = readControlPlaneRecordWithinTransaction<PlanContract>(
|
|
501
569
|
database,
|
|
502
570
|
'plan_contract',
|
|
503
|
-
options
|
|
571
|
+
requirePlanContractStoreScopeKey(options),
|
|
504
572
|
contract.planId,
|
|
505
573
|
);
|
|
506
574
|
// SQLite is authoritative per Plan row. A sibling Plan appearing in an
|
|
@@ -508,7 +576,7 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
|
|
|
508
576
|
if (current && JSON.stringify(current.value) === JSON.stringify(contract)) continue;
|
|
509
577
|
writeControlPlaneRecordWithinTransaction(database, {
|
|
510
578
|
namespace: 'plan_contract',
|
|
511
|
-
scope: options
|
|
579
|
+
scope: requirePlanContractStoreScopeKey(options),
|
|
512
580
|
key: contract.planId,
|
|
513
581
|
schemaVersion: 1,
|
|
514
582
|
value: contract,
|
|
@@ -520,73 +588,6 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
|
|
|
520
588
|
return store;
|
|
521
589
|
}
|
|
522
590
|
|
|
523
|
-
function writePlanSupersessionWithWorkRetirement(
|
|
524
|
-
options: PlanContractStoreOptions,
|
|
525
|
-
store: PlanContractStore,
|
|
526
|
-
predecessor: PlanContract,
|
|
527
|
-
successor: PlanContract,
|
|
528
|
-
at: string,
|
|
529
|
-
): PlanContract {
|
|
530
|
-
if (!sqliteBacked(options)) {
|
|
531
|
-
writePlanContractStore(options, store);
|
|
532
|
-
return successor;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
let writtenSuccessor = successor;
|
|
536
|
-
|
|
537
|
-
withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
538
|
-
let successorNext = successor;
|
|
539
|
-
const workWrites: Array<{ value: WorkContract; revision: number; action: string }> = [];
|
|
540
|
-
const workRecords = listAllControlPlaneRecordsWithinTransaction<WorkContract>(database, {
|
|
541
|
-
namespace: 'work_contract', scope: options.repoId,
|
|
542
|
-
}).filter((record) => record.value.planId === predecessor.planId);
|
|
543
|
-
for (const current of workRecords) {
|
|
544
|
-
if (isTerminalWorkContractStatus(current.value.status)) continue;
|
|
545
|
-
const predecessorStep = predecessor.steps.find((step) => workMatchesPlanStep(current.value, predecessor, step));
|
|
546
|
-
const successorIndex = predecessorStep ? successorNext.steps.findIndex((step) => step.id === predecessorStep.id) : -1;
|
|
547
|
-
const successorStep = successorIndex >= 0 ? successorNext.steps[successorIndex] : undefined;
|
|
548
|
-
if (predecessorStep && successorStep
|
|
549
|
-
&& samePlanStepExecutionContract(predecessorStep, successorStep)
|
|
550
|
-
&& sameOrderedStrings(predecessorStep.acceptanceCriteria, successorStep.acceptanceCriteria)) {
|
|
551
|
-
const rebound = rebindPlanBoundWorkContract(current.value, {
|
|
552
|
-
predecessorPlanId: predecessor.planId, successorPlanId: successorNext.planId, planStepId: successorStep.id,
|
|
553
|
-
planSourceRevision: successorNext.sourceRevision, allowedPaths: successorStep.allowedPaths, forbiddenPaths: successorStep.forbiddenPaths,
|
|
554
|
-
checks: successorStep.checks, recordedAt: at, reason: 'Plan successor preserved this exact active Work contract.',
|
|
555
|
-
});
|
|
556
|
-
const steps = [...successorNext.steps];
|
|
557
|
-
steps[successorIndex] = {
|
|
558
|
-
...successorStep, status: 'executing', workId: rebound.workId,
|
|
559
|
-
evidenceRefs: [{ title: 'active Work carried to successor', summary: `Rebound exact Work ${rebound.workId} from ${predecessor.planId}; successor approval changed no Work contract semantics.`, detailLevel: 'summary' as const }, ...predecessorStep.evidenceRefs].slice(0, 20),
|
|
560
|
-
};
|
|
561
|
-
successorNext = { ...successorNext, status: 'executing', steps, updatedAt: at };
|
|
562
|
-
workWrites.push({ value: rebound, revision: current.revision, action: 'work_plan_authority_rebound' });
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
const retired = retirePlanBoundWorkContract(current.value, {
|
|
566
|
-
predecessorPlanId: predecessor.planId, successorPlanId: successorNext.planId, recordedAt: at,
|
|
567
|
-
reason: predecessor.supersessionReason ?? 'owning Plan was superseded',
|
|
568
|
-
});
|
|
569
|
-
workWrites.push({ value: retired, revision: current.revision, action: 'work_plan_authority_retired' });
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
for (const plan of [predecessor, successorNext]) {
|
|
573
|
-
const current = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', options.repoId, plan.planId);
|
|
574
|
-
writeControlPlaneRecordWithinTransaction(database, {
|
|
575
|
-
namespace: 'plan_contract', scope: options.repoId, key: plan.planId, schemaVersion: 1, value: plan,
|
|
576
|
-
action: plan.planId === predecessor.planId ? 'plan_contract_superseded' : 'plan_contract_successor_written',
|
|
577
|
-
expectedRevision: current?.revision ?? null,
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
for (const write of workWrites) {
|
|
581
|
-
writeControlPlaneRecordWithinTransaction(database, {
|
|
582
|
-
namespace: 'work_contract', scope: options.repoId, key: write.value.workId, schemaVersion: 2, value: write.value,
|
|
583
|
-
action: write.action, expectedRevision: write.revision,
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
writtenSuccessor = successorNext;
|
|
587
|
-
});
|
|
588
|
-
return writtenSuccessor;
|
|
589
|
-
}
|
|
590
591
|
|
|
591
592
|
function buildPlanContract(input: CreatePlanContractInput, at: string): PlanContract {
|
|
592
593
|
const planId = sanitizeFileComponent(input.planId);
|
|
@@ -641,175 +642,13 @@ function draftContentErrors(plan: PlanContract): string[] {
|
|
|
641
642
|
return [...new Set(errors)];
|
|
642
643
|
}
|
|
643
644
|
|
|
644
|
-
function sameOrderedStrings(left: readonly string[], right: readonly string[]): boolean {
|
|
645
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function samePlanStepExecutionContract(predecessor: PlanStep, successor: PlanStep): boolean {
|
|
649
|
-
return predecessor.id === successor.id
|
|
650
|
-
&& predecessor.objective === successor.objective
|
|
651
|
-
&& sameOrderedStrings(predecessor.dependencies, successor.dependencies)
|
|
652
|
-
&& sameOrderedStrings(predecessor.authoritativeFiles, successor.authoritativeFiles)
|
|
653
|
-
&& sameOrderedStrings(predecessor.allowedPaths, successor.allowedPaths)
|
|
654
|
-
&& sameOrderedStrings(predecessor.forbiddenPaths, successor.forbiddenPaths)
|
|
655
|
-
&& sameOrderedStrings(predecessor.checks, successor.checks);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function workMatchesPlanStep(work: WorkContract, plan: PlanContract, step: PlanStep): boolean {
|
|
659
|
-
return step.status === 'executing' && work.planId === plan.planId && work.planStepId === step.id && step.workId === work.workId
|
|
660
|
-
&& work.requirementId === plan.requirementId && work.planSourceRevision === plan.sourceRevision
|
|
661
|
-
&& work.objective === step.objective
|
|
662
|
-
&& sameOrderedStrings(work.acceptanceCriteria, step.acceptanceCriteria)
|
|
663
|
-
&& sameOrderedStrings(work.allowedPaths, step.allowedPaths)
|
|
664
|
-
&& sameOrderedStrings(work.forbiddenPaths, step.forbiddenPaths)
|
|
665
|
-
&& sameOrderedStrings(work.checks, step.checks);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
function hasCoherentActivePlanBoundWork(options: PlanContractStoreOptions, plan: PlanContract): boolean {
|
|
669
|
-
if (!sqliteBacked(options)) return false;
|
|
670
|
-
return listControlPlaneRecords<WorkContract>(options.controllerHome, { namespace: 'work_contract', scope: options.repoId, limit: 5_000 })
|
|
671
|
-
.some(({ value: work }) => !isTerminalWorkContractStatus(work.status)
|
|
672
|
-
&& plan.steps.some((step) => workMatchesPlanStep(work, plan, step)));
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function acceptanceChangesExplicitlyReconciled(
|
|
676
|
-
predecessorPlan: PlanContract,
|
|
677
|
-
predecessorStep: PlanStep,
|
|
678
|
-
successorPlan: PlanContract,
|
|
679
|
-
successorStep: PlanStep,
|
|
680
|
-
): boolean {
|
|
681
|
-
if (sameOrderedStrings(predecessorStep.acceptanceCriteria, successorStep.acceptanceCriteria)) return true;
|
|
682
|
-
const obligations = listUnresolvedPlanObligations(predecessorPlan)
|
|
683
|
-
.filter((entry) => entry.kind === 'step_acceptance' && entry.sourceRef.startsWith(`step:${predecessorStep.id}:acceptance:`));
|
|
684
|
-
const dispositions = successorPlan.obligationDispositions ?? [];
|
|
685
|
-
const dispositionFor = (obligationId: string) => dispositions.find((entry) =>
|
|
686
|
-
entry.predecessorPlanId === predecessorPlan.planId && entry.obligationId === obligationId);
|
|
687
|
-
|
|
688
|
-
for (const obligation of obligations) {
|
|
689
|
-
if (successorStep.acceptanceCriteria.includes(obligation.summary)) continue;
|
|
690
|
-
const disposition = dispositionFor(obligation.obligationId);
|
|
691
|
-
if (!disposition || disposition.disposition === 'keep' || !disposition.rationale?.trim()) return false;
|
|
692
|
-
}
|
|
693
|
-
for (let index = 0; index < successorStep.acceptanceCriteria.length; index += 1) {
|
|
694
|
-
const criterion = successorStep.acceptanceCriteria[index]!;
|
|
695
|
-
if (predecessorStep.acceptanceCriteria.includes(criterion)) continue;
|
|
696
|
-
const successorRef = `step:${successorStep.id}:acceptance:${index}`;
|
|
697
|
-
const explicitlyChanged = obligations.some((obligation) => {
|
|
698
|
-
const disposition = dispositionFor(obligation.obligationId);
|
|
699
|
-
return disposition?.disposition === 'change'
|
|
700
|
-
&& Boolean(disposition.rationale?.trim())
|
|
701
|
-
&& disposition.successorRefs.includes(successorRef);
|
|
702
|
-
});
|
|
703
|
-
if (!explicitlyChanged) return false;
|
|
704
|
-
}
|
|
705
|
-
return true;
|
|
706
|
-
}
|
|
707
645
|
|
|
708
|
-
function deliveryCarryForStep(
|
|
709
|
-
options: PlanContractStoreOptions,
|
|
710
|
-
predecessorPlan: PlanContract,
|
|
711
|
-
predecessorStep: PlanStep,
|
|
712
|
-
successorPlan: PlanContract,
|
|
713
|
-
successorStep: PlanStep,
|
|
714
|
-
recordedAt: string,
|
|
715
|
-
): PlanStepDeliveryCarry | undefined {
|
|
716
|
-
if (predecessorStep.status !== 'validating' || !predecessorStep.workId) return undefined;
|
|
717
|
-
if (!samePlanStepExecutionContract(predecessorStep, successorStep)) return undefined;
|
|
718
|
-
if (!acceptanceChangesExplicitlyReconciled(predecessorPlan, predecessorStep, successorPlan, successorStep)) return undefined;
|
|
719
|
-
if (!predecessorPlan.requirementId || successorPlan.requirementId !== predecessorPlan.requirementId) return undefined;
|
|
720
|
-
|
|
721
|
-
const work = getWorkContract(options, predecessorStep.workId);
|
|
722
|
-
if (!work
|
|
723
|
-
|| work.status !== 'completed'
|
|
724
|
-
|| work.phase !== 'cleanup'
|
|
725
|
-
|| work.evidenceState !== 'valid'
|
|
726
|
-
|| !work.completionOutcome
|
|
727
|
-
|| work.completionOutcome === 'superseded'
|
|
728
|
-
|| work.requirementId !== predecessorPlan.requirementId
|
|
729
|
-
|| work.planId !== predecessorPlan.planId
|
|
730
|
-
|| work.planStepId !== predecessorStep.id
|
|
731
|
-
|| work.planSourceRevision !== predecessorPlan.sourceRevision
|
|
732
|
-
|| !work.completionReceipt) return undefined;
|
|
733
|
-
|
|
734
|
-
const receipt = work.completionReceipt;
|
|
735
|
-
if (!isRepositoryCompletionReceipt(receipt) && !isDirectEditWorkCompletionReceipt(receipt)) return undefined;
|
|
736
|
-
if (receipt.workId !== work.workId
|
|
737
|
-
|| receipt.delivery.status !== 'integrated'
|
|
738
|
-
|| !receipt.delivery.reachable
|
|
739
|
-
|| receipt.cleanup.status === 'blocked') return undefined;
|
|
740
|
-
const targetRevision = receipt.targetRevision.trim();
|
|
741
|
-
const successorSourceRevision = successorPlan.sourceRevision.trim();
|
|
742
|
-
if (!targetRevision || !successorSourceRevision) return undefined;
|
|
743
|
-
if (targetRevision !== successorSourceRevision) {
|
|
744
|
-
try {
|
|
745
|
-
if (options.revisionContains?.(targetRevision, successorSourceRevision) !== true) return undefined;
|
|
746
|
-
} catch {
|
|
747
|
-
return undefined;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
646
|
|
|
751
|
-
return {
|
|
752
|
-
predecessorPlanId: predecessorPlan.planId,
|
|
753
|
-
predecessorStepId: predecessorStep.id,
|
|
754
|
-
successorStepId: successorStep.id,
|
|
755
|
-
workId: work.workId,
|
|
756
|
-
completionReceiptId: receipt.receiptId,
|
|
757
|
-
deliveredSourceRevision: targetRevision,
|
|
758
|
-
recordedAt,
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
647
|
|
|
762
|
-
function derivePlanStepDeliveryCarries(
|
|
763
|
-
options: PlanContractStoreOptions,
|
|
764
|
-
predecessorPlan: PlanContract,
|
|
765
|
-
successorPlan: PlanContract,
|
|
766
|
-
recordedAt: string,
|
|
767
|
-
): PlanStepDeliveryCarry[] {
|
|
768
|
-
return successorPlan.steps.flatMap((successorStep) => {
|
|
769
|
-
const predecessorStep = predecessorPlan.steps.find((candidate) => candidate.id === successorStep.id);
|
|
770
|
-
if (!predecessorStep) return [];
|
|
771
|
-
const carry = deliveryCarryForStep(options, predecessorPlan, predecessorStep, successorPlan, successorStep, recordedAt);
|
|
772
|
-
return carry ? [carry] : [];
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
648
|
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
allPlans: readonly PlanContract[],
|
|
780
|
-
): { steps: PlanStep[]; carriedCount: number } {
|
|
781
|
-
if (!plan.deliveryCarries?.length) return { steps: plan.steps, carriedCount: 0 };
|
|
782
|
-
const carriedSteps = new Set<string>();
|
|
783
|
-
const steps = plan.steps.map((step) => ({ ...step }));
|
|
784
|
-
for (const carry of plan.deliveryCarries) {
|
|
785
|
-
if (carriedSteps.has(carry.successorStepId)) throw new Error(`PLAN_DELIVERY_CARRY_DUPLICATE_STEP: ${carry.successorStepId}`);
|
|
786
|
-
const predecessorPlan = allPlans.find((candidate) => candidate.planId === carry.predecessorPlanId);
|
|
787
|
-
const predecessorStep = predecessorPlan?.steps.find((candidate) => candidate.id === carry.predecessorStepId);
|
|
788
|
-
const successorIndex = steps.findIndex((candidate) => candidate.id === carry.successorStepId);
|
|
789
|
-
const successorStep = successorIndex >= 0 ? steps[successorIndex] : undefined;
|
|
790
|
-
if (!predecessorPlan || !predecessorStep || !successorStep) throw new Error(`PLAN_DELIVERY_CARRY_INVALID: ${carry.successorStepId}`);
|
|
791
|
-
const expected = deliveryCarryForStep(options, predecessorPlan, predecessorStep, plan, successorStep, carry.recordedAt);
|
|
792
|
-
if (!expected
|
|
793
|
-
|| expected.workId !== carry.workId
|
|
794
|
-
|| expected.completionReceiptId !== carry.completionReceiptId
|
|
795
|
-
|| expected.deliveredSourceRevision !== carry.deliveredSourceRevision) {
|
|
796
|
-
throw new Error(`PLAN_DELIVERY_CARRY_INVALID: ${carry.successorStepId}`);
|
|
797
|
-
}
|
|
798
|
-
steps[successorIndex] = {
|
|
799
|
-
...successorStep,
|
|
800
|
-
status: 'validating',
|
|
801
|
-
workId: carry.workId,
|
|
802
|
-
evidenceRefs: [{
|
|
803
|
-
evidenceId: carry.completionReceiptId,
|
|
804
|
-
title: 'successor delivery carried',
|
|
805
|
-
summary: `Reused immutable delivery from ${carry.predecessorPlanId}/${carry.predecessorStepId}; semantic acceptance remains explicit.`,
|
|
806
|
-
detailLevel: 'summary' as const,
|
|
807
|
-
}, ...predecessorStep.evidenceRefs].slice(0, 20),
|
|
808
|
-
};
|
|
809
|
-
carriedSteps.add(carry.successorStepId);
|
|
810
|
-
}
|
|
811
|
-
return { steps, carriedCount: carriedSteps.size };
|
|
812
|
-
}
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
|
|
813
652
|
|
|
814
653
|
function createPlanContractUnlocked(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
|
|
815
654
|
assertRequirementReference(options, input.requirementId);
|
|
@@ -837,20 +676,9 @@ function replacePlanContractUnlocked(
|
|
|
837
676
|
const current = store.contracts[predecessorIndex]!;
|
|
838
677
|
if (!isPlanExtensionPredecessor(current)) throw new Error(`plan contract ${current.planId} is terminal (${current.status})`);
|
|
839
678
|
|
|
840
|
-
//
|
|
841
|
-
//
|
|
679
|
+
// Draft extension keeps the stable Plan identity, but authored content is still
|
|
680
|
+
// a semantic revision: the previous head is archived and the current head advances.
|
|
842
681
|
if (current.status === 'draft') {
|
|
843
|
-
const draftCandidate = buildPlanContract({
|
|
844
|
-
...input,
|
|
845
|
-
planId: current.planId,
|
|
846
|
-
repoId: current.repoId,
|
|
847
|
-
requirementId: current.requirementId,
|
|
848
|
-
scopeKey: current.scopeKey,
|
|
849
|
-
}, at);
|
|
850
|
-
const continuityErrors = revisionContinuityErrors(current, draftCandidate, store.contracts);
|
|
851
|
-
if (continuityErrors.length > 0) {
|
|
852
|
-
throw new Error(`PLAN_OBLIGATION_CONTINUITY_REQUIRED: ${continuityErrors.join('; ')}`);
|
|
853
|
-
}
|
|
854
682
|
return repairDraftPlanContractUnlocked(options, current.planId, {
|
|
855
683
|
scopeKey: input.scopeKey, sourceRevision: input.sourceRevision, goal: input.goal, nonGoals: input.nonGoals, assumptions: input.assumptions,
|
|
856
684
|
resolvedDecisions: input.resolvedDecisions, stopConditions: input.stopConditions, replanConditions: input.replanConditions,
|
|
@@ -868,13 +696,9 @@ function replacePlanContractUnlocked(
|
|
|
868
696
|
};
|
|
869
697
|
const contentErrors = draftContentErrors(candidate);
|
|
870
698
|
if (contentErrors.length > 0) throw new Error(`PLAN_REVISION_DRAFT_INVALID: ${contentErrors.join('; ')}`);
|
|
871
|
-
const continuityErrors = revisionContinuityErrors(current, candidate, store.contracts);
|
|
872
|
-
if (continuityErrors.length > 0) throw new Error(`PLAN_OBLIGATION_CONTINUITY_REQUIRED: ${continuityErrors.join('; ')}`);
|
|
873
699
|
assertRevisionScopeAuthority(current, store.contracts);
|
|
874
|
-
const deliveryCarries = deriveRequiredRevisionDeliveryCarries(options, current, candidate, at);
|
|
875
|
-
const stagedCandidate = deliveryCarries.length > 0 ? { ...candidate, deliveryCarries } : candidate;
|
|
876
700
|
const requestedRevisionLabel = sanitizeFileComponent(input.planId);
|
|
877
|
-
const pendingRevision = revisionDraftFromCandidate(
|
|
701
|
+
const pendingRevision = revisionDraftFromCandidate(candidate, {
|
|
878
702
|
requestedRevisionLabel: requestedRevisionLabel !== current.planId ? requestedRevisionLabel : undefined, createdAt: at, updatedAt: at,
|
|
879
703
|
});
|
|
880
704
|
const next: PlanContract = { ...current, status: 'replanning', pendingRevision, updatedAt: at };
|
|
@@ -909,27 +733,18 @@ function repairDraftPlanContractUnlocked(
|
|
|
909
733
|
requirementId: current.requirementId,
|
|
910
734
|
evidenceRefs: current.evidenceRefs,
|
|
911
735
|
}, at);
|
|
912
|
-
const candidateWithLineage =
|
|
736
|
+
const candidateWithLineage = {
|
|
913
737
|
...candidate,
|
|
914
738
|
...(current.supersedes?.length ? { supersedes: [...current.supersedes] } : {}),
|
|
915
|
-
}
|
|
739
|
+
};
|
|
916
740
|
const contentErrors = draftContentErrors(candidateWithLineage);
|
|
917
741
|
if (contentErrors.length > 0) {
|
|
918
742
|
throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${contentErrors.join('; ')}`);
|
|
919
743
|
}
|
|
920
|
-
const continuityErrors = obligationContinuityErrors(candidateWithLineage, store.contracts);
|
|
921
|
-
if (continuityErrors.length > 0) {
|
|
922
|
-
throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${continuityErrors.join('; ')}`);
|
|
923
|
-
}
|
|
924
744
|
if (pending) {
|
|
925
745
|
if (normalizeScopeKey(input.scopeKey) !== current.scopeKey) throw new Error(`PLAN_REVISION_SCOPE_IDENTITY_MISMATCH: ${current.scopeKey}`);
|
|
926
746
|
assertRevisionScopeAuthority(current, store.contracts);
|
|
927
747
|
let revisionCandidate: PlanContract = { ...candidateWithLineage, revision: pending.revision, supersedes: current.supersedes ? [...current.supersedes] : undefined };
|
|
928
|
-
revisionCandidate = retireResolvedRevisionObligationDispositions(current, revisionCandidate);
|
|
929
|
-
const revisionErrors = revisionContinuityErrors(current, revisionCandidate, store.contracts);
|
|
930
|
-
if (revisionErrors.length > 0) throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${revisionErrors.join('; ')}`);
|
|
931
|
-
const deliveryCarries = deriveRequiredRevisionDeliveryCarries(options, current, revisionCandidate, at);
|
|
932
|
-
if (deliveryCarries.length > 0) revisionCandidate = { ...revisionCandidate, deliveryCarries };
|
|
933
748
|
const repairedPending = revisionDraftFromCandidate(revisionCandidate, {
|
|
934
749
|
requestedRevisionLabel: pending.requestedRevisionLabel, createdAt: pending.createdAt, updatedAt: at,
|
|
935
750
|
});
|
|
@@ -945,18 +760,46 @@ function repairDraftPlanContractUnlocked(
|
|
|
945
760
|
&& existing.scopeKey === candidateWithLineage.scopeKey
|
|
946
761
|
&& !explicitPredecessors.has(existing.planId));
|
|
947
762
|
if (conflictingScope) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${candidateWithLineage.scopeKey}:${conflictingScope.planId}`);
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
: undefined;
|
|
951
|
-
const deliveryCarries = predecessor
|
|
952
|
-
? derivePlanStepDeliveryCarries(options, predecessor, candidateWithLineage, at)
|
|
953
|
-
: [];
|
|
763
|
+
const previousSemantic = planSemanticView(current);
|
|
764
|
+
const candidateSemantic = legacyPlanSemanticContext(candidateWithLineage);
|
|
954
765
|
const repaired: PlanContract = {
|
|
955
766
|
...candidateWithLineage,
|
|
956
|
-
|
|
767
|
+
// Mechanical lifecycle revision is independent from the model-facing semantic
|
|
768
|
+
// head. Draft/extend compatibility must never silently overwrite semantic r1.
|
|
769
|
+
revision: current.revision,
|
|
770
|
+
semanticRevision: previousSemantic.revision + 1,
|
|
771
|
+
semanticUpdatedAt: at,
|
|
772
|
+
semanticContext: {
|
|
773
|
+
...candidateSemantic,
|
|
774
|
+
requirementBasisRevision: previousSemantic.requirementBasisRevision,
|
|
775
|
+
},
|
|
957
776
|
createdAt: current.createdAt,
|
|
958
777
|
updatedAt: at,
|
|
959
778
|
};
|
|
779
|
+
if (sqliteBacked(options)) {
|
|
780
|
+
return withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
781
|
+
const scope = requirePlanContractStoreScopeKey(options);
|
|
782
|
+
const currentRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', scope, current.planId);
|
|
783
|
+
if (!currentRecord) throw new Error(`plan contract not found: ${current.planId}`);
|
|
784
|
+
const actualSemanticRevision = currentPlanSemanticRevision(currentRecord.value);
|
|
785
|
+
if (actualSemanticRevision !== previousSemantic.revision) {
|
|
786
|
+
throw new Error(`PLAN_REVISION_CONFLICT:${current.planId}:expected=${previousSemantic.revision}:actual=${actualSemanticRevision}`);
|
|
787
|
+
}
|
|
788
|
+
const semanticRevisionKey = `${current.planId}:r${previousSemantic.revision}`;
|
|
789
|
+
if (!readControlPlaneRecordWithinTransaction<PlanSemanticRevisionRecord>(database, 'plan_semantic_revision', scope, semanticRevisionKey)) {
|
|
790
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
791
|
+
namespace: 'plan_semantic_revision', scope, key: semanticRevisionKey, schemaVersion: 1,
|
|
792
|
+
value: { schemaVersion: 1, ...previousSemantic, recordedAt: at },
|
|
793
|
+
action: 'plan_semantic_revision_archived', expectedRevision: null,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
return writeControlPlaneRecordWithinTransaction(database, {
|
|
797
|
+
namespace: 'plan_contract', scope, key: current.planId, schemaVersion: 1,
|
|
798
|
+
value: repaired, action: 'plan_semantic_revised', expectedRevision: currentRecord.revision,
|
|
799
|
+
}).value;
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
appendJsonPlanSemanticRevisionRecord(options, { schemaVersion: 1, ...previousSemantic, recordedAt: at });
|
|
960
803
|
const contracts = [...store.contracts];
|
|
961
804
|
contracts[index] = repaired;
|
|
962
805
|
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
|
|
@@ -996,6 +839,83 @@ export function createPlanContract(options: PlanContractStoreOptions, input: Cre
|
|
|
996
839
|
});
|
|
997
840
|
}
|
|
998
841
|
|
|
842
|
+
export function createPlanSemanticContext(options: PlanContractStoreOptions, input: CreatePlanSemanticInput): PlanContract {
|
|
843
|
+
return withPlanAdmissionLock(options, () => {
|
|
844
|
+
assertRequirementReference(options, input.requirementId);
|
|
845
|
+
const store = readPlanContractStore(options);
|
|
846
|
+
const planId = sanitizeFileComponent(input.planId);
|
|
847
|
+
if (!String(input.planId ?? '').trim() || planId === 'unknown') throw new Error('plan_id is required');
|
|
848
|
+
if (store.contracts.some((existing) => existing.planId === planId)) throw new Error(`plan contract already exists: ${planId}`);
|
|
849
|
+
|
|
850
|
+
// Thin Plan scope identifies one semantic planning lineage; it is not an
|
|
851
|
+
// execution mutex. The same Requirement + scope has one current Plan head,
|
|
852
|
+
// while genuinely independent goals remain free to use distinct scopes.
|
|
853
|
+
const scopeKey = normalizeScopeKey(input.scopeKey?.trim() || planId);
|
|
854
|
+
const requirementId = input.requirementId?.trim().slice(0, 160) || undefined;
|
|
855
|
+
const existingLineage = store.contracts.find((existing) =>
|
|
856
|
+
existing.planId !== planId
|
|
857
|
+
&& isCurrentPlanContract(existing)
|
|
858
|
+
&& existing.scopeKey === scopeKey
|
|
859
|
+
&& (existing.requirementId?.trim() || undefined) === requirementId);
|
|
860
|
+
if (existingLineage) {
|
|
861
|
+
throw new Error(`PLAN_SEMANTIC_LINEAGE_ALREADY_EXISTS:${scopeKey}:${existingLineage.planId}`);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const goal = String(input.goal ?? '').trim().slice(0, 2_000);
|
|
865
|
+
if (!goal) throw new Error('PLAN_GOAL_REQUIRED');
|
|
866
|
+
const requirementBasisRevision = input.requirementBasisRevision === undefined
|
|
867
|
+
? undefined
|
|
868
|
+
: (() => {
|
|
869
|
+
if (!Number.isInteger(input.requirementBasisRevision) || Number(input.requirementBasisRevision) < 1) {
|
|
870
|
+
throw new Error('PLAN_REQUIREMENT_BASIS_REVISION_INVALID');
|
|
871
|
+
}
|
|
872
|
+
return Number(input.requirementBasisRevision);
|
|
873
|
+
})();
|
|
874
|
+
const sourceBasisRevision = String(input.sourceBasisRevision ?? '').trim().slice(0, 200);
|
|
875
|
+
const items = normalizePlanSemanticItems(input.items);
|
|
876
|
+
const at = nowIso(options);
|
|
877
|
+
const semanticContext = {
|
|
878
|
+
requirementBasisRevision,
|
|
879
|
+
sourceBasisRevision,
|
|
880
|
+
goal,
|
|
881
|
+
nonGoals: bounded(input.nonGoals, 20),
|
|
882
|
+
assumptions: bounded(input.assumptions, 30),
|
|
883
|
+
resolvedDecisions: bounded(input.resolvedDecisions, 30),
|
|
884
|
+
stopConditions: bounded(input.stopConditions, 20),
|
|
885
|
+
replanConditions: bounded(input.replanConditions, 20),
|
|
886
|
+
integrationStrategy: input.integrationStrategy?.trim().slice(0, 1_000) || undefined,
|
|
887
|
+
items,
|
|
888
|
+
};
|
|
889
|
+
const plan: PlanContract = {
|
|
890
|
+
schemaVersion: 1,
|
|
891
|
+
planId,
|
|
892
|
+
revision: 1,
|
|
893
|
+
semanticRevision: 1,
|
|
894
|
+
semanticUpdatedAt: at,
|
|
895
|
+
semanticContext,
|
|
896
|
+
repoId: input.repoId,
|
|
897
|
+
requirementId,
|
|
898
|
+
scopeKey,
|
|
899
|
+
sourceRevision: sourceBasisRevision,
|
|
900
|
+
goal,
|
|
901
|
+
nonGoals: [...semanticContext.nonGoals],
|
|
902
|
+
assumptions: [...semanticContext.assumptions],
|
|
903
|
+
resolvedDecisions: [...semanticContext.resolvedDecisions],
|
|
904
|
+
stopConditions: [...semanticContext.stopConditions],
|
|
905
|
+
replanConditions: [...semanticContext.replanConditions],
|
|
906
|
+
integrationStrategy: semanticContext.integrationStrategy,
|
|
907
|
+
// Legacy lifecycle projection only. Thin Plan items never become PlanSteps.
|
|
908
|
+
status: 'approved',
|
|
909
|
+
steps: [],
|
|
910
|
+
evidenceRefs: [],
|
|
911
|
+
createdAt: at,
|
|
912
|
+
updatedAt: at,
|
|
913
|
+
};
|
|
914
|
+
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts: [plan, ...store.contracts] });
|
|
915
|
+
return plan;
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
999
919
|
function admitPlanContractUnlocked(options: PlanContractStoreOptions, input: AdmitPlanContractInput): AdmitPlanContractResult {
|
|
1000
920
|
assertRequirementReference(options, input.requirementId);
|
|
1001
921
|
const plans = listPlanContracts({ ...options, status: 'all', limit: 100 });
|
|
@@ -1025,6 +945,113 @@ export function getPlanContract(options: PlanContractStoreOptions, planId: strin
|
|
|
1025
945
|
return readPlanContractStore(options).contracts.find((contract) => contract.planId === sanitizeFileComponent(planId));
|
|
1026
946
|
}
|
|
1027
947
|
|
|
948
|
+
export function revisePlanSemanticContext(
|
|
949
|
+
options: PlanContractStoreOptions,
|
|
950
|
+
planIdInput: string,
|
|
951
|
+
input: RevisePlanSemanticInput,
|
|
952
|
+
): PlanContract {
|
|
953
|
+
if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 1) throw new Error('PLAN_EXPECTED_REVISION_INVALID');
|
|
954
|
+
const planId = sanitizeFileComponent(planIdInput);
|
|
955
|
+
const requestedSuccessor = input.supersededBy === undefined
|
|
956
|
+
? undefined
|
|
957
|
+
: (() => {
|
|
958
|
+
const raw = String(input.supersededBy).trim();
|
|
959
|
+
if (!raw) throw new Error('PLAN_SUCCESSOR_REQUIRED');
|
|
960
|
+
const successorId = sanitizeFileComponent(raw);
|
|
961
|
+
if (!successorId || successorId === 'unknown') throw new Error('PLAN_SUCCESSOR_INVALID');
|
|
962
|
+
if (successorId === planId) throw new Error(`PLAN_SUCCESSOR_SELF_REFERENCE:${planId}`);
|
|
963
|
+
return successorId;
|
|
964
|
+
})();
|
|
965
|
+
return withPlanAdmissionLock(options, () => {
|
|
966
|
+
const applyRevision = (current: PlanContract, at: string): { previous: PlanSemanticView; next: PlanContract } => {
|
|
967
|
+
const revision = currentPlanSemanticRevision(current);
|
|
968
|
+
if (revision !== input.expectedRevision) throw new Error(`PLAN_REVISION_CONFLICT:${planId}:expected=${input.expectedRevision}:actual=${revision}`);
|
|
969
|
+
const previous = planSemanticView(current);
|
|
970
|
+
const existingSuccessor = current.supersededBy?.trim() || undefined;
|
|
971
|
+
if (existingSuccessor && requestedSuccessor && existingSuccessor !== requestedSuccessor) {
|
|
972
|
+
throw new Error(`PLAN_SUCCESSOR_IMMUTABLE:${planId}:current=${existingSuccessor}:requested=${requestedSuccessor}`);
|
|
973
|
+
}
|
|
974
|
+
const supersededBy = requestedSuccessor ?? existingSuccessor;
|
|
975
|
+
const goal = input.goal === undefined ? previous.goal : String(input.goal).trim().slice(0, 2_000);
|
|
976
|
+
if (!goal) throw new Error('PLAN_GOAL_REQUIRED');
|
|
977
|
+
const requirementBasisRevision = input.requirementBasisRevision === undefined
|
|
978
|
+
? previous.requirementBasisRevision
|
|
979
|
+
: (() => {
|
|
980
|
+
if (!Number.isInteger(input.requirementBasisRevision) || Number(input.requirementBasisRevision) < 1) throw new Error('PLAN_REQUIREMENT_BASIS_REVISION_INVALID');
|
|
981
|
+
return Number(input.requirementBasisRevision);
|
|
982
|
+
})();
|
|
983
|
+
const sourceBasisRevision = input.sourceBasisRevision === undefined ? previous.sourceBasisRevision : String(input.sourceBasisRevision).trim().slice(0, 200);
|
|
984
|
+
const items = input.items === undefined ? previous.items : normalizePlanSemanticItems(input.items);
|
|
985
|
+
const semanticContext = {
|
|
986
|
+
requirementBasisRevision,
|
|
987
|
+
sourceBasisRevision,
|
|
988
|
+
goal,
|
|
989
|
+
nonGoals: input.nonGoals === undefined ? previous.nonGoals : bounded(input.nonGoals, 20),
|
|
990
|
+
assumptions: input.assumptions === undefined ? previous.assumptions : bounded(input.assumptions, 30),
|
|
991
|
+
resolvedDecisions: input.resolvedDecisions === undefined ? previous.resolvedDecisions : bounded(input.resolvedDecisions, 30),
|
|
992
|
+
stopConditions: input.stopConditions === undefined ? previous.stopConditions : bounded(input.stopConditions, 20),
|
|
993
|
+
replanConditions: input.replanConditions === undefined ? previous.replanConditions : bounded(input.replanConditions, 20),
|
|
994
|
+
integrationStrategy: input.integrationStrategy === undefined ? previous.integrationStrategy : input.integrationStrategy === null ? undefined : String(input.integrationStrategy).trim().slice(0, 1_000) || undefined,
|
|
995
|
+
items,
|
|
996
|
+
};
|
|
997
|
+
return {
|
|
998
|
+
previous,
|
|
999
|
+
next: {
|
|
1000
|
+
...current,
|
|
1001
|
+
...(supersededBy ? { supersededBy } : {}),
|
|
1002
|
+
semanticRevision: revision + 1,
|
|
1003
|
+
semanticUpdatedAt: at,
|
|
1004
|
+
semanticContext,
|
|
1005
|
+
updatedAt: at,
|
|
1006
|
+
},
|
|
1007
|
+
};
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
if (sqliteBacked(options)) {
|
|
1011
|
+
return withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
1012
|
+
const scope = requirePlanContractStoreScopeKey(options);
|
|
1013
|
+
const currentRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', scope, planId);
|
|
1014
|
+
if (!currentRecord) throw new Error(`plan contract not found: ${planId}`);
|
|
1015
|
+
if (requestedSuccessor) {
|
|
1016
|
+
const successorRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', scope, requestedSuccessor);
|
|
1017
|
+
if (!successorRecord) throw new Error(`PLAN_SUCCESSOR_NOT_FOUND:${requestedSuccessor}`);
|
|
1018
|
+
if (!isCurrentPlanContract(successorRecord.value)) throw new Error(`PLAN_SUCCESSOR_NOT_CURRENT:${requestedSuccessor}`);
|
|
1019
|
+
}
|
|
1020
|
+
const at = nowIso(options);
|
|
1021
|
+
const { previous, next } = applyRevision(currentRecord.value, at);
|
|
1022
|
+
const semanticRevisionKey = `${planId}:r${previous.revision}`;
|
|
1023
|
+
if (!readControlPlaneRecordWithinTransaction<PlanSemanticRevisionRecord>(database, 'plan_semantic_revision', requirePlanContractStoreScopeKey(options), semanticRevisionKey)) {
|
|
1024
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
1025
|
+
namespace: 'plan_semantic_revision', scope, key: semanticRevisionKey, schemaVersion: 1,
|
|
1026
|
+
value: { schemaVersion: 1, ...previous, recordedAt: at },
|
|
1027
|
+
action: 'plan_semantic_revision_archived', expectedRevision: null,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
return writeControlPlaneRecordWithinTransaction(database, {
|
|
1031
|
+
namespace: 'plan_contract', scope, key: planId, schemaVersion: 1,
|
|
1032
|
+
value: next, action: 'plan_semantic_revised', expectedRevision: currentRecord.revision,
|
|
1033
|
+
}).value;
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
const store = readPlanContractStore(options);
|
|
1038
|
+
const index = store.contracts.findIndex((contract) => contract.planId === planId);
|
|
1039
|
+
if (index < 0) throw new Error(`plan contract not found: ${planId}`);
|
|
1040
|
+
if (requestedSuccessor) {
|
|
1041
|
+
const successor = store.contracts.find((contract) => contract.planId === requestedSuccessor);
|
|
1042
|
+
if (!successor) throw new Error(`PLAN_SUCCESSOR_NOT_FOUND:${requestedSuccessor}`);
|
|
1043
|
+
if (!isCurrentPlanContract(successor)) throw new Error(`PLAN_SUCCESSOR_NOT_CURRENT:${requestedSuccessor}`);
|
|
1044
|
+
}
|
|
1045
|
+
const at = nowIso(options);
|
|
1046
|
+
const { previous, next } = applyRevision(store.contracts[index]!, at);
|
|
1047
|
+
appendJsonPlanSemanticRevisionRecord(options, { schemaVersion: 1, ...previous, recordedAt: at });
|
|
1048
|
+
const contracts = [...store.contracts];
|
|
1049
|
+
contracts[index] = next;
|
|
1050
|
+
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
|
|
1051
|
+
return next;
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1028
1055
|
export function isCurrentPlanContract(contract: PlanContract): boolean {
|
|
1029
1056
|
return !isTerminalPlanContractStatus(contract.status) && !contract.supersededBy?.trim();
|
|
1030
1057
|
}
|
|
@@ -1053,570 +1080,3 @@ export function summarizePlanContract(plan: PlanContract): PlanContractSummary {
|
|
|
1053
1080
|
updatedAt: plan.updatedAt,
|
|
1054
1081
|
};
|
|
1055
1082
|
}
|
|
1056
|
-
|
|
1057
|
-
function approvalErrors(plan: PlanContract, allPlans: readonly PlanContract[]): string[] {
|
|
1058
|
-
const errors = draftContentErrors(plan).map((error) => `${error} before approval`);
|
|
1059
|
-
const scopeIsCommitted = (status: PlanContractStatus): boolean => [
|
|
1060
|
-
'approved',
|
|
1061
|
-
'executing',
|
|
1062
|
-
'replanning',
|
|
1063
|
-
'verifying',
|
|
1064
|
-
'ready_to_finalize',
|
|
1065
|
-
].includes(status);
|
|
1066
|
-
const stagedPredecessorId = plan.supersedes?.length === 1 ? plan.supersedes[0] : undefined;
|
|
1067
|
-
if (allPlans.some((existing) => existing.planId !== plan.planId && existing.scopeKey === plan.scopeKey
|
|
1068
|
-
&& scopeIsCommitted(existing.status)
|
|
1069
|
-
&& !(existing.planId === stagedPredecessorId && existing.status === 'replanning'))) {
|
|
1070
|
-
errors.push(`active plan already owns scope_key ${plan.scopeKey}`);
|
|
1071
|
-
}
|
|
1072
|
-
errors.push(...obligationContinuityErrors(plan, allPlans));
|
|
1073
|
-
return [...new Set(errors)];
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function approvePendingPlanRevisionUnlocked(
|
|
1077
|
-
options: PlanContractStoreOptions & { controllerHome: string; repoId: string },
|
|
1078
|
-
current: PlanContract,
|
|
1079
|
-
): PlanContract {
|
|
1080
|
-
const pending = current.pendingRevision;
|
|
1081
|
-
if (!pending) throw new Error('PLAN_PENDING_REVISION_REQUIRED');
|
|
1082
|
-
const at = nowIso(options);
|
|
1083
|
-
return withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
1084
|
-
const currentRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', options.repoId, current.planId);
|
|
1085
|
-
if (!currentRecord?.value.pendingRevision) throw new Error('PLAN_PENDING_REVISION_STALE');
|
|
1086
|
-
const predecessor = currentRecord.value;
|
|
1087
|
-
const staged = predecessor.pendingRevision!;
|
|
1088
|
-
const archived = planRevisionRecord(predecessor, { recordedAt: at, reason: 'approved Plan revision', requestedRevisionLabel: staged.requestedRevisionLabel });
|
|
1089
|
-
let candidate = planFromPendingRevision(predecessor, staged, at);
|
|
1090
|
-
candidate = { ...candidate, status: 'approved', steps: projectDependencyReadySteps(candidate.steps) };
|
|
1091
|
-
const allPlans = readPlanContractStore(options).contracts;
|
|
1092
|
-
assertRevisionScopeAuthority(predecessor, allPlans);
|
|
1093
|
-
const continuityErrors = revisionContinuityErrors(predecessor, candidate, allPlans);
|
|
1094
|
-
if (continuityErrors.length > 0) throw new Error(`PLAN_OBLIGATION_CONTINUITY_REQUIRED: ${continuityErrors.join('; ')}`);
|
|
1095
|
-
const revalidatedCarries = deriveRequiredRevisionDeliveryCarries(options, predecessor, candidate, at);
|
|
1096
|
-
const stagedCarryKeys = new Set((staged.deliveryCarries ?? []).map((carry) => `${carry.successorStepId}:${carry.workId}:${carry.completionReceiptId}`));
|
|
1097
|
-
const revalidatedCarryKeys = new Set(revalidatedCarries.map((carry) => `${carry.successorStepId}:${carry.workId}:${carry.completionReceiptId}`));
|
|
1098
|
-
for (const carry of revalidatedCarries) {
|
|
1099
|
-
const key = `${carry.successorStepId}:${carry.workId}:${carry.completionReceiptId}`;
|
|
1100
|
-
if (!stagedCarryKeys.has(key)) throw new Error(`PLAN_DELIVERY_CARRY_STAGING_MISMATCH: ${carry.successorStepId}`);
|
|
1101
|
-
}
|
|
1102
|
-
for (const carry of staged.deliveryCarries ?? []) {
|
|
1103
|
-
const key = `${carry.successorStepId}:${carry.workId}:${carry.completionReceiptId}`;
|
|
1104
|
-
if (!revalidatedCarryKeys.has(key)) throw new Error(`PLAN_DELIVERY_CARRY_INVALID: ${carry.successorStepId}`);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
const workWrites: Array<{ value: WorkContract; revision: number; action: string }> = [];
|
|
1108
|
-
let hasExecuting = false;
|
|
1109
|
-
let hasValidating = false;
|
|
1110
|
-
const steps = [...candidate.steps];
|
|
1111
|
-
// Preserve already completed immutable semantics without replay.
|
|
1112
|
-
for (let index = 0; index < steps.length; index += 1) {
|
|
1113
|
-
const nextStep = steps[index]!;
|
|
1114
|
-
const previousStep = predecessor.steps.find((step) => step.id === nextStep.id);
|
|
1115
|
-
if (!previousStep) continue;
|
|
1116
|
-
if (previousStep.status === 'completed' && samePlanStepExecutionContract(previousStep, nextStep)
|
|
1117
|
-
&& sameOrderedStrings(previousStep.acceptanceCriteria, nextStep.acceptanceCriteria)) {
|
|
1118
|
-
steps[index] = { ...nextStep, status: 'completed', workId: previousStep.workId, evidenceRefs: previousStep.evidenceRefs };
|
|
1119
|
-
} else if (previousStep.status === 'validating' && deliveryCarryForStep(options, predecessor, previousStep, candidate, nextStep, at)) {
|
|
1120
|
-
steps[index] = { ...nextStep, status: 'validating', workId: previousStep.workId, evidenceRefs: previousStep.evidenceRefs };
|
|
1121
|
-
hasValidating = true;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
candidate = { ...candidate, steps };
|
|
1125
|
-
|
|
1126
|
-
const workRecords = listAllControlPlaneRecordsWithinTransaction<WorkContract>(database, { namespace: 'work_contract', scope: options.repoId })
|
|
1127
|
-
.filter((record) => record.value.planId === predecessor.planId && !isTerminalWorkContractStatus(record.value.status));
|
|
1128
|
-
for (const workRecord of workRecords) {
|
|
1129
|
-
const previousStep = predecessor.steps.find((step) => workMatchesPlanStep(workRecord.value, predecessor, step));
|
|
1130
|
-
const nextIndex = previousStep ? candidate.steps.findIndex((step) => step.id === previousStep.id) : -1;
|
|
1131
|
-
const nextStep = nextIndex >= 0 ? candidate.steps[nextIndex] : undefined;
|
|
1132
|
-
if (previousStep && nextStep && previousStep.workId === workRecord.value.workId
|
|
1133
|
-
&& samePlanStepExecutionContract(previousStep, nextStep)
|
|
1134
|
-
&& sameOrderedStrings(previousStep.acceptanceCriteria, nextStep.acceptanceCriteria)) {
|
|
1135
|
-
const refreshed = refreshPlanBoundWorkRevision(workRecord.value, {
|
|
1136
|
-
planId: predecessor.planId, planStepId: nextStep.id, planSourceRevision: candidate.sourceRevision,
|
|
1137
|
-
allowedPaths: nextStep.allowedPaths, forbiddenPaths: nextStep.forbiddenPaths, checks: nextStep.checks,
|
|
1138
|
-
recordedAt: at, reason: `Plan revision r${currentPlanRevision(predecessor)} -> r${staged.revision} preserved the exact Work contract.`,
|
|
1139
|
-
});
|
|
1140
|
-
const nextSteps = [...candidate.steps];
|
|
1141
|
-
nextSteps[nextIndex] = { ...nextStep, status: 'executing', workId: refreshed.workId, evidenceRefs: previousStep.evidenceRefs };
|
|
1142
|
-
candidate = { ...candidate, steps: nextSteps };
|
|
1143
|
-
hasExecuting = true;
|
|
1144
|
-
workWrites.push({ value: refreshed, revision: workRecord.revision, action: 'plan_revision_work_refreshed' });
|
|
1145
|
-
} else {
|
|
1146
|
-
const retired = retirePlanBoundWorkContract(workRecord.value, {
|
|
1147
|
-
predecessorPlanId: predecessor.planId, recordedAt: at, reason: `Plan revision r${currentPlanRevision(predecessor)} -> r${staged.revision} changed the frozen Work execution contract.`,
|
|
1148
|
-
});
|
|
1149
|
-
workWrites.push({ value: retired, revision: workRecord.revision, action: 'plan_revision_work_retired' });
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
const allCompleted = candidate.steps.length > 0 && candidate.steps.every((step) => step.status === 'completed');
|
|
1153
|
-
candidate = { ...candidate, deliveryCarries: undefined, status: hasExecuting ? 'executing' : hasValidating ? 'verifying' : allCompleted ? 'ready_to_finalize' : 'approved', updatedAt: at };
|
|
1154
|
-
const revisionKey = planRevisionRecordKey(predecessor.planId, currentPlanRevision(predecessor));
|
|
1155
|
-
if (readControlPlaneRecordWithinTransaction<PlanRevisionRecord>(database, 'plan_revision', options.repoId, revisionKey)) {
|
|
1156
|
-
throw new Error(`PLAN_REVISION_ALREADY_EXISTS: ${predecessor.planId}:r${currentPlanRevision(predecessor)}`);
|
|
1157
|
-
}
|
|
1158
|
-
writeControlPlaneRecordWithinTransaction(database, { namespace: 'plan_revision', scope: options.repoId, key: revisionKey, schemaVersion: 1, value: archived, action: 'plan_revision_archived', expectedRevision: null });
|
|
1159
|
-
writeControlPlaneRecordWithinTransaction(database, { namespace: 'plan_contract', scope: options.repoId, key: predecessor.planId, schemaVersion: 1, value: candidate, action: 'plan_revision_approved', expectedRevision: currentRecord.revision });
|
|
1160
|
-
for (const write of workWrites) {
|
|
1161
|
-
writeControlPlaneRecordWithinTransaction(database, { namespace: 'work_contract', scope: options.repoId, key: write.value.workId, schemaVersion: 2, value: write.value, action: write.action, expectedRevision: write.revision });
|
|
1162
|
-
}
|
|
1163
|
-
return candidate;
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
function approvePlanContractUnlocked(options: PlanContractStoreOptions, planId: string): PlanContract {
|
|
1168
|
-
const store = readPlanContractStore(options);
|
|
1169
|
-
const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
|
|
1170
|
-
if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
|
|
1171
|
-
const current = store.contracts[index];
|
|
1172
|
-
assertRequirementReference(options, current.requirementId);
|
|
1173
|
-
if (current.status === 'replanning' && current.pendingRevision) {
|
|
1174
|
-
if (!sqliteBacked(options)) {
|
|
1175
|
-
const at = nowIso(options);
|
|
1176
|
-
const candidate = { ...planFromPendingRevision(current, current.pendingRevision, at), status: 'approved' as const };
|
|
1177
|
-
appendJsonPlanRevisionRecord(options, planRevisionRecord(current, { recordedAt: at, reason: 'approved Plan revision', requestedRevisionLabel: current.pendingRevision.requestedRevisionLabel }));
|
|
1178
|
-
const contracts = [...store.contracts];
|
|
1179
|
-
contracts[index] = candidate;
|
|
1180
|
-
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
|
|
1181
|
-
return candidate;
|
|
1182
|
-
}
|
|
1183
|
-
return approvePendingPlanRevisionUnlocked(options, current);
|
|
1184
|
-
}
|
|
1185
|
-
if (current.status !== 'draft' && current.status !== 'reviewing') throw new Error(`plan contract ${current.planId} cannot be approved from ${current.status}`);
|
|
1186
|
-
const errors = approvalErrors(current, store.contracts);
|
|
1187
|
-
if (errors.length > 0) throw new Error(`plan contract ${current.planId} cannot be approved: ${errors.join('; ')}`);
|
|
1188
|
-
const at = nowIso(options);
|
|
1189
|
-
const materializedCarry = materializePlanStepDeliveryCarries(options, current, store.contracts);
|
|
1190
|
-
const next = {
|
|
1191
|
-
...current,
|
|
1192
|
-
status: materializedCarry.carriedCount > 0 ? 'verifying' as const : 'approved' as const,
|
|
1193
|
-
steps: projectDependencyReadySteps(materializedCarry.steps),
|
|
1194
|
-
updatedAt: at,
|
|
1195
|
-
};
|
|
1196
|
-
const stagedPredecessor = current.supersedes?.length === 1
|
|
1197
|
-
? store.contracts.find((candidate) => candidate.planId === current.supersedes![0] && candidate.status === 'replanning')
|
|
1198
|
-
: undefined;
|
|
1199
|
-
if (stagedPredecessor) {
|
|
1200
|
-
const predecessorNext: PlanContract = { ...stagedPredecessor, status: 'superseded', supersededBy: current.planId, supersessionReason: 'extend_existing', updatedAt: at };
|
|
1201
|
-
const contracts = store.contracts.map((candidate) => candidate.planId === predecessorNext.planId ? predecessorNext : candidate.planId === next.planId ? next : candidate);
|
|
1202
|
-
return writePlanSupersessionWithWorkRetirement(options, { schemaVersion: 1, updatedAt: at, contracts }, predecessorNext, next, at);
|
|
1203
|
-
}
|
|
1204
|
-
const contracts = [...store.contracts];
|
|
1205
|
-
contracts[index] = next;
|
|
1206
|
-
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
|
|
1207
|
-
return next;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
export function approvePlanContract(options: PlanContractStoreOptions, planId: string): PlanContract {
|
|
1211
|
-
return withPlanAdmissionLock(options, () => approvePlanContractUnlocked(options, planId));
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
export async function approvePlanContractAsync(options: PlanContractStoreOptions, planId: string): Promise<PlanContract> {
|
|
1215
|
-
return await withPlanAdmissionLockAsync(options, () => approvePlanContractUnlocked(options, planId));
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
export function supersedePlanContract(options: PlanContractStoreOptions, planId: string, supersededBy: string, reason = 'explicit_supersession'): PlanContract {
|
|
1219
|
-
return withPlanAdmissionLock(options, () => {
|
|
1220
|
-
const store = readPlanContractStore(options);
|
|
1221
|
-
const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
|
|
1222
|
-
if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
|
|
1223
|
-
const current = store.contracts[index]!;
|
|
1224
|
-
if (isTerminalPlanContractStatus(current.status)) throw new Error(`plan contract ${current.planId} is terminal (${current.status})`);
|
|
1225
|
-
const replacement = sanitizeFileComponent(supersededBy);
|
|
1226
|
-
if (!replacement || replacement === 'unknown') throw new Error('superseded_by is required');
|
|
1227
|
-
if (replacement === current.planId) throw new Error('PLAN_SUCCESSOR_ID_MUST_CHANGE');
|
|
1228
|
-
const successor = store.contracts.find((contract) => contract.planId === replacement);
|
|
1229
|
-
if (!successor) throw new Error(`PLAN_SUCCESSOR_NOT_FOUND: ${replacement}`);
|
|
1230
|
-
const successorCandidate = { ...successor, supersedes: [...new Set([...(successor.supersedes ?? []), current.planId])] };
|
|
1231
|
-
const continuityErrors = obligationContinuityErrors(successorCandidate, store.contracts);
|
|
1232
|
-
if (continuityErrors.length > 0) throw new Error(`PLAN_OBLIGATION_CONTINUITY_REQUIRED: ${continuityErrors.join('; ')}`);
|
|
1233
|
-
const at = nowIso(options);
|
|
1234
|
-
const boundedReason = String(reason ?? '').trim().slice(0, 500) || 'explicit_supersession';
|
|
1235
|
-
const next = {
|
|
1236
|
-
...current,
|
|
1237
|
-
status: 'superseded' as const,
|
|
1238
|
-
supersededBy: successor.planId,
|
|
1239
|
-
supersessionReason: boundedReason,
|
|
1240
|
-
updatedAt: at,
|
|
1241
|
-
};
|
|
1242
|
-
const successorNext = {
|
|
1243
|
-
...successorCandidate,
|
|
1244
|
-
updatedAt: at,
|
|
1245
|
-
};
|
|
1246
|
-
const contracts = [...store.contracts];
|
|
1247
|
-
contracts[index] = next;
|
|
1248
|
-
contracts[contracts.findIndex((contract) => contract.planId === successor.planId)] = successorNext;
|
|
1249
|
-
const nextStore = { schemaVersion: 1 as const, updatedAt: at, contracts };
|
|
1250
|
-
writePlanSupersessionWithWorkRetirement(options, nextStore, next, successorNext, at);
|
|
1251
|
-
return next;
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
/**
|
|
1256
|
-
* Maintenance reconciliation for historical V2 state created before Plan ->
|
|
1257
|
-
* Work authority retirement was atomic. It terminalizes only non-terminal Work
|
|
1258
|
-
* whose bound Plan is already terminal; it never deletes history and never
|
|
1259
|
-
* touches Work bound to a current Plan.
|
|
1260
|
-
*/
|
|
1261
|
-
export function retireTerminalPlanBoundWorkAuthorities(options: PlanContractStoreOptions): string[] {
|
|
1262
|
-
if (!sqliteBacked(options)) return [];
|
|
1263
|
-
const terminalPlans = new Map(
|
|
1264
|
-
readPlanContractStore(options).contracts
|
|
1265
|
-
.filter((plan) => isTerminalPlanContractStatus(plan.status))
|
|
1266
|
-
.map((plan) => [plan.planId, plan] as const),
|
|
1267
|
-
);
|
|
1268
|
-
if (terminalPlans.size === 0) return [];
|
|
1269
|
-
const candidates = listControlPlaneRecords<WorkContract>(options.controllerHome, {
|
|
1270
|
-
namespace: 'work_contract',
|
|
1271
|
-
scope: options.repoId,
|
|
1272
|
-
limit: 5_000,
|
|
1273
|
-
}).filter((record) => record.value.planId && terminalPlans.has(record.value.planId) && !isTerminalWorkContractStatus(record.value.status));
|
|
1274
|
-
if (candidates.length === 0) return [];
|
|
1275
|
-
const retired: string[] = [];
|
|
1276
|
-
const at = nowIso(options);
|
|
1277
|
-
withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
1278
|
-
for (const candidate of candidates) {
|
|
1279
|
-
const current = readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', options.repoId, candidate.key);
|
|
1280
|
-
const planId = current?.value.planId;
|
|
1281
|
-
if (!current || !planId || isTerminalWorkContractStatus(current.value.status)) continue;
|
|
1282
|
-
const plan = terminalPlans.get(planId);
|
|
1283
|
-
if (!plan || !isTerminalPlanContractStatus(plan.status)) continue;
|
|
1284
|
-
const next = retirePlanBoundWorkContract(current.value, {
|
|
1285
|
-
predecessorPlanId: plan.planId,
|
|
1286
|
-
successorPlanId: plan.supersededBy,
|
|
1287
|
-
recordedAt: at,
|
|
1288
|
-
reason: `maintenance reconciliation: owning Plan is ${plan.status}`,
|
|
1289
|
-
});
|
|
1290
|
-
writeControlPlaneRecordWithinTransaction(database, {
|
|
1291
|
-
namespace: 'work_contract',
|
|
1292
|
-
scope: options.repoId,
|
|
1293
|
-
key: next.workId,
|
|
1294
|
-
schemaVersion: 2,
|
|
1295
|
-
value: next,
|
|
1296
|
-
action: 'work_terminal_plan_authority_reconciled',
|
|
1297
|
-
expectedRevision: current.revision,
|
|
1298
|
-
});
|
|
1299
|
-
retired.push(next.workId);
|
|
1300
|
-
}
|
|
1301
|
-
});
|
|
1302
|
-
return retired.sort();
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
export interface ReplanActivePlanBoundWorkScopeInput {
|
|
1306
|
-
planId: string;
|
|
1307
|
-
stepId: string;
|
|
1308
|
-
workId: string;
|
|
1309
|
-
requestedRevisionLabel: string;
|
|
1310
|
-
sourceRevision: string;
|
|
1311
|
-
allowedPaths: string[];
|
|
1312
|
-
reason: string;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
export interface ReplanActivePlanBoundWorkScopeResult {
|
|
1316
|
-
priorPlan: PlanContract;
|
|
1317
|
-
currentPlan: PlanContract;
|
|
1318
|
-
work: WorkContract;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
/**
|
|
1322
|
-
* Atomically advance one executing Plan revision while retaining its exact
|
|
1323
|
-
* active Work binding. This repair is intentionally scope-only: objective,
|
|
1324
|
-
* acceptance, checks, forbidden paths, dependencies, Plan identity, and Work
|
|
1325
|
-
* identity remain unchanged; allowed paths may only widen.
|
|
1326
|
-
*/
|
|
1327
|
-
export function replanActivePlanBoundWorkScope(
|
|
1328
|
-
options: PlanContractStoreOptions,
|
|
1329
|
-
input: ReplanActivePlanBoundWorkScopeInput,
|
|
1330
|
-
): ReplanActivePlanBoundWorkScopeResult {
|
|
1331
|
-
if (!sqliteBacked(options)) throw new Error('PLAN_WORK_REPLAN_REQUIRES_CONTROLLER_STORE');
|
|
1332
|
-
const planId = sanitizeFileComponent(input.planId);
|
|
1333
|
-
const requestedRevisionLabel = sanitizeFileComponent(input.requestedRevisionLabel);
|
|
1334
|
-
const stepId = sanitizeFileComponent(input.stepId);
|
|
1335
|
-
const workId = sanitizeFileComponent(input.workId);
|
|
1336
|
-
const sourceRevision = input.sourceRevision.trim();
|
|
1337
|
-
const reason = input.reason.trim();
|
|
1338
|
-
if (!requestedRevisionLabel || requestedRevisionLabel === 'unknown' || requestedRevisionLabel === planId) throw new Error('PLAN_REVISION_LABEL_MUST_CHANGE');
|
|
1339
|
-
if (!sourceRevision) throw new Error('PLAN_WORK_REPLAN_SOURCE_REVISION_REQUIRED');
|
|
1340
|
-
if (!reason) throw new Error('PLAN_WORK_REPLAN_REASON_REQUIRED');
|
|
1341
|
-
const requestedAllowedPaths = [...new Set(input.allowedPaths.map((value) => value.trim()).filter(Boolean))].slice(0, 50);
|
|
1342
|
-
|
|
1343
|
-
return withPlanAdmissionLock(options, () => {
|
|
1344
|
-
const observed = getPlanContract(options, planId);
|
|
1345
|
-
if (!observed) throw new Error(`plan contract not found: ${planId}`);
|
|
1346
|
-
assertRequirementReference(options, observed.requirementId);
|
|
1347
|
-
return withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
1348
|
-
const planRecord = readControlPlaneRecordWithinTransaction<PlanContract>(database, 'plan_contract', options.repoId, planId);
|
|
1349
|
-
if (!planRecord) throw new Error(`plan contract not found: ${planId}`);
|
|
1350
|
-
const priorPlan = planRecord.value;
|
|
1351
|
-
if (priorPlan.supersededBy || isTerminalPlanContractStatus(priorPlan.status)) {
|
|
1352
|
-
throw new Error(`PLAN_WORK_REPLAN_PREDECESSOR_TERMINAL: ${priorPlan.planId}:${priorPlan.status}`);
|
|
1353
|
-
}
|
|
1354
|
-
if (priorPlan.status !== 'executing' && priorPlan.status !== 'replanning') {
|
|
1355
|
-
throw new Error(`PLAN_WORK_REPLAN_STATUS_INVALID: ${priorPlan.planId}:${priorPlan.status}`);
|
|
1356
|
-
}
|
|
1357
|
-
const stepIndex = priorPlan.steps.findIndex((candidate) => candidate.id === stepId);
|
|
1358
|
-
if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${stepId}`);
|
|
1359
|
-
const step = priorPlan.steps[stepIndex]!;
|
|
1360
|
-
if (step.status !== 'executing' || step.workId !== workId) {
|
|
1361
|
-
throw new Error(`PLAN_WORK_REPLAN_STEP_BINDING_MISMATCH: ${priorPlan.planId}/${stepId}:${step.status}:${step.workId ?? 'none'}`);
|
|
1362
|
-
}
|
|
1363
|
-
for (const path of step.allowedPaths) {
|
|
1364
|
-
if (!requestedAllowedPaths.includes(path)) throw new Error(`PLAN_WORK_REPLAN_SCOPE_NARROWING_FORBIDDEN: ${path}`);
|
|
1365
|
-
}
|
|
1366
|
-
const widened = requestedAllowedPaths.some((path) => !step.allowedPaths.includes(path));
|
|
1367
|
-
if (!widened) throw new Error('PLAN_WORK_REPLAN_SCOPE_NOT_WIDENED');
|
|
1368
|
-
const workRecord = readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', options.repoId, workId);
|
|
1369
|
-
if (!workRecord) throw new Error(`work contract not found: ${workId}`);
|
|
1370
|
-
// Replan runs inside the Plan + Work transaction. Normalize the exact
|
|
1371
|
-
// row read there so a legacy Work missing the first-class review
|
|
1372
|
-
// checkpoint is migrated by the canonical Work authority before the
|
|
1373
|
-
// scope-only refresh validates phase evidence.
|
|
1374
|
-
const work = canonicalizeWorkContractForAuthority(workRecord.value);
|
|
1375
|
-
if (work.requirementId !== priorPlan.requirementId) throw new Error('PLAN_WORK_REPLAN_REQUIREMENT_MISMATCH');
|
|
1376
|
-
const at = nowIso(options);
|
|
1377
|
-
const priorRevision = currentPlanRevision(priorPlan);
|
|
1378
|
-
const archived = planRevisionRecord(priorPlan, { recordedAt: at, reason, requestedRevisionLabel });
|
|
1379
|
-
const steps = [...priorPlan.steps];
|
|
1380
|
-
steps[stepIndex] = {
|
|
1381
|
-
...step,
|
|
1382
|
-
allowedPaths: requestedAllowedPaths,
|
|
1383
|
-
evidenceRefs: [{
|
|
1384
|
-
title: 'scope-only Plan revision',
|
|
1385
|
-
summary: `${priorPlan.planId} r${priorRevision} -> r${priorRevision + 1}: ${reason}`.slice(0, 2_000),
|
|
1386
|
-
detailLevel: 'summary' as const,
|
|
1387
|
-
}, ...step.evidenceRefs].slice(0, 20),
|
|
1388
|
-
};
|
|
1389
|
-
const currentPlan: PlanContract = {
|
|
1390
|
-
...priorPlan,
|
|
1391
|
-
revision: priorRevision + 1,
|
|
1392
|
-
sourceRevision,
|
|
1393
|
-
status: 'executing',
|
|
1394
|
-
steps,
|
|
1395
|
-
evidenceRefs: [{
|
|
1396
|
-
title: 'active Work Plan revision advanced',
|
|
1397
|
-
summary: `${priorPlan.planId} retained exact Work ${workId}; current revision advanced r${priorRevision} -> r${priorRevision + 1} without creating another Plan authority.`.slice(0, 2_000),
|
|
1398
|
-
detailLevel: 'summary' as const,
|
|
1399
|
-
}, ...priorPlan.evidenceRefs].slice(0, 20),
|
|
1400
|
-
updatedAt: at,
|
|
1401
|
-
};
|
|
1402
|
-
const workNext = refreshPlanBoundWorkRevision(work, {
|
|
1403
|
-
planId: priorPlan.planId, planStepId: stepId, planSourceRevision: sourceRevision,
|
|
1404
|
-
allowedPaths: requestedAllowedPaths, forbiddenPaths: step.forbiddenPaths, checks: step.checks, recordedAt: at, reason,
|
|
1405
|
-
});
|
|
1406
|
-
const revisionKey = planRevisionRecordKey(priorPlan.planId, priorRevision);
|
|
1407
|
-
const existingRevision = readControlPlaneRecordWithinTransaction<PlanRevisionRecord>(database, 'plan_revision', options.repoId, revisionKey);
|
|
1408
|
-
if (existingRevision) throw new Error(`PLAN_REVISION_ALREADY_EXISTS: ${priorPlan.planId}:r${priorRevision}`);
|
|
1409
|
-
writeControlPlaneRecordWithinTransaction(database, {
|
|
1410
|
-
namespace: 'plan_revision', scope: options.repoId, key: revisionKey, schemaVersion: 1,
|
|
1411
|
-
value: archived, action: 'plan_revision_archived', expectedRevision: null,
|
|
1412
|
-
});
|
|
1413
|
-
writeControlPlaneRecordWithinTransaction(database, {
|
|
1414
|
-
namespace: 'plan_contract', scope: options.repoId, key: priorPlan.planId, schemaVersion: 1,
|
|
1415
|
-
value: currentPlan, action: 'plan_current_revision_advanced', expectedRevision: planRecord.revision,
|
|
1416
|
-
});
|
|
1417
|
-
writeControlPlaneRecordWithinTransaction(database, {
|
|
1418
|
-
namespace: 'work_contract', scope: options.repoId, key: workNext.workId, schemaVersion: 2,
|
|
1419
|
-
value: workNext, action: 'plan_revision_work_refreshed', expectedRevision: workRecord.revision,
|
|
1420
|
-
});
|
|
1421
|
-
return { priorPlan, currentPlan, work: workNext };
|
|
1422
|
-
});
|
|
1423
|
-
});
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
/**
|
|
1427
|
-
* Atomically reserve one eligible step for one WorkContract. This is the
|
|
1428
|
-
* Plan-to-Work concurrency boundary: competing callers cannot bind the same
|
|
1429
|
-
* step, and a stale source revision causes no WorkContract side effect.
|
|
1430
|
-
*/
|
|
1431
|
-
export function claimPlanStepForWork(
|
|
1432
|
-
options: PlanContractStoreOptions,
|
|
1433
|
-
input: { planId: string; stepId: string; workId: string; sourceRevision: string },
|
|
1434
|
-
): PlanContract {
|
|
1435
|
-
return updatePlanContract(options, input.planId, (current) => {
|
|
1436
|
-
assertRequirementReference(options, current.requirementId);
|
|
1437
|
-
if (current.status !== 'approved' && current.status !== 'executing') {
|
|
1438
|
-
throw new Error(`PLAN_NOT_EXECUTABLE: ${current.planId} is ${current.status}`);
|
|
1439
|
-
}
|
|
1440
|
-
if (current.sourceRevision !== input.sourceRevision) {
|
|
1441
|
-
return { ...current, status: 'invalidated_by_drift', updatedAt: nowIso(options) };
|
|
1442
|
-
}
|
|
1443
|
-
const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
|
|
1444
|
-
if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
|
|
1445
|
-
const step = current.steps[stepIndex];
|
|
1446
|
-
if (step.status === 'executing' || step.workId) throw new Error(`PLAN_STEP_ALREADY_ACTIVE: ${step.id}`);
|
|
1447
|
-
if (step.status === 'completed') throw new Error(`PLAN_STEP_ALREADY_COMPLETED: ${step.id}`);
|
|
1448
|
-
const unresolved = step.dependencies.filter((dependency) => current.steps.find((candidate) => candidate.id === dependency)?.status !== 'completed');
|
|
1449
|
-
if (unresolved.length > 0) throw new Error(`PLAN_STEP_DEPENDENCIES_PENDING: ${unresolved.join(', ')}`);
|
|
1450
|
-
const at = nowIso(options);
|
|
1451
|
-
const steps = [...current.steps];
|
|
1452
|
-
steps[stepIndex] = { ...step, status: 'executing', workId: input.workId };
|
|
1453
|
-
return { ...current, status: 'executing', steps, updatedAt: at };
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
export function repairDanglingPlanStepWorkBinding(
|
|
1458
|
-
options: PlanContractStoreOptions,
|
|
1459
|
-
input: { planId: string; stepId: string; expectedWorkId: string; reason: string },
|
|
1460
|
-
): PlanContract {
|
|
1461
|
-
const expectedWorkId = input.expectedWorkId.trim();
|
|
1462
|
-
const reason = input.reason.trim();
|
|
1463
|
-
if (!expectedWorkId) throw new Error('PLAN_STEP_REPAIR_EXPECTED_WORK_REQUIRED');
|
|
1464
|
-
if (!reason) throw new Error('PLAN_STEP_REPAIR_REASON_REQUIRED');
|
|
1465
|
-
return updatePlanContract(options, input.planId, (current) => {
|
|
1466
|
-
if (isTerminalPlanContractStatus(current.status)) throw new Error(`PLAN_STEP_REPAIR_PLAN_TERMINAL: ${current.planId} is ${current.status}`);
|
|
1467
|
-
const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
|
|
1468
|
-
if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
|
|
1469
|
-
const step = current.steps[stepIndex];
|
|
1470
|
-
if (step.workId !== expectedWorkId) {
|
|
1471
|
-
throw new Error(`PLAN_STEP_REPAIR_BINDING_CHANGED: ${step.id} is bound to ${step.workId ?? 'no Work'}, expected ${expectedWorkId}`);
|
|
1472
|
-
}
|
|
1473
|
-
if (step.status !== 'executing') {
|
|
1474
|
-
throw new Error(`PLAN_STEP_REPAIR_STATUS_INVALID: ${step.id} is ${step.status}`);
|
|
1475
|
-
}
|
|
1476
|
-
const at = nowIso(options);
|
|
1477
|
-
const steps = [...current.steps];
|
|
1478
|
-
steps[stepIndex] = {
|
|
1479
|
-
...step,
|
|
1480
|
-
status: 'ready',
|
|
1481
|
-
workId: undefined,
|
|
1482
|
-
evidenceRefs: [{
|
|
1483
|
-
title: 'dangling Work binding repaired',
|
|
1484
|
-
summary: `${expectedWorkId}: ${reason}`.slice(0, 2_000),
|
|
1485
|
-
detailLevel: 'summary' as const,
|
|
1486
|
-
}, ...step.evidenceRefs].slice(0, 20),
|
|
1487
|
-
};
|
|
1488
|
-
// This is admission-state repair, not semantic replanning: the Plan and its
|
|
1489
|
-
// acceptance criteria remain authoritative. Return the exact step to ready
|
|
1490
|
-
// so a later start can atomically admit one replacement for the missing
|
|
1491
|
-
// record without forcing a second Plan authority.
|
|
1492
|
-
return { ...current, status: 'executing', steps, updatedAt: at };
|
|
1493
|
-
});
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
export function repairPlanStepForTechnicalRetry(
|
|
1497
|
-
options: PlanContractStoreOptions,
|
|
1498
|
-
input: { work: WorkContract; cleanupComplete: boolean; reason: string },
|
|
1499
|
-
): PlanContract {
|
|
1500
|
-
const work = input.work;
|
|
1501
|
-
const reason = input.reason.trim();
|
|
1502
|
-
if (!reason) throw new Error('PLAN_STEP_TECHNICAL_RETRY_REASON_REQUIRED');
|
|
1503
|
-
if (work.status !== 'cancelled' && work.status !== 'failed') {
|
|
1504
|
-
throw new Error(`PLAN_STEP_TECHNICAL_RETRY_WORK_STATUS_INVALID: ${work.workId}:${work.status}`);
|
|
1505
|
-
}
|
|
1506
|
-
if (work.phase !== 'cleanup') throw new Error(`PLAN_STEP_TECHNICAL_RETRY_WORK_PHASE_INVALID: ${work.workId}:${work.phase}`);
|
|
1507
|
-
if (work.completionReceipt || work.completionOutcome) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_DELIVERY_PRESENT: ${work.workId}`);
|
|
1508
|
-
if (work.scopeEvidence?.actualChangedPaths.length !== 0) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_SOURCE_DELTA_PRESENT: ${work.workId}`);
|
|
1509
|
-
if (!input.cleanupComplete) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_CLEANUP_INCOMPLETE: ${work.workId}`);
|
|
1510
|
-
if (!work.planId?.trim() || !work.planStepId?.trim() || !work.planSourceRevision?.trim()) {
|
|
1511
|
-
throw new Error(`PLAN_STEP_TECHNICAL_RETRY_LINEAGE_REQUIRED: ${work.workId}`);
|
|
1512
|
-
}
|
|
1513
|
-
if (work.status === 'failed') {
|
|
1514
|
-
const checks = work.checkRefs ?? [];
|
|
1515
|
-
if (checks.length === 0 || checks.some((check) => check.outcome !== 'infrastructure_failure')) {
|
|
1516
|
-
throw new Error(`PLAN_STEP_TECHNICAL_RETRY_FAILED_WORK_NOT_INFRASTRUCTURE_ONLY: ${work.workId}`);
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
return updatePlanContract(options, work.planId, (current) => {
|
|
1520
|
-
if (current.status !== 'replanning') throw new Error(`PLAN_STEP_TECHNICAL_RETRY_PLAN_STATUS_INVALID: ${current.planId}:${current.status}`);
|
|
1521
|
-
if (current.sourceRevision !== work.planSourceRevision) {
|
|
1522
|
-
throw new Error(`PLAN_STEP_TECHNICAL_RETRY_SOURCE_MISMATCH: ${current.planId}:plan=${current.sourceRevision}:work=${work.planSourceRevision}`);
|
|
1523
|
-
}
|
|
1524
|
-
if (work.requirementId !== current.requirementId) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_REQUIREMENT_MISMATCH: ${work.workId}`);
|
|
1525
|
-
const stepIndex = current.steps.findIndex((step) => step.id === work.planStepId);
|
|
1526
|
-
if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${work.planStepId}`);
|
|
1527
|
-
const step = current.steps[stepIndex]!;
|
|
1528
|
-
if (step.status !== 'ready' || step.workId) {
|
|
1529
|
-
throw new Error(`PLAN_STEP_TECHNICAL_RETRY_STEP_STATE_INVALID: ${step.id}:${step.status}:${step.workId ?? 'unbound'}`);
|
|
1530
|
-
}
|
|
1531
|
-
const dependenciesSatisfied = step.dependencies.every((dependency) => current.steps.find((candidate) => candidate.id === dependency)?.status === 'completed');
|
|
1532
|
-
if (!dependenciesSatisfied) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_DEPENDENCY_INVALID: ${step.id}`);
|
|
1533
|
-
const contractMatches = work.objective === step.objective
|
|
1534
|
-
&& sameOrderedStrings(work.acceptanceCriteria, step.acceptanceCriteria)
|
|
1535
|
-
&& sameOrderedStrings(work.allowedPaths, step.allowedPaths)
|
|
1536
|
-
&& sameOrderedStrings(work.forbiddenPaths, step.forbiddenPaths)
|
|
1537
|
-
&& sameOrderedStrings(work.checks, step.checks);
|
|
1538
|
-
if (!contractMatches) throw new Error(`PLAN_STEP_TECHNICAL_RETRY_CONTRACT_MISMATCH: ${work.workId}`);
|
|
1539
|
-
const at = nowIso(options);
|
|
1540
|
-
const steps = [...current.steps];
|
|
1541
|
-
steps[stepIndex] = {
|
|
1542
|
-
...step,
|
|
1543
|
-
evidenceRefs: [{
|
|
1544
|
-
title: 'technical Work retry authorized',
|
|
1545
|
-
summary: `${work.workId}: ${reason}`.slice(0, 2_000),
|
|
1546
|
-
detailLevel: 'summary' as const,
|
|
1547
|
-
}, ...step.evidenceRefs].slice(0, 20),
|
|
1548
|
-
};
|
|
1549
|
-
return { ...current, status: 'executing', steps, updatedAt: at };
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
export function completePlanStepForWork(
|
|
1554
|
-
options: PlanContractStoreOptions,
|
|
1555
|
-
input: {
|
|
1556
|
-
planId: string;
|
|
1557
|
-
stepId: string;
|
|
1558
|
-
work: Pick<WorkContract, 'workId' | 'status' | 'phase' | 'evidenceState' | 'completionOutcome' | 'completionReceipt' | 'evidenceRefs'>;
|
|
1559
|
-
},
|
|
1560
|
-
): PlanContract {
|
|
1561
|
-
return updatePlanContract(options, input.planId, (current) => {
|
|
1562
|
-
const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
|
|
1563
|
-
if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
|
|
1564
|
-
const step = current.steps[stepIndex];
|
|
1565
|
-
if (step.workId !== input.work.workId) throw new Error(`PLAN_STEP_WORK_MISMATCH: ${input.stepId}`);
|
|
1566
|
-
if (step.status === 'completed') return current;
|
|
1567
|
-
const delivered = input.work.status === 'completed'
|
|
1568
|
-
&& input.work.phase === 'cleanup'
|
|
1569
|
-
&& input.work.evidenceState === 'valid'
|
|
1570
|
-
&& Boolean(input.work.completionOutcome && input.work.completionOutcome !== 'superseded')
|
|
1571
|
-
&& Boolean(input.work.completionReceipt);
|
|
1572
|
-
const failed = input.work.status === 'failed' || input.work.status === 'cancelled';
|
|
1573
|
-
if (!delivered && !failed) throw new Error(`PLAN_STEP_WORK_NOT_TERMINAL: ${input.work.workId}`);
|
|
1574
|
-
const at = nowIso(options);
|
|
1575
|
-
const steps = [...current.steps];
|
|
1576
|
-
steps[stepIndex] = {
|
|
1577
|
-
...step,
|
|
1578
|
-
// Work completion proves machine delivery only. The Plan step remains in
|
|
1579
|
-
// semantic validation until an explicit Controller acceptance records it.
|
|
1580
|
-
// Failed/cancelled Work returns the slice to ready for replanning.
|
|
1581
|
-
status: delivered ? 'validating' : 'ready',
|
|
1582
|
-
// Successful delivery keeps the exact Work identity through semantic
|
|
1583
|
-
// validation. Failed/cancelled Work releases the step authority so the
|
|
1584
|
-
// replanning path cannot retain a ghost binding to a terminal Work.
|
|
1585
|
-
workId: delivered ? step.workId : undefined,
|
|
1586
|
-
evidenceRefs: input.work.evidenceRefs.length > 0 ? input.work.evidenceRefs.slice(0, 20) : step.evidenceRefs,
|
|
1587
|
-
};
|
|
1588
|
-
return { ...current, status: current.pendingRevision ? 'replanning' : delivered ? 'verifying' : 'replanning', steps, updatedAt: at };
|
|
1589
|
-
});
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
export function acceptPlanStepEvidence(
|
|
1593
|
-
options: PlanContractStoreOptions,
|
|
1594
|
-
input: { planId: string; stepId: string; reviewer: string; rationale: string; acceptedSourceRevision?: string },
|
|
1595
|
-
): PlanContract {
|
|
1596
|
-
const reviewer = input.reviewer.trim();
|
|
1597
|
-
const rationale = input.rationale.trim();
|
|
1598
|
-
if (!reviewer || !rationale) throw new Error('PLAN_STEP_SEMANTIC_ACCEPTANCE_METADATA_REQUIRED');
|
|
1599
|
-
return updatePlanContract(options, input.planId, (current) => {
|
|
1600
|
-
const stepIndex = current.steps.findIndex((step) => step.id === sanitizeFileComponent(input.stepId));
|
|
1601
|
-
if (stepIndex < 0) throw new Error(`PLAN_STEP_NOT_FOUND: ${input.stepId}`);
|
|
1602
|
-
const step = current.steps[stepIndex];
|
|
1603
|
-
if (step.status === 'completed') return current;
|
|
1604
|
-
if (step.status !== 'validating') throw new Error(`PLAN_STEP_NOT_READY_FOR_SEMANTIC_ACCEPTANCE: ${step.id} is ${step.status}`);
|
|
1605
|
-
const steps = [...current.steps];
|
|
1606
|
-
steps[stepIndex] = {
|
|
1607
|
-
...step,
|
|
1608
|
-
status: 'completed',
|
|
1609
|
-
evidenceRefs: [{ title: 'semantic acceptance', summary: `${reviewer}: ${rationale}`, detailLevel: 'summary' as const }, ...step.evidenceRefs].slice(0, 20),
|
|
1610
|
-
};
|
|
1611
|
-
const allCompleted = steps.every((candidate) => candidate.status === 'completed');
|
|
1612
|
-
const projectedSteps = allCompleted ? steps : projectDependencyReadySteps(steps);
|
|
1613
|
-
const acceptedSourceRevision = input.acceptedSourceRevision?.trim();
|
|
1614
|
-
return {
|
|
1615
|
-
...current,
|
|
1616
|
-
...(acceptedSourceRevision ? { sourceRevision: acceptedSourceRevision } : {}),
|
|
1617
|
-
status: current.pendingRevision ? 'replanning' : allCompleted ? 'finalized' : 'executing',
|
|
1618
|
-
steps: projectedSteps,
|
|
1619
|
-
updatedAt: nowIso(options),
|
|
1620
|
-
};
|
|
1621
|
-
});
|
|
1622
|
-
}
|