@moretea-labs/forge 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +58 -62
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +42 -62
- package/README.md +2 -2
- package/SKILL.md +0 -1
- package/THIRD_PARTY_NOTICES.md +5 -2
- package/adapters/browser/index.ts +2 -0
- package/adapters/chatgpt/browser-delivery-host.ts +71 -0
- package/adapters/chatgpt/browser-delivery-runtime.ts +783 -0
- package/adapters/chatgpt/controller-binding-store.ts +55 -0
- package/adapters/chatgpt/controller-host.ts +119 -0
- package/adapters/chatgpt/controller-round-host.ts +78 -0
- package/adapters/chatgpt/controller-round-settlement-store.ts +45 -0
- package/adapters/chatgpt/provider-delivery.ts +113 -0
- package/adapters/chatgpt/work-conversation-binding-store.ts +192 -0
- package/adapters/chatgpt/wsl-bridge-delivery-host.ts +63 -0
- package/adapters/computer/desktop-operator-contract.ts +7 -0
- package/adapters/computer/desktop-operator-discovery.ts +201 -0
- package/adapters/computer/desktop-operator-negotiation.ts +174 -0
- package/adapters/computer/desktop-operator-provider.ts +312 -0
- package/adapters/computer/index.ts +1 -0
- package/adapters/computer/interaction-target-authority.ts +944 -0
- package/adapters/controller-process/binding-store.ts +34 -0
- package/adapters/controller-process/controller-host.ts +26 -0
- package/adapters/mcp/access-mode.ts +132 -0
- package/adapters/mcp/audit.ts +32 -0
- package/adapters/mcp/auth.ts +407 -0
- package/adapters/mcp/controller-round-compatibility.ts +184 -0
- package/adapters/mcp/frozen-client-semantic-compatibility.ts +292 -0
- package/adapters/mcp/instructions.ts +36 -0
- package/adapters/mcp/legacy-context.ts +18 -0
- package/adapters/mcp/multi-repository.ts +437 -0
- package/adapters/mcp/oauth.ts +391 -0
- package/adapters/mcp/paths.ts +140 -0
- package/adapters/mcp/policy.ts +232 -0
- package/adapters/mcp/proxy-env.ts +62 -0
- package/adapters/mcp/readiness-projection.ts +24 -0
- package/adapters/mcp/redaction.ts +21 -0
- package/adapters/mcp/repo.ts +24 -0
- package/adapters/mcp/runtime-gateway/artifact-adapter.ts +52 -0
- package/adapters/mcp/runtime-gateway/context-adapter.ts +829 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +191 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +147 -0
- package/adapters/mcp/runtime-gateway/execution-control-adapter.ts +28 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +207 -0
- package/adapters/mcp/runtime-gateway/filesystem-adapter.ts +38 -0
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +411 -0
- package/adapters/mcp/runtime-gateway/legacy-ios-tool-adapter.ts +105 -0
- package/adapters/mcp/runtime-gateway/maintenance-adapter.ts +44 -0
- package/adapters/mcp/runtime-gateway/model-adapter.ts +63 -0
- package/adapters/mcp/runtime-gateway/persisted-check-process.ts +3 -0
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +308 -0
- package/adapters/mcp/runtime-gateway/process-tools.ts +289 -0
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +76 -0
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +268 -0
- package/adapters/mcp/runtime-gateway/recovery-client-adapter.ts +70 -0
- package/adapters/mcp/runtime-gateway/recovery-tool-contract.ts +99 -0
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +110 -0
- package/adapters/mcp/runtime-gateway/result-adapter.ts +108 -0
- package/adapters/mcp/runtime-gateway/router.ts +728 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1251 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +634 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-shared.ts +27 -0
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +68 -0
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +186 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +25 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +870 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +3186 -0
- package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +502 -0
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +1 -0
- package/adapters/mcp/server.ts +1048 -0
- package/adapters/mcp/setup.ts +791 -0
- package/adapters/mcp/tool-capability-cache.ts +87 -0
- package/adapters/mcp/tool-mapping/access-tools.ts +387 -0
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +5265 -0
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +150 -0
- package/adapters/mcp/tool-mapping/repository-tools.ts +1691 -0
- package/adapters/mcp/tool-mapping/tools.ts +9 -0
- package/adapters/mcp/tool-mapping/umbrella-tools.ts +117 -0
- package/adapters/mcp/toolset-names.ts +74 -0
- package/adapters/mcp/toolset-selection.ts +41 -0
- package/adapters/mcp/toolset.ts +336 -0
- package/adapters/mcp/transports/http.ts +1456 -0
- package/adapters/mcp/transports/session-registry.ts +513 -0
- package/adapters/mcp/transports/stdio.ts +8 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +139 -0
- package/adapters/mcp/types.ts +41 -0
- package/assets/AGENTS.md +2 -16
- package/assets/CLAUDE.md +2 -16
- package/assets/hooks/AGENTS.md +3 -14
- package/assets/hooks/CLAUDE.md +3 -14
- package/assets/hooks/hook-input.sh +9 -2
- package/assets/hooks/lib/workflow-state.sh +28 -10
- package/assets/hooks/prompt-guard.sh +63 -37
- package/assets/hooks/security-sentinel.sh +2 -1
- package/assets/hooks/session-start-context.sh +7 -5
- package/assets/hooks/stop-orchestrator.sh +13 -3
- package/assets/initializer-question-pack.v4.json +11 -7
- package/assets/partials/05-workflow.partial.md +18 -73
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -2
- package/assets/partials-agents/04-task-protocol.partial.md +14 -59
- package/assets/partials-agents/06-quality-safety.partial.md +11 -16
- package/assets/partials-agents/08-deep-docs.partial.md +1 -2
- package/assets/plan-map.json +11 -11
- package/assets/plugin-registry.v1.json +9 -3
- package/assets/plugin-trust-policy.v1.json +6 -0
- package/assets/recovery/ForgeRecovery.ps1 +100 -0
- package/assets/recovery/forge-wsl-rescue +271 -0
- package/assets/reference-configs/development-protocol.md +11 -15
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/global-working-rules.md +9 -1
- package/assets/reference-configs/harness-overview.md +22 -112
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +2 -2
- package/assets/skill-version.json +2 -2
- package/assets/templates/helpers/architecture-event.ts +3 -34
- package/assets/templates/helpers/architecture-queue.sh +61 -10
- package/assets/templates/helpers/archive-workflow.sh +90 -180
- package/assets/templates/helpers/capability-config.ts +8 -11
- package/assets/templates/helpers/capability-resolver.ts +37 -19
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +1 -9
- package/assets/templates/helpers/check-task-sync.sh +82 -11
- package/assets/templates/helpers/check-task-workflow.sh +1 -3
- package/assets/templates/helpers/context-contract-sync.sh +1 -12
- package/assets/templates/helpers/contract-worktree.sh +26 -24
- package/assets/templates/helpers/ensure-task-workflow.sh +6 -8
- package/assets/templates/helpers/inspect-project-state.ts +1 -1
- package/assets/templates/helpers/migrate-project-template.sh +1 -1
- package/assets/templates/helpers/migrate-workflow-docs.ts +53 -239
- package/assets/templates/helpers/new-plan.sh +2 -2
- package/assets/templates/helpers/plan-to-todo.sh +3 -31
- package/assets/templates/helpers/sprint-backlog.sh +1 -1
- package/assets/templates/helpers/switch-plan.sh +1 -1
- package/assets/templates/helpers/workflow-contract.ts +1 -0
- package/assets/workflow-contract.v1.json +63 -171
- package/assets/workflows/xiaohongshu/xiaohongshu.collect-metrics.v1.draft.json +118 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-image-note.v1.draft.json +316 -0
- package/assets/workflows/xiaohongshu/xiaohongshu.publish-long-text.v1.draft.json +324 -0
- package/docs/architecture/CURRENT.md +148 -13
- package/docs/architecture/EVOLUTION.md +13 -0
- package/docs/architecture/decisions/20260801-controller-home-sqlite-state.md +2 -1
- package/docs/architecture/decisions/20260831-independent-windows-wsl-host-rescue.md +59 -0
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +120 -0
- package/docs/architecture/decisions/20260904-repository-runtime-data-boundary.md +42 -0
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +35 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +296 -0
- package/docs/architecture/decisions/20260907-maintenance-diagnosis-bounded-read.md +39 -0
- package/docs/architecture/domains/verification.md +7 -0
- package/docs/architecture/domains/workflow-engine.md +1 -1
- package/docs/architecture/index.md +3 -3
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +19 -8
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +124 -0
- package/docs/forge-chatgpt-mcp-setup.md +5 -3
- package/docs/forge-plugin-management.md +20 -4
- package/docs/forge-runtime-self-healing-loop.md +2 -0
- package/docs/operations/benchmark-cross-version-freeze.md +38 -0
- package/docs/operations/controller-browser-plugin.md +3 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/platform-support.md +16 -8
- package/docs/operations/platform-support.zh-CN.md +2 -2
- package/docs/operations/standalone-disaster-recovery.md +26 -1
- package/docs/operations/uu-remote-rescue.md +41 -0
- package/docs/operations/windows-wsl-host-rescue.md +133 -0
- package/docs/public-usage-guide.md +1 -1
- package/docs/public-usage-guide.zh-CN.md +1 -1
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/02-connect-chatgpt.md +4 -4
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +4 -4
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +23 -14
- package/packages/kernel/controller/api/index.ts +2 -0
- package/packages/kernel/controller/application/controller-round-service.ts +29 -0
- package/packages/kernel/controller/application/controller-service.ts +30 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +308 -0
- package/packages/kernel/controller/domain/controller-round.ts +107 -0
- package/packages/kernel/controller/domain/execution-quality.ts +133 -0
- package/packages/kernel/controller/domain/types.ts +56 -0
- package/packages/kernel/controller/index.ts +1 -0
- package/packages/kernel/controller/infrastructure/controller-binding-store.ts +84 -0
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +1384 -0
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +578 -0
- package/packages/kernel/controller/ports/controller-host.ts +16 -0
- package/packages/kernel/goal/api/index.ts +1 -0
- package/packages/kernel/goal/domain/types.ts +40 -0
- package/packages/kernel/goal/index.ts +1 -0
- package/packages/kernel/identity/api/index.ts +5 -0
- package/packages/kernel/identity/application/identity-service.ts +67 -0
- package/packages/kernel/identity/domain/scope.ts +63 -0
- package/packages/kernel/identity/domain/semantic-sync.ts +81 -0
- package/packages/kernel/identity/domain/types.ts +44 -0
- package/packages/kernel/identity/domain/workspace.ts +56 -0
- package/packages/kernel/identity/index.ts +1 -0
- package/packages/kernel/identity/infrastructure/identity-store.ts +75 -0
- package/packages/kernel/memory/api/index.ts +5 -0
- package/packages/kernel/memory/application/experience-service.ts +118 -0
- package/packages/kernel/memory/application/outcome-service.ts +77 -0
- package/packages/kernel/memory/domain/experience.ts +103 -0
- package/packages/kernel/memory/domain/operational-prior.ts +92 -0
- package/packages/kernel/memory/index.ts +1 -0
- package/packages/kernel/progression/api/index.ts +2 -0
- package/packages/kernel/progression/application/projector.ts +211 -0
- package/packages/kernel/progression/domain/types.ts +116 -0
- package/packages/kernel/progression/index.ts +1 -0
- package/packages/kernel/scheduler/api/index.ts +6 -0
- package/packages/kernel/scheduler/application/continuation-service.ts +212 -0
- package/packages/kernel/scheduler/application/eligibility.ts +252 -0
- package/packages/kernel/scheduler/application/schedule-service.ts +24 -0
- package/packages/kernel/scheduler/application/settlement.ts +197 -0
- package/packages/kernel/scheduler/domain/continuation.ts +38 -0
- package/packages/kernel/scheduler/domain/schedule.ts +149 -0
- package/packages/kernel/scheduler/index.ts +1 -0
- package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +38 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +785 -0
- package/packages/kernel/work/api/index.ts +15 -0
- package/packages/kernel/work/application/work-service.ts +148 -0
- package/packages/kernel/work/domain/admission-policy.ts +34 -0
- package/packages/kernel/work/domain/check-receipt.ts +43 -0
- package/packages/kernel/work/domain/context-closure-receipt.ts +32 -0
- package/packages/kernel/work/domain/engineering-contracts.ts +157 -0
- package/packages/kernel/work/domain/engineering-design.ts +281 -0
- package/packages/kernel/work/domain/engineering-profile.ts +242 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +254 -0
- package/packages/kernel/work/domain/execution-snapshot.ts +33 -0
- package/packages/kernel/work/domain/implementation-review.ts +510 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +202 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +73 -0
- package/packages/kernel/work/domain/state-machine.ts +259 -0
- package/packages/kernel/work/domain/types.ts +528 -0
- package/packages/kernel/work/index.ts +1 -0
- package/packages/kernel/work/infrastructure/work-contract-store.ts +1876 -0
- package/packages/kernel/work/ports/work-contract-store.ts +18 -0
- package/packages/plugin-runtime/browser/index.ts +3 -0
- package/packages/plugin-runtime/browser/provider-registry.ts +110 -0
- package/packages/plugin-runtime/browser/runtime-contract.ts +150 -0
- package/packages/plugin-runtime/browser/session-authority.ts +61 -0
- package/packages/plugin-runtime/computer/index.ts +6 -0
- package/packages/plugin-runtime/computer/provider-error.ts +26 -0
- package/packages/plugin-runtime/computer/provider-registration.ts +52 -0
- package/packages/plugin-runtime/computer/provider-registry.ts +63 -0
- package/packages/plugin-runtime/computer/provider.ts +19 -0
- package/packages/plugin-runtime/computer/target-authority.ts +190 -0
- package/packages/plugin-runtime/computer/target-persistence.ts +54 -0
- package/packages/plugin-runtime/external/index.ts +1 -0
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +457 -0
- package/packages/plugin-runtime/index.ts +3 -0
- package/packages/protocols/browser/index.ts +1 -0
- package/packages/protocols/browser/session.ts +98 -0
- package/packages/protocols/computer/contract.ts +199 -0
- package/packages/protocols/computer/index.ts +1 -0
- package/packages/protocols/failure/contract.ts +158 -0
- package/packages/protocols/failure/index.ts +1 -0
- package/packages/protocols/handoff/index.ts +1 -0
- package/packages/protocols/handoff/status.ts +22 -0
- package/packages/protocols/mcp/execution-context.ts +11 -0
- package/packages/protocols/mcp/index.ts +2 -0
- package/packages/protocols/mcp/tool-contract.ts +42 -0
- package/packages/workflow-runtime/api/index.ts +2 -0
- package/packages/workflow-runtime/application/interpreter.ts +342 -0
- package/packages/workflow-runtime/domain/workflow-asset.ts +223 -0
- package/packages/workflow-runtime/index.ts +1 -0
- package/scripts/assemble-template.ts +1 -1
- package/scripts/benchmark-computer-hotpath.ts +255 -0
- package/scripts/benchmark-controller-round2.ts +54 -2
- package/scripts/benchmark-ios-agent-device.ts +3 -6
- package/scripts/benchmark-mcp-transport.ts +3 -4
- package/scripts/benchmark-operational-memory-activation.ts +235 -0
- package/scripts/benchmark-operational-shadow.ts +28 -0
- package/scripts/bootstrap-runtime-maintenance-recovery.sh +4 -1
- package/scripts/canary-chatgpt-provider-delivery.ts +97 -0
- package/scripts/canary-controller-semantic-wait.ts +195 -0
- package/scripts/canary-global-hook.sh +2 -2
- package/scripts/check-mcp-compatibility.ts +173 -7
- package/scripts/check-platform-support.mjs +28 -2
- package/scripts/check-public-docs.sh +1 -1
- package/scripts/check-release-readiness.sh +1 -1
- package/scripts/check-repository-hygiene.mjs +92 -0
- package/scripts/check-runtime-architecture.mjs +1206 -95
- package/scripts/check-source-duplication.mjs +9 -1
- package/scripts/check-tarball-install-smoke.sh +98 -15
- package/scripts/controller-ngrok-rotation.sh +5 -4
- package/scripts/create-project-dirs.sh +10 -74
- package/scripts/forge-local-recovery-helper.mjs +204 -0
- package/scripts/forge-uu-remote-rescue-helper.d.mts +31 -0
- package/scripts/forge-uu-remote-rescue-helper.mjs +409 -0
- package/scripts/forge-windows-host-recovery-helper.mjs +136 -0
- package/scripts/hook-shim.sh +2 -2
- package/scripts/init-project.sh +12 -70
- package/scripts/install-desktop-operator-registration.ts +4 -2
- package/scripts/install-independent-windows-wsl-recovery.ts +125 -0
- package/scripts/install-standalone-recovery.ts +122 -10
- package/scripts/install-uu-remote-rescue-registration.ts +74 -0
- package/scripts/install-windows-host-recovery-registration.ts +22 -0
- package/scripts/lib/controller-home-env.sh +6 -3
- package/scripts/lib/project-init-lib.sh +39 -582
- package/scripts/migrate-chatgpt-automation-schedules.ts +2 -2
- package/scripts/migrate-windows-wsl-controller-home.ts +306 -0
- package/scripts/open-source-audit-allowlist.txt +12 -1
- package/scripts/portability-bindings.mjs +70 -0
- package/scripts/run-bounded-command.mjs +55 -0
- package/scripts/run-bun-test-file.ts +22 -6
- package/scripts/run-governed-gate.ts +4 -1
- package/scripts/smoke-mcp-http-runtime.ts +1 -1
- package/scripts/smoke-runtime-recovery.ts +34 -1
- package/scripts/smoke-schedule-engine.ts +1 -1
- package/scripts/stage-runtime-release.ts +6 -1
- package/scripts/test-governance.ts +51 -2
- package/src/cli/agent-jobs/job-manager.ts +10 -1
- package/src/cli/chatgpt-browser/bind-server.ts +2 -1
- package/src/cli/chatgpt-browser/binding.ts +20 -10
- package/src/cli/chatgpt-browser/bridge-extension.ts +75 -0
- package/src/cli/chatgpt-browser/bridge-provider.ts +376 -11
- package/src/cli/chatgpt-browser/engine.ts +10 -11
- package/src/cli/chatgpt-browser/native-provider.ts +24 -83
- package/src/cli/chatgpt-browser/oracle-provider.ts +5 -3
- package/src/cli/chatgpt-browser/types.ts +1 -0
- package/src/cli/commands/bootstrap-control.ts +334 -0
- package/src/cli/commands/brain-assistant.ts +38 -0
- package/src/cli/commands/brain.ts +193 -40
- package/src/cli/commands/chatgpt.ts +127 -0
- package/src/cli/commands/computer.ts +437 -0
- package/src/cli/commands/doctor.ts +18 -1
- package/src/cli/commands/init-hook.ts +71 -61
- package/src/cli/commands/init.ts +3 -1
- package/src/cli/commands/mcp.ts +16 -10
- package/src/cli/commands/plugin.ts +38 -12
- package/src/cli/commands/recovery.ts +493 -80
- package/src/cli/commands/runtime.ts +9 -4
- package/src/cli/commands/setup-profile.ts +89 -39
- package/src/cli/commands/status.ts +2 -1
- package/src/cli/controller/check-runner.ts +487 -98
- package/src/cli/controller/context/types.ts +7 -1
- package/src/cli/controller/context-pack.ts +23 -3
- package/src/cli/controller/issue-store.ts +1 -1
- package/src/cli/controller/systemd-user.ts +110 -0
- package/src/cli/controller/types.ts +10 -70
- package/src/cli/controller/worklog.ts +67 -13
- package/src/cli/editing/edit-operation-contract.ts +92 -0
- package/src/cli/editing/edit-session.ts +289 -13
- package/src/cli/github/plugin.ts +17 -12
- package/src/cli/hook/route-registry.ts +3 -5
- package/src/cli/hook/runtime.ts +49 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/local-bridge/connector-freshness.ts +2 -2
- package/src/cli/local-bridge/console-automations.ts +2 -2
- package/src/cli/local-bridge/facade-api.ts +41 -14
- package/src/cli/local-bridge/job-store.ts +1 -1
- package/src/cli/local-bridge/server.ts +24 -27
- package/src/cli/mcp/access-mode.ts +2 -132
- package/src/cli/mcp/access-tools.ts +2 -387
- package/src/cli/mcp/audit.ts +2 -32
- package/src/cli/mcp/auth.ts +2 -404
- package/src/cli/mcp/instructions.ts +2 -34
- package/src/cli/mcp/legacy-context.ts +2 -16
- package/src/cli/mcp/legacy-tool-service.ts +2 -5308
- package/src/cli/mcp/multi-repository.ts +2 -436
- package/src/cli/mcp/oauth.ts +2 -375
- package/src/cli/mcp/openai-secure-tunnel.ts +2 -0
- package/src/cli/mcp/paths.ts +2 -140
- package/src/cli/mcp/policy.ts +2 -232
- package/src/cli/mcp/post-finalize-work-attribution.ts +2 -150
- package/src/cli/mcp/proxy-env.ts +2 -62
- package/src/cli/mcp/readiness-projection.ts +2 -24
- package/src/cli/mcp/redaction.ts +2 -21
- package/src/cli/mcp/repo.ts +2 -24
- package/src/cli/mcp/repository-tools.ts +2 -1585
- package/src/cli/mcp/server.ts +2 -951
- package/src/cli/mcp/setup.ts +2 -737
- package/src/cli/mcp/tool-capability-cache.ts +2 -87
- package/src/cli/mcp/tool-contract.ts +2 -14
- package/src/cli/mcp/tools.ts +2 -9
- package/src/cli/mcp/toolset-names.ts +2 -70
- package/src/cli/mcp/toolset-selection.ts +2 -41
- package/src/cli/mcp/toolset.ts +2 -336
- package/src/cli/mcp/transports/AGENTS.md +1 -10
- package/src/cli/mcp/transports/CLAUDE.md +1 -10
- package/src/cli/mcp/transports/http.ts +2 -1414
- package/src/cli/mcp/transports/session-registry.ts +2 -490
- package/src/cli/mcp/transports/stdio.ts +2 -8
- package/src/cli/mcp/types.ts +2 -41
- package/src/cli/mcp/umbrella-tools.ts +2 -117
- package/src/cli/repositories/command-executor.ts +64 -18
- package/src/cli/repositories/command-scope.ts +117 -1
- package/src/cli/repositories/controller-home.ts +119 -1
- package/src/cli/repositories/identity.ts +13 -0
- package/src/cli/repositories/registry.ts +68 -49
- package/src/cli/repositories/repository-snapshot.ts +53 -7
- package/src/cli/repositories/runtime-storage.ts +27 -2
- package/src/cli/repositories/safe-patch.ts +34 -38
- package/src/cli/repositories/selected-path-actions.ts +22 -2
- package/src/cli/repositories/structured-git.ts +105 -25
- package/src/cli/runtime/helper-runner.ts +7 -0
- package/src/cli/runtime-invocation.ts +8 -1
- package/src/cli/tools/codegraph.ts +44 -25
- package/src/core/adoption/manifest-templates.ts +1 -1
- package/src/core/adoption/plan.ts +3 -26
- package/src/core/adoption/workflow-contract-plan.ts +17 -9
- package/src/effects/process-runner.ts +8 -14
- package/src/runtime/context/assistant-context.ts +154 -0
- package/src/runtime/context/assistant-work-context.ts +147 -0
- package/src/runtime/context/codegraph-cache-boundary.ts +255 -0
- package/src/runtime/context/codegraph-read-provider.ts +55 -22
- package/src/runtime/context/codegraph-sidecar.cjs +8 -1
- package/src/runtime/context/context-closure.ts +268 -0
- package/src/runtime/context/context-plane.ts +211 -0
- package/src/runtime/context/project-engineering-contract.ts +60 -0
- package/src/runtime/control-plane/bootstrap/capability-provider.ts +74 -0
- package/src/runtime/control-plane/bootstrap/control-api.ts +57 -0
- package/src/runtime/control-plane/bootstrap/index.ts +5 -0
- package/src/runtime/control-plane/bootstrap/state-machine.ts +141 -0
- package/src/runtime/control-plane/bootstrap/store.ts +21 -0
- package/src/runtime/control-plane/bootstrap/types.ts +87 -0
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +328 -0
- package/src/runtime/control-plane/codegraph-cache-retention.ts +164 -0
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +320 -0
- package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +169 -0
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +350 -0
- package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +85 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +417 -100
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +103 -15
- package/src/runtime/control-plane/execution/implementation-review-content.ts +95 -0
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +2 -2
- package/src/runtime/control-plane/execution/retained-work-resume.ts +336 -6
- package/src/runtime/control-plane/execution/validation.ts +28 -3
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +116 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +44 -2
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +34 -3
- package/src/runtime/control-plane/execution/work-execution-support.ts +42 -22
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1103 -166
- package/src/runtime/control-plane/execution/work-handle-authority.ts +267 -16
- package/src/runtime/control-plane/execution/work-handle-store.ts +77 -0
- package/src/runtime/control-plane/execution/work-head-settlement.ts +75 -0
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +145 -23
- package/src/runtime/control-plane/execution/work-operation-service.ts +49 -13
- package/src/runtime/control-plane/execution/work-preparation-service.ts +31 -15
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +1 -1
- package/src/runtime/control-plane/execution/work-task-receipt.ts +20 -1
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +407 -76
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +88 -18
- package/src/runtime/control-plane/execution/work-verification-context.ts +1 -1
- package/src/runtime/control-plane/execution/work-verification-service.ts +258 -4
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +200 -15
- package/src/runtime/control-plane/facade/capability-registry.ts +21 -10
- package/src/runtime/control-plane/facade/controller-round-relay.ts +3 -1118
- package/src/runtime/control-plane/facade/controller-session-store.ts +2 -488
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +6 -3
- package/src/runtime/control-plane/facade/goal-workloop.ts +972 -137
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +158 -0
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +10 -0
- package/src/runtime/control-plane/facade/index.ts +4 -2
- package/src/runtime/control-plane/facade/operation-digest.ts +22 -58
- package/src/runtime/control-plane/facade/plan-contract-store.ts +1004 -38
- package/src/runtime/control-plane/facade/repository-work-admission.ts +19 -1
- package/src/runtime/control-plane/facade/requirement-authority.ts +218 -0
- package/src/runtime/control-plane/facade/requirement-board.ts +6 -1
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +48 -0
- package/src/runtime/control-plane/facade/semantic-admission.ts +8 -16
- package/src/runtime/control-plane/facade/suggested-actions.ts +4 -1
- package/src/runtime/control-plane/facade/types.ts +189 -532
- package/src/runtime/control-plane/facade/work-admission-policy.ts +84 -35
- package/src/runtime/control-plane/facade/work-contract-store.ts +2 -1043
- package/src/runtime/control-plane/facade/work-implementation-review.ts +2 -0
- package/src/runtime/control-plane/facade/work-state-machine.ts +2 -250
- package/src/runtime/control-plane/generated-cache-retention.ts +239 -18
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +12 -1
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +76 -13
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-entry.ts +55 -0
- package/src/runtime/control-plane/global-scheduler/periodic-cleanup-process.ts +98 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -1
- package/src/runtime/control-plane/global-scheduler/scheduler-worker-entry.ts +10 -0
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +173 -81
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +18 -6
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +397 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +2 -177
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +301 -560
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +20 -4
- package/src/runtime/control-plane/launcher/thin-launcher.ts +50 -4
- package/src/runtime/control-plane/lifecycle-retention-metrics.ts +60 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +169 -0
- package/src/runtime/control-plane/persistence/operational-prior-store.ts +348 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +30 -22
- package/src/runtime/control-plane/persistence/sqlite-store.ts +545 -76
- package/src/runtime/control-plane/persistence/workflow-content-store.ts +98 -0
- package/src/runtime/control-plane/persistence/workflow-registry-store.ts +186 -0
- package/src/runtime/control-plane/persistence/workflow-run-store.ts +123 -0
- package/src/runtime/control-plane/release-retention.ts +56 -16
- package/src/runtime/control-plane/repository-namespace-retention.ts +208 -0
- package/src/runtime/control-plane/routing/route-policy.ts +19 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +574 -37
- package/src/runtime/control-plane/runtime-generation.ts +6 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +465 -0
- package/src/runtime/control-plane/workspace/workspace-store.ts +80 -0
- package/src/runtime/diagnostics/incident-repair.ts +406 -0
- package/src/runtime/diagnostics/mcp-timing.ts +125 -6
- package/src/runtime/diagnostics/performance.ts +6 -14
- package/src/runtime/diagnostics/read-only-tool.ts +1 -1
- package/src/runtime/evidence/artifact-store.ts +65 -2
- package/src/runtime/evidence/operational-shadow.ts +38 -0
- package/src/runtime/evidence/process-check-receipt.ts +5 -43
- package/src/runtime/evidence/sensitive-output.ts +4 -4
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -1
- package/src/runtime/execution/jobs/store.ts +121 -2
- package/src/runtime/execution/managed-workspace.ts +55 -5
- package/src/runtime/execution/process-runtime/canary.ts +19 -5
- package/src/runtime/execution/process-runtime/check-classification.ts +48 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +23 -9
- package/src/runtime/execution/process-runtime/check-receipt.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +132 -0
- package/src/runtime/execution/process-runtime/check-result.ts +73 -2
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -3
- package/src/runtime/execution/process-runtime/check-storage.ts +187 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +38 -87
- package/src/runtime/execution/process-runtime/index.ts +1 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +2 -1
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +32 -1
- package/src/runtime/execution/process-runtime/persisted-check.ts +128 -21
- package/src/runtime/execution/process-runtime/resource-claims.ts +60 -3
- package/src/runtime/execution/process-runtime/runtime.ts +132 -15
- package/src/runtime/execution/process-runtime/types.ts +22 -0
- package/src/runtime/execution/workers/executor.ts +16 -13
- package/src/runtime/execution/workers/worker-entry.ts +1 -1
- package/src/runtime/gateway/mcp/execution-tools.ts +2 -207
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +2 -61
- package/src/runtime/gateway/mcp/persisted-check-process.ts +2 -3
- package/src/runtime/gateway/mcp/process-tools.ts +2 -289
- package/src/runtime/gateway/mcp/router.ts +2 -1125
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +2 -565
- package/src/runtime/gateway/mcp/runtime-tools.ts +2 -6651
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +2 -1
- package/src/runtime/maintenance/cleanup.ts +38 -24
- package/src/runtime/platform/browser-product-discovery.ts +174 -0
- package/src/runtime/platform/computer-platform.ts +14 -0
- package/src/runtime/platform/executable-discovery.ts +54 -0
- package/src/runtime/platform/fetch-http-server.ts +90 -0
- package/src/runtime/platform/service-manager.ts +138 -0
- package/src/runtime/platform/windows-host-discovery.ts +118 -0
- package/src/runtime/platform/windows-recovery-deployment.ts +67 -0
- package/src/runtime/plugins/action-application.ts +135 -0
- package/src/runtime/plugins/app-store-connect-adapter.ts +9 -21
- package/src/runtime/plugins/browser-adapter.ts +140 -100
- package/src/runtime/plugins/browser-automation-service.ts +15 -36
- package/src/runtime/plugins/browser-macos-bridge.ts +69 -21
- package/src/runtime/plugins/browser-manifest-surface.ts +30 -6
- package/src/runtime/plugins/browser-native-session-service.ts +1 -1
- package/src/runtime/plugins/browser-provider-registry.ts +2 -109
- package/src/runtime/plugins/browser-registration.ts +16 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +2 -154
- package/src/runtime/plugins/browser-runtime.ts +41 -9
- package/src/runtime/plugins/browser-session-authority.ts +101 -291
- package/src/runtime/plugins/browser-session-legacy-migration.ts +203 -0
- package/src/runtime/plugins/browser-session-store.ts +229 -36
- package/src/runtime/plugins/browser-session-types.ts +2 -98
- package/src/runtime/plugins/capability-authorization-grants.ts +17 -0
- package/src/runtime/plugins/catalog-trust.ts +82 -0
- package/src/runtime/plugins/computer-registration.ts +839 -0
- package/src/runtime/plugins/config-store.ts +119 -0
- package/src/runtime/plugins/desktop-operator-external-policy.ts +932 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +111 -8
- package/src/runtime/plugins/errors.ts +8 -1
- package/src/runtime/plugins/external-adapter.ts +45 -322
- package/src/runtime/plugins/external-provider-policy.ts +48 -0
- package/src/runtime/plugins/external-registration.ts +9 -1
- package/src/runtime/plugins/external-unix-socket-probe.cjs +9 -3
- package/src/runtime/plugins/external-unix-socket.ts +64 -193
- package/src/runtime/plugins/first-party-external-registration.ts +163 -0
- package/src/runtime/plugins/first-party-registry.ts +3 -1
- package/src/runtime/plugins/github-adapter.ts +1 -2
- package/src/runtime/plugins/gmail-adapter.ts +6 -6
- package/src/runtime/plugins/google-calendar-adapter.ts +6 -6
- package/src/runtime/plugins/google-shared.ts +31 -46
- package/src/runtime/plugins/google-tasks-adapter.ts +6 -6
- package/src/runtime/plugins/ios-adapter.ts +1 -2
- package/src/runtime/plugins/local-system-adapter.ts +180 -0
- package/src/runtime/plugins/macos-capability-broker.ts +15 -105
- package/src/runtime/plugins/resend-adapter.ts +15 -24
- package/src/runtime/plugins/store.ts +493 -51
- package/src/runtime/plugins/types.ts +8 -1
- package/src/runtime/plugins/uu-remote-rescue-registration.ts +155 -0
- package/src/runtime/plugins/windows-host-recovery-install.ts +68 -0
- package/src/runtime/plugins/windows-host-recovery-registration.ts +85 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +86 -430
- package/src/runtime/projections/controller-context.ts +12 -6
- package/src/runtime/projections/git-status-sampler.ts +73 -22
- package/src/runtime/projections/materialized-view.ts +14 -8
- package/src/runtime/recovery/capability-action-application.ts +228 -0
- package/src/runtime/recovery/index.ts +3 -0
- package/src/runtime/recovery/lifecycle-inventory.ts +338 -0
- package/src/runtime/recovery/local-jobs-repair.ts +14 -14
- package/src/runtime/recovery/maintenance-executor.ts +358 -76
- package/src/runtime/recovery/quarantine-retention.ts +256 -0
- package/src/runtime/release/identity.ts +6 -0
- package/src/runtime/resources/leases/store.ts +1 -1
- package/src/runtime/root/active-execution-power-assertion.ts +139 -0
- package/src/runtime/root/assistant-learning-loop.ts +45 -0
- package/src/runtime/root/browser-session-composition.ts +59 -0
- package/src/runtime/root/computer-composition.ts +191 -0
- package/src/runtime/root/computer-target-composition.ts +16 -0
- package/src/runtime/root/computer-target-persistence.ts +39 -0
- package/src/runtime/root/controller-round-composition.ts +107 -0
- package/src/runtime/root/controller-services.ts +2 -0
- package/src/runtime/root/entry.ts +4 -3
- package/src/runtime/root/gateway-adapter.ts +3 -3
- package/src/runtime/root/local-bridge.ts +5 -2
- package/src/runtime/root/mcp-transport.ts +5 -5
- package/src/runtime/root/package-connector-service.ts +108 -56
- package/src/runtime/root/package-runtime-release.ts +86 -18
- package/src/runtime/root/package-runtime-service.ts +63 -86
- package/src/runtime/root/release-execution-canary.ts +71 -0
- package/src/runtime/root/release-loader.ts +68 -0
- package/src/runtime/root/release-manifest.ts +106 -2
- package/src/runtime/root/release-materialize.ts +103 -4
- package/src/runtime/root/release-store.ts +31 -2
- package/src/runtime/root/runtime.ts +240 -25
- package/src/runtime/root/scheduled-controller-composition.ts +51 -0
- package/src/runtime/root/scheduler.ts +12 -4
- package/src/runtime/root/service-runner.ts +1 -1
- package/src/runtime/root/service.ts +41 -16
- package/src/runtime/root/status.ts +1 -0
- package/src/runtime/root/types.ts +24 -1
- package/src/runtime/shared/local-bridge-surface.ts +1 -1
- package/src/runtime/shared/process-environment.ts +19 -4
- package/src/runtime/shared/storage-capacity.ts +68 -0
- package/src/runtime/standalone-recovery/controller-home-migration.ts +1116 -0
- package/src/runtime/standalone-recovery/core.ts +984 -230
- package/src/runtime/standalone-recovery/entry.ts +182 -51
- package/src/runtime/standalone-recovery/independent-host-rescue.ts +172 -0
- package/src/runtime/standalone-recovery/installer.ts +212 -104
- package/src/runtime/standalone-recovery/mcp-server.ts +243 -0
- package/src/runtime/standalone-recovery/mutation-identity-contract.ts +21 -0
- package/src/runtime/standalone-recovery/operation-lock.ts +160 -0
- package/src/runtime/standalone-recovery/performance.ts +135 -0
- package/src/runtime/standalone-recovery/release.ts +5 -0
- package/src/runtime/watchdog/workflow-watchdog.ts +1 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -1
- package/src/runtime/workflow/schedules/engine.ts +191 -343
- package/src/runtime/workflow/schedules/pending-activation.ts +1 -1
- package/src/runtime/workflow/schedules/settlement.ts +2 -191
- package/src/runtime/workflow/schedules/store.ts +2 -607
- package/src/runtime/workflow/schedules/types.ts +2 -127
- package/src/runtime/workflow/schedules/work-continuation.ts +180 -15
- package/src/runtime/workflows/first-party/xiaohongshu.ts +122 -0
- package/src/runtime/workflows/runtime.ts +486 -0
- package/src/testing/test-governance.ts +90 -29
- package/assets/templates/helpers/archive-architecture-request.sh +0 -244
- package/docs/architecture/history/README.md +0 -7
- package/docs/architecture/history/chatgpt-handoff-facade.md +0 -397
- package/docs/architecture/history/global-hook-runtime.md +0 -166
- package/docs/architecture/history/ios-semantic-automation-provider-v2.md +0 -170
- package/docs/architecture/requests/archive/2026/20260528-154740-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-154816-assets-hooks-assets-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161851-ai-harness-workflow-contract-json-ai-harness-workflow-contract-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161853-scripts-check-task-workflow-sh-scripts-check-task-workflow-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-161911-assets-workflow-contract-v1-json-assets-workflow-contract-v1-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222416-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222444-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222457-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222648-ai-hooks-ai-hooks-pre-edit-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222705-ai-hooks-ai-hooks-worktree-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222726-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-222727-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-224736-ai-hooks-ai-hooks-hook-input-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233259-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233335-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233456-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233457-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233637-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260528-233638-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-000322-root-package-json.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-004238-ai-hooks-ai-hooks-lib-workflow-state-sh.md +0 -59
- package/docs/architecture/requests/archive/2026/20260529-020654-ai-harness-policy-json-ai-harness-policy-json.md +0 -63
- package/docs/architecture/requests/archive/2026/20260529-094236-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094323-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094331-ai-hooks-ai-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094339-assets-hooks-assets-hooks-prompt-guard-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094430-assets-hooks-assets-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260529-094446-ai-hooks-ai-hooks-session-start-context-sh.md +0 -60
- package/docs/architecture/requests/archive/2026/20260613-002040-runtime-harness-hook-adapters.md +0 -68
- package/docs/architecture/requests/archive/2026/root.md +0 -68
- package/docs/architecture/requests/archive/2026/runtime-harness-hook-adapters.md +0 -76
- package/docs/architecture/requests/archive/2026/verification-evals-checks.md +0 -68
- package/docs/architecture/requests/archive/2026/workflow-engine-contract-assets.md +0 -69
- package/scripts/archive-architecture-request.sh +0 -4
- package/scripts/run-skill-evals.ts +0 -1022
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
1
2
|
import { mkdirSync } from 'fs';
|
|
2
3
|
import { join } from 'path';
|
|
3
4
|
import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
|
|
@@ -5,6 +6,7 @@ import { withControllerLock } from '../../../cli/repositories/locks';
|
|
|
5
6
|
import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../shared/json-files';
|
|
6
7
|
import {
|
|
7
8
|
normalizePlanScopeKey,
|
|
9
|
+
isPlanExtensionPredecessor,
|
|
8
10
|
resolvePlanAdmission,
|
|
9
11
|
withPlanAdmissionLock,
|
|
10
12
|
withPlanAdmissionLockAsync,
|
|
@@ -13,19 +15,34 @@ import {
|
|
|
13
15
|
} from './semantic-admission';
|
|
14
16
|
import {
|
|
15
17
|
listControlPlaneRecords,
|
|
18
|
+
listAllControlPlaneRecordsWithinTransaction,
|
|
16
19
|
readControlPlaneRecordWithinTransaction,
|
|
17
20
|
withControlPlaneTransaction,
|
|
18
21
|
writeControlPlaneRecordWithinTransaction,
|
|
19
22
|
} from '../persistence/sqlite-store';
|
|
20
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';
|
|
21
35
|
import {
|
|
22
36
|
isTerminalPlanContractStatus,
|
|
23
37
|
type EvidenceRef,
|
|
24
38
|
type PlanContract,
|
|
25
39
|
type PlanContractStatus,
|
|
26
40
|
type PlanContractStore,
|
|
41
|
+
type PlanObligationDisposition,
|
|
42
|
+
type PlanRevisionDraft,
|
|
43
|
+
type PlanRevisionRecord,
|
|
27
44
|
type PlanStep,
|
|
28
|
-
type
|
|
45
|
+
type PlanStepDeliveryCarry,
|
|
29
46
|
} from './types';
|
|
30
47
|
|
|
31
48
|
export interface PlanContractStoreLocation {
|
|
@@ -36,6 +53,8 @@ export interface PlanContractStoreLocation {
|
|
|
36
53
|
|
|
37
54
|
export interface PlanContractStoreOptions extends PlanContractStoreLocation {
|
|
38
55
|
now?: () => string;
|
|
56
|
+
/** Prove that one delivered Git revision is contained in a later Plan source. Missing/failed proof is fail-closed. */
|
|
57
|
+
revisionContains?: (ancestorRevision: string, descendantRevision: string) => boolean;
|
|
39
58
|
}
|
|
40
59
|
|
|
41
60
|
export interface CreatePlanContractInput {
|
|
@@ -53,6 +72,7 @@ export interface CreatePlanContractInput {
|
|
|
53
72
|
integrationStrategy?: string;
|
|
54
73
|
steps: Array<Omit<PlanStep, 'status' | 'evidenceRefs'> & Partial<Pick<PlanStep, 'status' | 'evidenceRefs'>>>;
|
|
55
74
|
evidenceRefs?: EvidenceRef[];
|
|
75
|
+
obligationDispositions?: PlanObligationDisposition[];
|
|
56
76
|
}
|
|
57
77
|
|
|
58
78
|
export interface AdmitPlanContractInput extends CreatePlanContractInput {
|
|
@@ -69,6 +89,7 @@ export type AdmitPlanContractResult = PlanAdmissionResolution;
|
|
|
69
89
|
|
|
70
90
|
export interface PlanContractSummary {
|
|
71
91
|
planId: string;
|
|
92
|
+
revision: number;
|
|
72
93
|
repoId: string;
|
|
73
94
|
requirementId?: string;
|
|
74
95
|
scopeKey: string;
|
|
@@ -92,6 +113,29 @@ function normalizeScopeKey(value: string): string {
|
|
|
92
113
|
return normalizePlanScopeKey(value) || 'unknown';
|
|
93
114
|
}
|
|
94
115
|
|
|
116
|
+
export function currentPlanRevision(plan: PlanContract): number {
|
|
117
|
+
const revision = Number(plan.revision);
|
|
118
|
+
return Number.isInteger(revision) && revision > 0 ? revision : 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface PlanRevisionRecordStore {
|
|
122
|
+
schemaVersion: 1;
|
|
123
|
+
updatedAt: string;
|
|
124
|
+
revisions: PlanRevisionRecord[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function planRevisionRecordKey(planId: string, revision: number): string {
|
|
128
|
+
return `${sanitizeFileComponent(planId)}:r${revision}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
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
|
+
|
|
95
139
|
function normalizeStep(step: CreatePlanContractInput['steps'][number]): PlanStep {
|
|
96
140
|
return {
|
|
97
141
|
id: sanitizeFileComponent(step.id).slice(0, 120),
|
|
@@ -111,6 +155,133 @@ function normalizeStep(step: CreatePlanContractInput['steps'][number]): PlanStep
|
|
|
111
155
|
};
|
|
112
156
|
}
|
|
113
157
|
|
|
158
|
+
function normalizeObligationDisposition(value: PlanObligationDisposition): PlanObligationDisposition {
|
|
159
|
+
const disposition = value.disposition;
|
|
160
|
+
if (!['keep', 'change', 'defer', 'drop'].includes(disposition)) throw new Error(`PLAN_OBLIGATION_DISPOSITION_INVALID: ${String(disposition)}`);
|
|
161
|
+
return {
|
|
162
|
+
predecessorPlanId: sanitizeFileComponent(value.predecessorPlanId).slice(0, 160),
|
|
163
|
+
obligationId: String(value.obligationId ?? '').trim().slice(0, 160),
|
|
164
|
+
disposition,
|
|
165
|
+
successorRefs: bounded(value.successorRefs, 20, 240),
|
|
166
|
+
...(value.rationale?.trim() ? { rationale: value.rationale.trim().slice(0, 1_000) } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface PlanObligation {
|
|
171
|
+
obligationId: string;
|
|
172
|
+
predecessorPlanId: string;
|
|
173
|
+
kind: 'step_objective' | 'step_acceptance' | 'non_goal' | 'resolved_decision' | 'stop_condition' | 'replan_condition';
|
|
174
|
+
sourceRef: string;
|
|
175
|
+
summary: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function planObligationId(planId: string, kind: PlanObligation['kind'], sourceRef: string, summary: string): string {
|
|
179
|
+
const digest = createHash('sha256').update(JSON.stringify({ planId, kind, sourceRef, summary })).digest('hex').slice(0, 24);
|
|
180
|
+
return `obl_${digest}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function listPlanObligations(plan: PlanContract, mode: 'unresolved' | 'completed_steps'): PlanObligation[] {
|
|
184
|
+
const out: PlanObligation[] = [];
|
|
185
|
+
const add = (kind: PlanObligation['kind'], sourceRef: string, summary: string) => {
|
|
186
|
+
const normalized = summary.trim();
|
|
187
|
+
if (!normalized) return;
|
|
188
|
+
out.push({ obligationId: planObligationId(plan.planId, kind, sourceRef, normalized), predecessorPlanId: plan.planId, kind, sourceRef, summary: normalized });
|
|
189
|
+
};
|
|
190
|
+
for (const step of plan.steps) {
|
|
191
|
+
if (mode === 'unresolved' && step.status === 'completed') continue;
|
|
192
|
+
if (mode === 'completed_steps' && step.status !== 'completed') continue;
|
|
193
|
+
add('step_objective', `step:${step.id}`, step.objective);
|
|
194
|
+
step.acceptanceCriteria.forEach((criterion, index) => add('step_acceptance', `step:${step.id}:acceptance:${index}`, criterion));
|
|
195
|
+
}
|
|
196
|
+
if (mode === 'unresolved') {
|
|
197
|
+
plan.nonGoals.forEach((value, index) => add('non_goal', `non_goal:${index}`, value));
|
|
198
|
+
plan.resolvedDecisions.forEach((value, index) => add('resolved_decision', `resolved_decision:${index}`, value));
|
|
199
|
+
plan.stopConditions.forEach((value, index) => add('stop_condition', `stop_condition:${index}`, value));
|
|
200
|
+
plan.replanConditions.forEach((value, index) => add('replan_condition', `replan_condition:${index}`, value));
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function listUnresolvedPlanObligations(plan: PlanContract): PlanObligation[] {
|
|
206
|
+
return listPlanObligations(plan, 'unresolved');
|
|
207
|
+
}
|
|
208
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
114
285
|
function updatePlanContract(
|
|
115
286
|
options: PlanContractStoreOptions,
|
|
116
287
|
planId: string,
|
|
@@ -139,8 +310,10 @@ function updatePlanContract(
|
|
|
139
310
|
function assertRequirementReference(options: PlanContractStoreOptions, requirementId: string | undefined): void {
|
|
140
311
|
const normalized = requirementId?.trim();
|
|
141
312
|
if (!normalized || !options.controllerHome) return;
|
|
142
|
-
|
|
143
|
-
|
|
313
|
+
const requirement = readRequirement({ controllerHome: options.controllerHome, now: options.now }, normalized)?.value;
|
|
314
|
+
if (!requirement) throw new Error(`PLAN_REQUIREMENT_NOT_FOUND: ${normalized}`);
|
|
315
|
+
if (requirement.state === 'done' || requirement.state === 'cancelled') {
|
|
316
|
+
throw new Error(`PLAN_REQUIREMENT_TERMINAL: ${normalized}:${requirement.state}`);
|
|
144
317
|
}
|
|
145
318
|
}
|
|
146
319
|
|
|
@@ -161,6 +334,116 @@ export function planContractStorePath(location: PlanContractStoreLocation): stri
|
|
|
161
334
|
return join(planContractRoot(location), 'index.json');
|
|
162
335
|
}
|
|
163
336
|
|
|
337
|
+
export function planRevisionStorePath(location: PlanContractStoreLocation): string {
|
|
338
|
+
return join(planContractRoot(location), 'revisions.json');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function listPlanRevisionRecords(
|
|
342
|
+
options: PlanContractStoreOptions,
|
|
343
|
+
planId?: string,
|
|
344
|
+
): PlanRevisionRecord[] {
|
|
345
|
+
const normalizedPlanId = planId ? sanitizeFileComponent(planId) : undefined;
|
|
346
|
+
if (!sqliteBacked(options)) {
|
|
347
|
+
const store = readJsonFile<PlanRevisionRecordStore>(planRevisionStorePath(options), { schemaVersion: 1, updatedAt: nowIso(options), revisions: [] });
|
|
348
|
+
return store.revisions
|
|
349
|
+
.filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
|
|
350
|
+
.sort((left, right) => right.revision - left.revision);
|
|
351
|
+
}
|
|
352
|
+
return listControlPlaneRecords<PlanRevisionRecord>(options.controllerHome, {
|
|
353
|
+
namespace: 'plan_revision', scope: options.repoId, limit: 5_000,
|
|
354
|
+
}).map((record) => record.value)
|
|
355
|
+
.filter((record) => !normalizedPlanId || record.planId === normalizedPlanId)
|
|
356
|
+
.sort((left, right) => right.revision - left.revision);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function appendJsonPlanRevisionRecord(options: PlanContractStoreOptions, record: PlanRevisionRecord): void {
|
|
360
|
+
const path = planRevisionStorePath(options);
|
|
361
|
+
const store = readJsonFile<PlanRevisionRecordStore>(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [] });
|
|
362
|
+
if (store.revisions.some((existing) => existing.planId === record.planId && existing.revision === record.revision)) {
|
|
363
|
+
throw new Error(`PLAN_REVISION_ALREADY_EXISTS: ${record.planId}:r${record.revision}`);
|
|
364
|
+
}
|
|
365
|
+
writeJsonAtomic(path, { schemaVersion: 1, updatedAt: record.recordedAt, revisions: [record, ...store.revisions] });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function planRevisionRecord(plan: PlanContract, input: { recordedAt: string; reason: string; requestedRevisionLabel?: string }): PlanRevisionRecord {
|
|
369
|
+
return {
|
|
370
|
+
schemaVersion: 1, repoId: plan.repoId, planId: plan.planId, revision: currentPlanRevision(plan),
|
|
371
|
+
requirementId: plan.requirementId, scopeKey: plan.scopeKey, sourceRevision: plan.sourceRevision, goal: plan.goal,
|
|
372
|
+
nonGoals: [...plan.nonGoals], assumptions: [...plan.assumptions], resolvedDecisions: [...plan.resolvedDecisions],
|
|
373
|
+
stopConditions: [...plan.stopConditions], replanConditions: [...plan.replanConditions], integrationStrategy: plan.integrationStrategy,
|
|
374
|
+
status: plan.status, steps: structuredClone(plan.steps), evidenceRefs: structuredClone(plan.evidenceRefs),
|
|
375
|
+
obligationDispositions: plan.obligationDispositions ? structuredClone(plan.obligationDispositions) : undefined,
|
|
376
|
+
recordedAt: input.recordedAt, reason: input.reason.slice(0, 1_000), requestedRevisionLabel: input.requestedRevisionLabel,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function revisionDraftFromCandidate(candidate: PlanContract, input: { requestedRevisionLabel?: string; createdAt: string; updatedAt: string }): PlanRevisionDraft {
|
|
381
|
+
return {
|
|
382
|
+
revision: currentPlanRevision(candidate), requestedRevisionLabel: input.requestedRevisionLabel,
|
|
383
|
+
sourceRevision: candidate.sourceRevision, goal: candidate.goal, nonGoals: [...candidate.nonGoals], assumptions: [...candidate.assumptions],
|
|
384
|
+
resolvedDecisions: [...candidate.resolvedDecisions], stopConditions: [...candidate.stopConditions], replanConditions: [...candidate.replanConditions],
|
|
385
|
+
integrationStrategy: candidate.integrationStrategy, steps: structuredClone(candidate.steps),
|
|
386
|
+
obligationDispositions: candidate.obligationDispositions ? structuredClone(candidate.obligationDispositions) : undefined,
|
|
387
|
+
deliveryCarries: candidate.deliveryCarries ? structuredClone(candidate.deliveryCarries) : undefined,
|
|
388
|
+
createdAt: input.createdAt, updatedAt: input.updatedAt,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
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
|
+
|
|
417
|
+
function assertRevisionScopeAuthority(current: PlanContract, allPlans: readonly PlanContract[]): void {
|
|
418
|
+
const conflicting = allPlans.find((plan) => plan.planId !== current.planId
|
|
419
|
+
&& !isTerminalPlanContractStatus(plan.status) && plan.scopeKey === current.scopeKey);
|
|
420
|
+
if (conflicting) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${current.scopeKey}:${conflicting.planId}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
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
|
+
|
|
164
447
|
export function emptyPlanContractStore(updatedAt: string): PlanContractStore {
|
|
165
448
|
return { schemaVersion: 1, updatedAt, contracts: [] };
|
|
166
449
|
}
|
|
@@ -220,6 +503,9 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
|
|
|
220
503
|
options.repoId,
|
|
221
504
|
contract.planId,
|
|
222
505
|
);
|
|
506
|
+
// SQLite is authoritative per Plan row. A sibling Plan appearing in an
|
|
507
|
+
// aggregate compatibility snapshot is not itself a mutation.
|
|
508
|
+
if (current && JSON.stringify(current.value) === JSON.stringify(contract)) continue;
|
|
223
509
|
writeControlPlaneRecordWithinTransaction(database, {
|
|
224
510
|
namespace: 'plan_contract',
|
|
225
511
|
scope: options.repoId,
|
|
@@ -234,12 +520,81 @@ function writePlanContractStore(options: PlanContractStoreOptions, store: PlanCo
|
|
|
234
520
|
return store;
|
|
235
521
|
}
|
|
236
522
|
|
|
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
|
+
|
|
237
591
|
function buildPlanContract(input: CreatePlanContractInput, at: string): PlanContract {
|
|
238
592
|
const planId = sanitizeFileComponent(input.planId);
|
|
239
593
|
if (!String(input.planId ?? '').trim() || planId === 'unknown') throw new Error('plan_id is required');
|
|
240
594
|
return {
|
|
241
595
|
schemaVersion: 1,
|
|
242
596
|
planId,
|
|
597
|
+
revision: 1,
|
|
243
598
|
repoId: input.repoId,
|
|
244
599
|
requirementId: input.requirementId?.trim().slice(0, 160) || undefined,
|
|
245
600
|
scopeKey: normalizeScopeKey(input.scopeKey),
|
|
@@ -254,6 +609,7 @@ function buildPlanContract(input: CreatePlanContractInput, at: string): PlanCont
|
|
|
254
609
|
status: 'draft',
|
|
255
610
|
steps: input.steps.slice(0, 30).map(normalizeStep),
|
|
256
611
|
evidenceRefs: (input.evidenceRefs ?? []).slice(0, 20),
|
|
612
|
+
obligationDispositions: (input.obligationDispositions ?? []).slice(0, 512).map(normalizeObligationDisposition),
|
|
257
613
|
createdAt: at,
|
|
258
614
|
updatedAt: at,
|
|
259
615
|
};
|
|
@@ -285,6 +641,176 @@ function draftContentErrors(plan: PlanContract): string[] {
|
|
|
285
641
|
return [...new Set(errors)];
|
|
286
642
|
}
|
|
287
643
|
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
776
|
+
function materializePlanStepDeliveryCarries(
|
|
777
|
+
options: PlanContractStoreOptions,
|
|
778
|
+
plan: PlanContract,
|
|
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
|
+
}
|
|
813
|
+
|
|
288
814
|
function createPlanContractUnlocked(options: PlanContractStoreOptions, input: CreatePlanContractInput): PlanContract {
|
|
289
815
|
assertRequirementReference(options, input.requirementId);
|
|
290
816
|
const at = nowIso(options);
|
|
@@ -308,22 +834,54 @@ function replacePlanContractUnlocked(
|
|
|
308
834
|
const predecessorKey = sanitizeFileComponent(predecessorId);
|
|
309
835
|
const predecessorIndex = store.contracts.findIndex((contract) => contract.planId === predecessorKey);
|
|
310
836
|
if (predecessorIndex < 0) throw new Error(`plan contract not found: ${predecessorKey}`);
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
837
|
+
const current = store.contracts[predecessorIndex]!;
|
|
838
|
+
if (!isPlanExtensionPredecessor(current)) throw new Error(`plan contract ${current.planId} is terminal (${current.status})`);
|
|
839
|
+
|
|
840
|
+
// Before approval Plan identity is already provisional, so an extension simply
|
|
841
|
+
// repairs that exact draft instead of minting an r2/r3 entity.
|
|
842
|
+
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
|
+
return repairDraftPlanContractUnlocked(options, current.planId, {
|
|
855
|
+
scopeKey: input.scopeKey, sourceRevision: input.sourceRevision, goal: input.goal, nonGoals: input.nonGoals, assumptions: input.assumptions,
|
|
856
|
+
resolvedDecisions: input.resolvedDecisions, stopConditions: input.stopConditions, replanConditions: input.replanConditions,
|
|
857
|
+
integrationStrategy: input.integrationStrategy, steps: input.steps, obligationDispositions: input.obligationDispositions,
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
if (current.pendingRevision) throw new Error(`PLAN_REVISION_DRAFT_ALREADY_EXISTS: ${current.planId}:r${current.pendingRevision.revision}`);
|
|
861
|
+
if (input.requirementId?.trim() !== current.requirementId?.trim()) throw new Error('PLAN_REVISION_REQUIREMENT_IDENTITY_MISMATCH');
|
|
862
|
+
if (normalizeScopeKey(input.scopeKey) !== current.scopeKey) throw new Error(`PLAN_REVISION_SCOPE_IDENTITY_MISMATCH: ${current.scopeKey}`);
|
|
863
|
+
|
|
864
|
+
const candidate = {
|
|
865
|
+
...buildPlanContract({ ...input, planId: current.planId, repoId: current.repoId, requirementId: current.requirementId, scopeKey: current.scopeKey }, at),
|
|
866
|
+
revision: currentPlanRevision(current) + 1,
|
|
867
|
+
supersedes: current.supersedes ? [...current.supersedes] : undefined,
|
|
868
|
+
};
|
|
869
|
+
const contentErrors = draftContentErrors(candidate);
|
|
870
|
+
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
|
+
assertRevisionScopeAuthority(current, store.contracts);
|
|
874
|
+
const deliveryCarries = deriveRequiredRevisionDeliveryCarries(options, current, candidate, at);
|
|
875
|
+
const stagedCandidate = deliveryCarries.length > 0 ? { ...candidate, deliveryCarries } : candidate;
|
|
876
|
+
const requestedRevisionLabel = sanitizeFileComponent(input.planId);
|
|
877
|
+
const pendingRevision = revisionDraftFromCandidate(stagedCandidate, {
|
|
878
|
+
requestedRevisionLabel: requestedRevisionLabel !== current.planId ? requestedRevisionLabel : undefined, createdAt: at, updatedAt: at,
|
|
879
|
+
});
|
|
880
|
+
const next: PlanContract = { ...current, status: 'replanning', pendingRevision, updatedAt: at };
|
|
323
881
|
const contracts = [...store.contracts];
|
|
324
|
-
contracts[predecessorIndex] =
|
|
325
|
-
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts
|
|
326
|
-
return
|
|
882
|
+
contracts[predecessorIndex] = next;
|
|
883
|
+
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
|
|
884
|
+
return next;
|
|
327
885
|
}
|
|
328
886
|
|
|
329
887
|
function repairDraftPlanContractUnlocked(
|
|
@@ -336,9 +894,11 @@ function repairDraftPlanContractUnlocked(
|
|
|
336
894
|
const index = store.contracts.findIndex((contract) => contract.planId === key);
|
|
337
895
|
if (index < 0) throw new Error(`plan contract not found: ${key}`);
|
|
338
896
|
const current = store.contracts[index]!;
|
|
339
|
-
|
|
340
|
-
if (
|
|
341
|
-
|
|
897
|
+
const pending = current.status === 'replanning' ? current.pendingRevision : undefined;
|
|
898
|
+
if (current.status !== 'draft' && !pending) throw new Error(`PLAN_DRAFT_REPAIR_STATUS_INVALID: ${current.planId}:${current.status}`);
|
|
899
|
+
const observedSourceRevision = pending?.sourceRevision ?? current.sourceRevision;
|
|
900
|
+
if (input.expectedSourceRevision !== undefined && observedSourceRevision !== input.expectedSourceRevision) {
|
|
901
|
+
throw new Error(`PLAN_DRAFT_REPAIR_STALE_SOURCE: ${current.planId}:expected=${input.expectedSourceRevision}:actual=${observedSourceRevision}`);
|
|
342
902
|
}
|
|
343
903
|
assertRequirementReference(options, current.requirementId);
|
|
344
904
|
const at = nowIso(options);
|
|
@@ -349,16 +909,51 @@ function repairDraftPlanContractUnlocked(
|
|
|
349
909
|
requirementId: current.requirementId,
|
|
350
910
|
evidenceRefs: current.evidenceRefs,
|
|
351
911
|
}, at);
|
|
352
|
-
const
|
|
912
|
+
const candidateWithLineage = retireResolvedPredecessorObligationDispositions({
|
|
913
|
+
...candidate,
|
|
914
|
+
...(current.supersedes?.length ? { supersedes: [...current.supersedes] } : {}),
|
|
915
|
+
}, store.contracts);
|
|
916
|
+
const contentErrors = draftContentErrors(candidateWithLineage);
|
|
353
917
|
if (contentErrors.length > 0) {
|
|
354
918
|
throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${contentErrors.join('; ')}`);
|
|
355
919
|
}
|
|
920
|
+
const continuityErrors = obligationContinuityErrors(candidateWithLineage, store.contracts);
|
|
921
|
+
if (continuityErrors.length > 0) {
|
|
922
|
+
throw new Error(`PLAN_DRAFT_REPAIR_INVALID: ${continuityErrors.join('; ')}`);
|
|
923
|
+
}
|
|
924
|
+
if (pending) {
|
|
925
|
+
if (normalizeScopeKey(input.scopeKey) !== current.scopeKey) throw new Error(`PLAN_REVISION_SCOPE_IDENTITY_MISMATCH: ${current.scopeKey}`);
|
|
926
|
+
assertRevisionScopeAuthority(current, store.contracts);
|
|
927
|
+
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
|
+
const repairedPending = revisionDraftFromCandidate(revisionCandidate, {
|
|
934
|
+
requestedRevisionLabel: pending.requestedRevisionLabel, createdAt: pending.createdAt, updatedAt: at,
|
|
935
|
+
});
|
|
936
|
+
const repairedCurrent: PlanContract = { ...current, pendingRevision: repairedPending, updatedAt: at };
|
|
937
|
+
const contracts = [...store.contracts];
|
|
938
|
+
contracts[index] = repairedCurrent;
|
|
939
|
+
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
|
|
940
|
+
return repairedCurrent;
|
|
941
|
+
}
|
|
942
|
+
const explicitPredecessors = new Set(candidateWithLineage.supersedes ?? []);
|
|
356
943
|
const conflictingScope = store.contracts.find((existing, candidateIndex) => candidateIndex !== index
|
|
357
944
|
&& !isTerminalPlanContractStatus(existing.status)
|
|
358
|
-
&& existing.scopeKey ===
|
|
359
|
-
|
|
945
|
+
&& existing.scopeKey === candidateWithLineage.scopeKey
|
|
946
|
+
&& !explicitPredecessors.has(existing.planId));
|
|
947
|
+
if (conflictingScope) throw new Error(`PLAN_SCOPE_ALREADY_OWNED: ${candidateWithLineage.scopeKey}:${conflictingScope.planId}`);
|
|
948
|
+
const predecessor = current.supersedes?.length === 1
|
|
949
|
+
? store.contracts.find((existing) => existing.planId === current.supersedes![0])
|
|
950
|
+
: undefined;
|
|
951
|
+
const deliveryCarries = predecessor
|
|
952
|
+
? derivePlanStepDeliveryCarries(options, predecessor, candidateWithLineage, at)
|
|
953
|
+
: [];
|
|
360
954
|
const repaired: PlanContract = {
|
|
361
|
-
...
|
|
955
|
+
...candidateWithLineage,
|
|
956
|
+
...(deliveryCarries.length ? { deliveryCarries } : {}),
|
|
362
957
|
createdAt: current.createdAt,
|
|
363
958
|
updatedAt: at,
|
|
364
959
|
};
|
|
@@ -403,16 +998,16 @@ export function createPlanContract(options: PlanContractStoreOptions, input: Cre
|
|
|
403
998
|
|
|
404
999
|
function admitPlanContractUnlocked(options: PlanContractStoreOptions, input: AdmitPlanContractInput): AdmitPlanContractResult {
|
|
405
1000
|
assertRequirementReference(options, input.requirementId);
|
|
406
|
-
const
|
|
407
|
-
const resolution = resolvePlanAdmission(
|
|
1001
|
+
const plans = listPlanContracts({ ...options, status: 'all', limit: 100 });
|
|
1002
|
+
const resolution = resolvePlanAdmission(plans, {
|
|
408
1003
|
requirementId: input.requirementId,
|
|
409
1004
|
scopeKey: input.scopeKey,
|
|
410
1005
|
planRelation: input.planRelation,
|
|
411
1006
|
relatedPlanId: input.relatedPlanId,
|
|
412
1007
|
});
|
|
413
1008
|
if (resolution.admissionDecision === 'extend_existing' && resolution.plan) {
|
|
414
|
-
const
|
|
415
|
-
return { ...resolution, admissionDecision: '
|
|
1009
|
+
const revised = replacePlanContractUnlocked(options, resolution.plan.planId, input);
|
|
1010
|
+
return { ...resolution, admissionDecision: 'reuse_existing', plan: revised };
|
|
416
1011
|
}
|
|
417
1012
|
if (resolution.admissionDecision !== 'create_new') return resolution;
|
|
418
1013
|
return { ...resolution, plan: createPlanContractUnlocked(options, input) };
|
|
@@ -430,11 +1025,15 @@ export function getPlanContract(options: PlanContractStoreOptions, planId: strin
|
|
|
430
1025
|
return readPlanContractStore(options).contracts.find((contract) => contract.planId === sanitizeFileComponent(planId));
|
|
431
1026
|
}
|
|
432
1027
|
|
|
1028
|
+
export function isCurrentPlanContract(contract: PlanContract): boolean {
|
|
1029
|
+
return !isTerminalPlanContractStatus(contract.status) && !contract.supersededBy?.trim();
|
|
1030
|
+
}
|
|
1031
|
+
|
|
433
1032
|
export function listPlanContracts(options: PlanContractStoreOptions & { status?: PlanContractStatus | 'active' | 'all'; limit?: number }): PlanContract[] {
|
|
434
1033
|
const status = options.status ?? 'active';
|
|
435
1034
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 50), 100));
|
|
436
1035
|
return readPlanContractStore(options).contracts
|
|
437
|
-
.filter((contract) => status === 'all' || (status === 'active' ?
|
|
1036
|
+
.filter((contract) => status === 'all' || (status === 'active' ? isCurrentPlanContract(contract) : contract.status === status))
|
|
438
1037
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
439
1038
|
.slice(0, limit);
|
|
440
1039
|
}
|
|
@@ -442,6 +1041,7 @@ export function listPlanContracts(options: PlanContractStoreOptions & { status?:
|
|
|
442
1041
|
export function summarizePlanContract(plan: PlanContract): PlanContractSummary {
|
|
443
1042
|
return {
|
|
444
1043
|
planId: plan.planId,
|
|
1044
|
+
revision: currentPlanRevision(plan),
|
|
445
1045
|
repoId: plan.repoId,
|
|
446
1046
|
requirementId: plan.requirementId,
|
|
447
1047
|
scopeKey: plan.scopeKey,
|
|
@@ -463,23 +1063,144 @@ function approvalErrors(plan: PlanContract, allPlans: readonly PlanContract[]):
|
|
|
463
1063
|
'verifying',
|
|
464
1064
|
'ready_to_finalize',
|
|
465
1065
|
].includes(status);
|
|
466
|
-
|
|
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'))) {
|
|
467
1070
|
errors.push(`active plan already owns scope_key ${plan.scopeKey}`);
|
|
468
1071
|
}
|
|
1072
|
+
errors.push(...obligationContinuityErrors(plan, allPlans));
|
|
469
1073
|
return [...new Set(errors)];
|
|
470
1074
|
}
|
|
471
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
|
+
|
|
472
1167
|
function approvePlanContractUnlocked(options: PlanContractStoreOptions, planId: string): PlanContract {
|
|
473
1168
|
const store = readPlanContractStore(options);
|
|
474
1169
|
const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
|
|
475
1170
|
if (index < 0) throw new Error(`plan contract not found: ${sanitizeFileComponent(planId)}`);
|
|
476
1171
|
const current = store.contracts[index];
|
|
477
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
|
+
}
|
|
478
1185
|
if (current.status !== 'draft' && current.status !== 'reviewing') throw new Error(`plan contract ${current.planId} cannot be approved from ${current.status}`);
|
|
479
1186
|
const errors = approvalErrors(current, store.contracts);
|
|
480
1187
|
if (errors.length > 0) throw new Error(`plan contract ${current.planId} cannot be approved: ${errors.join('; ')}`);
|
|
481
1188
|
const at = nowIso(options);
|
|
482
|
-
const
|
|
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
|
+
}
|
|
483
1204
|
const contracts = [...store.contracts];
|
|
484
1205
|
contracts[index] = next;
|
|
485
1206
|
writePlanContractStore(options, { schemaVersion: 1, updatedAt: at, contracts });
|
|
@@ -494,7 +1215,7 @@ export async function approvePlanContractAsync(options: PlanContractStoreOptions
|
|
|
494
1215
|
return await withPlanAdmissionLockAsync(options, () => approvePlanContractUnlocked(options, planId));
|
|
495
1216
|
}
|
|
496
1217
|
|
|
497
|
-
export function supersedePlanContract(options: PlanContractStoreOptions, planId: string, supersededBy: string): PlanContract {
|
|
1218
|
+
export function supersedePlanContract(options: PlanContractStoreOptions, planId: string, supersededBy: string, reason = 'explicit_supersession'): PlanContract {
|
|
498
1219
|
return withPlanAdmissionLock(options, () => {
|
|
499
1220
|
const store = readPlanContractStore(options);
|
|
500
1221
|
const index = store.contracts.findIndex((contract) => contract.planId === sanitizeFileComponent(planId));
|
|
@@ -506,15 +1227,202 @@ export function supersedePlanContract(options: PlanContractStoreOptions, planId:
|
|
|
506
1227
|
if (replacement === current.planId) throw new Error('PLAN_SUCCESSOR_ID_MUST_CHANGE');
|
|
507
1228
|
const successor = store.contracts.find((contract) => contract.planId === replacement);
|
|
508
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('; ')}`);
|
|
509
1233
|
const at = nowIso(options);
|
|
510
|
-
const
|
|
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
|
+
};
|
|
511
1246
|
const contracts = [...store.contracts];
|
|
512
1247
|
contracts[index] = next;
|
|
513
|
-
|
|
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);
|
|
514
1251
|
return next;
|
|
515
1252
|
});
|
|
516
1253
|
}
|
|
517
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
|
+
|
|
518
1426
|
/**
|
|
519
1427
|
* Atomically reserve one eligible step for one WorkContract. This is the
|
|
520
1428
|
* Plan-to-Work concurrency boundary: competing callers cannot bind the same
|
|
@@ -585,6 +1493,63 @@ export function repairDanglingPlanStepWorkBinding(
|
|
|
585
1493
|
});
|
|
586
1494
|
}
|
|
587
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
|
+
|
|
588
1553
|
export function completePlanStepForWork(
|
|
589
1554
|
options: PlanContractStoreOptions,
|
|
590
1555
|
input: {
|
|
@@ -620,7 +1585,7 @@ export function completePlanStepForWork(
|
|
|
620
1585
|
workId: delivered ? step.workId : undefined,
|
|
621
1586
|
evidenceRefs: input.work.evidenceRefs.length > 0 ? input.work.evidenceRefs.slice(0, 20) : step.evidenceRefs,
|
|
622
1587
|
};
|
|
623
|
-
return { ...current, status: delivered ? 'verifying' : 'replanning', steps, updatedAt: at };
|
|
1588
|
+
return { ...current, status: current.pendingRevision ? 'replanning' : delivered ? 'verifying' : 'replanning', steps, updatedAt: at };
|
|
624
1589
|
});
|
|
625
1590
|
}
|
|
626
1591
|
|
|
@@ -644,12 +1609,13 @@ export function acceptPlanStepEvidence(
|
|
|
644
1609
|
evidenceRefs: [{ title: 'semantic acceptance', summary: `${reviewer}: ${rationale}`, detailLevel: 'summary' as const }, ...step.evidenceRefs].slice(0, 20),
|
|
645
1610
|
};
|
|
646
1611
|
const allCompleted = steps.every((candidate) => candidate.status === 'completed');
|
|
1612
|
+
const projectedSteps = allCompleted ? steps : projectDependencyReadySteps(steps);
|
|
647
1613
|
const acceptedSourceRevision = input.acceptedSourceRevision?.trim();
|
|
648
1614
|
return {
|
|
649
1615
|
...current,
|
|
650
1616
|
...(acceptedSourceRevision ? { sourceRevision: acceptedSourceRevision } : {}),
|
|
651
|
-
status: allCompleted ? 'finalized' : 'executing',
|
|
652
|
-
steps,
|
|
1617
|
+
status: current.pendingRevision ? 'replanning' : allCompleted ? 'finalized' : 'executing',
|
|
1618
|
+
steps: projectedSteps,
|
|
653
1619
|
updatedAt: nowIso(options),
|
|
654
1620
|
};
|
|
655
1621
|
});
|