@moretea-labs/forge 1.8.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +70 -7
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +17 -2
- package/CONTRIBUTING.md +3 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/adapters/chatgpt/browser-delivery-host.ts +7 -1
- package/adapters/chatgpt/browser-delivery-runtime.ts +529 -56
- package/adapters/chatgpt/controller-binding-store.ts +101 -22
- package/adapters/chatgpt/controller-round-host.ts +15 -25
- package/adapters/chatgpt/controller-round-settlement-store.ts +58 -4
- package/adapters/chatgpt/provider-delivery.ts +228 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +79 -53
- package/adapters/computer/chatgpt-extension-target.ts +463 -0
- package/adapters/computer/chatgpt-target-router.ts +103 -0
- package/adapters/computer/chrome-extension/background.js +385 -0
- package/adapters/computer/chrome-extension/content.js +378 -0
- package/adapters/computer/chrome-extension/core.js +24 -0
- package/adapters/computer/chrome-extension/manifest.json +30 -0
- package/adapters/computer/desktop-operator-discovery.ts +11 -31
- package/adapters/computer/interaction-target-authority.ts +142 -18
- package/adapters/mcp/auth.ts +0 -1
- package/adapters/mcp/instructions.ts +5 -4
- package/adapters/mcp/multi-repository.ts +3 -19
- package/adapters/mcp/oauth.ts +73 -0
- package/adapters/mcp/paths.ts +4 -1
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +87 -0
- package/adapters/mcp/runtime-gateway/bounded-result-runtime.ts +62 -0
- package/adapters/mcp/runtime-gateway/cognition-settlement.ts +201 -0
- package/adapters/mcp/runtime-gateway/context-adapter.ts +411 -73
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +91 -0
- package/adapters/mcp/runtime-gateway/controller-authority-adapter.ts +20 -5
- package/adapters/mcp/runtime-gateway/core-capability-adapter.ts +258 -0
- package/adapters/mcp/runtime-gateway/engineering-preconditions.ts +10 -27
- package/adapters/mcp/runtime-gateway/engineering-tool-contract.ts +15 -0
- package/adapters/mcp/runtime-gateway/execution-tools.ts +8 -42
- package/adapters/mcp/runtime-gateway/gateway-execution-adapter.ts +2 -1
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +117 -45
- package/adapters/mcp/runtime-gateway/process-tools.ts +190 -22
- package/adapters/mcp/runtime-gateway/protected-computer-adapter.ts +16 -4
- package/adapters/mcp/runtime-gateway/recovery-adapter.ts +69 -52
- package/adapters/mcp/runtime-gateway/repository-compat-adapter.ts +4 -35
- package/adapters/mcp/runtime-gateway/result-adapter.ts +10 -4
- package/adapters/mcp/runtime-gateway/router.ts +18 -504
- package/adapters/mcp/runtime-gateway/routing-policy.ts +499 -0
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +14 -65
- package/adapters/mcp/runtime-gateway/runtime-readiness-observation.ts +218 -0
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +113 -173
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +15 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +1 -0
- package/adapters/mcp/runtime-gateway/shared-adapter.ts +20 -0
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +358 -266
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +136 -0
- package/adapters/mcp/runtime-gateway/work-adapter.ts +261 -3122
- package/adapters/mcp/runtime-gateway/work-controller-operations.ts +257 -0
- package/adapters/mcp/runtime-gateway/work-detail-projection.ts +60 -0
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +246 -0
- package/adapters/mcp/runtime-gateway/work-learning-operations.ts +145 -0
- package/adapters/mcp/runtime-gateway/work-list-projection.ts +38 -0
- package/adapters/mcp/runtime-gateway/work-plan-operations.ts +382 -0
- package/adapters/mcp/runtime-gateway/work-repair-adapter.ts +308 -0
- package/adapters/mcp/runtime-gateway/work-requirement-operations.ts +148 -0
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +263 -0
- package/adapters/mcp/runtime-gateway/work-workflow-operations.ts +113 -0
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +156 -0
- package/adapters/mcp/server.ts +67 -23
- package/adapters/mcp/setup.ts +1 -3
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +44 -170
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +4 -9
- package/adapters/mcp/tool-mapping/repository-tools.ts +133 -142
- package/adapters/mcp/toolset-names.ts +5 -0
- package/adapters/mcp/toolset.ts +6 -5
- package/adapters/mcp/transports/http-observation.ts +365 -0
- package/adapters/mcp/transports/http.ts +154 -677
- package/adapters/mcp/transports/oauth-http.ts +322 -0
- package/adapters/mcp/transports/session-registry.ts +6 -0
- package/adapters/mcp/tunnels/openai-secure-tunnel.ts +89 -5
- package/assets/plugin-registry.v1.json +6 -4
- package/assets/reference-configs/agentic-development-flow.md +25 -0
- package/assets/reference-configs/global-working-rules.md +19 -0
- package/assets/skill-version.json +2 -2
- package/docs/architecture/CURRENT.md +111 -41
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +14 -5
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +6 -3
- package/docs/architecture/decisions/20260907-assistant-learning-loop.md +2 -0
- package/docs/architecture/decisions/20260907-controller-round-transition-policy.md +19 -4
- package/docs/architecture/decisions/20260916-workflow-supervisor-authority.md +249 -0
- package/docs/architecture/decisions/20260917-cognitive-memory-foundation.md +82 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +221 -0
- package/docs/architecture/decisions/20260919-autonomous-work-liveness.md +62 -0
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +72 -0
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +237 -0
- package/docs/architecture/decisions/20261005-forge-v3-desktop-client-boundary.md +77 -0
- package/docs/architecture/domains/verification.md +1 -0
- package/docs/architecture/domains/workflow-engine.md +6 -0
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/forge-plugin-management.md +2 -2
- package/docs/operations/controller-performance-and-502.md +17 -0
- package/docs/operations/controller-reliability-runbook.md +2 -2
- package/docs/operations/features.md +1 -1
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +2 -0
- package/docs/operations/releasing.md +2 -0
- package/docs/operations/standalone-disaster-recovery.md +14 -6
- package/docs/wiki/Implementation.md +1 -1
- package/package.json +25 -14
- package/packages/kernel/cognition/api/index.ts +6 -0
- package/packages/kernel/cognition/application/activation-service.ts +451 -0
- package/packages/kernel/cognition/application/consolidation-service.ts +185 -0
- package/packages/kernel/cognition/application/legacy-experience-adapter.ts +38 -0
- package/packages/kernel/cognition/application/memory-service.ts +64 -0
- package/packages/kernel/cognition/domain/learning.ts +106 -0
- package/packages/kernel/cognition/domain/memory.ts +254 -0
- package/packages/kernel/controller/api/index.ts +1 -0
- package/packages/kernel/controller/application/continuation-service.ts +281 -0
- package/packages/kernel/controller/application/controller-round-service.ts +12 -0
- package/packages/kernel/controller/domain/controller-round-transition-policy.ts +242 -21
- package/packages/kernel/controller/domain/controller-round.ts +4 -1
- package/packages/kernel/controller/domain/execution-quality.ts +3 -1
- package/packages/kernel/controller/infrastructure/controller-round-store.ts +689 -103
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +297 -71
- package/packages/kernel/controller/ports/controller-host.ts +2 -0
- package/packages/kernel/identity/api/index.ts +10 -0
- package/packages/kernel/identity/domain/activity-projection.ts +43 -0
- package/packages/kernel/identity/domain/capability-broker.ts +60 -0
- package/packages/kernel/identity/domain/grant.ts +72 -0
- package/packages/kernel/identity/domain/owned-resource.ts +116 -0
- package/packages/kernel/identity/domain/user-request.ts +93 -0
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +77 -0
- package/packages/kernel/identity/infrastructure/capability-broker.ts +81 -0
- package/packages/kernel/identity/infrastructure/grant-store.ts +284 -0
- package/packages/kernel/identity/infrastructure/owned-resource-store.ts +259 -0
- package/packages/kernel/identity/infrastructure/user-request-store.ts +182 -0
- package/packages/kernel/scheduler/api/index.ts +0 -2
- package/packages/kernel/scheduler/application/schedule-service.ts +3 -0
- package/packages/kernel/scheduler/application/settlement.ts +0 -15
- package/packages/kernel/scheduler/domain/schedule.ts +87 -0
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +153 -133
- package/packages/kernel/work/api/index.ts +3 -2
- package/packages/kernel/work/application/work-service.ts +17 -42
- package/packages/kernel/work/domain/current-task-lineage.ts +104 -0
- package/packages/kernel/work/domain/engineering-design.ts +10 -5
- package/packages/kernel/work/domain/engineering-profile.ts +8 -12
- package/packages/kernel/work/domain/evaluation-promotion-receipt.ts +186 -0
- package/packages/kernel/work/domain/execution-concurrency.ts +19 -26
- package/packages/kernel/work/domain/execution-snapshot.ts +0 -31
- package/packages/kernel/work/domain/implementation-review.ts +14 -331
- package/packages/kernel/work/domain/objective-graph.ts +119 -0
- package/packages/kernel/work/domain/project-engineering-contract.ts +23 -1
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +25 -63
- package/packages/kernel/work/domain/types.ts +57 -51
- package/packages/kernel/work/infrastructure/work-contract-store.ts +819 -728
- package/packages/kernel/work/ports/work-contract-store.ts +5 -2
- package/packages/plugin-runtime/browser/session-authority.ts +2 -1
- package/packages/plugin-runtime/computer/chatgpt-identity.ts +24 -0
- package/packages/plugin-runtime/computer/conversation-target.ts +143 -0
- package/packages/plugin-runtime/computer/index.ts +2 -0
- package/packages/plugin-runtime/computer/target-authority.ts +26 -1
- package/packages/plugin-runtime/external/unix-jsonl-transport.ts +142 -18
- package/packages/protocols/computer/contract.ts +27 -1
- package/scripts/activate-source-baseline.ts +7 -9
- package/scripts/canary-controller-semantic-wait.ts +12 -24
- package/scripts/check-mcp-compatibility.ts +48 -136
- package/scripts/check-runtime-architecture.mjs +622 -217
- package/scripts/forge-local-recovery-helper.mjs +93 -15
- package/scripts/lib/project-init-lib.sh +3 -0
- package/scripts/open-source-audit-allowlist.txt +1 -2
- package/scripts/run-bounded-command.mjs +5 -0
- package/scripts/run-governed-gate.ts +0 -1
- package/scripts/run-mobile-monitor-bridge.ts +0 -1
- package/scripts/smoke-control-plane-failure-domain.ts +94 -0
- package/scripts/smoke-mcp-http-runtime.ts +5 -0
- package/scripts/smoke-runtime-recovery.ts +12 -43
- package/scripts/smoke-schedule-engine.ts +23 -15
- package/scripts/smoke-unattended-transport-recovery.ts +223 -0
- package/scripts/smoke-user-request-continuation-restart.ts +232 -0
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +92 -0
- package/scripts/smoke-workflow-supervisor-process-restart.ts +276 -0
- package/scripts/smoke-workflow-supervisor-restart-recovery.ts +150 -0
- package/scripts/smoke-workflow-supervisor.ts +103 -0
- package/scripts/verify-forge-runtime.sh +2 -1
- package/src/cli/agent-jobs/executor-health.ts +11 -34
- package/src/cli/agent-jobs/integration.ts +18 -49
- package/src/cli/agent-jobs/job-manager.ts +7 -3
- package/src/cli/commands/brain-assistant.ts +27 -13
- package/src/cli/commands/brain.ts +13 -15
- package/src/cli/commands/chatgpt.ts +1 -163
- package/src/cli/commands/recovery.ts +135 -121
- package/src/cli/commands/runtime.ts +3 -3
- package/src/cli/commands/setup-profile.ts +8 -0
- package/src/cli/controller/check-runner.ts +13 -2
- package/src/cli/controller/completion-backlog.ts +3 -3
- package/src/cli/controller/completion-orchestrator.ts +4 -43
- package/src/cli/controller/completion-target.ts +3 -72
- package/src/cli/controller/context/source-materializer.ts +81 -100
- package/src/cli/controller/context-pack.ts +10 -8
- package/src/cli/controller/execution-completion.ts +4 -7
- package/src/cli/controller/execution-policy.ts +36 -153
- package/src/cli/controller/governance.ts +2 -3
- package/src/cli/controller/issue-store.ts +4 -99
- package/src/cli/controller/launch-agents.ts +59 -18
- package/src/cli/controller/operational-plan.ts +24 -51
- package/src/cli/controller/runtime-config.ts +5 -1
- package/src/cli/controller/stuck-state-migration.ts +2 -3
- package/src/cli/controller/systemd-user.ts +39 -1
- package/src/cli/editing/edit-session.ts +1 -7
- package/src/cli/local-bridge/console-automations.ts +18 -13
- package/src/cli/local-bridge/console-view-models.ts +3 -12
- package/src/cli/local-bridge/facade-api.ts +64 -191
- package/src/cli/local-bridge/server.ts +24 -147
- package/src/cli/local-bridge/types.ts +0 -1
- package/src/cli/mcp/oauth-client-retirement.ts +74 -0
- package/src/cli/repositories/command-executor.ts +29 -23
- package/src/cli/repositories/command-scope.ts +35 -16
- package/src/cli/repositories/controller-home.ts +48 -0
- package/src/cli/repositories/ephemeral-workspace.ts +20 -30
- package/src/cli/repositories/git-command-executor.ts +2 -2
- package/src/cli/repositories/registry.ts +77 -11
- package/src/cli/repositories/structured-git.ts +62 -2
- package/src/cli/repository/session-cache.ts +92 -6
- package/src/runtime/context/assistant-context.ts +20 -7
- package/src/runtime/context/assistant-work-context.ts +89 -27
- package/src/runtime/context/automatic-learning.ts +565 -0
- package/src/runtime/context/cognitive-attention.ts +173 -0
- package/src/runtime/context/cognitive-skill-canary.ts +158 -0
- package/src/runtime/context/context-closure.ts +46 -9
- package/src/runtime/context/context-pack-process.ts +103 -0
- package/src/runtime/context/context-plane.ts +1 -9
- package/src/runtime/context/direct-controller-learning.ts +298 -0
- package/src/runtime/context/semantic-navigation-contract.ts +9 -0
- package/src/runtime/context/semantic-navigation.ts +6 -34
- package/src/runtime/context/typescript-navigation-process.ts +145 -0
- package/src/runtime/context/typescript-navigation.ts +89 -3
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +212 -9
- package/src/runtime/control-plane/concurrency/work-execution-concurrency.ts +9 -47
- package/src/runtime/control-plane/execution/controller-authority-recovery.ts +34 -4
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +29 -20
- package/src/runtime/control-plane/execution/execution-identity.ts +23 -3
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +7 -30
- package/src/runtime/control-plane/execution/retained-work-resume.ts +206 -199
- package/src/runtime/control-plane/execution/session-store.ts +31 -1
- package/src/runtime/control-plane/execution/validation.ts +6 -8
- package/src/runtime/control-plane/execution/work-authority-reconciler.ts +13 -42
- package/src/runtime/control-plane/execution/work-check-evidence.ts +137 -0
- package/src/runtime/control-plane/execution/work-completion-authority.ts +30 -85
- package/src/runtime/control-plane/execution/work-delivery-service.ts +497 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +12 -50
- package/src/runtime/control-plane/execution/work-execution-support.ts +50 -149
- package/src/runtime/control-plane/execution/work-handle-authority.ts +188 -109
- package/src/runtime/control-plane/execution/work-handle-store.ts +19 -43
- package/src/runtime/control-plane/execution/work-head-settlement.ts +53 -20
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +52 -29
- package/src/runtime/control-plane/execution/work-operation-service.ts +43 -27
- package/src/runtime/control-plane/execution/work-preparation-service.ts +126 -266
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +2 -3
- package/src/runtime/control-plane/execution/work-revision-diff.ts +39 -0
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +173 -186
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -84
- package/src/runtime/control-plane/execution/work-verification-service.ts +406 -50
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +362 -37
- package/src/runtime/control-plane/facade/check-normalization.ts +3 -9
- package/src/runtime/control-plane/facade/codex-delegation.ts +10 -11
- package/src/runtime/control-plane/facade/controller-connection.ts +135 -0
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +1 -20
- package/src/runtime/control-plane/facade/goal-workloop.ts +281 -1753
- package/src/runtime/control-plane/facade/handoff-inbox-application.ts +12 -25
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +256 -52
- package/src/runtime/control-plane/facade/index.ts +0 -1
- package/src/runtime/control-plane/facade/operation-digest.ts +12 -15
- package/src/runtime/control-plane/facade/plan-contract-store.ts +451 -991
- package/src/runtime/control-plane/facade/policy-gate.ts +0 -23
- package/src/runtime/control-plane/facade/repository-work-admission.ts +12 -22
- package/src/runtime/control-plane/facade/requirement-authority.ts +102 -199
- package/src/runtime/control-plane/facade/requirement-board.ts +3 -3
- package/src/runtime/control-plane/facade/rh-work-operation-contract.ts +51 -14
- package/src/runtime/control-plane/facade/self-healing-loop.ts +18 -53
- package/src/runtime/control-plane/facade/suggested-actions.ts +15 -18
- package/src/runtime/control-plane/facade/types.ts +49 -108
- package/src/runtime/control-plane/facade/work-continuation.ts +8 -8
- package/src/runtime/control-plane/generated-cache-retention.ts +97 -1
- package/src/runtime/control-plane/global-scheduler/autonomous-continuation.ts +248 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +1 -1
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +239 -112
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +2 -1
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +66 -40
- package/src/runtime/control-plane/global-scheduler/wake-signal.ts +42 -2
- package/src/runtime/control-plane/governance/access-policy.ts +19 -2
- package/src/runtime/control-plane/governance/authorization.ts +20 -3
- package/src/runtime/control-plane/launcher/chatgpt-round-continuation.ts +63 -300
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +28 -300
- package/src/runtime/control-plane/launcher/local-conversation-controller.ts +238 -0
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +5 -2
- package/src/runtime/control-plane/launcher/thin-launcher.ts +96 -240
- package/src/runtime/control-plane/persistence/cognition-store.ts +820 -0
- package/src/runtime/control-plane/persistence/experience-store.ts +162 -29
- package/src/runtime/control-plane/persistence/requirement-store.ts +216 -74
- package/src/runtime/control-plane/persistence/sqlite-store.ts +113 -6
- package/src/runtime/control-plane/release-retention.ts +135 -47
- package/src/runtime/control-plane/repository-namespace-retention.ts +2 -2
- package/src/runtime/control-plane/runtime-cleanup.ts +290 -21
- package/src/runtime/control-plane/workspace/project-onboarding.ts +80 -0
- package/src/runtime/control-plane/workspace/semantic-sync-service.ts +19 -5
- package/src/runtime/control-plane/workspace/workspace-store.ts +169 -0
- package/src/runtime/diagnostics/incident-repair.ts +313 -146
- package/src/runtime/diagnostics/mcp-timing.ts +114 -0
- package/src/runtime/evidence/artifact-store.ts +158 -40
- package/src/runtime/evidence/evidence-store.ts +112 -8
- package/src/runtime/evidence/output-handle-index.ts +225 -0
- package/src/runtime/evidence/result-store.ts +158 -44
- package/src/runtime/execution/jobs/store.ts +3 -4
- package/src/runtime/execution/managed-workspace.ts +57 -0
- package/src/runtime/execution/process-runtime/check-facade.ts +1 -1
- package/src/runtime/execution/process-runtime/check-result-retention.ts +2 -2
- package/src/runtime/execution/process-runtime/check-result.ts +105 -1
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +18 -8
- package/src/runtime/execution/process-runtime/command-facade.ts +79 -27
- package/src/runtime/execution/process-runtime/gc.ts +15 -2
- package/src/runtime/execution/process-runtime/handle-index.ts +176 -0
- package/src/runtime/execution/process-runtime/host-command-lane.ts +139 -0
- package/src/runtime/execution/process-runtime/index.ts +3 -0
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +251 -72
- package/src/runtime/execution/process-runtime/persisted-check.ts +3 -3
- package/src/runtime/execution/process-runtime/process-runner-entry.ts +47 -5
- package/src/runtime/execution/process-runtime/process-scope.ts +160 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +65 -61
- package/src/runtime/execution/process-runtime/runtime.ts +35 -2
- package/src/runtime/execution/process-runtime/store.ts +30 -2
- package/src/runtime/execution/process-runtime/types.ts +6 -1
- package/src/runtime/execution/workers/executor.ts +2 -6
- package/src/runtime/platform/service-inventory.ts +130 -0
- package/src/runtime/platform/service-manager.ts +8 -0
- package/src/runtime/plugins/action-application.ts +85 -28
- package/src/runtime/plugins/app-store-connect-adapter.ts +38 -14
- package/src/runtime/plugins/browser-adapter.ts +757 -34
- package/src/runtime/plugins/browser-automation-service.ts +23 -3
- package/src/runtime/plugins/browser-macos-bridge.ts +321 -29
- package/src/runtime/plugins/browser-manifest-surface.ts +45 -11
- package/src/runtime/plugins/browser-native-session-service.ts +63 -13
- package/src/runtime/plugins/browser-registration.ts +16 -0
- package/src/runtime/plugins/browser-runtime.ts +14 -1
- package/src/runtime/plugins/browser-session-authority.ts +2 -2
- package/src/runtime/plugins/browser-session-store.ts +46 -76
- package/src/runtime/plugins/capability-authorization-grants.ts +258 -62
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +219 -0
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +891 -0
- package/src/runtime/plugins/computer-registration.ts +222 -11
- package/src/runtime/plugins/desktop-operator-external-policy.ts +36 -4
- package/src/runtime/plugins/desktop-operator-registration.ts +12 -4
- package/src/runtime/plugins/external-adapter.ts +42 -6
- package/src/runtime/plugins/external-registration.ts +21 -1
- package/src/runtime/plugins/gmail-adapter.ts +23 -2
- package/src/runtime/plugins/google-calendar-adapter.ts +2 -2
- package/src/runtime/plugins/google-shared.ts +14 -7
- package/src/runtime/plugins/google-tasks-adapter.ts +2 -2
- package/src/runtime/plugins/ios-adapter.ts +1 -1
- package/src/runtime/plugins/lightweight-action.ts +208 -2
- package/src/runtime/plugins/local-system-adapter.ts +12 -2
- package/src/runtime/plugins/managed-process-adapter.ts +12 -5
- package/src/runtime/plugins/plugin-action-sidecar.ts +17 -13
- package/src/runtime/plugins/plugin-management-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +602 -428
- package/src/runtime/plugins/types.ts +20 -0
- package/src/runtime/plugins/xiaohongshu-publish.ts +16 -0
- package/src/runtime/projections/invalidation.ts +7 -3
- package/src/runtime/projections/materialized-view.ts +80 -36
- package/src/runtime/recovery/actions.ts +3 -3
- package/src/runtime/recovery/capability-action-application.ts +46 -45
- package/src/runtime/recovery/lifecycle-inventory.ts +29 -29
- package/src/runtime/recovery/maintenance-executor.ts +374 -123
- package/src/runtime/recovery/store.ts +29 -11
- package/src/runtime/release/release-coordinator.ts +296 -0
- package/src/runtime/release/release-session.ts +716 -0
- package/src/runtime/release/release-state-migration.ts +25 -0
- package/src/runtime/resources/leases/store.ts +56 -25
- package/src/runtime/root/computer-composition.ts +191 -6
- package/src/runtime/root/controller-progression-composition.ts +192 -0
- package/src/runtime/root/controller-round-composition.ts +20 -1
- package/src/runtime/root/deployment-topology.ts +106 -0
- package/src/runtime/root/entry.ts +4 -3
- package/src/runtime/root/known-good-recovery.ts +184 -0
- package/src/runtime/root/local-bridge.ts +0 -1
- package/src/runtime/root/mcp-transport.ts +111 -18
- package/src/runtime/root/ownership.ts +261 -9
- package/src/runtime/root/package-connector-service.ts +105 -19
- package/src/runtime/root/package-runtime-release.ts +117 -18
- package/src/runtime/root/package-runtime-service.ts +17 -11
- package/src/runtime/root/release-execution-canary.ts +44 -12
- package/src/runtime/root/release-loader.ts +90 -8
- package/src/runtime/root/release-manifest.ts +80 -6
- package/src/runtime/root/release-materialize.ts +372 -28
- package/src/runtime/root/release-store.ts +395 -36
- package/src/runtime/root/runtime-lane.ts +191 -0
- package/src/runtime/root/runtime.ts +159 -27
- package/src/runtime/root/scheduled-controller-composition.ts +28 -3
- package/src/runtime/root/service-runner.ts +2 -2
- package/src/runtime/root/service.ts +36 -33
- package/src/runtime/root/status.ts +48 -0
- package/src/runtime/root/types.ts +32 -3
- package/src/runtime/root/workflow-supervisor-composition.ts +493 -0
- package/src/runtime/root/workflow-supervisor-runtime.ts +209 -0
- package/src/runtime/root/write-fence.ts +50 -2
- package/src/runtime/safe-tooling/google-credential-store.ts +76 -21
- package/src/runtime/safe-tooling/google-oauth-broker.ts +25 -7
- package/src/runtime/safe-tooling/workspace-auth.ts +1 -1
- package/src/runtime/shared/bounded-child-supervisor.ts +9 -1
- package/src/runtime/shared/process-environment.ts +20 -9
- package/src/runtime/shared/response-budget.ts +1 -3
- package/src/runtime/standalone-recovery/controller-home-migration.ts +16 -24
- package/src/runtime/standalone-recovery/core.ts +3702 -342
- package/src/runtime/standalone-recovery/entry.ts +469 -39
- package/src/runtime/standalone-recovery/installer.ts +262 -148
- package/src/runtime/standalone-recovery/mcp-server.ts +29 -193
- package/src/runtime/standalone-recovery/operation-lock.ts +1 -1
- package/src/runtime/standalone-recovery/performance.ts +54 -16
- package/src/runtime/standalone-recovery/release.ts +53 -5
- package/src/runtime/standalone-recovery/service-labels.ts +5 -2
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +5 -2
- package/src/runtime/workflow/schedules/browser-probe.ts +17 -6
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +10 -3
- package/src/runtime/workflow/schedules/engine.ts +138 -172
- package/src/runtime/workflow/schedules/work-continuation.ts +44 -5
- package/src/runtime/workflows/runtime.ts +15 -19
- package/src/testing/test-governance.ts +5 -1
- package/supervisor/chatgpt-conversation.ts +23 -0
- package/supervisor/client.ts +201 -0
- package/supervisor/control-plane.ts +868 -0
- package/supervisor/entry.ts +15 -0
- package/supervisor/forge-validators.ts +80 -0
- package/supervisor/index.ts +5 -0
- package/supervisor/native-browser-adapter.ts +786 -0
- package/supervisor/native-messaging/host.ts +39 -0
- package/supervisor/native-messaging/manifest.ts +26 -0
- package/supervisor/native-messaging/protocol.ts +27 -0
- package/supervisor/paths.ts +18 -0
- package/supervisor/protocol.ts +202 -0
- package/supervisor/server.ts +404 -0
- package/supervisor/store.ts +1451 -0
- package/supervisor/tsconfig.json +8 -0
- package/supervisor/types.ts +255 -0
- package/adapters/chatgpt/controller-host.ts +0 -119
- package/adapters/mcp/controller-round-compatibility.ts +0 -184
- package/adapters/mcp/frozen-client-semantic-compatibility.ts +0 -292
- package/adapters/mcp/runtime-gateway/work-compat-adapter.ts +0 -502
- package/packages/kernel/progression/api/index.ts +0 -2
- package/packages/kernel/progression/application/projector.ts +0 -211
- package/packages/kernel/progression/domain/types.ts +0 -116
- package/packages/kernel/progression/index.ts +0 -1
- package/packages/kernel/scheduler/application/continuation-service.ts +0 -212
- package/packages/kernel/scheduler/domain/continuation.ts +0 -38
- package/packages/kernel/scheduler/infrastructure/continuation-dispatch-store.ts +0 -38
- package/packages/kernel/work/domain/admission-policy.ts +0 -34
- package/scripts/benchmark-background-check-overlap.ts +0 -228
- package/scripts/benchmark-computer-hotpath.ts +0 -255
- package/scripts/benchmark-controller-hotpath.ts +0 -212
- package/scripts/benchmark-controller-round2.ts +0 -191
- package/scripts/benchmark-ios-agent-device.ts +0 -511
- package/scripts/benchmark-mcp-host.sh +0 -117
- package/scripts/benchmark-mcp-transport.ts +0 -169
- package/scripts/benchmark-operational-memory-activation.ts +0 -235
- package/scripts/benchmark-operational-shadow.ts +0 -28
- package/scripts/benchmark-quality-harness.ts +0 -146
- package/scripts/benchmark-route-session-concurrency.ts +0 -915
- package/scripts/benchmark-thin-harness-gateway-ab.ts +0 -632
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +0 -498
- package/scripts/benchmark-thin-harness.ts +0 -251
- package/scripts/benchmark-typescript-navigation.ts +0 -186
- package/scripts/benchmark-work-efficiency.ts +0 -334
- package/scripts/build-controller-ui.ts +0 -28
- package/scripts/check-npm-release.sh +0 -32
- package/scripts/check-public-release-surface.sh +0 -19
- package/scripts/check-release-published.sh +0 -57
- package/scripts/check-release-readiness.sh +0 -37
- package/scripts/check-release-version.mjs +0 -74
- package/scripts/check-tarball-install-smoke.sh +0 -150
- package/scripts/package-source-archive.sh +0 -147
- package/scripts/public-release-files.txt +0 -39
- package/scripts/public-release.ts +0 -174
- package/scripts/publish-release-tarball.sh +0 -27
- package/scripts/route-nl-vs-ts-eval.ts +0 -679
- package/scripts/stage-runtime-release.ts +0 -44
- package/src/cli/controller/progress.ts +0 -656
- package/src/cli/controller/work-mode.ts +0 -181
- package/src/cli/local-bridge/console-assets.ts +0 -17
- package/src/cli/local-bridge/dashboard.ts +0 -17
- package/src/cli/local-bridge/frontend/src/App.tsx +0 -10
- package/src/cli/local-bridge/frontend/src/components/Button.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +0 -3
- package/src/cli/local-bridge/frontend/src/design/icons.tsx +0 -11
- package/src/cli/local-bridge/frontend/src/design/tokens.css +0 -8
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +0 -153
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +0 -37
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +0 -183
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +0 -30
- package/src/cli/local-bridge/frontend/src/lib/format.ts +0 -4
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +0 -7
- package/src/cli/local-bridge/frontend/src/main.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +0 -1
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +0 -2
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +0 -4
- package/src/cli/local-bridge/frontend/src/styles.css +0 -28
- package/src/cli/local-bridge/frontend/src/types.ts +0 -15
- package/src/cli/local-bridge/frontend/src/vite-env.d.ts +0 -1
- package/src/cli/local-bridge/frontend/vite.config.ts +0 -27
- package/src/cli/local-bridge/ui-dist/app.css +0 -1
- package/src/cli/local-bridge/ui-dist/app.js +0 -11228
- package/src/runtime/control-plane/execution/content-equivalent-commit-authority.ts +0 -169
- package/src/runtime/control-plane/execution/direct-canonical-work-reconciliation.ts +0 -85
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +0 -655
- package/src/runtime/control-plane/execution/work-finalization-service.ts +0 -2850
- package/src/runtime/control-plane/execution/work-path-scope.ts +0 -37
- package/src/runtime/control-plane/execution/work-task-receipt.ts +0 -468
- package/src/runtime/control-plane/facade/controller-round-relay.ts +0 -3
- package/src/runtime/control-plane/facade/work-admission-policy.ts +0 -142
- package/src/runtime/control-plane/facade/work-implementation-review.ts +0 -2
- package/src/runtime/control-plane/facade/work-state-machine.ts +0 -2
- package/src/runtime/control-plane/facade/work-task-projection.ts +0 -66
- package/src/runtime/control-plane/routing/index.ts +0 -1
- package/src/runtime/control-plane/routing/route-policy.ts +0 -401
- package/src/runtime/control-plane/routing/workspace-admission.ts +0 -70
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'crypto';
|
|
2
2
|
import { existsSync, mkdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { ensureControllerHome,
|
|
4
|
+
import { ensureControllerHome, scopedOperationRoot, SEMANTIC_SCOPE_KEY } from '../../../../src/cli/repositories/controller-home';
|
|
5
5
|
import { withControllerLock } from '../../../../src/cli/repositories/locks';
|
|
6
6
|
import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../../../src/runtime/shared/json-files';
|
|
7
7
|
import {
|
|
8
|
+
listControlPlaneRecordKeys,
|
|
8
9
|
listControlPlaneRecords,
|
|
10
|
+
listControlPlaneRecordsWithinTransaction,
|
|
9
11
|
listControlPlaneRecordsExcludingPayloadTextValues,
|
|
10
12
|
initializeControlPlanePayloadTextExclusionIndex,
|
|
11
13
|
readControlPlaneRecord,
|
|
@@ -15,24 +17,13 @@ import {
|
|
|
15
17
|
} from '../../../../src/runtime/control-plane/persistence/sqlite-store';
|
|
16
18
|
import {
|
|
17
19
|
MAX_IMPLEMENTATION_REVIEW_HISTORY,
|
|
18
|
-
authoritativeImplementationReviewVerificationEvidence,
|
|
19
|
-
implementationReviewDecisionTarget,
|
|
20
|
-
latestImplementationReview,
|
|
21
|
-
normalizeImplementationReviewEvidence,
|
|
22
20
|
validateImplementationReviewRecord,
|
|
23
|
-
workRequiresImplementationReview,
|
|
24
21
|
type WorkImplementationReviewRecord,
|
|
25
22
|
} from '../domain/implementation-review';
|
|
26
|
-
import { phaseIndex,
|
|
27
|
-
import {
|
|
28
|
-
WORK_ADMISSION_POLICY_KEY,
|
|
29
|
-
WORK_ADMISSION_POLICY_NAMESPACE,
|
|
30
|
-
WORK_ADMISSION_POLICY_SCOPE,
|
|
31
|
-
assertWorkAdmissionPolicyAllows,
|
|
32
|
-
normalWorkAdmissionPolicy,
|
|
33
|
-
type WorkAdmissionPolicy,
|
|
34
|
-
} from '../domain/admission-policy';
|
|
23
|
+
import { phaseIndex, suggestedActionsForSemanticState, transitionPhaseEvidence, validateWorkSemanticTransition, validateWorkSemantics } from '../domain/state-machine';
|
|
24
|
+
import { normalizeWorkObjectiveRelationIds, validateWorkObjectiveRelationShape } from '../domain/objective-graph';
|
|
35
25
|
import {
|
|
26
|
+
WORK_PHASES,
|
|
36
27
|
type EvidenceRef,
|
|
37
28
|
type CompletionOutcome,
|
|
38
29
|
type DispatchState,
|
|
@@ -43,7 +34,8 @@ import {
|
|
|
43
34
|
type WorkReconciliationRecord,
|
|
44
35
|
type SubmittedWorkOperation,
|
|
45
36
|
type WorkContract,
|
|
46
|
-
type
|
|
37
|
+
type WorkSemanticView,
|
|
38
|
+
type SemanticWorkState,
|
|
47
39
|
type WorkRisk,
|
|
48
40
|
type WorkKind,
|
|
49
41
|
type WorkPhase,
|
|
@@ -54,8 +46,8 @@ import {
|
|
|
54
46
|
executionPlacementForWork,
|
|
55
47
|
isDirectEditWorkCompletionReceipt,
|
|
56
48
|
isRepositoryCompletionReceipt,
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
isTerminalSemanticWorkState,
|
|
50
|
+
TERMINAL_SEMANTIC_WORK_STATES,
|
|
59
51
|
semanticScopeRefForWork,
|
|
60
52
|
} from '../domain/types';
|
|
61
53
|
|
|
@@ -64,10 +56,9 @@ export type { WorkContractStoreLocation, WorkContractStoreOptions } from '../por
|
|
|
64
56
|
|
|
65
57
|
export type CreateWorkContractInput = Omit<
|
|
66
58
|
WorkContract,
|
|
67
|
-
'schemaVersion' | '
|
|
59
|
+
'schemaVersion' | 'semanticState' | 'createdAt' | 'updatedAt' | 'risk' | 'workKind' | 'dispatchState' | 'evidenceState' | 'completionOutcome' | 'phase' | 'phaseEvidence' | 'completionReceipt' | 'evidenceRefs' | 'handoffRefs' | 'suggestedNextActions' | 'policyDecisions' | 'checkRefs' | 'implementationReviews' | 'reconciliations' | 'worktreePolicy' | 'evidencePolicy' | 'approvalPolicy' | 'recoveryPolicy'
|
|
68
60
|
> & {
|
|
69
61
|
risk?: WorkRisk;
|
|
70
|
-
status?: WorkContractStatus;
|
|
71
62
|
createdAt?: string;
|
|
72
63
|
updatedAt?: string;
|
|
73
64
|
workKind?: WorkKind;
|
|
@@ -82,15 +73,29 @@ export type CreateWorkContractInput = Omit<
|
|
|
82
73
|
policyDecisions?: PolicyDecision[];
|
|
83
74
|
checkRefs?: VerificationRecord[];
|
|
84
75
|
reconciliations?: WorkReconciliationRecord[];
|
|
85
|
-
driver?: WorkContract['driver'];
|
|
86
76
|
worktreePolicy?: WorkContract['worktreePolicy'];
|
|
87
77
|
evidencePolicy?: WorkContract['evidencePolicy'];
|
|
88
78
|
approvalPolicy?: WorkContract['approvalPolicy'];
|
|
89
79
|
recoveryPolicy?: WorkContract['recoveryPolicy'];
|
|
80
|
+
/**
|
|
81
|
+
* Legacy ingress-only fields, accepted only so a pre-20261004 frozen MCP
|
|
82
|
+
* client can still submit without the retired fields being recreated on the
|
|
83
|
+
* persisted Work contract.
|
|
84
|
+
*
|
|
85
|
+
* Replacement owner: concrete mutation scope belongs to the EditSession (and
|
|
86
|
+
* the execution resource bound to it); a Work carries semantic scope and
|
|
87
|
+
* evidence only. Bounded consumers: frozen MCP clients whose advertised
|
|
88
|
+
* `work_prepare` schema still includes `allowed_paths`. Removal condition:
|
|
89
|
+
* delete these fields (and the matching `AcceptSubmittedWorkInput` pair) as
|
|
90
|
+
* soon as no supported client sends them; a non-empty value is never
|
|
91
|
+
* enforced and must never be treated as authorization evidence.
|
|
92
|
+
*/
|
|
93
|
+
allowedPaths?: string[];
|
|
94
|
+
forbiddenPaths?: string[];
|
|
90
95
|
};
|
|
91
96
|
|
|
92
97
|
export interface ListWorkContractOptions extends WorkContractStoreOptions {
|
|
93
|
-
|
|
98
|
+
state?: SemanticWorkState | 'active' | 'all';
|
|
94
99
|
limit?: number;
|
|
95
100
|
detailLevel?: 'summary' | 'detail' | 'raw';
|
|
96
101
|
}
|
|
@@ -112,12 +117,58 @@ export interface ActiveWorkCandidateSnapshot {
|
|
|
112
117
|
invalid: InvalidActiveWorkCandidate[];
|
|
113
118
|
}
|
|
114
119
|
|
|
120
|
+
export interface WorkContractLineageSnapshot {
|
|
121
|
+
contracts: WorkContract[];
|
|
122
|
+
invalid: Array<{
|
|
123
|
+
workId: string;
|
|
124
|
+
parentWorkId?: string;
|
|
125
|
+
predecessorWorkId?: string;
|
|
126
|
+
supersedes: string[];
|
|
127
|
+
supersededBy?: string;
|
|
128
|
+
error: string;
|
|
129
|
+
}>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface WorkSemanticRevisionRecord extends WorkSemanticView {
|
|
133
|
+
schemaVersion: 1;
|
|
134
|
+
recordedAt: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface CreateWorkSemanticInput {
|
|
138
|
+
workId: string;
|
|
139
|
+
objective: string;
|
|
140
|
+
requestId?: string;
|
|
141
|
+
requirementId?: string;
|
|
142
|
+
requirementRevision?: number;
|
|
143
|
+
planId?: string;
|
|
144
|
+
planRevision?: number;
|
|
145
|
+
semanticParentWorkId?: string;
|
|
146
|
+
dependsOnWorkIds?: string[];
|
|
147
|
+
requestedBy?: 'chatgpt' | 'user' | 'system' | 'scheduler';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ReviseWorkSemanticInput {
|
|
151
|
+
expectedRevision: number;
|
|
152
|
+
objective?: string;
|
|
153
|
+
state?: SemanticWorkState;
|
|
154
|
+
requirementRevision?: number;
|
|
155
|
+
planRevision?: number;
|
|
156
|
+
semanticParentWorkId?: string;
|
|
157
|
+
dependsOnWorkIds?: string[];
|
|
158
|
+
resultRefs?: string[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface WorkSemanticRevisionStore {
|
|
162
|
+
schemaVersion: 1;
|
|
163
|
+
records: WorkSemanticRevisionRecord[];
|
|
164
|
+
}
|
|
165
|
+
|
|
115
166
|
export interface WorkContractSummary {
|
|
116
167
|
workId: string;
|
|
117
168
|
repoId: string;
|
|
118
|
-
|
|
169
|
+
/** The one thin authored Work state. */
|
|
170
|
+
state: SemanticWorkState;
|
|
119
171
|
phase: WorkContract['phase'];
|
|
120
|
-
status: WorkContractStatus;
|
|
121
172
|
objective: string;
|
|
122
173
|
updatedAt: string;
|
|
123
174
|
handoffCount: number;
|
|
@@ -135,15 +186,21 @@ function nowIso(options: WorkContractStoreOptions): string {
|
|
|
135
186
|
return options.now?.() ?? new Date().toISOString();
|
|
136
187
|
}
|
|
137
188
|
|
|
189
|
+
export function workContractStoreScopeKey(location: WorkContractStoreLocation): string {
|
|
190
|
+
const key = location.scopeKey?.trim() || location.repoId?.trim() || '';
|
|
191
|
+
if (!key) throw new Error('WORK_STORE_SCOPE_REQUIRED: controllerHome requires scopeKey or repoId');
|
|
192
|
+
return key;
|
|
193
|
+
}
|
|
194
|
+
|
|
138
195
|
export function workContractRoot(location: WorkContractStoreLocation): string {
|
|
139
196
|
if (location.root) {
|
|
140
197
|
mkdirSync(location.root, { recursive: true });
|
|
141
198
|
return location.root;
|
|
142
199
|
}
|
|
143
|
-
if (!location.controllerHome
|
|
144
|
-
throw new Error('work contract store requires either root or controllerHome + repoId');
|
|
200
|
+
if (!location.controllerHome) {
|
|
201
|
+
throw new Error('work contract store requires either root or controllerHome + scopeKey/repoId');
|
|
145
202
|
}
|
|
146
|
-
const root = join(
|
|
203
|
+
const root = join(scopedOperationRoot(location.controllerHome, workContractStoreScopeKey(location)), 'work-contracts');
|
|
147
204
|
mkdirSync(root, { recursive: true });
|
|
148
205
|
return root;
|
|
149
206
|
}
|
|
@@ -153,40 +210,98 @@ export function workContractStorePath(location: WorkContractStoreLocation): stri
|
|
|
153
210
|
}
|
|
154
211
|
|
|
155
212
|
export function emptyWorkContractStore(updatedAt: string): WorkContractStore {
|
|
156
|
-
return { schemaVersion:
|
|
213
|
+
return { schemaVersion: 4, updatedAt, contracts: [] };
|
|
157
214
|
}
|
|
158
215
|
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
216
|
+
function currentWorkSemanticRevision(work: WorkContract): number {
|
|
217
|
+
const revision = Number(work.semanticRevision);
|
|
218
|
+
return Number.isInteger(revision) && revision > 0 ? revision : 1;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the one thin semantic Work state. An explicit authored state is
|
|
223
|
+
* authoritative; a row that predates explicit semantic state derives only its
|
|
224
|
+
* terminal projection from mechanically recorded completion/cancellation.
|
|
225
|
+
* Execution/verification/review/delivery/cleanup vocabulary is never a semantic
|
|
226
|
+
* state: a mechanically blocked or failed Work stays semantically `open` until
|
|
227
|
+
* an explicit CAS close or cancellation.
|
|
228
|
+
*/
|
|
229
|
+
export function semanticWorkState(work: Pick<WorkContract, 'semanticState'>): SemanticWorkState {
|
|
230
|
+
return work.semanticState;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function workSemanticView(work: WorkContract): WorkSemanticView {
|
|
234
|
+
const resultRefs = [...new Set((work.semanticResultRefs ?? []).map((value) => value.trim()).filter(Boolean))].slice(0, 100);
|
|
235
|
+
return {
|
|
236
|
+
workId: work.workId,
|
|
237
|
+
revision: currentWorkSemanticRevision(work),
|
|
238
|
+
semanticScope: semanticScopeRefForWork(work),
|
|
239
|
+
objective: work.objective,
|
|
240
|
+
state: semanticWorkState(work),
|
|
241
|
+
...(work.requirementId?.trim() ? { requirementId: work.requirementId.trim() } : {}),
|
|
242
|
+
...(Number.isInteger(work.requirementRevision) && Number(work.requirementRevision) > 0 ? { requirementRevision: Number(work.requirementRevision) } : {}),
|
|
243
|
+
...(work.planId?.trim() ? { planId: work.planId.trim() } : {}),
|
|
244
|
+
...(Number.isInteger(work.planRevision) && Number(work.planRevision) > 0 ? { planRevision: Number(work.planRevision) } : {}),
|
|
245
|
+
...(work.semanticParentWorkId?.trim() ? { semanticParentWorkId: work.semanticParentWorkId.trim() } : {}),
|
|
246
|
+
...(normalizeWorkObjectiveRelationIds(work.dependsOnWorkIds).length > 0 ? { dependsOnWorkIds: normalizeWorkObjectiveRelationIds(work.dependsOnWorkIds) } : {}),
|
|
247
|
+
resultRefs,
|
|
248
|
+
createdAt: work.createdAt,
|
|
249
|
+
updatedAt: work.semanticUpdatedAt ?? work.createdAt,
|
|
250
|
+
};
|
|
167
251
|
}
|
|
168
252
|
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
if (status === 'running') return 'running';
|
|
172
|
-
if (status === 'blocked') return 'blocked';
|
|
173
|
-
return 'terminal';
|
|
253
|
+
function workSemanticRevisionKey(workId: string, revision: number): string {
|
|
254
|
+
return `${sanitizeFileComponent(workId)}-r${revision}`;
|
|
174
255
|
}
|
|
175
256
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
257
|
+
function validateWorkObjectiveRelationIntegrity(candidate: WorkContract, contracts: readonly WorkContract[]): void {
|
|
258
|
+
validateWorkObjectiveRelationShape(candidate);
|
|
259
|
+
const parent = candidate.semanticParentWorkId?.trim();
|
|
260
|
+
const dependencies = normalizeWorkObjectiveRelationIds(candidate.dependsOnWorkIds);
|
|
261
|
+
if (!parent && dependencies.length === 0) return;
|
|
262
|
+
const byId = new Map(contracts.map((contract) => [contract.workId, contract] as const));
|
|
263
|
+
byId.set(candidate.workId, candidate);
|
|
264
|
+
for (const relatedWorkId of [...(parent ? [parent] : []), ...dependencies]) {
|
|
265
|
+
const related = byId.get(relatedWorkId);
|
|
266
|
+
if (!related) throw new Error(`WORK_OBJECTIVE_RELATION_NOT_FOUND: ${relatedWorkId}`);
|
|
267
|
+
if ((related.lifecycleRole ?? 'primary') !== 'primary') throw new Error(`WORK_OBJECTIVE_RELATION_TARGET_NOT_PRIMARY: ${relatedWorkId}`);
|
|
268
|
+
}
|
|
269
|
+
const seenParents = new Set<string>();
|
|
270
|
+
let parentCursor = parent;
|
|
271
|
+
while (parentCursor) {
|
|
272
|
+
if (parentCursor === candidate.workId) throw new Error('WORK_SEMANTIC_PARENT_CYCLE');
|
|
273
|
+
if (seenParents.has(parentCursor)) throw new Error('WORK_SEMANTIC_PARENT_TARGET_CYCLE');
|
|
274
|
+
seenParents.add(parentCursor);
|
|
275
|
+
parentCursor = byId.get(parentCursor)?.semanticParentWorkId?.trim();
|
|
276
|
+
}
|
|
277
|
+
const dependencyReachesCandidate = (workId: string, visiting: Set<string>): boolean => {
|
|
278
|
+
if (workId === candidate.workId) return true;
|
|
279
|
+
if (visiting.has(workId)) return false;
|
|
280
|
+
visiting.add(workId);
|
|
281
|
+
const work = byId.get(workId);
|
|
282
|
+
if (!work) return false;
|
|
283
|
+
return normalizeWorkObjectiveRelationIds(work.dependsOnWorkIds)
|
|
284
|
+
.some((dependency) => dependencyReachesCandidate(dependency, visiting));
|
|
285
|
+
};
|
|
286
|
+
for (const dependency of dependencies) {
|
|
287
|
+
if (dependencyReachesCandidate(dependency, new Set())) throw new Error('WORK_DEPENDENCY_CYCLE');
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function workSemanticRevisionStorePath(options: WorkContractStoreOptions): string {
|
|
292
|
+
return join(workContractRoot(options), 'semantic-revisions.json');
|
|
180
293
|
}
|
|
181
294
|
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
295
|
+
function readWorkSemanticRevisionStore(options: WorkContractStoreOptions): WorkSemanticRevisionStore {
|
|
296
|
+
return readJsonFile<WorkSemanticRevisionStore>(workSemanticRevisionStorePath(options), { schemaVersion: 1, records: [] });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function initialLifecycleForNewWork(): Pick<WorkContract, 'phase' | 'dispatchState' | 'evidenceState'> {
|
|
300
|
+
return { phase: 'implementation', dispatchState: 'not_dispatched', evidenceState: 'none' };
|
|
186
301
|
}
|
|
187
302
|
|
|
188
303
|
function initialPhaseEvidenceForNewWork(
|
|
189
|
-
input: Pick<WorkContract, 'phase' | '
|
|
304
|
+
input: Pick<WorkContract, 'phase' | 'evidenceRefs' | 'updatedAt'> & { phaseState?: WorkPhaseEvidenceState },
|
|
190
305
|
): WorkPhaseEvidenceMap {
|
|
191
306
|
const currentIndex = phaseIndex(input.phase);
|
|
192
307
|
return Object.fromEntries((['implementation', 'verification', 'review', 'delivery', 'cleanup'] as WorkPhase[]).map((phase) => {
|
|
@@ -195,13 +310,7 @@ function initialPhaseEvidenceForNewWork(
|
|
|
195
310
|
? 'satisfied'
|
|
196
311
|
: index > currentIndex
|
|
197
312
|
? 'pending'
|
|
198
|
-
: input.
|
|
199
|
-
? 'failed'
|
|
200
|
-
: input.status === 'cancelled'
|
|
201
|
-
? 'skipped'
|
|
202
|
-
: input.status === 'blocked' || input.status === 'ready'
|
|
203
|
-
? 'blocked'
|
|
204
|
-
: 'active';
|
|
313
|
+
: input.phaseState ?? 'active';
|
|
205
314
|
return [phase, {
|
|
206
315
|
state,
|
|
207
316
|
source: 'recorded' as const,
|
|
@@ -209,7 +318,7 @@ function initialPhaseEvidenceForNewWork(
|
|
|
209
318
|
? `Canonical Work was admitted after phase ${phase}.`
|
|
210
319
|
: index > currentIndex
|
|
211
320
|
? `Waiting for Work phase ${phase}.`
|
|
212
|
-
: `Canonical Work admitted in ${phase}
|
|
321
|
+
: `Canonical Work admitted in ${phase}.`,
|
|
213
322
|
evidenceRefs: index <= currentIndex ? input.evidenceRefs.slice(0, 20) : [],
|
|
214
323
|
recordedAt: input.updatedAt,
|
|
215
324
|
} satisfies WorkPhaseEvidence];
|
|
@@ -217,7 +326,7 @@ function initialPhaseEvidenceForNewWork(
|
|
|
217
326
|
}
|
|
218
327
|
|
|
219
328
|
function legacyPhaseEvidence(
|
|
220
|
-
contract: Pick<WorkContract, 'phase' | '
|
|
329
|
+
contract: Pick<WorkContract, 'phase' | 'evidenceRefs' | 'completionReceipt' | 'updatedAt'> & { legacyStatus?: 'open' | 'running' | 'blocked' | 'ready' | 'completed' | 'failed' | 'cancelled' },
|
|
221
330
|
source: WorkPhaseEvidence['source'] = 'legacy_inferred',
|
|
222
331
|
): WorkPhaseEvidenceMap {
|
|
223
332
|
const currentIndex = phaseIndex(contract.phase);
|
|
@@ -226,8 +335,8 @@ function legacyPhaseEvidence(
|
|
|
226
335
|
const index = phaseIndex(phase);
|
|
227
336
|
let state: WorkPhaseEvidenceState = index < currentIndex ? 'satisfied' : index === currentIndex ? 'active' : 'pending';
|
|
228
337
|
if (completedByReceipt) state = phase === 'review' ? 'skipped' : 'satisfied';
|
|
229
|
-
else if (contract.
|
|
230
|
-
else if (contract.
|
|
338
|
+
else if (contract.legacyStatus === 'failed' && phase === contract.phase) state = 'failed';
|
|
339
|
+
else if (contract.legacyStatus === 'cancelled' && phase === contract.phase) state = 'skipped';
|
|
231
340
|
const receiptId = contract.completionReceipt && (phase === 'delivery' || phase === 'cleanup')
|
|
232
341
|
? contract.completionReceipt.receiptId
|
|
233
342
|
: undefined;
|
|
@@ -248,26 +357,31 @@ function legacyPhaseEvidence(
|
|
|
248
357
|
})) as WorkPhaseEvidenceMap;
|
|
249
358
|
}
|
|
250
359
|
|
|
251
|
-
function sqliteBacked(options: WorkContractStoreOptions): options is WorkContractStoreOptions & { controllerHome: string
|
|
252
|
-
// A caller-provided root is a test/portable compatibility store.
|
|
253
|
-
//
|
|
254
|
-
|
|
360
|
+
function sqliteBacked(options: WorkContractStoreOptions): options is WorkContractStoreOptions & { controllerHome: string } {
|
|
361
|
+
// A caller-provided root is a test/portable compatibility store. Runtime
|
|
362
|
+
// control-plane state uses one explicit scope key: semantic scope for authored
|
|
363
|
+
// Work, repository scope for legacy/repository execution Work.
|
|
364
|
+
return Boolean(!options.root && options.controllerHome?.trim() && (options.scopeKey?.trim() || options.repoId?.trim()));
|
|
255
365
|
}
|
|
256
366
|
|
|
257
367
|
function migrateLegacyWorkContract(legacy: WorkContract): WorkContract {
|
|
258
|
-
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
const
|
|
368
|
+
// One-way schema migration only: v1-v3 rows may still carry retired status.
|
|
369
|
+
// v4 strips it permanently and keeps semanticState as the only Work lifecycle state.
|
|
370
|
+
// Removal condition: once supported Controller Homes contain no schema<4 Work rows for one full release boundary, delete legacyStatus handling entirely.
|
|
371
|
+
const legacyStatus = (legacy as WorkContract & { status?: 'open' | 'running' | 'blocked' | 'ready' | 'completed' | 'failed' | 'cancelled' }).status;
|
|
372
|
+
const semanticState: SemanticWorkState = legacy.semanticState
|
|
373
|
+
?? (legacyStatus === 'completed' ? 'completed' : legacyStatus === 'cancelled' ? 'cancelled' : 'open');
|
|
374
|
+
// Phase is a mechanical checkpoint projection only. A legacy row without a
|
|
375
|
+
// persisted phase is admitted at the neutral first checkpoint; terminal
|
|
376
|
+
// status never advances phase, and status is never a phase-transition authority.
|
|
263
377
|
const phase = legacy.completionReceipt
|
|
264
378
|
? 'cleanup'
|
|
265
|
-
:
|
|
379
|
+
: semanticState === 'completed'
|
|
266
380
|
? 'delivery'
|
|
267
|
-
: legacy.phase ??
|
|
381
|
+
: legacy.phase ?? 'implementation';
|
|
268
382
|
const legacyDefaults = legacyPhaseEvidence({
|
|
269
383
|
phase,
|
|
270
|
-
|
|
384
|
+
legacyStatus,
|
|
271
385
|
evidenceRefs: legacy.evidenceRefs ?? [],
|
|
272
386
|
completionReceipt: legacy.completionReceipt,
|
|
273
387
|
updatedAt: legacy.updatedAt,
|
|
@@ -303,54 +417,92 @@ function migrateLegacyWorkContract(legacy: WorkContract): WorkContract {
|
|
|
303
417
|
},
|
|
304
418
|
}
|
|
305
419
|
: legacyDefaults;
|
|
420
|
+
const {
|
|
421
|
+
status: _retiredStatus,
|
|
422
|
+
allowedPaths: _retiredAllowedPaths,
|
|
423
|
+
forbiddenPaths: _retiredForbiddenPaths,
|
|
424
|
+
...legacyWithoutRetiredFields
|
|
425
|
+
} = legacy as WorkContract & { status?: unknown; allowedPaths?: unknown; forbiddenPaths?: unknown };
|
|
426
|
+
void _retiredStatus;
|
|
427
|
+
void _retiredAllowedPaths;
|
|
428
|
+
void _retiredForbiddenPaths;
|
|
429
|
+
// Before Work kind and completion outcome were made a single typed contract,
|
|
430
|
+
// delivery-reconciliation records retained their operational label even after
|
|
431
|
+
// a repository receipt proved that they advanced the target revision. The
|
|
432
|
+
// current contract rightly rejects that combination. Preserve the receipt and
|
|
433
|
+
// semantic terminal state, but migrate the obsolete label to the current
|
|
434
|
+
// repository-change classification so terminal history remains readable and
|
|
435
|
+
// cannot poison scheduler relay discovery forever.
|
|
436
|
+
const workKind = legacy.workKind === 'reconciliation'
|
|
437
|
+
&& legacy.completionOutcome === 'completed_changed'
|
|
438
|
+
&& legacy.completionReceipt
|
|
439
|
+
&& isRepositoryCompletionReceipt(legacy.completionReceipt)
|
|
440
|
+
? 'repository_change' as const
|
|
441
|
+
: legacy.workKind ?? 'repository_change';
|
|
306
442
|
return validateWorkSemantics({
|
|
307
|
-
...
|
|
308
|
-
schemaVersion:
|
|
443
|
+
...legacyWithoutRetiredFields,
|
|
444
|
+
schemaVersion: 4,
|
|
309
445
|
scopeRef: semanticScopeRefForWork(legacy),
|
|
310
446
|
executionPlacement: executionPlacementForWork(legacy),
|
|
311
|
-
|
|
447
|
+
semanticState,
|
|
448
|
+
semanticRevision: legacy.semanticRevision && legacy.semanticRevision > 0 ? legacy.semanticRevision : 1,
|
|
449
|
+
semanticUpdatedAt: legacy.semanticUpdatedAt ?? legacy.updatedAt,
|
|
312
450
|
phase,
|
|
313
451
|
phaseEvidence,
|
|
314
452
|
risk: legacy.risk ?? 'medium',
|
|
315
|
-
workKind
|
|
316
|
-
dispatchState: legacy.dispatchState ??
|
|
317
|
-
evidenceState: legacy.evidenceState ??
|
|
318
|
-
suggestedNextActions:
|
|
453
|
+
workKind,
|
|
454
|
+
dispatchState: legacy.dispatchState ?? 'not_dispatched',
|
|
455
|
+
evidenceState: legacy.evidenceState ?? 'none',
|
|
456
|
+
suggestedNextActions: suggestedActionsForSemanticState(semanticState, legacy.suggestedNextActions ?? []),
|
|
319
457
|
implementationReviews: legacy.implementationReviews ?? [],
|
|
320
458
|
reconciliations: legacy.reconciliations ?? [],
|
|
321
|
-
driver: (legacy.driver as unknown as { preferred?: string } | undefined)?.preferred === 'codex_worker'
|
|
322
|
-
? { ...legacy.driver, preferred: 'external_controller', allowWorker: false }
|
|
323
|
-
: legacy.driver,
|
|
324
459
|
});
|
|
325
460
|
}
|
|
326
461
|
|
|
327
462
|
function validateCanonicalWorkContract(contract: WorkContract): WorkContract {
|
|
328
|
-
if (contract.schemaVersion !==
|
|
463
|
+
if (contract.schemaVersion !== 4) {
|
|
329
464
|
throw new Error(`WORK_CONTRACT_SCHEMA_MIGRATION_REQUIRED: ${contract.workId}:schema=${String(contract.schemaVersion)}`);
|
|
330
465
|
}
|
|
331
466
|
return validateWorkSemantics(contract);
|
|
332
467
|
}
|
|
333
468
|
|
|
334
|
-
function storedWorkContractNeedsMigration(contract: WorkContract): boolean {
|
|
469
|
+
function storedWorkContractNeedsMigration(contract: WorkContract, persistedSchemaVersion: number = contract.schemaVersion): boolean {
|
|
335
470
|
const phaseEvidence = contract.phaseEvidence as Partial<WorkPhaseEvidenceMap> | undefined;
|
|
336
471
|
// Some pre-review-checkpoint rows were stamped with the current outer
|
|
337
472
|
// schema while omitting only `review`. That exact historical shape remains
|
|
338
473
|
// migration-compatible, but it must be persisted once rather than inferred
|
|
339
474
|
// repeatedly by the normal read path.
|
|
340
|
-
const legacyReviewGap =
|
|
475
|
+
const legacyReviewGap = persistedSchemaVersion === 3
|
|
341
476
|
&& phaseEvidence
|
|
342
477
|
&& !phaseEvidence.review
|
|
343
478
|
&& phaseEvidence.implementation
|
|
344
479
|
&& phaseEvidence.verification
|
|
345
480
|
&& phaseEvidence.delivery
|
|
346
481
|
&& phaseEvidence.cleanup;
|
|
347
|
-
return
|
|
482
|
+
return persistedSchemaVersion !== 4
|
|
483
|
+
|| contract.semanticState === undefined
|
|
484
|
+
|| Object.prototype.hasOwnProperty.call(contract as object, 'status')
|
|
485
|
+
|| Object.prototype.hasOwnProperty.call(contract as object, 'allowedPaths')
|
|
486
|
+
|| Object.prototype.hasOwnProperty.call(contract as object, 'forbiddenPaths')
|
|
487
|
+
|| Boolean(legacyReviewGap);
|
|
348
488
|
}
|
|
349
489
|
|
|
350
|
-
function
|
|
351
|
-
return
|
|
352
|
-
?
|
|
353
|
-
:
|
|
490
|
+
function workContractAtPersistedSchema(contract: WorkContract, persistedSchemaVersion: number): WorkContract {
|
|
491
|
+
return contract.schemaVersion === persistedSchemaVersion
|
|
492
|
+
? contract
|
|
493
|
+
: { ...contract, schemaVersion: persistedSchemaVersion as WorkContract['schemaVersion'] };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function canonicalizeStoredWorkContract(contract: WorkContract, persistedSchemaVersion: number = contract.schemaVersion): WorkContract {
|
|
497
|
+
// The control-plane record envelope is the persisted schema authority. Some
|
|
498
|
+
// historical writers advanced the payload first, leaving it stamped v4 under
|
|
499
|
+
// a v3 envelope. Read it as the envelope declares so the one-way migration
|
|
500
|
+
// repairs the whole stored row rather than accepting a partially migrated
|
|
501
|
+
// contract indefinitely.
|
|
502
|
+
const persisted = workContractAtPersistedSchema(contract, persistedSchemaVersion);
|
|
503
|
+
return storedWorkContractNeedsMigration(persisted, persistedSchemaVersion)
|
|
504
|
+
? migrateLegacyWorkContract(persisted)
|
|
505
|
+
: validateCanonicalWorkContract(persisted);
|
|
354
506
|
}
|
|
355
507
|
|
|
356
508
|
/**
|
|
@@ -363,44 +515,45 @@ export function canonicalizeWorkContractForAuthority(contract: WorkContract): Wo
|
|
|
363
515
|
}
|
|
364
516
|
|
|
365
517
|
function normalizeWorkContractStore(store: WorkContractStore): WorkContractStore {
|
|
366
|
-
// v1
|
|
367
|
-
//
|
|
368
|
-
|
|
518
|
+
// v1-v3 rows are a one-way schema migration into v4. v4 rows are validated as-is;
|
|
519
|
+
// retired status is never inferred or exposed during normal reads.
|
|
520
|
+
// Removal condition: delete this migration branch after supported Controller Homes report zero schema<4 Work rows across one full release boundary.
|
|
521
|
+
return { schemaVersion: 4, updatedAt: store.updatedAt, contracts: store.contracts.map((contract) => canonicalizeStoredWorkContract(contract)) };
|
|
369
522
|
}
|
|
370
523
|
|
|
371
524
|
export function readWorkContractStore(options: WorkContractStoreOptions): WorkContractStore {
|
|
372
525
|
if (!sqliteBacked(options)) {
|
|
373
526
|
const raw = readJsonFile<WorkContractStore>(workContractStorePath(options), emptyWorkContractStore(nowIso(options)));
|
|
374
527
|
const normalized = normalizeWorkContractStore(raw);
|
|
375
|
-
if (raw.schemaVersion !==
|
|
528
|
+
if (raw.schemaVersion !== 4 || raw.contracts.some((contract) => storedWorkContractNeedsMigration(contract))) {
|
|
376
529
|
writeJsonAtomic(workContractStorePath(options), normalized);
|
|
377
530
|
}
|
|
378
531
|
return normalized;
|
|
379
532
|
}
|
|
380
533
|
const records = listControlPlaneRecords<WorkContract>(options.controllerHome, {
|
|
381
534
|
namespace: 'work_contract',
|
|
382
|
-
scope: options
|
|
535
|
+
scope: workContractStoreScopeKey(options),
|
|
383
536
|
limit: 5_000,
|
|
384
537
|
});
|
|
385
538
|
if (records.length > 0) {
|
|
386
539
|
const normalized = normalizeWorkContractStore({
|
|
387
|
-
schemaVersion:
|
|
540
|
+
schemaVersion: 4,
|
|
388
541
|
updatedAt: records[0]?.updatedAt ?? nowIso(options),
|
|
389
|
-
contracts: records.map((record) => record.value),
|
|
542
|
+
contracts: records.map((record) => workContractAtPersistedSchema(record.value, record.schemaVersion)),
|
|
390
543
|
});
|
|
391
544
|
const legacyRows = records
|
|
392
545
|
.map((record, index) => ({ record, contract: normalized.contracts[index]! }))
|
|
393
|
-
.filter(({ record }) => storedWorkContractNeedsMigration(record.value));
|
|
546
|
+
.filter(({ record }) => storedWorkContractNeedsMigration(record.value, record.schemaVersion));
|
|
394
547
|
if (legacyRows.length > 0) {
|
|
395
548
|
withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
396
549
|
for (const { record, contract } of legacyRows) {
|
|
397
550
|
writeControlPlaneRecordWithinTransaction(database, {
|
|
398
551
|
namespace: 'work_contract',
|
|
399
|
-
scope: options
|
|
552
|
+
scope: workContractStoreScopeKey(options),
|
|
400
553
|
key: contract.workId,
|
|
401
|
-
schemaVersion:
|
|
554
|
+
schemaVersion: 4,
|
|
402
555
|
value: contract,
|
|
403
|
-
action: '
|
|
556
|
+
action: 'work_contract_schema_v4_migrated',
|
|
404
557
|
expectedRevision: record.revision,
|
|
405
558
|
});
|
|
406
559
|
}
|
|
@@ -414,7 +567,7 @@ export function readWorkContractStore(options: WorkContractStoreOptions): WorkCo
|
|
|
414
567
|
const legacyRecord = readControlPlaneRecord<WorkContractStore>(
|
|
415
568
|
options.controllerHome,
|
|
416
569
|
'work_contract_store',
|
|
417
|
-
options
|
|
570
|
+
workContractStoreScopeKey(options),
|
|
418
571
|
'index',
|
|
419
572
|
);
|
|
420
573
|
const legacy = legacyRecord?.value
|
|
@@ -423,12 +576,12 @@ export function readWorkContractStore(options: WorkContractStoreOptions): WorkCo
|
|
|
423
576
|
if (normalized.contracts.length > 0) {
|
|
424
577
|
withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
425
578
|
for (const contract of normalized.contracts) {
|
|
426
|
-
if (readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', options
|
|
579
|
+
if (readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', workContractStoreScopeKey(options), contract.workId)) continue;
|
|
427
580
|
writeControlPlaneRecordWithinTransaction(database, {
|
|
428
581
|
namespace: 'work_contract',
|
|
429
|
-
scope: options
|
|
582
|
+
scope: workContractStoreScopeKey(options),
|
|
430
583
|
key: contract.workId,
|
|
431
|
-
schemaVersion:
|
|
584
|
+
schemaVersion: 4,
|
|
432
585
|
value: contract,
|
|
433
586
|
action: 'work_contract_legacy_import',
|
|
434
587
|
expectedRevision: null,
|
|
@@ -439,6 +592,62 @@ export function readWorkContractStore(options: WorkContractStoreOptions): WorkCo
|
|
|
439
592
|
return normalized;
|
|
440
593
|
}
|
|
441
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Read valid Work rows without allowing an unrelated corrupt historical row to
|
|
597
|
+
* poison a lineage-scoped ControllerRound projection. Invalid rows retain only
|
|
598
|
+
* their identity and explicit lineage references so callers can fail closed if
|
|
599
|
+
* the malformed row may belong to the requested lineage.
|
|
600
|
+
*/
|
|
601
|
+
export function readWorkContractLineageSnapshot(options: WorkContractStoreOptions): WorkContractLineageSnapshot {
|
|
602
|
+
if (!sqliteBacked(options)) {
|
|
603
|
+
const store = readJsonFile<WorkContractStore>(workContractStorePath(options), emptyWorkContractStore(nowIso(options)));
|
|
604
|
+
const contracts: WorkContract[] = [];
|
|
605
|
+
const invalid: WorkContractLineageSnapshot['invalid'] = [];
|
|
606
|
+
for (const raw of store.contracts) {
|
|
607
|
+
try {
|
|
608
|
+
contracts.push(canonicalizeStoredWorkContract(raw));
|
|
609
|
+
} catch (error) {
|
|
610
|
+
invalid.push(lineageInvalidWork(raw, error));
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return { contracts, invalid };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const records = listControlPlaneRecords<WorkContract>(options.controllerHome, {
|
|
617
|
+
namespace: 'work_contract',
|
|
618
|
+
scope: workContractStoreScopeKey(options),
|
|
619
|
+
limit: 5_000,
|
|
620
|
+
});
|
|
621
|
+
if (records.length === 0) return { contracts: readWorkContractStore(options).contracts, invalid: [] };
|
|
622
|
+
|
|
623
|
+
const contracts: WorkContract[] = [];
|
|
624
|
+
const invalid: WorkContractLineageSnapshot['invalid'] = [];
|
|
625
|
+
for (const record of records) {
|
|
626
|
+
try {
|
|
627
|
+
contracts.push(canonicalizeStoredWorkContract(record.value, record.schemaVersion));
|
|
628
|
+
} catch (error) {
|
|
629
|
+
invalid.push(lineageInvalidWork(record.value, error, record.key));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return { contracts, invalid };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function lineageInvalidWork(
|
|
636
|
+
raw: WorkContract,
|
|
637
|
+
error: unknown,
|
|
638
|
+
recordKey?: string,
|
|
639
|
+
): WorkContractLineageSnapshot['invalid'][number] {
|
|
640
|
+
const clean = (id: unknown): string | undefined => typeof id === 'string' && id.trim() ? id.trim() : undefined;
|
|
641
|
+
return {
|
|
642
|
+
workId: clean(raw.workId) ?? recordKey ?? '',
|
|
643
|
+
...(clean(raw.parentWorkId) ? { parentWorkId: clean(raw.parentWorkId) } : {}),
|
|
644
|
+
...(clean(raw.predecessorWorkId) ? { predecessorWorkId: clean(raw.predecessorWorkId) } : {}),
|
|
645
|
+
supersedes: Array.isArray(raw.supersedes) ? raw.supersedes.map(clean).filter((id): id is string => Boolean(id)) : [],
|
|
646
|
+
...(clean(raw.supersededBy) ? { supersededBy: clean(raw.supersededBy) } : {}),
|
|
647
|
+
error: error instanceof Error ? error.message : String(error),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
442
651
|
export function writeWorkContractStore(options: WorkContractStoreOptions, store: WorkContractStore): WorkContractStore {
|
|
443
652
|
if (!sqliteBacked(options)) {
|
|
444
653
|
writeJsonAtomic(workContractStorePath(options), store);
|
|
@@ -450,7 +659,7 @@ export function writeWorkContractStore(options: WorkContractStoreOptions, store:
|
|
|
450
659
|
const current = readControlPlaneRecordWithinTransaction<WorkContract>(
|
|
451
660
|
database,
|
|
452
661
|
'work_contract',
|
|
453
|
-
options
|
|
662
|
+
workContractStoreScopeKey(options),
|
|
454
663
|
contract.workId,
|
|
455
664
|
);
|
|
456
665
|
// SQLite is authoritative per Work row. Aggregate-store callers may still
|
|
@@ -459,9 +668,9 @@ export function writeWorkContractStore(options: WorkContractStoreOptions, store:
|
|
|
459
668
|
if (current && JSON.stringify(current.value) === JSON.stringify(value)) continue;
|
|
460
669
|
writeControlPlaneRecordWithinTransaction(database, {
|
|
461
670
|
namespace: 'work_contract',
|
|
462
|
-
scope: options
|
|
671
|
+
scope: workContractStoreScopeKey(options),
|
|
463
672
|
key: contract.workId,
|
|
464
|
-
schemaVersion:
|
|
673
|
+
schemaVersion: 4,
|
|
465
674
|
value,
|
|
466
675
|
action: 'work_contract_write',
|
|
467
676
|
expectedRevision: current?.revision ?? null,
|
|
@@ -475,44 +684,65 @@ function withWorkContractStoreWrite<T>(options: WorkContractStoreOptions, operat
|
|
|
475
684
|
if (!sqliteBacked(options)) return operation();
|
|
476
685
|
return withControllerLock(
|
|
477
686
|
options.controllerHome,
|
|
478
|
-
{ scope: 'global', resource: `work-contract-store-${sanitizeFileComponent(options
|
|
479
|
-
`work-contract-store:${options
|
|
687
|
+
{ scope: 'global', resource: `work-contract-store-${sanitizeFileComponent(workContractStoreScopeKey(options))}` },
|
|
688
|
+
`work-contract-store:${workContractStoreScopeKey(options)}`,
|
|
480
689
|
operation,
|
|
481
690
|
undefined,
|
|
482
691
|
5_000,
|
|
483
692
|
);
|
|
484
693
|
}
|
|
485
694
|
|
|
486
|
-
|
|
487
|
-
function assertCanonicalWorkAdmissionAllowed(
|
|
695
|
+
function withExactWorkContractWrite<T>(
|
|
488
696
|
options: WorkContractStoreOptions,
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
697
|
+
workIdInput: string,
|
|
698
|
+
operation: () => T,
|
|
699
|
+
): T {
|
|
700
|
+
if (!sqliteBacked(options)) return operation();
|
|
701
|
+
const workId = sanitizeFileComponent(workIdInput);
|
|
702
|
+
return withControllerLock(
|
|
493
703
|
options.controllerHome,
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
704
|
+
{ scope: 'global', resource: `work-contract-${workId}` },
|
|
705
|
+
`work-contract:${workId}`,
|
|
706
|
+
operation,
|
|
707
|
+
undefined,
|
|
708
|
+
5_000,
|
|
709
|
+
);
|
|
499
710
|
}
|
|
500
711
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
712
|
+
|
|
713
|
+
export function createWorkSemanticContext(options: WorkContractStoreOptions, input: CreateWorkSemanticInput): WorkContract {
|
|
714
|
+
const semanticOptions = options.scopeKey?.trim() || !options.repoId?.trim()
|
|
715
|
+
? options
|
|
716
|
+
: { ...options, scopeKey: options.repoId.trim() };
|
|
717
|
+
return createWorkContract(semanticOptions, {
|
|
718
|
+
workId: input.workId,
|
|
719
|
+
repoId: options.repoId?.trim() ?? '',
|
|
720
|
+
objective: input.objective,
|
|
721
|
+
acceptanceCriteria: [],
|
|
722
|
+
constraints: {},
|
|
723
|
+
workKind: 'investigation',
|
|
724
|
+
lifecycleRole: 'primary',
|
|
725
|
+
requestedBy: input.requestedBy ?? 'chatgpt',
|
|
726
|
+
checks: [],
|
|
727
|
+
...(input.requirementId?.trim() ? { requirementId: input.requirementId.trim() } : {}),
|
|
728
|
+
...(Number.isInteger(input.requirementRevision) ? { requirementRevision: input.requirementRevision } : {}),
|
|
729
|
+
...(input.planId?.trim() ? { planId: input.planId.trim() } : {}),
|
|
730
|
+
...(Number.isInteger(input.planRevision) ? { planRevision: input.planRevision } : {}),
|
|
731
|
+
...(input.semanticParentWorkId !== undefined ? { semanticParentWorkId: input.semanticParentWorkId.trim() || undefined } : {}),
|
|
732
|
+
...(input.dependsOnWorkIds !== undefined ? { dependsOnWorkIds: normalizeWorkObjectiveRelationIds(input.dependsOnWorkIds) } : {}),
|
|
733
|
+
...(input.requestId?.trim() ? { requestId: input.requestId.trim() } : {}),
|
|
734
|
+
});
|
|
509
735
|
}
|
|
510
736
|
|
|
511
737
|
export function createWorkContract(options: WorkContractStoreOptions, input: CreateWorkContractInput): WorkContract {
|
|
512
|
-
|
|
513
|
-
|
|
738
|
+
// Thin semantic Work is authored context. Repository execution and concurrency
|
|
739
|
+
// are fenced by placement/resource ownership rather than a global migration gate.
|
|
740
|
+
const runtimeInput = input as CreateWorkContractInput & Record<string, unknown>;
|
|
741
|
+
if (Object.prototype.hasOwnProperty.call(runtimeInput, 'status')) throw new Error('WORK_RETIRED_FIELD_REJECTED: status');
|
|
742
|
+
for (const field of ['semanticState', 'semanticRevision', 'semanticUpdatedAt']) {
|
|
743
|
+
if (Object.prototype.hasOwnProperty.call(runtimeInput, field)) throw new Error(`WORK_SEMANTIC_FIELDS_REQUIRE_REVISION_API: ${field}`);
|
|
514
744
|
}
|
|
515
|
-
if (input.
|
|
745
|
+
if (input.completionReceipt || input.completionOutcome) {
|
|
516
746
|
throw new Error('WORK_COMPLETION_REQUIRES_RECORD_API');
|
|
517
747
|
}
|
|
518
748
|
const create = (): WorkContract => {
|
|
@@ -521,7 +751,7 @@ export function createWorkContract(options: WorkContractStoreOptions, input: Cre
|
|
|
521
751
|
const predecessorWorkId = input.predecessorWorkId ? sanitizeFileComponent(input.predecessorWorkId) : undefined;
|
|
522
752
|
if (predecessorWorkId === workId) throw new Error('WORK_PREDECESSOR_SELF_REFERENCE');
|
|
523
753
|
const contract: WorkContract = validateWorkSemantics({
|
|
524
|
-
schemaVersion:
|
|
754
|
+
schemaVersion: 4,
|
|
525
755
|
workId,
|
|
526
756
|
scopeRef: semanticScopeRefForWork({
|
|
527
757
|
workId,
|
|
@@ -530,7 +760,8 @@ export function createWorkContract(options: WorkContractStoreOptions, input: Cre
|
|
|
530
760
|
planId: input.planId,
|
|
531
761
|
planStepId: input.planStepId,
|
|
532
762
|
}),
|
|
533
|
-
executionPlacement: input.executionPlacement
|
|
763
|
+
executionPlacement: input.executionPlacement
|
|
764
|
+
?? (input.repoId?.trim() ? executionPlacementForWork({ repoId: input.repoId, checkoutId: input.checkoutId }) : undefined),
|
|
534
765
|
repoId: input.repoId,
|
|
535
766
|
checkoutId: input.checkoutId,
|
|
536
767
|
principalId: input.principalId,
|
|
@@ -538,39 +769,41 @@ export function createWorkContract(options: WorkContractStoreOptions, input: Cre
|
|
|
538
769
|
baseRevision: input.baseRevision,
|
|
539
770
|
repositoryBaseState: input.repositoryBaseState,
|
|
540
771
|
workspaceFingerprint: input.workspaceFingerprint,
|
|
541
|
-
routeDecisionFingerprint: input.routeDecisionFingerprint,
|
|
542
|
-
routeDecision: input.routeDecision,
|
|
543
|
-
mode: input.mode,
|
|
544
772
|
objective: input.objective.slice(0, 2_000),
|
|
773
|
+
semanticRevision: 1,
|
|
774
|
+
semanticUpdatedAt: input.updatedAt ?? at,
|
|
775
|
+
semanticState: 'open',
|
|
545
776
|
acceptanceCriteria: (input.acceptanceCriteria ?? []).slice(0, 20).map((item) => item.slice(0, 500)),
|
|
546
|
-
constraints: input.constraints ?? {
|
|
777
|
+
constraints: input.constraints ?? {},
|
|
547
778
|
risk: input.risk ?? 'medium',
|
|
548
779
|
engineeringContext: input.engineeringContext,
|
|
549
780
|
workKind: input.workKind ?? 'repository_change',
|
|
550
781
|
lifecycleRole: input.lifecycleRole ?? 'primary',
|
|
551
782
|
parentWorkId: input.parentWorkId?.trim() || undefined,
|
|
783
|
+
semanticParentWorkId: input.semanticParentWorkId?.trim() || undefined,
|
|
784
|
+
dependsOnWorkIds: normalizeWorkObjectiveRelationIds(input.dependsOnWorkIds),
|
|
552
785
|
predecessorWorkId: predecessorWorkId && predecessorWorkId !== 'unknown' ? predecessorWorkId : undefined,
|
|
553
786
|
supersedes: input.supersedes?.map((value) => sanitizeFileComponent(value)).filter((value) => value !== 'unknown').slice(0, 50),
|
|
554
787
|
supersededBy: input.supersededBy ? sanitizeFileComponent(input.supersededBy) : undefined,
|
|
555
788
|
supersessionReason: input.supersessionReason?.trim().slice(0, 500),
|
|
556
|
-
dispatchState: input.dispatchState ?? initialLifecycleForNewWork(
|
|
557
|
-
evidenceState: input.evidenceState ?? initialLifecycleForNewWork(
|
|
789
|
+
dispatchState: input.dispatchState ?? initialLifecycleForNewWork().dispatchState,
|
|
790
|
+
evidenceState: input.evidenceState ?? initialLifecycleForNewWork().evidenceState,
|
|
558
791
|
completionOutcome: input.completionOutcome,
|
|
559
|
-
phase: input.phase ?? initialLifecycleForNewWork(
|
|
792
|
+
phase: input.phase ?? initialLifecycleForNewWork().phase,
|
|
560
793
|
phaseEvidence: initialPhaseEvidenceForNewWork({
|
|
561
|
-
phase: input.phase ?? initialLifecycleForNewWork(
|
|
562
|
-
status: input.status ?? 'open',
|
|
794
|
+
phase: input.phase ?? initialLifecycleForNewWork().phase,
|
|
563
795
|
evidenceRefs: input.evidenceRefs ?? [],
|
|
564
796
|
updatedAt: input.updatedAt ?? at,
|
|
565
797
|
}),
|
|
566
798
|
completionReceipt: input.completionReceipt,
|
|
567
|
-
status: input.status ?? 'open',
|
|
568
799
|
createdAt: at,
|
|
569
800
|
updatedAt: input.updatedAt ?? at,
|
|
570
801
|
issueId: input.issueId,
|
|
571
802
|
taskId: input.taskId,
|
|
572
803
|
requirementId: input.requirementId,
|
|
804
|
+
requirementRevision: input.requirementRevision,
|
|
573
805
|
planId: input.planId,
|
|
806
|
+
planRevision: input.planRevision,
|
|
574
807
|
planStepId: input.planStepId,
|
|
575
808
|
planSourceRevision: input.planSourceRevision,
|
|
576
809
|
scopeSummary: input.scopeSummary?.slice(0, 1_000),
|
|
@@ -580,13 +813,12 @@ export function createWorkContract(options: WorkContractStoreOptions, input: Cre
|
|
|
580
813
|
actualChangedPaths: [...new Set(input.scopeEvidence.actualChangedPaths)].slice(0, 500),
|
|
581
814
|
recordedAt: input.scopeEvidence.recordedAt,
|
|
582
815
|
} : undefined,
|
|
583
|
-
allowedPaths: (input.allowedPaths ?? []).slice(0, 50),
|
|
584
|
-
forbiddenPaths: (input.forbiddenPaths ?? []).slice(0, 50),
|
|
585
816
|
checks: (input.checks ?? []).slice(0, 30),
|
|
586
|
-
driver: input.driver ?? defaultDriver(input.mode),
|
|
587
817
|
worktreePolicy: input.worktreePolicy ?? {
|
|
588
|
-
required: input.
|
|
589
|
-
reason: input.
|
|
818
|
+
required: input.constraints?.workspaceMode === 'isolated',
|
|
819
|
+
reason: input.constraints?.workspaceMode === 'isolated'
|
|
820
|
+
? 'Typed workspace placement requires an isolated worktree.'
|
|
821
|
+
: undefined,
|
|
590
822
|
},
|
|
591
823
|
evidencePolicy: input.evidencePolicy ?? {
|
|
592
824
|
defaultDetailLevel: 'summary',
|
|
@@ -616,14 +848,21 @@ export function createWorkContract(options: WorkContractStoreOptions, input: Cre
|
|
|
616
848
|
|
|
617
849
|
if (sqliteBacked(options)) {
|
|
618
850
|
withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
619
|
-
|
|
851
|
+
const scope = workContractStoreScopeKey(options);
|
|
852
|
+
if (readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', scope, contract.workId)) {
|
|
620
853
|
throw new Error(`work contract already exists: ${contract.workId}`);
|
|
621
854
|
}
|
|
855
|
+
if (contract.semanticParentWorkId?.trim() || contract.dependsOnWorkIds?.length) {
|
|
856
|
+
const relationContracts = listControlPlaneRecordsWithinTransaction<WorkContract>(database, {
|
|
857
|
+
namespace: 'work_contract', scope, limit: 5_000,
|
|
858
|
+
}).map((record) => canonicalizeStoredWorkContract(record.value));
|
|
859
|
+
validateWorkObjectiveRelationIntegrity(contract, relationContracts);
|
|
860
|
+
}
|
|
622
861
|
writeControlPlaneRecordWithinTransaction(database, {
|
|
623
862
|
namespace: 'work_contract',
|
|
624
|
-
scope
|
|
863
|
+
scope,
|
|
625
864
|
key: contract.workId,
|
|
626
|
-
schemaVersion:
|
|
865
|
+
schemaVersion: 4,
|
|
627
866
|
value: contract,
|
|
628
867
|
action: 'work_contract_created',
|
|
629
868
|
expectedRevision: null,
|
|
@@ -635,15 +874,19 @@ export function createWorkContract(options: WorkContractStoreOptions, input: Cre
|
|
|
635
874
|
if (store.contracts.some((existing) => existing.workId === contract.workId)) {
|
|
636
875
|
throw new Error(`work contract already exists: ${contract.workId}`);
|
|
637
876
|
}
|
|
877
|
+
if (contract.semanticParentWorkId?.trim() || contract.dependsOnWorkIds?.length) {
|
|
878
|
+
validateWorkObjectiveRelationIntegrity(contract, store.contracts);
|
|
879
|
+
}
|
|
638
880
|
const nextStore: WorkContractStore = {
|
|
639
|
-
schemaVersion:
|
|
881
|
+
schemaVersion: 4,
|
|
640
882
|
updatedAt: contract.updatedAt,
|
|
641
883
|
contracts: [contract, ...store.contracts],
|
|
642
884
|
};
|
|
643
885
|
writeWorkContractStore(options, nextStore);
|
|
644
886
|
return contract;
|
|
645
887
|
};
|
|
646
|
-
|
|
888
|
+
const relationAware = Boolean(input.semanticParentWorkId?.trim() || input.dependsOnWorkIds?.length);
|
|
889
|
+
return withExactWorkContractWrite(options, input.workId, () => relationAware ? withWorkContractStoreWrite(options, create) : create());
|
|
647
890
|
}
|
|
648
891
|
|
|
649
892
|
interface WorkRequestIndexRecord {
|
|
@@ -700,7 +943,7 @@ export function acceptSubmittedWorkContract(
|
|
|
700
943
|
if (parentWorkId) {
|
|
701
944
|
const parent = getWorkContract({ controllerHome: home, repoId: input.repoId, now: options.now }, parentWorkId);
|
|
702
945
|
if (!parent) throw new Error(`PARENT_WORK_NOT_FOUND: ${parentWorkId}`);
|
|
703
|
-
if (
|
|
946
|
+
if (semanticWorkState(parent) !== 'open') throw new Error(`PARENT_WORK_TERMINAL: ${parentWorkId}:${semanticWorkState(parent)}`);
|
|
704
947
|
if ((parent.lifecycleRole ?? 'primary') !== 'primary') throw new Error(`PARENT_WORK_NOT_PRIMARY: ${parentWorkId}`);
|
|
705
948
|
}
|
|
706
949
|
const lockId = createHash('sha256').update(requestId).digest('hex').slice(0, 24);
|
|
@@ -719,30 +962,18 @@ export function acceptSubmittedWorkContract(
|
|
|
719
962
|
repoId: input.repoId,
|
|
720
963
|
principalId: input.principalId,
|
|
721
964
|
controllerInstanceId: input.controllerInstanceId,
|
|
722
|
-
mode: input.mode ?? 'direct_control',
|
|
723
965
|
lifecycleRole: 'execution_child',
|
|
724
966
|
parentWorkId,
|
|
725
967
|
objective: (input.objective ?? `Typed operation ${input.operation.name}`).slice(0, 2_000),
|
|
726
968
|
acceptanceCriteria: input.acceptanceCriteria ?? [],
|
|
727
969
|
workKind: input.workKind,
|
|
728
970
|
risk: input.risk,
|
|
729
|
-
constraints: input.constraints ?? {
|
|
730
|
-
allowedPaths: input.allowedPaths ?? [],
|
|
731
|
-
forbiddenPaths: input.forbiddenPaths ?? [],
|
|
971
|
+
constraints: input.constraints ?? {},
|
|
732
972
|
checks: input.checks ?? [],
|
|
733
973
|
requestedBy: input.requestedBy ?? 'chatgpt',
|
|
734
|
-
status: 'open',
|
|
735
974
|
requestId,
|
|
736
975
|
submittedOperation: input.operation,
|
|
737
|
-
|
|
738
|
-
suggestedNextActions: [{
|
|
739
|
-
label: 'Claim controller ownership',
|
|
740
|
-
tool: 'rh_work',
|
|
741
|
-
operation: 'controller_claim',
|
|
742
|
-
risk: 'readonly',
|
|
743
|
-
confidence: 'high',
|
|
744
|
-
reason: 'Claim this Work before launching an external SuperController or Process Runtime command.',
|
|
745
|
-
}],
|
|
976
|
+
suggestedNextActions: [],
|
|
746
977
|
});
|
|
747
978
|
writeJsonAtomic(recordPath, { requestId, repoId: input.repoId, workId: contract.workId, semanticKey, createdAt: contract.createdAt } satisfies WorkRequestIndexRecord);
|
|
748
979
|
return { contract, deduplicated: false };
|
|
@@ -750,21 +981,31 @@ export function acceptSubmittedWorkContract(
|
|
|
750
981
|
}
|
|
751
982
|
|
|
752
983
|
export function isCurrentWorkContract(contract: WorkContract): boolean {
|
|
753
|
-
return
|
|
984
|
+
return semanticWorkState(contract) === 'open'
|
|
754
985
|
&& !contract.supersededBy?.trim()
|
|
755
986
|
&& contract.workKind !== 'superseded'
|
|
756
987
|
&& contract.completionOutcome !== 'superseded';
|
|
757
988
|
}
|
|
758
989
|
|
|
759
990
|
export function listWorkContracts(options: ListWorkContractOptions): WorkContract[] {
|
|
760
|
-
const
|
|
761
|
-
const status = options.status ?? 'active';
|
|
991
|
+
const state = options.state ?? 'active';
|
|
762
992
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 50), 100));
|
|
993
|
+
if (state === 'active' && sqliteBacked(options)) {
|
|
994
|
+
const active = readActiveWorkCandidates({ ...options, limit });
|
|
995
|
+
if (active.invalid.length > 0) {
|
|
996
|
+
// Preserve the aggregate list API's fail-closed semantics. Callers that
|
|
997
|
+
// intentionally need row-isolated corruption diagnostics use
|
|
998
|
+
// readActiveWorkCandidates() directly and can inspect every invalid row.
|
|
999
|
+
throw new Error(active.invalid[0]!.error);
|
|
1000
|
+
}
|
|
1001
|
+
return active.contracts;
|
|
1002
|
+
}
|
|
1003
|
+
const store = readWorkContractStore(options);
|
|
763
1004
|
return store.contracts
|
|
764
1005
|
.filter((contract) => {
|
|
765
|
-
if (
|
|
766
|
-
if (
|
|
767
|
-
return contract
|
|
1006
|
+
if (state === 'all') return true;
|
|
1007
|
+
if (state === 'active') return isCurrentWorkContract(contract);
|
|
1008
|
+
return semanticWorkState(contract) === state;
|
|
768
1009
|
})
|
|
769
1010
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
770
1011
|
.slice(0, limit);
|
|
@@ -784,13 +1025,12 @@ function workExecutionSemanticScopeKeys(contract: WorkContract): string[] {
|
|
|
784
1025
|
function workExecutionIsolation(contract: WorkContract): 'shared' | 'isolated' {
|
|
785
1026
|
return contract.worktreePolicy?.required === true
|
|
786
1027
|
|| contract.constraints?.workspaceMode === 'isolated'
|
|
787
|
-
|| contract.constraints?.requireWorktree === true
|
|
788
1028
|
? 'isolated'
|
|
789
1029
|
: 'shared';
|
|
790
1030
|
}
|
|
791
1031
|
|
|
792
1032
|
function rawWorkMayBeCurrent(contract: WorkContract): boolean {
|
|
793
|
-
return
|
|
1033
|
+
return semanticWorkState(contract) === 'open'
|
|
794
1034
|
&& !contract.supersededBy?.trim()
|
|
795
1035
|
&& contract.workKind !== 'superseded'
|
|
796
1036
|
&& contract.completionOutcome !== 'superseded';
|
|
@@ -805,7 +1045,7 @@ function rawWorkMayBeCurrent(contract: WorkContract): boolean {
|
|
|
805
1045
|
*/
|
|
806
1046
|
export function initializeWorkCandidateIndex(controllerHome: string): void {
|
|
807
1047
|
initializeControlPlanePayloadTextExclusionIndex(controllerHome, {
|
|
808
|
-
namespace: 'work_contract', field: '
|
|
1048
|
+
namespace: 'work_contract', field: 'semanticState', excludedValues: TERMINAL_SEMANTIC_WORK_STATES,
|
|
809
1049
|
});
|
|
810
1050
|
}
|
|
811
1051
|
|
|
@@ -814,24 +1054,36 @@ export function readActiveWorkCandidates(
|
|
|
814
1054
|
): ActiveWorkCandidateSnapshot {
|
|
815
1055
|
const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 1_000), 1_000));
|
|
816
1056
|
if (!sqliteBacked(options)) {
|
|
817
|
-
return { contracts: listWorkContracts({ ...options,
|
|
1057
|
+
return { contracts: listWorkContracts({ ...options, state: 'active', limit }), invalid: [] };
|
|
818
1058
|
}
|
|
1059
|
+
const requestedScope = workContractStoreScopeKey(options);
|
|
819
1060
|
const records = listControlPlaneRecordsExcludingPayloadTextValues<WorkContract>(options.controllerHome, {
|
|
820
1061
|
namespace: 'work_contract',
|
|
821
|
-
scope:
|
|
822
|
-
field: '
|
|
823
|
-
excludedValues:
|
|
1062
|
+
scope: requestedScope,
|
|
1063
|
+
field: 'semanticState',
|
|
1064
|
+
excludedValues: TERMINAL_SEMANTIC_WORK_STATES,
|
|
824
1065
|
limit: 5_000,
|
|
825
1066
|
});
|
|
1067
|
+
const semanticAuthorityWorkIds = requestedScope === SEMANTIC_SCOPE_KEY
|
|
1068
|
+
? new Set<string>()
|
|
1069
|
+
: new Set(listControlPlaneRecordKeys(options.controllerHome, {
|
|
1070
|
+
namespace: 'work_contract',
|
|
1071
|
+
scope: SEMANTIC_SCOPE_KEY,
|
|
1072
|
+
limit: 5_000,
|
|
1073
|
+
}));
|
|
826
1074
|
const contracts: WorkContract[] = [];
|
|
827
1075
|
const invalid: InvalidActiveWorkCandidate[] = [];
|
|
828
1076
|
const migrations: Array<{ record: (typeof records)[number]; contract: WorkContract }> = [];
|
|
829
1077
|
for (const record of records) {
|
|
1078
|
+
// Exact Work reads already prefer semantic scope over repository-scoped
|
|
1079
|
+
// compatibility rows. Collection reads must preserve the same one-authority
|
|
1080
|
+
// rule or a stale open compatibility shadow can resurrect terminal Work.
|
|
1081
|
+
if (semanticAuthorityWorkIds.has(record.key)) continue;
|
|
830
1082
|
const raw = record.value;
|
|
831
1083
|
if (!rawWorkMayBeCurrent(raw)) continue;
|
|
832
1084
|
try {
|
|
833
|
-
const normalized = canonicalizeStoredWorkContract(raw);
|
|
834
|
-
if (storedWorkContractNeedsMigration(raw)) migrations.push({ record, contract: normalized });
|
|
1085
|
+
const normalized = canonicalizeStoredWorkContract(raw, record.schemaVersion);
|
|
1086
|
+
if (storedWorkContractNeedsMigration(raw, record.schemaVersion)) migrations.push({ record, contract: normalized });
|
|
835
1087
|
if (isCurrentWorkContract(normalized)) contracts.push(normalized);
|
|
836
1088
|
} catch (error) {
|
|
837
1089
|
invalid.push({
|
|
@@ -852,11 +1104,11 @@ export function readActiveWorkCandidates(
|
|
|
852
1104
|
for (const { record, contract } of migrations) {
|
|
853
1105
|
writeControlPlaneRecordWithinTransaction(database, {
|
|
854
1106
|
namespace: 'work_contract',
|
|
855
|
-
scope: options
|
|
1107
|
+
scope: workContractStoreScopeKey(options),
|
|
856
1108
|
key: contract.workId,
|
|
857
|
-
schemaVersion:
|
|
1109
|
+
schemaVersion: 4,
|
|
858
1110
|
value: contract,
|
|
859
|
-
action: '
|
|
1111
|
+
action: 'work_contract_schema_v4_migrated',
|
|
860
1112
|
expectedRevision: record.revision,
|
|
861
1113
|
});
|
|
862
1114
|
}
|
|
@@ -867,35 +1119,201 @@ export function readActiveWorkCandidates(
|
|
|
867
1119
|
return { contracts: contracts.slice(0, limit), invalid };
|
|
868
1120
|
}
|
|
869
1121
|
|
|
1122
|
+
export function listWorkSemanticRevisionRecords(
|
|
1123
|
+
options: WorkContractStoreOptions,
|
|
1124
|
+
workId?: string,
|
|
1125
|
+
limit = 200,
|
|
1126
|
+
): WorkSemanticRevisionRecord[] {
|
|
1127
|
+
const normalizedId = workId ? sanitizeFileComponent(workId) : undefined;
|
|
1128
|
+
const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 1000));
|
|
1129
|
+
const authoritativeOptions = normalizedId
|
|
1130
|
+
? authoritativeExistingWorkStoreOptions(options, normalizedId)
|
|
1131
|
+
: options;
|
|
1132
|
+
const records = sqliteBacked(authoritativeOptions)
|
|
1133
|
+
? listControlPlaneRecords<WorkSemanticRevisionRecord>(authoritativeOptions.controllerHome, {
|
|
1134
|
+
namespace: 'work_semantic_revision', scope: workContractStoreScopeKey(authoritativeOptions), limit: 5_000,
|
|
1135
|
+
}).map((record) => record.value)
|
|
1136
|
+
: readWorkSemanticRevisionStore(authoritativeOptions).records;
|
|
1137
|
+
return records
|
|
1138
|
+
.filter((record) => !normalizedId || record.workId === normalizedId)
|
|
1139
|
+
.map((record) => ({
|
|
1140
|
+
...record,
|
|
1141
|
+
semanticScope: record.semanticScope ?? semanticScopeRefForWork({
|
|
1142
|
+
workId: record.workId,
|
|
1143
|
+
requirementId: record.requirementId,
|
|
1144
|
+
planId: record.planId,
|
|
1145
|
+
planStepId: undefined,
|
|
1146
|
+
}),
|
|
1147
|
+
}))
|
|
1148
|
+
.sort((left, right) => right.revision - left.revision)
|
|
1149
|
+
.slice(0, boundedLimit);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function authoritativeExistingWorkStoreOptions(
|
|
1153
|
+
options: WorkContractStoreOptions,
|
|
1154
|
+
workIdInput: string,
|
|
1155
|
+
): WorkContractStoreOptions {
|
|
1156
|
+
if (!sqliteBacked(options)) return options;
|
|
1157
|
+
const requestedScope = workContractStoreScopeKey(options);
|
|
1158
|
+
if (requestedScope === SEMANTIC_SCOPE_KEY) return options;
|
|
1159
|
+
const workId = sanitizeFileComponent(workIdInput);
|
|
1160
|
+
const semantic = readControlPlaneRecord<WorkContract>(
|
|
1161
|
+
options.controllerHome,
|
|
1162
|
+
'work_contract',
|
|
1163
|
+
SEMANTIC_SCOPE_KEY,
|
|
1164
|
+
workId,
|
|
1165
|
+
);
|
|
1166
|
+
return semantic ? { ...options, scopeKey: SEMANTIC_SCOPE_KEY } : options;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export function reviseWorkSemanticContext(
|
|
1170
|
+
options: WorkContractStoreOptions,
|
|
1171
|
+
workIdInput: string,
|
|
1172
|
+
input: ReviseWorkSemanticInput,
|
|
1173
|
+
): WorkContract {
|
|
1174
|
+
if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 1) throw new Error('WORK_EXPECTED_REVISION_INVALID');
|
|
1175
|
+
return withExactWorkContractWrite(options, workIdInput, () => {
|
|
1176
|
+
const authoritativeOptions = authoritativeExistingWorkStoreOptions(options, workIdInput);
|
|
1177
|
+
return withWorkContractStoreWrite(authoritativeOptions, () => {
|
|
1178
|
+
const workId = sanitizeFileComponent(workIdInput);
|
|
1179
|
+
const relationRevisionRequested = input.semanticParentWorkId !== undefined || input.dependsOnWorkIds !== undefined;
|
|
1180
|
+
const applyRevision = (current: WorkContract, at: string, relationContracts: readonly WorkContract[] = []): WorkContract => {
|
|
1181
|
+
const semanticRevision = currentWorkSemanticRevision(current);
|
|
1182
|
+
if (semanticRevision !== input.expectedRevision) {
|
|
1183
|
+
throw new Error(`WORK_REVISION_CONFLICT:${workId}:expected=${input.expectedRevision}:actual=${semanticRevision}`);
|
|
1184
|
+
}
|
|
1185
|
+
const currentSemanticState = semanticWorkState(current);
|
|
1186
|
+
if (currentSemanticState !== 'open' && input.state === 'open') {
|
|
1187
|
+
throw new Error(`WORK_SEMANTIC_REOPEN_FORBIDDEN:${workId}:${currentSemanticState}`);
|
|
1188
|
+
}
|
|
1189
|
+
const objective = input.objective === undefined ? current.objective : String(input.objective).trim().slice(0, 2_000);
|
|
1190
|
+
if (!objective) throw new Error('WORK_OBJECTIVE_REQUIRED');
|
|
1191
|
+
const positiveRevision = (value: number | undefined, code: string): number | undefined => {
|
|
1192
|
+
if (value === undefined) return undefined;
|
|
1193
|
+
if (!Number.isInteger(value) || value < 1) throw new Error(code);
|
|
1194
|
+
return value;
|
|
1195
|
+
};
|
|
1196
|
+
const nextSemanticState = input.state ?? currentSemanticState;
|
|
1197
|
+
if (currentSemanticState !== 'open' && (input.semanticParentWorkId !== undefined || input.dependsOnWorkIds !== undefined)) {
|
|
1198
|
+
throw new Error(`WORK_OBJECTIVE_RELATION_TERMINAL_IMMUTABLE:${workId}:${currentSemanticState}`);
|
|
1199
|
+
}
|
|
1200
|
+
const next = validateWorkSemantics({
|
|
1201
|
+
...current,
|
|
1202
|
+
objective,
|
|
1203
|
+
semanticRevision: semanticRevision + 1,
|
|
1204
|
+
semanticUpdatedAt: at,
|
|
1205
|
+
semanticState: nextSemanticState,
|
|
1206
|
+
...(input.requirementRevision !== undefined ? { requirementRevision: positiveRevision(input.requirementRevision, 'WORK_REQUIREMENT_REVISION_INVALID') } : {}),
|
|
1207
|
+
...(input.planRevision !== undefined ? { planRevision: positiveRevision(input.planRevision, 'WORK_PLAN_REVISION_INVALID') } : {}),
|
|
1208
|
+
...(input.semanticParentWorkId !== undefined ? { semanticParentWorkId: input.semanticParentWorkId.trim() || undefined } : {}),
|
|
1209
|
+
...(input.dependsOnWorkIds !== undefined ? { dependsOnWorkIds: normalizeWorkObjectiveRelationIds(input.dependsOnWorkIds) } : {}),
|
|
1210
|
+
...(input.resultRefs !== undefined ? { semanticResultRefs: [...new Set(input.resultRefs.map(String).map((value) => value.trim()).filter(Boolean))].slice(0, 100) } : {}),
|
|
1211
|
+
updatedAt: at,
|
|
1212
|
+
});
|
|
1213
|
+
if (relationRevisionRequested) validateWorkObjectiveRelationIntegrity(next, relationContracts);
|
|
1214
|
+
return next;
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
if (sqliteBacked(authoritativeOptions)) {
|
|
1218
|
+
return withControlPlaneTransaction(authoritativeOptions.controllerHome, (database) => {
|
|
1219
|
+
const scope = workContractStoreScopeKey(authoritativeOptions);
|
|
1220
|
+
const currentRecord = readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', scope, workId);
|
|
1221
|
+
if (!currentRecord) throw new Error(`work contract not found: ${workId}`);
|
|
1222
|
+
const current = canonicalizeStoredWorkContract(currentRecord.value);
|
|
1223
|
+
const at = nowIso(authoritativeOptions);
|
|
1224
|
+
const relationContracts = relationRevisionRequested
|
|
1225
|
+
? listControlPlaneRecordsWithinTransaction<WorkContract>(database, {
|
|
1226
|
+
namespace: 'work_contract', scope, limit: 5_000,
|
|
1227
|
+
}).map((record) => canonicalizeStoredWorkContract(record.value))
|
|
1228
|
+
: [];
|
|
1229
|
+
const next = applyRevision(current, at, relationContracts);
|
|
1230
|
+
const semanticRevision = currentWorkSemanticRevision(current);
|
|
1231
|
+
const revisionKey = workSemanticRevisionKey(workId, semanticRevision);
|
|
1232
|
+
if (!readControlPlaneRecordWithinTransaction<WorkSemanticRevisionRecord>(database, 'work_semantic_revision', scope, revisionKey)) {
|
|
1233
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
1234
|
+
namespace: 'work_semantic_revision', scope, key: revisionKey, schemaVersion: 1,
|
|
1235
|
+
value: { schemaVersion: 1, ...workSemanticView(current), recordedAt: at },
|
|
1236
|
+
action: 'work_semantic_revision_archived', expectedRevision: null,
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
return writeControlPlaneRecordWithinTransaction(database, {
|
|
1240
|
+
namespace: 'work_contract', scope, key: workId, schemaVersion: 4,
|
|
1241
|
+
value: next, action: 'work_semantic_revised', expectedRevision: currentRecord.revision,
|
|
1242
|
+
}).value;
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Semantic CAS validation must be side-effect free. The public read path may
|
|
1247
|
+
// persist a one-way legacy migration, so normalize the file-backed store only
|
|
1248
|
+
// in memory here and write it exactly once after the requested revision has
|
|
1249
|
+
// been validated successfully.
|
|
1250
|
+
const rawStore = readJsonFile<WorkContractStore>(
|
|
1251
|
+
workContractStorePath(authoritativeOptions),
|
|
1252
|
+
emptyWorkContractStore(nowIso(authoritativeOptions)),
|
|
1253
|
+
);
|
|
1254
|
+
const store = normalizeWorkContractStore(rawStore);
|
|
1255
|
+
const index = store.contracts.findIndex((contract) => contract.workId === workId);
|
|
1256
|
+
if (index < 0) throw new Error(`work contract not found: ${workId}`);
|
|
1257
|
+
const current = store.contracts[index]!;
|
|
1258
|
+
const at = nowIso(authoritativeOptions);
|
|
1259
|
+
const next = applyRevision(current, at, relationRevisionRequested ? store.contracts : []);
|
|
1260
|
+
const archived = { schemaVersion: 1 as const, ...workSemanticView(current), recordedAt: at };
|
|
1261
|
+
const history = readWorkSemanticRevisionStore(authoritativeOptions);
|
|
1262
|
+
if (!history.records.some((record) => record.workId === workId && record.revision === archived.revision)) {
|
|
1263
|
+
writeJsonAtomic(workSemanticRevisionStorePath(authoritativeOptions), { schemaVersion: 1, records: [...history.records, archived].slice(-5_000) });
|
|
1264
|
+
}
|
|
1265
|
+
const contracts = [...store.contracts];
|
|
1266
|
+
contracts[index] = next;
|
|
1267
|
+
writeWorkContractStore(authoritativeOptions, { schemaVersion: 4, updatedAt: at, contracts });
|
|
1268
|
+
return next;
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function readExactSqliteWorkContract(
|
|
1274
|
+
options: WorkContractStoreOptions & { controllerHome: string },
|
|
1275
|
+
scope: string,
|
|
1276
|
+
sanitizedId: string,
|
|
1277
|
+
): WorkContract | undefined {
|
|
1278
|
+
const exact = readControlPlaneRecord<WorkContract>(options.controllerHome, 'work_contract', scope, sanitizedId);
|
|
1279
|
+
if (!exact) return undefined;
|
|
1280
|
+
const canonical = canonicalizeStoredWorkContract(exact.value, exact.schemaVersion);
|
|
1281
|
+
if (storedWorkContractNeedsMigration(exact.value, exact.schemaVersion)) {
|
|
1282
|
+
withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
1283
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
1284
|
+
namespace: 'work_contract',
|
|
1285
|
+
scope,
|
|
1286
|
+
key: canonical.workId,
|
|
1287
|
+
schemaVersion: 4,
|
|
1288
|
+
value: canonical,
|
|
1289
|
+
action: 'work_contract_schema_v4_migrated',
|
|
1290
|
+
expectedRevision: exact.revision,
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
return canonical;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
870
1297
|
export function getWorkContract(options: WorkContractStoreOptions, workId: string): WorkContract | undefined {
|
|
871
1298
|
const sanitizedId = sanitizeFileComponent(workId);
|
|
872
1299
|
if (!sqliteBacked(options)) {
|
|
873
1300
|
return readWorkContractStore(options).contracts.find((contract) => contract.workId === sanitizedId);
|
|
874
1301
|
}
|
|
875
|
-
const
|
|
876
|
-
if (
|
|
877
|
-
const
|
|
878
|
-
if (
|
|
879
|
-
withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
880
|
-
writeControlPlaneRecordWithinTransaction(database, {
|
|
881
|
-
namespace: 'work_contract',
|
|
882
|
-
scope: options.repoId,
|
|
883
|
-
key: canonical.workId,
|
|
884
|
-
schemaVersion: 3,
|
|
885
|
-
value: canonical,
|
|
886
|
-
action: 'work_contract_schema_v3_migrated',
|
|
887
|
-
expectedRevision: exact.revision,
|
|
888
|
-
});
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
return canonical;
|
|
1302
|
+
const requestedScope = workContractStoreScopeKey(options);
|
|
1303
|
+
if (requestedScope !== SEMANTIC_SCOPE_KEY) {
|
|
1304
|
+
const semantic = readExactSqliteWorkContract(options, SEMANTIC_SCOPE_KEY, sanitizedId);
|
|
1305
|
+
if (semantic) return semantic;
|
|
892
1306
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1307
|
+
const exact = readExactSqliteWorkContract(options, requestedScope, sanitizedId);
|
|
1308
|
+
if (exact) return exact;
|
|
1309
|
+
|
|
1310
|
+
// Preserve the one-time legacy import path only while this requested scope has
|
|
1311
|
+
// no per-Work rows. Canonical semantic rows are checked first and never copied
|
|
1312
|
+
// into repository scope; repository-scoped rows remain a read-only compatibility
|
|
1313
|
+
// fallback until their consumers are retired.
|
|
896
1314
|
const hasPerWorkRows = listControlPlaneRecords<WorkContract>(options.controllerHome, {
|
|
897
1315
|
namespace: 'work_contract',
|
|
898
|
-
scope:
|
|
1316
|
+
scope: requestedScope,
|
|
899
1317
|
limit: 1,
|
|
900
1318
|
}).length > 0;
|
|
901
1319
|
if (hasPerWorkRows) return undefined;
|
|
@@ -949,13 +1367,13 @@ export function supersedeWorkContract(
|
|
|
949
1367
|
contracts[predecessorIndex] = predecessorNext;
|
|
950
1368
|
contracts[successorIndex] = successorNext;
|
|
951
1369
|
if (!sqliteBacked(options)) {
|
|
952
|
-
writeJsonAtomic(workContractStorePath(options), { schemaVersion:
|
|
1370
|
+
writeJsonAtomic(workContractStorePath(options), { schemaVersion: 4, updatedAt: at, contracts });
|
|
953
1371
|
} else {
|
|
954
1372
|
withControlPlaneTransaction(options.controllerHome, (database) => {
|
|
955
1373
|
for (const contract of [predecessorNext, successorNext]) {
|
|
956
|
-
const current = readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', options
|
|
1374
|
+
const current = readControlPlaneRecordWithinTransaction<WorkContract>(database, 'work_contract', workContractStoreScopeKey(options), contract.workId);
|
|
957
1375
|
if (!current) throw new Error(`WORK_LINEAGE_RECORD_MISSING: ${contract.workId}`);
|
|
958
|
-
writeControlPlaneRecordWithinTransaction(database, { namespace: 'work_contract', scope: options
|
|
1376
|
+
writeControlPlaneRecordWithinTransaction(database, { namespace: 'work_contract', scope: workContractStoreScopeKey(options), key: contract.workId, schemaVersion: 4, value: contract, action: 'work_contract_supersession_linked', expectedRevision: current.revision });
|
|
959
1377
|
}
|
|
960
1378
|
});
|
|
961
1379
|
}
|
|
@@ -967,9 +1385,8 @@ export function summarizeWorkContract(contract: WorkContract): WorkContractSumma
|
|
|
967
1385
|
return {
|
|
968
1386
|
workId: contract.workId,
|
|
969
1387
|
repoId: contract.repoId,
|
|
970
|
-
|
|
1388
|
+
state: semanticWorkState(contract),
|
|
971
1389
|
phase: contract.phase,
|
|
972
|
-
status: contract.status,
|
|
973
1390
|
objective: contract.objective.slice(0, 240),
|
|
974
1391
|
updatedAt: contract.updatedAt,
|
|
975
1392
|
handoffCount: contract.handoffRefs.length,
|
|
@@ -995,32 +1412,61 @@ function updateWorkContractInternal(
|
|
|
995
1412
|
mutation: WorkContractMutation,
|
|
996
1413
|
allowCompletionWrite: boolean,
|
|
997
1414
|
allowLifecycleWrite = false,
|
|
998
|
-
allowRetainedCancelledResume = false,
|
|
999
1415
|
allowImplementationReviewWrite = false,
|
|
1000
1416
|
allowPhaseRegression = false,
|
|
1001
1417
|
): WorkContract {
|
|
1002
|
-
return
|
|
1418
|
+
return withExactWorkContractWrite(options, workId, () => {
|
|
1419
|
+
const authoritativeOptions = authoritativeExistingWorkStoreOptions(options, workId);
|
|
1420
|
+
return withWorkContractStoreWrite(authoritativeOptions, () => {
|
|
1003
1421
|
const sanitizedId = sanitizeFileComponent(workId);
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1422
|
+
const exact = sqliteBacked(authoritativeOptions)
|
|
1423
|
+
? readControlPlaneRecord<WorkContract>(
|
|
1424
|
+
authoritativeOptions.controllerHome,
|
|
1425
|
+
'work_contract',
|
|
1426
|
+
workContractStoreScopeKey(authoritativeOptions),
|
|
1427
|
+
sanitizedId,
|
|
1428
|
+
)
|
|
1429
|
+
: undefined;
|
|
1430
|
+
const store = exact ? undefined : readWorkContractStore(authoritativeOptions);
|
|
1431
|
+
const index = store?.contracts.findIndex((contract) => contract.workId === sanitizedId) ?? -1;
|
|
1432
|
+
if (!exact && index < 0) throw new Error(`work contract not found: ${sanitizedId}`);
|
|
1433
|
+
const at = nowIso(authoritativeOptions);
|
|
1434
|
+
// A per-Work SQLite row is the mutable authority. Do not normalize every
|
|
1435
|
+
// sibling just to update this row: malformed historical evidence must stay
|
|
1436
|
+
// visible and fenced, but cannot prevent a different terminal Work from
|
|
1437
|
+
// recording its physical cleanup.
|
|
1438
|
+
const current = exact
|
|
1439
|
+
? canonicalizeStoredWorkContract(exact.value)
|
|
1440
|
+
: store!.contracts[index]!;
|
|
1441
|
+
const mutationPatch = typeof mutation === 'function' ? mutation(current, at) : mutation;
|
|
1442
|
+
if (!mutationPatch) return current;
|
|
1443
|
+
const placementChanged = (
|
|
1444
|
+
Object.prototype.hasOwnProperty.call(mutationPatch, 'checkoutId')
|
|
1445
|
+
&& mutationPatch.checkoutId !== current.checkoutId
|
|
1446
|
+
) || (
|
|
1447
|
+
Object.prototype.hasOwnProperty.call(mutationPatch, 'worktreeRef')
|
|
1448
|
+
&& mutationPatch.worktreeRef !== current.worktreeRef
|
|
1449
|
+
) || (
|
|
1450
|
+
Object.prototype.hasOwnProperty.call(mutationPatch, 'executionPlacement')
|
|
1451
|
+
&& JSON.stringify(mutationPatch.executionPlacement) !== JSON.stringify(current.executionPlacement)
|
|
1452
|
+
);
|
|
1453
|
+
// executionConcurrency is only a projection of the concrete placement that
|
|
1454
|
+
// existed when an execution attempt was admitted. A checkout/worktree move
|
|
1455
|
+
// makes that projection stale; clear it centrally unless the same atomic
|
|
1456
|
+
// mutation explicitly supplies a replacement projection for the new target.
|
|
1457
|
+
const patch = placementChanged && !Object.prototype.hasOwnProperty.call(mutationPatch, 'executionConcurrency')
|
|
1458
|
+
? { ...mutationPatch, executionConcurrency: undefined }
|
|
1459
|
+
: mutationPatch;
|
|
1460
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'status')) throw new Error('WORK_RETIRED_FIELD_REJECTED: status');
|
|
1461
|
+
for (const field of ['semanticState', 'semanticRevision', 'semanticUpdatedAt']) {
|
|
1462
|
+
if (!allowLifecycleWrite && Object.prototype.hasOwnProperty.call(patch, field)) {
|
|
1463
|
+
throw new Error(`WORK_SEMANTIC_FIELDS_REQUIRE_REVISION_API: ${field}`);
|
|
1464
|
+
}
|
|
1018
1465
|
}
|
|
1019
1466
|
const writesCompletionReceipt = Object.prototype.hasOwnProperty.call(patch, 'completionReceipt');
|
|
1020
1467
|
const changesCompletionOutcome = patch.completionOutcome !== undefined && patch.completionOutcome !== current.completionOutcome;
|
|
1021
1468
|
const writesPhase = Object.prototype.hasOwnProperty.call(patch, 'phase') || Object.prototype.hasOwnProperty.call(patch, 'phaseEvidence');
|
|
1022
1469
|
const writesLifecycle = writesPhase
|
|
1023
|
-
|| Object.prototype.hasOwnProperty.call(patch, 'status')
|
|
1024
1470
|
|| Object.prototype.hasOwnProperty.call(patch, 'dispatchState')
|
|
1025
1471
|
|| Object.prototype.hasOwnProperty.call(patch, 'evidenceState')
|
|
1026
1472
|
|| Object.prototype.hasOwnProperty.call(patch, 'workKind');
|
|
@@ -1029,19 +1475,16 @@ function updateWorkContractInternal(
|
|
|
1029
1475
|
if (!allowImplementationReviewWrite && writesImplementationReviews) throw new Error('WORK_IMPLEMENTATION_REVIEW_REQUIRES_RECORD_API');
|
|
1030
1476
|
const projectedPhase = patch.phase ?? current.phase;
|
|
1031
1477
|
const projectedPhaseEvidence = patch.phaseEvidence ?? current.phaseEvidence;
|
|
1032
|
-
if (!allowCompletionWrite && (writesCompletionReceipt || changesCompletionOutcome
|
|
1478
|
+
if (!allowCompletionWrite && (writesCompletionReceipt || changesCompletionOutcome)) {
|
|
1033
1479
|
throw new Error('WORK_COMPLETION_REQUIRES_RECORD_API');
|
|
1034
1480
|
}
|
|
1035
|
-
if (patch.status === 'completed' && !current.completionReceipt && !patch.completionReceipt) {
|
|
1036
|
-
throw new Error('WORK_COMPLETION_RECEIPT_REQUIRED');
|
|
1037
|
-
}
|
|
1038
1481
|
if (writesCompletionReceipt && patch.completionReceipt === undefined && current.completionReceipt) {
|
|
1039
1482
|
throw new Error('WORK_COMPLETION_RECEIPT_IMMUTABLE');
|
|
1040
1483
|
}
|
|
1041
1484
|
const next: WorkContract = validateWorkSemanticTransition(current, validateWorkSemantics({
|
|
1042
1485
|
...current,
|
|
1043
1486
|
...patch,
|
|
1044
|
-
schemaVersion:
|
|
1487
|
+
schemaVersion: 4,
|
|
1045
1488
|
workId: current.workId,
|
|
1046
1489
|
repoId: current.repoId,
|
|
1047
1490
|
createdAt: current.createdAt,
|
|
@@ -1055,206 +1498,83 @@ function updateWorkContractInternal(
|
|
|
1055
1498
|
completionOutcome: patch.completionOutcome ?? current.completionOutcome,
|
|
1056
1499
|
evidenceRefs: (patch.evidenceRefs ?? current.evidenceRefs).slice(0, current.evidencePolicy.maxEvidenceRefs),
|
|
1057
1500
|
handoffRefs: (patch.handoffRefs ?? current.handoffRefs).slice(0, 20),
|
|
1058
|
-
suggestedNextActions:
|
|
1501
|
+
suggestedNextActions: suggestedActionsForSemanticState(current.semanticState, patch.suggestedNextActions ?? current.suggestedNextActions),
|
|
1059
1502
|
policyDecisions: (patch.policyDecisions ?? current.policyDecisions).slice(0, 20),
|
|
1060
1503
|
checkRefs: (patch.checkRefs ?? current.checkRefs).slice(0, 50),
|
|
1061
1504
|
implementationReviews: (patch.implementationReviews ?? current.implementationReviews ?? []).slice(-MAX_IMPLEMENTATION_REVIEW_HISTORY),
|
|
1062
1505
|
reconciliations: (patch.reconciliations ?? current.reconciliations ?? []).slice(0, 20),
|
|
1063
1506
|
objective: (patch.objective ?? current.objective).slice(0, 2_000),
|
|
1064
1507
|
continuationPrompt: (patch.continuationPrompt ?? current.continuationPrompt)?.slice(0, 2_000),
|
|
1065
|
-
}), {
|
|
1066
|
-
|
|
1508
|
+
}), { allowPhaseRegression });
|
|
1509
|
+
if (exact && sqliteBacked(authoritativeOptions)) {
|
|
1510
|
+
withControlPlaneTransaction(authoritativeOptions.controllerHome, (database) => {
|
|
1511
|
+
writeControlPlaneRecordWithinTransaction(database, {
|
|
1512
|
+
namespace: 'work_contract',
|
|
1513
|
+
scope: workContractStoreScopeKey(authoritativeOptions),
|
|
1514
|
+
key: next.workId,
|
|
1515
|
+
schemaVersion: 4,
|
|
1516
|
+
value: next,
|
|
1517
|
+
action: 'work_contract_updated',
|
|
1518
|
+
expectedRevision: exact.revision,
|
|
1519
|
+
});
|
|
1520
|
+
});
|
|
1521
|
+
return next;
|
|
1522
|
+
}
|
|
1523
|
+
const contracts = [...store!.contracts];
|
|
1067
1524
|
contracts[index] = next;
|
|
1068
|
-
writeWorkContractStore(
|
|
1525
|
+
writeWorkContractStore(authoritativeOptions, { schemaVersion: 4, updatedAt: at, contracts });
|
|
1069
1526
|
return next;
|
|
1527
|
+
});
|
|
1070
1528
|
});
|
|
1071
1529
|
}
|
|
1072
1530
|
|
|
1073
1531
|
|
|
1074
1532
|
/**
|
|
1075
1533
|
* Build the only permitted in-place Plan rebind for an active Work: the same
|
|
1076
|
-
|
|
1077
|
-
* replan. This is deliberately pure so the Plan store can persist predecessor,
|
|
1078
|
-
* successor, and Work in one SQLite transaction.
|
|
1079
|
-
*/
|
|
1080
|
-
export function rebindPlanBoundWorkContract(
|
|
1081
|
-
current: WorkContract,
|
|
1082
|
-
input: {
|
|
1083
|
-
predecessorPlanId: string;
|
|
1084
|
-
successorPlanId: string;
|
|
1085
|
-
planStepId: string;
|
|
1086
|
-
planSourceRevision: string;
|
|
1087
|
-
allowedPaths: string[];
|
|
1088
|
-
forbiddenPaths: string[];
|
|
1089
|
-
checks: string[];
|
|
1090
|
-
recordedAt: string;
|
|
1091
|
-
reason: string;
|
|
1092
|
-
},
|
|
1093
|
-
): WorkContract {
|
|
1094
|
-
const canonicalCurrent = validateCanonicalWorkContract(current);
|
|
1095
|
-
if (isTerminalWorkContractStatus(canonicalCurrent.status) || canonicalCurrent.completionReceipt || canonicalCurrent.completionOutcome) {
|
|
1096
|
-
throw new Error(`WORK_PLAN_REBIND_TERMINAL: ${canonicalCurrent.workId}`);
|
|
1097
|
-
}
|
|
1098
|
-
if (canonicalCurrent.planId !== input.predecessorPlanId || canonicalCurrent.planStepId !== input.planStepId) {
|
|
1099
|
-
throw new Error(`WORK_PLAN_REBIND_SOURCE_MISMATCH: ${canonicalCurrent.workId}`);
|
|
1100
|
-
}
|
|
1101
|
-
const successorPlanId = sanitizeFileComponent(input.successorPlanId);
|
|
1102
|
-
const planStepId = sanitizeFileComponent(input.planStepId);
|
|
1103
|
-
if (!successorPlanId || successorPlanId === 'unknown' || successorPlanId === canonicalCurrent.planId) {
|
|
1104
|
-
throw new Error('WORK_PLAN_REBIND_SUCCESSOR_INVALID');
|
|
1105
|
-
}
|
|
1106
|
-
const planSourceRevision = input.planSourceRevision.trim();
|
|
1107
|
-
if (!planSourceRevision) throw new Error('WORK_PLAN_REBIND_SOURCE_REVISION_REQUIRED');
|
|
1108
|
-
const allowedPaths = [...new Set(input.allowedPaths.map((value) => value.trim()).filter(Boolean))].slice(0, 50);
|
|
1109
|
-
const forbiddenPaths = [...new Set(input.forbiddenPaths.map((value) => value.trim()).filter(Boolean))].slice(0, 50);
|
|
1110
|
-
const checks = [...new Set(input.checks.map((value) => value.trim()).filter(Boolean))].slice(0, 30);
|
|
1111
|
-
for (const path of canonicalCurrent.allowedPaths) {
|
|
1112
|
-
if (!allowedPaths.includes(path)) throw new Error(`WORK_PLAN_REBIND_SCOPE_NARROWING_FORBIDDEN: ${path}`);
|
|
1113
|
-
}
|
|
1114
|
-
if (JSON.stringify(forbiddenPaths) !== JSON.stringify(canonicalCurrent.forbiddenPaths)) {
|
|
1115
|
-
throw new Error('WORK_PLAN_REBIND_FORBIDDEN_SCOPE_CHANGE');
|
|
1116
|
-
}
|
|
1117
|
-
if (JSON.stringify(checks) !== JSON.stringify(canonicalCurrent.checks)) {
|
|
1118
|
-
throw new Error('WORK_PLAN_REBIND_CHECK_CHANGE');
|
|
1119
|
-
}
|
|
1120
|
-
const evidenceRefs = [{
|
|
1121
|
-
title: 'Plan-bound Work scope replanned',
|
|
1122
|
-
summary: `${canonicalCurrent.planId} -> ${successorPlanId}: ${input.reason.trim().slice(0, 1_000)}`,
|
|
1123
|
-
detailLevel: 'summary' as const,
|
|
1124
|
-
}, ...canonicalCurrent.evidenceRefs].slice(0, canonicalCurrent.evidencePolicy.maxEvidenceRefs);
|
|
1125
|
-
const next = validateWorkSemantics({
|
|
1126
|
-
...canonicalCurrent,
|
|
1127
|
-
scopeRef: semanticScopeRefForWork({
|
|
1128
|
-
workId: canonicalCurrent.workId,
|
|
1129
|
-
requirementId: canonicalCurrent.requirementId,
|
|
1130
|
-
planId: successorPlanId,
|
|
1131
|
-
planStepId,
|
|
1132
|
-
}),
|
|
1133
|
-
planId: successorPlanId,
|
|
1134
|
-
planStepId,
|
|
1135
|
-
planSourceRevision,
|
|
1136
|
-
allowedPaths,
|
|
1137
|
-
forbiddenPaths,
|
|
1138
|
-
checks,
|
|
1139
|
-
evidenceRefs,
|
|
1140
|
-
updatedAt: input.recordedAt,
|
|
1141
|
-
});
|
|
1142
|
-
return validateWorkSemanticTransition(canonicalCurrent, next);
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
/**
|
|
1146
|
-
* Refresh one active Work against a newer revision of the same stable Plan.
|
|
1147
|
-
* This deliberately does not change Plan identity or semantic scope. Only the
|
|
1148
|
-
* source fence and an allowed-path widening may change; forbidden paths and
|
|
1149
|
-
* machine checks remain exact.
|
|
1150
|
-
*/
|
|
1151
|
-
export function refreshPlanBoundWorkRevision(
|
|
1534
|
+
function cancellationPhaseEvidence(
|
|
1152
1535
|
current: WorkContract,
|
|
1153
|
-
input: {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
}
|
|
1168
|
-
const planId = sanitizeFileComponent(input.planId);
|
|
1169
|
-
const planStepId = sanitizeFileComponent(input.planStepId);
|
|
1170
|
-
if (!planId || planId === 'unknown' || canonicalCurrent.planId !== planId || canonicalCurrent.planStepId !== planStepId) {
|
|
1171
|
-
throw new Error(`WORK_PLAN_REVISION_REFRESH_SOURCE_MISMATCH: ${canonicalCurrent.workId}`);
|
|
1172
|
-
}
|
|
1173
|
-
const planSourceRevision = input.planSourceRevision.trim();
|
|
1174
|
-
if (!planSourceRevision) throw new Error('WORK_PLAN_REVISION_REFRESH_SOURCE_REVISION_REQUIRED');
|
|
1175
|
-
const allowedPaths = [...new Set(input.allowedPaths.map((value) => value.trim()).filter(Boolean))].slice(0, 50);
|
|
1176
|
-
const forbiddenPaths = [...new Set(input.forbiddenPaths.map((value) => value.trim()).filter(Boolean))].slice(0, 50);
|
|
1177
|
-
const checks = [...new Set(input.checks.map((value) => value.trim()).filter(Boolean))].slice(0, 30);
|
|
1178
|
-
for (const path of canonicalCurrent.allowedPaths) {
|
|
1179
|
-
if (!allowedPaths.includes(path)) throw new Error(`WORK_PLAN_REVISION_REFRESH_SCOPE_NARROWING_FORBIDDEN: ${path}`);
|
|
1180
|
-
}
|
|
1181
|
-
if (JSON.stringify(forbiddenPaths) !== JSON.stringify(canonicalCurrent.forbiddenPaths)) {
|
|
1182
|
-
throw new Error('WORK_PLAN_REVISION_REFRESH_FORBIDDEN_SCOPE_CHANGE');
|
|
1183
|
-
}
|
|
1184
|
-
if (JSON.stringify(checks) !== JSON.stringify(canonicalCurrent.checks)) {
|
|
1185
|
-
throw new Error('WORK_PLAN_REVISION_REFRESH_CHECK_CHANGE');
|
|
1186
|
-
}
|
|
1187
|
-
const reason = input.reason.trim().slice(0, 1_000);
|
|
1188
|
-
if (!reason) throw new Error('WORK_PLAN_REVISION_REFRESH_REASON_REQUIRED');
|
|
1189
|
-
const evidenceRefs = [{
|
|
1190
|
-
title: 'Plan revision refreshed Work authority',
|
|
1191
|
-
summary: `${planId}@${canonicalCurrent.planSourceRevision ?? 'unknown'} -> ${planId}@${planSourceRevision}: ${reason}`,
|
|
1192
|
-
detailLevel: 'summary' as const,
|
|
1193
|
-
}, ...canonicalCurrent.evidenceRefs].slice(0, canonicalCurrent.evidencePolicy.maxEvidenceRefs);
|
|
1194
|
-
const next = validateWorkSemantics({
|
|
1195
|
-
...canonicalCurrent,
|
|
1196
|
-
planSourceRevision,
|
|
1197
|
-
allowedPaths,
|
|
1198
|
-
forbiddenPaths,
|
|
1199
|
-
checks,
|
|
1200
|
-
evidenceRefs,
|
|
1201
|
-
updatedAt: input.recordedAt,
|
|
1202
|
-
});
|
|
1203
|
-
return validateWorkSemanticTransition(canonicalCurrent, next);
|
|
1536
|
+
input: { summary: string; evidenceRefs?: EvidenceRef[]; recordedAt: string },
|
|
1537
|
+
): WorkPhaseEvidenceMap {
|
|
1538
|
+
const evidenceRefs = (input.evidenceRefs ?? current.evidenceRefs).slice(0, current.evidencePolicy.maxEvidenceRefs);
|
|
1539
|
+
const existing = current.phaseEvidence[current.phase];
|
|
1540
|
+
if (!['pending', 'active'].includes(existing.state)) return current.phaseEvidence;
|
|
1541
|
+
return {
|
|
1542
|
+
...current.phaseEvidence,
|
|
1543
|
+
[current.phase]: {
|
|
1544
|
+
state: 'skipped',
|
|
1545
|
+
source: 'recorded',
|
|
1546
|
+
summary: input.summary.trim().slice(0, 1_000) || 'Work cancelled.',
|
|
1547
|
+
evidenceRefs,
|
|
1548
|
+
recordedAt: input.recordedAt,
|
|
1549
|
+
},
|
|
1550
|
+
};
|
|
1204
1551
|
}
|
|
1205
1552
|
|
|
1206
1553
|
/**
|
|
1207
1554
|
* Retire technical Work authority when its owning Plan is no longer current.
|
|
1208
1555
|
*
|
|
1209
1556
|
* This is authority retirement, not history deletion. The cancelled Work and
|
|
1210
|
-
* its evidence stay durable for audit/recovery/retention, while
|
|
1557
|
+
* its evidence stay durable for audit/recovery/retention, while semantic state and
|
|
1211
1558
|
* dispatch become terminal immediately so schedulers, concurrency admission,
|
|
1212
1559
|
* and UI current-state projections cannot keep executing an obsolete Plan.
|
|
1213
1560
|
*/
|
|
1214
|
-
|
|
1561
|
+
function cancellationPhaseEvidence(
|
|
1215
1562
|
current: WorkContract,
|
|
1216
|
-
input: {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
if (canonicalCurrent.completionReceipt || canonicalCurrent.completionOutcome) {
|
|
1229
|
-
throw new Error(`WORK_PLAN_RETIRE_COMPLETION_CONFLICT: ${canonicalCurrent.workId}`);
|
|
1230
|
-
}
|
|
1231
|
-
const reason = input.reason.trim().slice(0, 1_000);
|
|
1232
|
-
if (!reason) throw new Error('WORK_PLAN_RETIRE_REASON_REQUIRED');
|
|
1233
|
-
const successor = input.successorPlanId?.trim();
|
|
1234
|
-
const summary = successor
|
|
1235
|
-
? `Plan authority retired ${canonicalCurrent.planId} -> ${successor}: ${reason}`
|
|
1236
|
-
: `Plan authority retired ${canonicalCurrent.planId}: ${reason}`;
|
|
1237
|
-
const evidenceRefs = [{
|
|
1238
|
-
title: 'Plan-bound Work authority retired',
|
|
1239
|
-
summary,
|
|
1240
|
-
detailLevel: 'summary' as const,
|
|
1241
|
-
}, ...canonicalCurrent.evidenceRefs].slice(0, canonicalCurrent.evidencePolicy.maxEvidenceRefs);
|
|
1242
|
-
const next = validateWorkSemantics({
|
|
1243
|
-
...canonicalCurrent,
|
|
1244
|
-
status: 'cancelled',
|
|
1245
|
-
phase: 'cleanup',
|
|
1246
|
-
phaseEvidence: transitionPhaseEvidence(canonicalCurrent, 'cleanup', {
|
|
1247
|
-
status: 'cancelled',
|
|
1248
|
-
summary,
|
|
1563
|
+
input: { summary: string; evidenceRefs?: EvidenceRef[]; recordedAt: string },
|
|
1564
|
+
): WorkPhaseEvidenceMap {
|
|
1565
|
+
const evidenceRefs = (input.evidenceRefs ?? current.evidenceRefs).slice(0, current.evidencePolicy.maxEvidenceRefs);
|
|
1566
|
+
const existing = current.phaseEvidence[current.phase];
|
|
1567
|
+
if (!['pending', 'active'].includes(existing.state)) return current.phaseEvidence;
|
|
1568
|
+
return {
|
|
1569
|
+
...current.phaseEvidence,
|
|
1570
|
+
[current.phase]: {
|
|
1571
|
+
state: 'skipped',
|
|
1572
|
+
source: 'recorded',
|
|
1573
|
+
summary: input.summary.trim().slice(0, 1_000) || 'Work cancelled.',
|
|
1249
1574
|
evidenceRefs,
|
|
1250
1575
|
recordedAt: input.recordedAt,
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
evidenceRefs,
|
|
1254
|
-
suggestedNextActions: [],
|
|
1255
|
-
updatedAt: input.recordedAt,
|
|
1256
|
-
});
|
|
1257
|
-
return validateWorkSemanticTransition(canonicalCurrent, next);
|
|
1576
|
+
},
|
|
1577
|
+
};
|
|
1258
1578
|
}
|
|
1259
1579
|
|
|
1260
1580
|
export type WorkContractMetadataPatch = Partial<Omit<
|
|
@@ -1264,7 +1584,9 @@ export type WorkContractMetadataPatch = Partial<Omit<
|
|
|
1264
1584
|
| 'repoId'
|
|
1265
1585
|
| 'createdAt'
|
|
1266
1586
|
| 'updatedAt'
|
|
1267
|
-
| '
|
|
1587
|
+
| 'semanticState'
|
|
1588
|
+
| 'semanticRevision'
|
|
1589
|
+
| 'semanticUpdatedAt'
|
|
1268
1590
|
| 'phase'
|
|
1269
1591
|
| 'phaseEvidence'
|
|
1270
1592
|
| 'dispatchState'
|
|
@@ -1283,17 +1605,17 @@ export function updateWorkContract(
|
|
|
1283
1605
|
return updateWorkContractInternal(options, workId, patch, false);
|
|
1284
1606
|
}
|
|
1285
1607
|
|
|
1286
|
-
/** Explicit
|
|
1608
|
+
/** Explicit execution-classification transition when an open Work begins governed repository mutation. */
|
|
1287
1609
|
export function promoteWorkToRepositoryChange(
|
|
1288
1610
|
options: WorkContractStoreOptions,
|
|
1289
1611
|
workId: string,
|
|
1290
1612
|
): WorkContract {
|
|
1291
1613
|
return updateWorkContractInternal(options, workId, (current) => {
|
|
1292
|
-
if (
|
|
1614
|
+
if (semanticWorkState(current) !== 'open' || current.completionReceipt || current.completionOutcome) {
|
|
1293
1615
|
throw new Error(`WORK_KIND_PROMOTION_TERMINAL: ${workId}`);
|
|
1294
1616
|
}
|
|
1295
1617
|
if (current.workKind === 'repository_change') return undefined;
|
|
1296
|
-
if (
|
|
1618
|
+
if (!['investigation', 'local_effect', 'remote_effect'].includes(current.workKind)) {
|
|
1297
1619
|
throw new Error(`WORK_KIND_PROMOTION_INVALID: ${workId}:${current.workKind}`);
|
|
1298
1620
|
}
|
|
1299
1621
|
return { workKind: 'repository_change' };
|
|
@@ -1319,12 +1641,12 @@ export function activateWorkContract(
|
|
|
1319
1641
|
},
|
|
1320
1642
|
): WorkContract {
|
|
1321
1643
|
return updateWorkContractInternal(options, workId, (current, at) => {
|
|
1322
|
-
if (current.
|
|
1323
|
-
throw new Error(`WORK_ACTIVATION_TERMINAL: ${workId}:${current.
|
|
1644
|
+
if (current.semanticState !== 'open') {
|
|
1645
|
+
throw new Error(`WORK_ACTIVATION_TERMINAL: ${workId}:${current.semanticState}`);
|
|
1324
1646
|
}
|
|
1325
1647
|
const phase = input.phase ?? current.phase;
|
|
1326
1648
|
const phaseEvidence = transitionPhaseEvidence(current, phase, {
|
|
1327
|
-
|
|
1649
|
+
state: 'active',
|
|
1328
1650
|
summary: input.summary,
|
|
1329
1651
|
recordedAt: at,
|
|
1330
1652
|
source: 'recorded',
|
|
@@ -1332,14 +1654,13 @@ export function activateWorkContract(
|
|
|
1332
1654
|
const evidenceState = input.evidenceState
|
|
1333
1655
|
?? (current.evidenceState === 'failed' ? 'partial' : current.evidenceState);
|
|
1334
1656
|
return {
|
|
1335
|
-
status: 'running',
|
|
1336
1657
|
phase,
|
|
1337
1658
|
phaseEvidence,
|
|
1338
1659
|
dispatchState: 'running',
|
|
1339
1660
|
evidenceState,
|
|
1340
1661
|
worktreeRef: input.worktreeRef ?? current.worktreeRef,
|
|
1341
1662
|
};
|
|
1342
|
-
}, false, true, false,
|
|
1663
|
+
}, false, true, false, true);
|
|
1343
1664
|
}
|
|
1344
1665
|
|
|
1345
1666
|
export function failWorkContract(
|
|
@@ -1348,24 +1669,23 @@ export function failWorkContract(
|
|
|
1348
1669
|
input: { phase: WorkPhase; summary: string; evidenceRefs?: EvidenceRef[] },
|
|
1349
1670
|
): WorkContract {
|
|
1350
1671
|
return updateWorkContractInternal(options, workId, (current, at) => {
|
|
1351
|
-
if (current.
|
|
1352
|
-
throw new Error(`WORK_FAILURE_TERMINAL: ${workId}:${current.
|
|
1672
|
+
if (current.semanticState !== 'open') {
|
|
1673
|
+
throw new Error(`WORK_FAILURE_TERMINAL: ${workId}:${current.semanticState}`);
|
|
1353
1674
|
}
|
|
1354
1675
|
if (input.phase !== current.phase) {
|
|
1355
1676
|
throw new Error(`WORK_FAILURE_PHASE_MISMATCH: ${workId}:expected=${current.phase}:actual=${input.phase}`);
|
|
1356
1677
|
}
|
|
1357
1678
|
const phaseEvidence = transitionPhaseEvidence(current, current.phase, {
|
|
1358
|
-
|
|
1679
|
+
state: 'failed',
|
|
1359
1680
|
summary: input.summary,
|
|
1360
1681
|
evidenceRefs: input.evidenceRefs,
|
|
1361
1682
|
recordedAt: at,
|
|
1362
1683
|
source: 'recorded',
|
|
1363
1684
|
});
|
|
1364
1685
|
return {
|
|
1365
|
-
status: 'failed',
|
|
1366
1686
|
phase: current.phase,
|
|
1367
1687
|
phaseEvidence,
|
|
1368
|
-
dispatchState: '
|
|
1688
|
+
dispatchState: 'blocked',
|
|
1369
1689
|
evidenceState: 'failed',
|
|
1370
1690
|
...(input.evidenceRefs ? { evidenceRefs: input.evidenceRefs } : {}),
|
|
1371
1691
|
};
|
|
@@ -1377,19 +1697,27 @@ export function cancelWorkContract(
|
|
|
1377
1697
|
workId: string,
|
|
1378
1698
|
input: { summary: string; evidenceRefs?: EvidenceRef[] },
|
|
1379
1699
|
): WorkContract {
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1700
|
+
const current = getWorkContract(options, workId);
|
|
1701
|
+
if (!current) throw new Error(`work contract not found: ${sanitizeFileComponent(workId)}`);
|
|
1702
|
+
if (current.semanticState === 'completed') throw new Error(`WORK_CANCEL_COMPLETED: ${workId}`);
|
|
1703
|
+
if (current.semanticState === 'cancelled') return current;
|
|
1704
|
+
|
|
1705
|
+
reviseWorkSemanticContext(options, workId, {
|
|
1706
|
+
expectedRevision: currentWorkSemanticRevision(current),
|
|
1707
|
+
state: 'cancelled',
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
return updateWorkContractInternal(options, workId, (cancelled, at) => {
|
|
1711
|
+
if (cancelled.semanticState !== 'cancelled') {
|
|
1712
|
+
throw new Error(`WORK_CANCEL_SEMANTIC_STATE_REQUIRED: ${workId}:${cancelled.semanticState}`);
|
|
1713
|
+
}
|
|
1714
|
+
const phaseEvidence = cancellationPhaseEvidence(cancelled, {
|
|
1385
1715
|
summary: input.summary,
|
|
1386
1716
|
evidenceRefs: input.evidenceRefs,
|
|
1387
1717
|
recordedAt: at,
|
|
1388
|
-
source: 'recorded',
|
|
1389
1718
|
});
|
|
1390
1719
|
return {
|
|
1391
|
-
|
|
1392
|
-
phase: current.phase,
|
|
1720
|
+
phase: cancelled.phase,
|
|
1393
1721
|
phaseEvidence,
|
|
1394
1722
|
dispatchState: 'terminal',
|
|
1395
1723
|
...(input.evidenceRefs ? { evidenceRefs: input.evidenceRefs } : {}),
|
|
@@ -1399,58 +1727,54 @@ export function cancelWorkContract(
|
|
|
1399
1727
|
}
|
|
1400
1728
|
|
|
1401
1729
|
/**
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1730
|
+
* Record verified physical cleanup for an already-cancelled Work without
|
|
1731
|
+
* inventing semantic success. Phases that never ran are skipped, while prior
|
|
1732
|
+
* satisfied/skipped/blocked/failed evidence remains immutable. The cleanup
|
|
1733
|
+
* receipt is the authority for the only newly satisfied phase.
|
|
1405
1734
|
*/
|
|
1406
|
-
export function
|
|
1735
|
+
export function recordCancelledWorkCleanupCompleted(
|
|
1407
1736
|
options: WorkContractStoreOptions,
|
|
1408
1737
|
workId: string,
|
|
1409
|
-
input: {
|
|
1410
|
-
principalId: string;
|
|
1411
|
-
controllerInstanceId: string;
|
|
1412
|
-
summary: string;
|
|
1413
|
-
checkoutId?: string;
|
|
1414
|
-
worktreeRef?: string;
|
|
1415
|
-
},
|
|
1738
|
+
input: { summary: string; receiptId: string; evidenceRefs?: EvidenceRef[] },
|
|
1416
1739
|
): WorkContract {
|
|
1417
1740
|
return updateWorkContractInternal(options, workId, (current, at) => {
|
|
1418
|
-
if (current.
|
|
1419
|
-
throw new Error(`
|
|
1741
|
+
if (current.semanticState !== 'cancelled') {
|
|
1742
|
+
throw new Error(`WORK_CANCELLED_CLEANUP_STATE_REQUIRED: ${workId}:${current.semanticState}`);
|
|
1420
1743
|
}
|
|
1421
|
-
|
|
1422
|
-
if (
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1744
|
+
const summary = input.summary.trim().slice(0, 1_000);
|
|
1745
|
+
if (!summary) throw new Error('WORK_CANCELLED_CLEANUP_SUMMARY_REQUIRED');
|
|
1746
|
+
const receiptId = input.receiptId.trim();
|
|
1747
|
+
if (!receiptId) throw new Error('WORK_CANCELLED_CLEANUP_RECEIPT_REQUIRED');
|
|
1748
|
+
const evidenceRefs = (input.evidenceRefs ?? current.evidenceRefs).slice(0, current.evidencePolicy.maxEvidenceRefs);
|
|
1749
|
+
const phaseEvidence: WorkPhaseEvidenceMap = { ...current.phaseEvidence };
|
|
1750
|
+
for (const phase of WORK_PHASES) {
|
|
1751
|
+
if (phase === 'cleanup') continue;
|
|
1752
|
+
const existing = phaseEvidence[phase];
|
|
1753
|
+
if (!['pending', 'active'].includes(existing.state)) continue;
|
|
1754
|
+
phaseEvidence[phase] = {
|
|
1755
|
+
state: 'skipped',
|
|
1756
|
+
source: 'recorded',
|
|
1757
|
+
summary: `Skipped after Work cancellation: ${summary}`.slice(0, 1_000),
|
|
1758
|
+
evidenceRefs,
|
|
1759
|
+
recordedAt: at,
|
|
1760
|
+
};
|
|
1429
1761
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
}].slice(-current.evidencePolicy.maxEvidenceRefs);
|
|
1435
|
-
const phaseEvidence = transitionPhaseEvidence({ ...current, evidenceRefs }, 'implementation', {
|
|
1436
|
-
status: 'running',
|
|
1437
|
-
summary: input.summary,
|
|
1762
|
+
phaseEvidence.cleanup = {
|
|
1763
|
+
state: 'satisfied',
|
|
1764
|
+
source: 'recorded',
|
|
1765
|
+
summary,
|
|
1438
1766
|
evidenceRefs,
|
|
1439
1767
|
recordedAt: at,
|
|
1440
|
-
|
|
1441
|
-
}
|
|
1768
|
+
receiptId,
|
|
1769
|
+
};
|
|
1442
1770
|
return {
|
|
1443
|
-
|
|
1444
|
-
phase: 'implementation',
|
|
1771
|
+
phase: 'cleanup',
|
|
1445
1772
|
phaseEvidence,
|
|
1446
|
-
dispatchState: '
|
|
1773
|
+
dispatchState: 'terminal',
|
|
1447
1774
|
evidenceRefs,
|
|
1448
|
-
|
|
1449
|
-
worktreeRef: input.worktreeRef ?? current.worktreeRef,
|
|
1450
|
-
controllerInstanceId: input.controllerInstanceId,
|
|
1451
|
-
continuationPrompt: input.summary,
|
|
1775
|
+
suggestedNextActions: [],
|
|
1452
1776
|
};
|
|
1453
|
-
}, false, true
|
|
1777
|
+
}, false, true);
|
|
1454
1778
|
}
|
|
1455
1779
|
|
|
1456
1780
|
/** Merge non-authoritative discovery/change evidence without changing policy fences. */
|
|
@@ -1479,7 +1803,6 @@ export function transitionWorkContractPhase(
|
|
|
1479
1803
|
workId: string,
|
|
1480
1804
|
input: {
|
|
1481
1805
|
phase: WorkPhase;
|
|
1482
|
-
status: WorkContractStatus;
|
|
1483
1806
|
state?: Exclude<WorkPhaseEvidenceState, 'pending'>;
|
|
1484
1807
|
summary: string;
|
|
1485
1808
|
evidenceRefs?: EvidenceRef[];
|
|
@@ -1488,56 +1811,25 @@ export function transitionWorkContractPhase(
|
|
|
1488
1811
|
},
|
|
1489
1812
|
): WorkContract {
|
|
1490
1813
|
return updateWorkContractInternal(options, workId, (current, at) => {
|
|
1491
|
-
if (input.phase === 'review') {
|
|
1492
|
-
throw new Error('WORK_IMPLEMENTATION_REVIEW_REQUIRES_REQUEST_API');
|
|
1493
|
-
}
|
|
1494
|
-
if (input.phase === 'delivery'
|
|
1495
|
-
&& current.phase !== 'delivery'
|
|
1496
|
-
&& workRequiresImplementationReview(current.workKind, current.scopeEvidence?.actualChangedPaths ?? [])) {
|
|
1497
|
-
throw new Error('WORK_IMPLEMENTATION_REVIEW_REQUIRES_RECORD_API');
|
|
1498
|
-
}
|
|
1499
1814
|
const phaseEvidence = transitionPhaseEvidence(current, input.phase, {
|
|
1500
|
-
|
|
1815
|
+
state: input.state,
|
|
1501
1816
|
summary: input.summary,
|
|
1502
1817
|
evidenceRefs: input.evidenceRefs,
|
|
1503
1818
|
recordedAt: at,
|
|
1504
1819
|
});
|
|
1505
|
-
if (input.state) phaseEvidence[input.phase] = { ...phaseEvidence[input.phase], state: input.state };
|
|
1506
1820
|
return {
|
|
1507
1821
|
phase: input.phase,
|
|
1508
1822
|
phaseEvidence,
|
|
1509
|
-
status: input.status,
|
|
1510
1823
|
dispatchState: input.dispatchState ?? current.dispatchState,
|
|
1511
1824
|
evidenceState: input.evidenceState ?? current.evidenceState,
|
|
1512
1825
|
};
|
|
1513
|
-
}, false, true, false,
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
/** Enter the first-class implementation-review phase without recording a decision. */
|
|
1517
|
-
export function requestWorkImplementationReview(
|
|
1518
|
-
options: WorkContractStoreOptions,
|
|
1519
|
-
workId: string,
|
|
1520
|
-
summary: string,
|
|
1521
|
-
): WorkContract {
|
|
1522
|
-
return updateWorkContractInternal(options, workId, (current, at) => {
|
|
1523
|
-
if (isTerminalWorkContractStatus(current.status)) throw new Error(`WORK_IMPLEMENTATION_REVIEW_TERMINAL: ${workId}`);
|
|
1524
|
-
if (current.phase !== 'verification' || current.phaseEvidence.verification.state !== 'satisfied') {
|
|
1525
|
-
throw new Error('WORK_IMPLEMENTATION_REVIEW_VERIFIED_CANDIDATE_REQUIRED');
|
|
1526
|
-
}
|
|
1527
|
-
const phaseEvidence = transitionPhaseEvidence(current, 'review', {
|
|
1528
|
-
status: 'running',
|
|
1529
|
-
summary,
|
|
1530
|
-
recordedAt: at,
|
|
1531
|
-
});
|
|
1532
|
-
return {
|
|
1533
|
-
phase: 'review',
|
|
1534
|
-
phaseEvidence,
|
|
1535
|
-
status: 'running',
|
|
1536
|
-
};
|
|
1537
|
-
}, false, true);
|
|
1826
|
+
}, false, true, false, true);
|
|
1538
1827
|
}
|
|
1539
1828
|
|
|
1540
|
-
/**
|
|
1829
|
+
/**
|
|
1830
|
+
* Append immutable historical review evidence. This is evidence storage only:
|
|
1831
|
+
* the decision never changes semantic state or phase and never authorizes delivery.
|
|
1832
|
+
*/
|
|
1541
1833
|
export function recordWorkImplementationReview(
|
|
1542
1834
|
options: WorkContractStoreOptions,
|
|
1543
1835
|
workId: string,
|
|
@@ -1548,198 +1840,14 @@ export function recordWorkImplementationReview(
|
|
|
1548
1840
|
throw new Error('WORK_IMPLEMENTATION_REVIEW_DERIVATION_REQUIRES_TRANSFER_API');
|
|
1549
1841
|
}
|
|
1550
1842
|
return updateWorkContractInternal(options, workId, (current, at) => {
|
|
1551
|
-
if (
|
|
1843
|
+
if (semanticWorkState(current) !== 'open') throw new Error(`WORK_IMPLEMENTATION_REVIEW_TERMINAL: ${workId}`);
|
|
1552
1844
|
if (review.workId !== current.workId) throw new Error('WORK_IMPLEMENTATION_REVIEW_WORK_ID_MISMATCH');
|
|
1553
|
-
if (current.phase !== 'review') {
|
|
1554
|
-
throw new Error('WORK_IMPLEMENTATION_REVIEW_PHASE_REQUIRED');
|
|
1555
|
-
}
|
|
1556
|
-
const target = implementationReviewDecisionTarget(review.decision);
|
|
1557
1845
|
const history = [...(current.implementationReviews ?? []), review];
|
|
1558
1846
|
if (history.length > MAX_IMPLEMENTATION_REVIEW_HISTORY) throw new Error('WORK_IMPLEMENTATION_REVIEW_HISTORY_LIMIT');
|
|
1559
|
-
const phaseEvidence = transitionPhaseEvidence(current, target.phase, {
|
|
1560
|
-
status: target.status,
|
|
1561
|
-
summary: `Implementation review ${review.reviewId}: ${review.decision}. ${review.rationale}`,
|
|
1562
|
-
recordedAt: at,
|
|
1563
|
-
});
|
|
1564
|
-
if (review.decision === 'approved') {
|
|
1565
|
-
phaseEvidence.review = {
|
|
1566
|
-
state: 'satisfied',
|
|
1567
|
-
source: 'recorded',
|
|
1568
|
-
summary: `Controller approved exact implementation candidate in ${review.reviewId}.`,
|
|
1569
|
-
evidenceRefs: current.evidenceRefs.slice(0, 20),
|
|
1570
|
-
recordedAt: review.recordedAt,
|
|
1571
|
-
};
|
|
1572
|
-
} else if (review.decision === 'blocked') {
|
|
1573
|
-
phaseEvidence.review = { ...phaseEvidence.review, state: 'blocked', recordedAt: review.recordedAt };
|
|
1574
|
-
}
|
|
1575
1847
|
return {
|
|
1576
|
-
phase: target.phase,
|
|
1577
|
-
phaseEvidence,
|
|
1578
|
-
status: target.status,
|
|
1579
|
-
dispatchState: target.status === 'blocked'
|
|
1580
|
-
? 'blocked'
|
|
1581
|
-
: current.dispatchState === 'blocked'
|
|
1582
|
-
? 'running'
|
|
1583
|
-
: current.dispatchState,
|
|
1584
1848
|
implementationReviews: history,
|
|
1585
1849
|
};
|
|
1586
|
-
}, false,
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
export interface ContentEquivalentCommitAuthorityTransferInput {
|
|
1590
|
-
transferredVerificationRecords: VerificationRecord[];
|
|
1591
|
-
derivedReview: WorkImplementationReviewRecord;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
/**
|
|
1595
|
-
* Persist the representation-only commit authority transfer in one Work-store
|
|
1596
|
-
* transaction. The immutable parent review plus exact post-commit verification
|
|
1597
|
-
* evidence authorize this write; mutable lifecycle phase is only a projection.
|
|
1598
|
-
*/
|
|
1599
|
-
export function recordContentEquivalentCommitAuthorityTransfer(
|
|
1600
|
-
options: WorkContractStoreOptions,
|
|
1601
|
-
workId: string,
|
|
1602
|
-
input: ContentEquivalentCommitAuthorityTransferInput,
|
|
1603
|
-
): WorkContract {
|
|
1604
|
-
return withWorkContractStoreWrite(options, () => {
|
|
1605
|
-
const sanitizedId = sanitizeFileComponent(workId);
|
|
1606
|
-
if (options.controllerHome) assertCanonicalWorkAdmissionAllowed(options, { operation: 'continue', workId: sanitizedId });
|
|
1607
|
-
const store = readWorkContractStore(options);
|
|
1608
|
-
const index = store.contracts.findIndex((contract) => contract.workId === sanitizedId);
|
|
1609
|
-
if (index < 0) throw new Error(`work contract not found: ${sanitizedId}`);
|
|
1610
|
-
const current = store.contracts[index]!;
|
|
1611
|
-
if (current.completionReceipt || isTerminalWorkContractStatus(current.status)) {
|
|
1612
|
-
throw new Error(`WORK_IMPLEMENTATION_REVIEW_TRANSFER_TERMINAL: ${sanitizedId}`);
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
const review = input.derivedReview;
|
|
1616
|
-
validateImplementationReviewRecord(review);
|
|
1617
|
-
if (review.workId !== current.workId) throw new Error('WORK_IMPLEMENTATION_REVIEW_WORK_ID_MISMATCH');
|
|
1618
|
-
if (review.derivation !== 'content_equivalent_commit'
|
|
1619
|
-
|| review.decision !== 'approved'
|
|
1620
|
-
|| !review.derivedFromReviewId) {
|
|
1621
|
-
throw new Error('WORK_IMPLEMENTATION_REVIEW_TRANSFER_DERIVATION_REQUIRED');
|
|
1622
|
-
}
|
|
1623
|
-
const parent = latestImplementationReview(current.implementationReviews);
|
|
1624
|
-
if (!parent || parent.reviewId !== review.derivedFromReviewId || parent.decision !== 'approved') {
|
|
1625
|
-
throw new Error('WORK_IMPLEMENTATION_REVIEW_TRANSFER_PARENT_REQUIRED');
|
|
1626
|
-
}
|
|
1627
|
-
if (!['verification', 'review', 'delivery'].includes(current.phase)) {
|
|
1628
|
-
throw new Error(`WORK_IMPLEMENTATION_REVIEW_TRANSFER_PHASE_INVALID: ${current.phase}`);
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
const requestedCheckIds = new Set(current.checks);
|
|
1632
|
-
for (const record of input.transferredVerificationRecords) {
|
|
1633
|
-
if (!requestedCheckIds.has(record.checkId)) {
|
|
1634
|
-
throw new Error(`WORK_VERIFICATION_TRANSFER_UNDECLARED_CHECK: ${record.checkId}`);
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
const transferredVerificationRecords = input.transferredVerificationRecords.filter((record) =>
|
|
1638
|
-
!current.checkRefs.some((existing) => sameSuccessfulVerificationExecutionFact(existing, record)));
|
|
1639
|
-
const checkRefs = [...transferredVerificationRecords, ...current.checkRefs].slice(0, 50);
|
|
1640
|
-
const postVerification = authoritativeImplementationReviewVerificationEvidence({
|
|
1641
|
-
repoId: current.repoId,
|
|
1642
|
-
workId: current.workId,
|
|
1643
|
-
requiredCheckIds: current.checks,
|
|
1644
|
-
records: checkRefs,
|
|
1645
|
-
sourceRevision: review.sourceRevision,
|
|
1646
|
-
workspaceFingerprint: review.verificationWorkspaceFingerprint,
|
|
1647
|
-
});
|
|
1648
|
-
if (postVerification.missingCheckIds.length > 0) {
|
|
1649
|
-
throw new Error(`WORK_IMPLEMENTATION_REVIEW_TRANSFER_VERIFICATION_REQUIRED: ${postVerification.missingCheckIds.join(', ')}`);
|
|
1650
|
-
}
|
|
1651
|
-
const expectedEvidence = normalizeImplementationReviewEvidence(review.verificationEvidence);
|
|
1652
|
-
if (JSON.stringify(postVerification.evidence) !== JSON.stringify(expectedEvidence)) {
|
|
1653
|
-
throw new Error('WORK_IMPLEMENTATION_REVIEW_TRANSFER_VERIFICATION_IDENTITY_MISMATCH');
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
const history = [...current.implementationReviews, review];
|
|
1657
|
-
if (history.length > MAX_IMPLEMENTATION_REVIEW_HISTORY) throw new Error('WORK_IMPLEMENTATION_REVIEW_HISTORY_LIMIT');
|
|
1658
|
-
const at = nowIso(options);
|
|
1659
|
-
const summary = `Transferred verification and implementation-review authority to content-equivalent commit ${review.sourceRevision}.`;
|
|
1660
|
-
const phaseEvidence = transitionPhaseEvidence(current, 'delivery', {
|
|
1661
|
-
status: 'running',
|
|
1662
|
-
summary,
|
|
1663
|
-
evidenceRefs: current.evidenceRefs,
|
|
1664
|
-
recordedAt: at,
|
|
1665
|
-
});
|
|
1666
|
-
phaseEvidence.verification = {
|
|
1667
|
-
...phaseEvidence.verification,
|
|
1668
|
-
state: 'satisfied',
|
|
1669
|
-
source: 'recorded',
|
|
1670
|
-
summary: `Exact post-commit verification authority supports derived review ${review.reviewId}.`,
|
|
1671
|
-
evidenceRefs: current.evidenceRefs.slice(0, 20),
|
|
1672
|
-
recordedAt: review.recordedAt,
|
|
1673
|
-
};
|
|
1674
|
-
phaseEvidence.review = {
|
|
1675
|
-
state: 'satisfied',
|
|
1676
|
-
source: 'recorded',
|
|
1677
|
-
summary: `Controller approval ${parent.reviewId} was transferred as ${review.reviewId} after content-equivalence proof.`,
|
|
1678
|
-
evidenceRefs: current.evidenceRefs.slice(0, 20),
|
|
1679
|
-
recordedAt: review.recordedAt,
|
|
1680
|
-
};
|
|
1681
|
-
const next = validateWorkSemanticTransition(current, validateWorkSemantics({
|
|
1682
|
-
...current,
|
|
1683
|
-
updatedAt: at,
|
|
1684
|
-
status: 'running',
|
|
1685
|
-
phase: 'delivery',
|
|
1686
|
-
phaseEvidence,
|
|
1687
|
-
evidenceState: 'valid',
|
|
1688
|
-
checkRefs,
|
|
1689
|
-
implementationReviews: history,
|
|
1690
|
-
}));
|
|
1691
|
-
const contracts = [...store.contracts];
|
|
1692
|
-
contracts[index] = next;
|
|
1693
|
-
writeWorkContractStore(options, { schemaVersion: 3, updatedAt: at, contracts });
|
|
1694
|
-
return next;
|
|
1695
|
-
});
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
/**
|
|
1699
|
-
* Repair only the lifecycle projection for an already-durable exact approved
|
|
1700
|
-
* implementation review. This never appends or derives review authority. The
|
|
1701
|
-
* caller must first independently prove that the immutable delivery candidate
|
|
1702
|
-
* still matches the durable review/check boundary.
|
|
1703
|
-
*/
|
|
1704
|
-
export function reconcileApprovedWorkImplementationReviewProjection(
|
|
1705
|
-
options: WorkContractStoreOptions,
|
|
1706
|
-
workId: string,
|
|
1707
|
-
expected: { reviewId: string; sourceRevision: string; changedPathDigest: string },
|
|
1708
|
-
): WorkContract {
|
|
1709
|
-
return updateWorkContractInternal(options, workId, (current, at) => {
|
|
1710
|
-
if (current.completionReceipt) return undefined;
|
|
1711
|
-
if (isTerminalWorkContractStatus(current.status)) throw new Error(`WORK_IMPLEMENTATION_REVIEW_PROJECTION_TERMINAL: ${workId}`);
|
|
1712
|
-
const review = latestImplementationReview(current.implementationReviews);
|
|
1713
|
-
if (!review || review.decision !== 'approved') throw new Error('WORK_IMPLEMENTATION_REVIEW_REQUIRED');
|
|
1714
|
-
if (review.reviewId !== expected.reviewId
|
|
1715
|
-
|| review.sourceRevision !== expected.sourceRevision
|
|
1716
|
-
|| review.changedPathDigest !== expected.changedPathDigest) {
|
|
1717
|
-
throw new Error('WORK_IMPLEMENTATION_REVIEW_PROJECTION_IDENTITY_MISMATCH');
|
|
1718
|
-
}
|
|
1719
|
-
if (current.phase === 'delivery' && current.phaseEvidence.review.state === 'satisfied') return undefined;
|
|
1720
|
-
if (!['verification', 'review', 'delivery'].includes(current.phase)) {
|
|
1721
|
-
throw new Error(`WORK_IMPLEMENTATION_REVIEW_PROJECTION_PHASE_INVALID: ${current.phase}`);
|
|
1722
|
-
}
|
|
1723
|
-
const summary = `Recovered lifecycle projection from durable approved implementation review ${review.reviewId}; review authority was not rewritten.`;
|
|
1724
|
-
const phaseEvidence = transitionPhaseEvidence(current, 'delivery', {
|
|
1725
|
-
status: 'running',
|
|
1726
|
-
summary,
|
|
1727
|
-
evidenceRefs: current.evidenceRefs,
|
|
1728
|
-
recordedAt: at,
|
|
1729
|
-
});
|
|
1730
|
-
phaseEvidence.review = {
|
|
1731
|
-
state: 'satisfied',
|
|
1732
|
-
source: 'recorded',
|
|
1733
|
-
summary: `Controller approved exact implementation candidate in ${review.reviewId}.`,
|
|
1734
|
-
evidenceRefs: current.evidenceRefs.slice(0, 20),
|
|
1735
|
-
recordedAt: review.recordedAt,
|
|
1736
|
-
};
|
|
1737
|
-
return {
|
|
1738
|
-
phase: 'delivery',
|
|
1739
|
-
phaseEvidence,
|
|
1740
|
-
status: 'running',
|
|
1741
|
-
};
|
|
1742
|
-
}, false, true);
|
|
1850
|
+
}, false, false, true, false);
|
|
1743
1851
|
}
|
|
1744
1852
|
|
|
1745
1853
|
export function appendWorkEvidence(
|
|
@@ -1797,63 +1905,45 @@ export function appendVerificationRecord(
|
|
|
1797
1905
|
}
|
|
1798
1906
|
|
|
1799
1907
|
/**
|
|
1800
|
-
*
|
|
1801
|
-
*
|
|
1802
|
-
*
|
|
1908
|
+
* Legacy field name retained for storage compatibility. This records durable
|
|
1909
|
+
* delivery/effect evidence only. It MUST NOT complete semantic Work, advance a
|
|
1910
|
+
* Work lifecycle/phase, satisfy review policy, or infer the next action.
|
|
1911
|
+
* Semantic completion is exclusively reviseWorkSemanticContext/complete CAS.
|
|
1803
1912
|
*/
|
|
1804
1913
|
export function recordWorkCompletionReceipt(
|
|
1805
1914
|
options: WorkContractStoreOptions,
|
|
1806
1915
|
workId: string,
|
|
1807
1916
|
receipt: NonNullable<WorkContract['completionReceipt']>,
|
|
1808
1917
|
completionOutcome: NonNullable<WorkContract['completionOutcome']>,
|
|
1809
|
-
|
|
1918
|
+
_completionWorkKind?: WorkKind,
|
|
1810
1919
|
): WorkContract {
|
|
1811
1920
|
return updateWorkContractInternal(options, workId, (current) => {
|
|
1812
1921
|
if (receipt.workId !== current.workId) throw new Error('WORK_COMPLETION_RECEIPT_IDENTITY_MISMATCH');
|
|
1813
1922
|
if (current.completionReceipt) {
|
|
1814
|
-
if (current.completionReceipt.receiptId
|
|
1815
|
-
|
|
1923
|
+
if (current.completionReceipt.receiptId === receipt.receiptId) return undefined;
|
|
1924
|
+
// Legacy field names store current mechanical delivery evidence. An open
|
|
1925
|
+
// semantic Work may legitimately deliver again after a retained production
|
|
1926
|
+
// canary repair. Terminal semantic Work keeps its delivery receipt immutable.
|
|
1927
|
+
if (semanticWorkState(current) !== 'open') throw new Error('WORK_COMPLETION_RECEIPT_ALREADY_RECORDED');
|
|
1928
|
+
if ((isRepositoryCompletionReceipt(receipt) || isDirectEditWorkCompletionReceipt(receipt)) && current.evidenceState !== 'valid') {
|
|
1929
|
+
throw new Error('WORK_DELIVERY_RECEIPT_REPLACEMENT_VALIDATION_REQUIRED');
|
|
1930
|
+
}
|
|
1816
1931
|
}
|
|
1817
1932
|
const recordedAt = receipt.recordedAt;
|
|
1818
1933
|
const receiptChangedPaths = isRepositoryCompletionReceipt(receipt) || isDirectEditWorkCompletionReceipt(receipt)
|
|
1819
1934
|
? receipt.changedPaths
|
|
1820
1935
|
: [];
|
|
1821
|
-
const historicalReconciliationException = isDirectEditWorkCompletionReceipt(receipt)
|
|
1822
|
-
&& Boolean(receipt.reconciliationId?.trim())
|
|
1823
|
-
&& current.reconciliations.some((entry) => entry.reconciliationId === receipt.reconciliationId && entry.outcome === 'accepted_equivalence');
|
|
1824
|
-
const reviewRequired = workRequiresImplementationReview(completionWorkKind ?? current.workKind, receiptChangedPaths);
|
|
1825
|
-
if (reviewRequired && !historicalReconciliationException && !['satisfied', 'skipped'].includes(current.phaseEvidence.review.state)) {
|
|
1826
|
-
throw new Error('WORK_IMPLEMENTATION_REVIEW_REQUIRED');
|
|
1827
|
-
}
|
|
1828
|
-
const phaseEvidence: WorkPhaseEvidenceMap = {
|
|
1829
|
-
...current.phaseEvidence,
|
|
1830
|
-
implementation: { ...current.phaseEvidence.implementation, state: 'satisfied' },
|
|
1831
|
-
verification: { ...current.phaseEvidence.verification, state: 'satisfied' },
|
|
1832
|
-
review: reviewRequired
|
|
1833
|
-
? historicalReconciliationException
|
|
1834
|
-
? { state: 'skipped', source: 'recorded', summary: `Historical reviewed reconciliation ${receipt.reconciliationId} is the narrow compatibility authority for this already-delivered Direct Edit Work.`, evidenceRefs: current.evidenceRefs.slice(0, 20), recordedAt }
|
|
1835
|
-
: { ...current.phaseEvidence.review, state: 'satisfied' }
|
|
1836
|
-
: { state: 'skipped', source: 'recorded', summary: 'Implementation review is not required for this source-free Work completion.', evidenceRefs: [], recordedAt },
|
|
1837
|
-
delivery: { state: 'satisfied', source: 'recorded', summary: `Phase delivery accepted by Work completion receipt ${receipt.receiptId}.`, evidenceRefs: current.evidenceRefs.slice(0, 20), recordedAt, receiptId: receipt.receiptId },
|
|
1838
|
-
cleanup: { state: 'satisfied', source: 'recorded', summary: `Phase cleanup accepted by Work completion receipt ${receipt.receiptId}.`, evidenceRefs: current.evidenceRefs.slice(0, 20), recordedAt, receiptId: receipt.receiptId },
|
|
1839
|
-
};
|
|
1840
1936
|
return {
|
|
1841
|
-
phase: 'cleanup',
|
|
1842
|
-
phaseEvidence,
|
|
1843
|
-
status: 'completed',
|
|
1844
|
-
dispatchState: 'terminal',
|
|
1845
|
-
evidenceState: 'valid',
|
|
1846
1937
|
completionOutcome,
|
|
1847
|
-
...(completionWorkKind ? { workKind: completionWorkKind } : {}),
|
|
1848
1938
|
completionReceipt: receipt,
|
|
1849
1939
|
scopeEvidence: {
|
|
1850
|
-
initialLikelyPaths: current.scopeEvidence?.initialLikelyPaths ??
|
|
1940
|
+
initialLikelyPaths: current.scopeEvidence?.initialLikelyPaths ?? [],
|
|
1851
1941
|
inspectedPaths: current.scopeEvidence?.inspectedPaths ?? [],
|
|
1852
1942
|
actualChangedPaths: [...new Set(receiptChangedPaths)].slice(0, 500),
|
|
1853
1943
|
recordedAt,
|
|
1854
1944
|
},
|
|
1855
1945
|
};
|
|
1856
|
-
}, true,
|
|
1946
|
+
}, true, false);
|
|
1857
1947
|
}
|
|
1858
1948
|
export interface AcceptSubmittedWorkInput {
|
|
1859
1949
|
requestId: string;
|
|
@@ -1862,13 +1952,14 @@ export interface AcceptSubmittedWorkInput {
|
|
|
1862
1952
|
semanticKey: string;
|
|
1863
1953
|
operation: SubmittedWorkOperation;
|
|
1864
1954
|
objective?: string;
|
|
1865
|
-
mode?: WorkContract['mode'];
|
|
1866
1955
|
requestedBy?: WorkContract['requestedBy'];
|
|
1867
1956
|
principalId?: string;
|
|
1868
1957
|
controllerInstanceId?: string;
|
|
1869
1958
|
workKind?: WorkKind;
|
|
1870
1959
|
risk?: WorkRisk;
|
|
1960
|
+
/** Compatibility input only; execution scope is owned by EditSession/resources. */
|
|
1871
1961
|
allowedPaths?: string[];
|
|
1962
|
+
/** Compatibility input only; execution scope is owned by EditSession/resources. */
|
|
1872
1963
|
forbiddenPaths?: string[];
|
|
1873
1964
|
checks?: string[];
|
|
1874
1965
|
acceptanceCriteria?: string[];
|