@deksden-com/dd-flow-cli 0.9.0-beta.14 → 0.9.0-beta.140
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/CHANGELOG.md +733 -0
- package/README.md +143 -1
- package/dist/build-info.json +8 -8
- package/dist/cli/argument-values.js +38 -0
- package/dist/cli/command-inputs.js +376 -0
- package/dist/cli/command-metadata.js +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +328 -18
- package/dist/cli/hook-ingress.js +78 -0
- package/dist/cli/input-preparation.js +165 -0
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +80 -0
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +1689 -545
- package/dist/cli/runtime-maintenance-diagnostics.js +46 -0
- package/dist/cli/runtime-maintenance-ingress.js +52 -0
- package/dist/cli/runtime-process.js +153 -0
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +71 -2
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/DD-ZCODE.md +112 -0
- package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +56 -0
- package/dist/harness-runtime/bin/dd-codex.mjs +27 -0
- package/dist/harness-runtime/bin/dd-droid.mjs +34 -0
- package/dist/harness-runtime/bin/dd-grok.mjs +40 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +22 -0
- package/dist/harness-runtime/bin/dd-zcode.mjs +87 -0
- package/dist/harness-runtime/lib/adapter-timeouts.d.mts +5 -0
- package/dist/harness-runtime/lib/adapter-timeouts.mjs +34 -0
- package/dist/harness-runtime/lib/agy-hooks.d.mts +1 -0
- package/dist/harness-runtime/lib/agy-hooks.mjs +9 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +2 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +163 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +371 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +6 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +724 -0
- package/dist/harness-runtime/lib/dd-agy.mjs +70 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +196 -0
- package/dist/harness-runtime/lib/dd-codex.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex.mjs +817 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +129 -0
- package/dist/harness-runtime/lib/dd-droid.mjs +584 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +498 -0
- package/dist/harness-runtime/lib/dd-grok.mjs +177 -0
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +507 -0
- package/dist/harness-runtime/lib/dd-opencode.mjs +354 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +750 -0
- package/dist/harness-runtime/lib/dd-zcode.mjs +1145 -0
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +14 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +157 -0
- package/dist/harness-runtime/lib/dispatch-fence.mjs +14 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +139 -0
- package/dist/harness-runtime/lib/droid-observation.mjs +66 -0
- package/dist/harness-runtime/lib/lease-renewal.d.mts +8 -0
- package/dist/harness-runtime/lib/lease-renewal.mjs +37 -0
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +273 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +13 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +654 -0
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/model-observations.mjs +101 -0
- package/dist/harness-runtime/lib/native-children.d.mts +16 -0
- package/dist/harness-runtime/lib/native-children.mjs +113 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +7 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +251 -0
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/native-streams.mjs +45 -0
- package/dist/harness-runtime/lib/observation-clock.d.mts +18 -0
- package/dist/harness-runtime/lib/observation-clock.mjs +92 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +85 -0
- package/dist/harness-runtime/lib/operation-context.mjs +5 -0
- package/dist/harness-runtime/lib/operation-errors.d.mts +14 -0
- package/dist/harness-runtime/lib/operation-errors.mjs +116 -0
- package/dist/harness-runtime/lib/operation-progress.d.mts +7 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +114 -0
- package/dist/harness-runtime/lib/process-json.d.mts +5 -0
- package/dist/harness-runtime/lib/process-json.mjs +201 -0
- package/dist/harness-runtime/lib/process-snapshot.mjs +10 -0
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
- package/dist/harness-runtime/lib/runner-events.mjs +200 -0
- package/dist/harness-runtime/lib/runner-lock.mjs +42 -0
- package/dist/harness-runtime/lib/session-settlement.mjs +30 -0
- package/dist/harness-runtime/lib/setup-progress.d.mts +9 -0
- package/dist/harness-runtime/lib/setup-progress.mjs +51 -0
- package/dist/harness-runtime/lib/tool-observations.d.mts +21 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +228 -0
- package/dist/harness-runtime/lib/zcode-usage.mjs +55 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
- package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
- package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
- package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
- package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
- package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
- package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
- package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
- package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
- package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
- package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
- package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
- package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
- package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +8 -4
- package/dist/schemas/agent-profile.schema.json +1 -1
- package/dist/schemas/code-review-result.schema.json +1 -1
- package/dist/schemas/code-verification.schema.json +7 -5
- package/dist/schemas/code-work-batch.schema.json +2 -2
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +28 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/schemas/plan-review-result.schema.json +2 -2
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-control-receipt.schema.json +118 -0
- package/dist/schemas/run-control-request.schema.json +36 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/schemas/vnext-protocol-plan.schema.json +2 -2
- package/dist/services/branch-context.js +3 -3
- package/dist/services/canon.js +11 -8
- package/dist/services/check-runner-evidence.js +168 -0
- package/dist/services/cleanup.js +133 -40
- package/dist/services/cli-operation-classifier.js +25 -62
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +1156 -107
- package/dist/services/codex-hook-delivery.js +32 -0
- package/dist/services/command-context.js +80 -0
- package/dist/services/compatibility-preflight.js +13 -3
- package/dist/services/config.js +11 -8
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-child-capacity.js +13 -0
- package/dist/services/controller-child-observer.js +115 -0
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +202 -0
- package/dist/services/dashboard.js +143 -58
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +294 -153
- package/dist/services/eval-snapshots.js +1215 -116
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +185 -0
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +308 -0
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +250 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +355 -36
- package/dist/services/harness-config.js +2 -0
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +771 -325
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lanes.js +78 -65
- package/dist/services/lifecycle-command.js +1 -288
- package/dist/services/lifecycle-contract.js +1 -0
- package/dist/services/lifecycle-invocations.js +2007 -0
- package/dist/services/managed-daemon-binding.js +38 -0
- package/dist/services/managed-lease-monitor.js +133 -0
- package/dist/services/managed-processes.js +475 -55
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +296 -0
- package/dist/services/memory-init-preflight.js +330 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +181 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +178 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/merge-queue.js +228 -149
- package/dist/services/merge-server.js +161 -35
- package/dist/services/migrations.js +37 -16
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/native-daemon-history.js +49 -0
- package/dist/services/native-session-control.js +39 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/plan-runtime.js +26 -11
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +37 -17
- package/dist/services/prompts.js +28 -17
- package/dist/services/protocols.js +163 -87
- package/dist/services/provider-capacity.js +385 -0
- package/dist/services/recovery-observation-budget.js +121 -0
- package/dist/services/recovery-snapshot-database.js +560 -0
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +42 -0
- package/dist/services/review-copy.js +165 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-control-receipt.js +56 -0
- package/dist/services/run-control-worker.js +411 -0
- package/dist/services/run-control.js +1056 -0
- package/dist/services/run-controller-adapter.js +399 -0
- package/dist/services/run-controller-capture.js +186 -0
- package/dist/services/run-controller-process.js +208 -0
- package/dist/services/run-controller-recovery.js +289 -0
- package/dist/services/run-controller-state.js +97 -0
- package/dist/services/run-controller.js +1205 -0
- package/dist/services/run-engine-bindings.js +1 -1
- package/dist/services/run-fork.js +147 -0
- package/dist/services/run-health.js +149 -0
- package/dist/services/run-observations.js +152 -0
- package/dist/services/run-projection.js +22 -64
- package/dist/services/run-recovery-guard.js +77 -0
- package/dist/services/run-recovery-runtime.js +112 -0
- package/dist/services/run-recovery.js +489 -0
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +1019 -247
- package/dist/services/runtime-budget.js +430 -0
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/runtime-scope-capture.js +52 -0
- package/dist/services/runtime-scope-control.js +468 -0
- package/dist/services/runtime-scope-resume.js +599 -0
- package/dist/services/runtime-scope-stop.js +210 -0
- package/dist/services/runtime-scope-worker.js +287 -0
- package/dist/services/runtime-service.js +96 -0
- package/dist/services/schema-validation.js +10 -8
- package/dist/services/scope-writer-cleanup.js +27 -0
- package/dist/services/sessions.js +35 -69
- package/dist/services/setup.js +377 -0
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +193 -19
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-lifecycle.js +100 -108
- package/dist/services/stage-pause.js +89 -75
- package/dist/services/stage-report-renderer.js +234 -6
- package/dist/services/stage-work-graph.js +52 -0
- package/dist/services/tooling.js +32 -23
- package/dist/services/usage.js +49 -101
- package/dist/services/verification-matrix.js +374 -0
- package/dist/services/vnext-code-review.js +223 -88
- package/dist/services/vnext-code.js +327 -226
- package/dist/services/vnext-contracts.js +10 -1
- package/dist/services/vnext-execution-profile.js +80 -5
- package/dist/services/vnext-fanout.js +311 -19
- package/dist/services/vnext-merge.js +332 -111
- package/dist/services/vnext-plan-review.js +279 -127
- package/dist/services/vnext-plan.js +229 -104
- package/dist/services/vnext-protocolize.js +411 -86
- package/dist/services/vnext-specify.js +119 -81
- package/dist/services/vnext-workspace-policy.js +7 -2
- package/dist/services/work-registry.js +1627 -199
- package/dist/services/workspace-bootstrap.js +52 -15
- package/dist/services/workspace-files.js +85 -0
- package/dist/services/worktrees.js +98 -44
- package/dist/shared/entity-references.js +8 -0
- package/dist/shared/errors.js +17 -0
- package/dist/storage/agent-output.js +26 -0
- package/dist/storage/database.js +891 -39
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/paths.js +17 -4
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/work-references.js +23 -0
- package/dist/storage/writer-contract.js +89 -0
- package/dist/storage/writer-migration.js +85 -0
- package/package.json +43 -13
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
- package/tools/repair-paused-run-status.mjs +84 -0
package/dist/services/runs.js
CHANGED
|
@@ -2,10 +2,13 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import {
|
|
5
|
+
import { withProjectionLock } from "../storage/projection-lock.js";
|
|
6
|
+
import { reconcileRunStageReports } from "./stage-report-renderer.js";
|
|
7
|
+
import { isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
6
8
|
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadCanonicalFlowContract, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
7
9
|
import { AppError } from "../shared/errors.js";
|
|
8
|
-
import {
|
|
10
|
+
import { nextRunId } from "./ids.js";
|
|
11
|
+
import { ensureDir, canonicalPath, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
|
|
9
12
|
import { appendAudit } from "./audit.js";
|
|
10
13
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
11
14
|
import { buildRunFlowGuidance } from "./flow-guidance.js";
|
|
@@ -14,12 +17,18 @@ import { recalculateRunUsage } from "./usage.js";
|
|
|
14
17
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
15
18
|
import { resolveCanonRoot } from "./canon.js";
|
|
16
19
|
import { bindCurrentEngineToRun } from "./engines.js";
|
|
17
|
-
import { executionProfilePath,
|
|
20
|
+
import { assertOperationalDecisionSource, executionProfilePath, loadVnextExecutionProfileSnapshot } from "./vnext-execution-profile.js";
|
|
21
|
+
import { loadVnextWorkspacePolicy } from "./vnext-workspace-policy.js";
|
|
22
|
+
import { assertSameSessionMerge, loadExecutionRoutingInput, resolveStageTransition, snapshotAgentProfiles } from "./execution-policy.js";
|
|
18
23
|
import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
|
|
19
24
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
25
|
+
import { flowCommand } from "./stage-pause.js";
|
|
26
|
+
import { RetainedExecutionBundle } from "./execution-bundles.js";
|
|
27
|
+
import { admitBundledFlow, bundledHandlerContext } from "./stage-handlers.js";
|
|
28
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
29
|
+
import { projectPacketLocators, runContextRelocations } from "./stage-context.js";
|
|
20
30
|
const runSchemaId = "dd-flow/flow-run@3";
|
|
21
31
|
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
22
|
-
const runIdType = "RUN";
|
|
23
32
|
const allowedRunFlowKinds = [
|
|
24
33
|
"mb_sdlc",
|
|
25
34
|
"coding",
|
|
@@ -35,22 +44,26 @@ const allowedRunFlowKinds = [
|
|
|
35
44
|
"review",
|
|
36
45
|
"custom"
|
|
37
46
|
];
|
|
38
|
-
export function
|
|
47
|
+
export function prepareFlowRunStart(context, input) {
|
|
48
|
+
const subjectId = requiredPlain(input.subjectId, "subject-id");
|
|
49
|
+
const subjectType = requiredPlain(input.subjectType, "subject-type");
|
|
50
|
+
const slug = normalizeSlug(input.slug);
|
|
51
|
+
for (const [name, value] of [["task profile", input.taskProfile], ["protocol overrides", input.protocolOverrides], ["RUN overrides", input.runOverrides]]) {
|
|
52
|
+
if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value)))
|
|
53
|
+
throw new AppError("validation", `${name} must be a JSON object`, 2);
|
|
54
|
+
}
|
|
39
55
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
56
|
+
const flowKind = parseRunFlowKind(input.flowKind);
|
|
57
|
+
const vnext = flowKind === "vnext_protocolize" || flowKind === "vnext_specify";
|
|
58
|
+
if (input.executionRoutingFile !== undefined && !vnext)
|
|
59
|
+
throw new AppError("usage", "Execution routing input is supported only for vNext RUNs", 2);
|
|
60
|
+
const routingInput = input.executionRoutingFile !== undefined ? loadExecutionRoutingInput(input.executionRoutingFile) : undefined;
|
|
61
|
+
const executionProfile = vnext ? snapshotVnextExecutionProfile(projectRoot, context.ddFlowHome, routingInput, context.now()) : undefined;
|
|
40
62
|
const flowContract = input.flowKind === "mb-upgrade"
|
|
41
63
|
? loadUpgradeFlowContract(context, projectRoot)
|
|
42
64
|
: loadProjectFlowContract(projectRoot);
|
|
43
|
-
registerProject(context, { root: projectRoot });
|
|
44
|
-
const project = requireProjectByRoot(context, projectRoot);
|
|
45
65
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot ?? projectRoot);
|
|
46
|
-
const flowKind = parseRunFlowKind(input.flowKind);
|
|
47
|
-
const slug = normalizeSlug(input.slug);
|
|
48
|
-
const runId = nextRunId(context, project.id, slug);
|
|
49
|
-
const { shortId } = parseFullEntityId(runId);
|
|
50
66
|
const now = context.now();
|
|
51
|
-
const executionProfile = (flowKind === "vnext_protocolize" || flowKind === "vnext_specify")
|
|
52
|
-
? snapshotVnextExecutionProfile(projectRoot)
|
|
53
|
-
: undefined;
|
|
54
67
|
const flowFlags = resolveFlowFlags(flowContract, {
|
|
55
68
|
flowKind,
|
|
56
69
|
...(input.preset ? { preset: input.preset } : {}),
|
|
@@ -60,17 +73,42 @@ export function startFlowRun(context, input) {
|
|
|
60
73
|
snapshotRevision: 1,
|
|
61
74
|
now
|
|
62
75
|
});
|
|
76
|
+
return { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags };
|
|
77
|
+
}
|
|
78
|
+
export function startFlowRun(context, input, prepared = prepareFlowRunStart(context, input)) {
|
|
79
|
+
const { projectRoot, flowKind, routingInput, executionProfile, workspaceRoot, slug, subjectId, subjectType, now, flowFlags } = prepared;
|
|
80
|
+
// Ordinary settings are already frozen by preparation; only scoped authority
|
|
81
|
+
// requires its live acceptance source to survive final binding unchanged.
|
|
82
|
+
if (executionProfile?.operational_decision) {
|
|
83
|
+
if (crypto.createHash("sha256").update(fs.readFileSync(executionProfile.source_path)).digest("hex") !== executionProfile.source_checksum)
|
|
84
|
+
throw new AppError("execution_profile_changed", "Execution profile changed after RUN preparation", 2);
|
|
85
|
+
if (executionProfile.operational_decision)
|
|
86
|
+
assertOperationalDecisionSource(executionProfile.operational_decision, projectRoot, context.now());
|
|
87
|
+
}
|
|
88
|
+
registerProject(context, { root: projectRoot });
|
|
89
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
90
|
+
const runId = nextRunId(context, project.id, slug);
|
|
91
|
+
const { shortId } = parseFullEntityId(runId);
|
|
63
92
|
const runHome = projectRunHome(context.ddFlowHome, project.id, runId);
|
|
64
93
|
const runDirRelative = path.posix.join("runs", runId);
|
|
65
94
|
const runtimePath = projectRunJsonPath(context.ddFlowHome, project.id, runId);
|
|
95
|
+
const policyBytes = executionProfile?.operational_decision ? fs.readFileSync(path.resolve(projectRoot, executionProfile.operational_decision.policy.path)) : undefined;
|
|
96
|
+
const policySource = policyBytes ? { path: "operational-policy.source.md", sha256: crypto.createHash("sha256").update(policyBytes).digest("hex") } : undefined;
|
|
97
|
+
if (policySource && policySource.sha256 !== executionProfile?.operational_decision?.policy.sha256)
|
|
98
|
+
throw new AppError("operational_decision_policy_changed", "Policy changed during final RUN binding", 2);
|
|
99
|
+
const binding = { project_id: project.id, run_id: runId, project_root: projectRoot, ...(executionProfile?.operational_decision?.scope.eval_id ? { eval_id: executionProfile.operational_decision.scope.eval_id } : {}), ...(executionProfile?.operational_decision?.scope.execution_id ? { execution_id: executionProfile.operational_decision.scope.execution_id } : {}) };
|
|
100
|
+
const operationalBytes = executionProfile ? `${JSON.stringify({ schema_id: "dd-flow/run-operational-contract@1", binding, settings: { merge_delivery: executionProfile.settings.merge_delivery, merge_cleanup: executionProfile.settings.merge_cleanup }, ...(executionProfile.operational_decision ? { decision: executionProfile.operational_decision, policy_source: policySource } : {}) }, null, 2)}\n` : undefined;
|
|
101
|
+
if (executionProfile && operationalBytes)
|
|
102
|
+
executionProfile.operational_contract = { path: "operational-contract.json", sha256: crypto.createHash("sha256").update(operationalBytes).digest("hex"), ...(policySource ? { policy_source: policySource } : {}), binding };
|
|
66
103
|
const index = {
|
|
67
104
|
schema_id: runSchemaId,
|
|
68
105
|
run_id: runId,
|
|
69
106
|
short_id: shortId,
|
|
70
107
|
flow_kind: flowKind,
|
|
108
|
+
...(["vnext_specify", "vnext_protocolize"].includes(flowKind) ? { verification_matrix_contract: "dd-flow/verification-matrix@1", final_check_coverage_contract: "dd-flow/final-check-coverage@1" } : {}),
|
|
71
109
|
subject: {
|
|
72
|
-
id:
|
|
73
|
-
title:
|
|
110
|
+
id: subjectId,
|
|
111
|
+
title: subjectId
|
|
74
112
|
},
|
|
75
113
|
project: {
|
|
76
114
|
id: project.id,
|
|
@@ -126,9 +164,14 @@ export function startFlowRun(context, input) {
|
|
|
126
164
|
};
|
|
127
165
|
ensureDir(path.dirname(runtimePath));
|
|
128
166
|
ensureDir(runHome);
|
|
167
|
+
if (policyBytes && policySource)
|
|
168
|
+
fs.writeFileSync(path.join(runHome, policySource.path), policyBytes, { flag: "wx" });
|
|
169
|
+
if (operationalBytes)
|
|
170
|
+
fs.writeFileSync(path.join(runHome, "operational-contract.json"), operationalBytes, { flag: "wx" });
|
|
171
|
+
if (routingInput)
|
|
172
|
+
fs.writeFileSync(path.join(runHome, "execution-routing.input.json"), routingInput.bytes, { flag: "wx", mode: 0o600 });
|
|
129
173
|
bindCurrentEngineToRun(context, { projectRoot, runId, runRoot: runHome });
|
|
130
174
|
const persistedIndex = persistedVnextIndex(index);
|
|
131
|
-
writeJsonFile(runtimePath, runtimeSnapshotForIndex(persistedIndex, 1));
|
|
132
175
|
context.db.run(`INSERT INTO runs
|
|
133
176
|
(id, short_id, slug, project_id, project_root, workspace_root, flow_kind, subject_type, subject_id,
|
|
134
177
|
status, verdict, next_action, runtime_path, run_dir, run_index_path, run_home_path, run_root, layout_version, artifact_root_kind,
|
|
@@ -141,7 +184,7 @@ export function startFlowRun(context, input) {
|
|
|
141
184
|
project.root,
|
|
142
185
|
workspaceRoot,
|
|
143
186
|
flowKind,
|
|
144
|
-
|
|
187
|
+
subjectType,
|
|
145
188
|
index.subject.id,
|
|
146
189
|
index.status,
|
|
147
190
|
index.verdict,
|
|
@@ -162,8 +205,9 @@ export function startFlowRun(context, input) {
|
|
|
162
205
|
eventType: "flow_run.started",
|
|
163
206
|
payload: { run_id: runId, flow_kind: flowKind, subject: index.subject, workspace_root: workspaceRoot, run_home: runHome }
|
|
164
207
|
});
|
|
165
|
-
|
|
166
|
-
appendRunTimeline(
|
|
208
|
+
context.db.afterCommit(() => publishRunProjections(context, project.id, runId));
|
|
209
|
+
appendRunTimeline(context, project.id, runId, { at: now, type: "run_started", run_id: runId });
|
|
210
|
+
appendRunTimeline(context, project.id, runId, {
|
|
167
211
|
at: now,
|
|
168
212
|
type: "flow_flags_resolved",
|
|
169
213
|
run_id: runId,
|
|
@@ -173,13 +217,16 @@ export function startFlowRun(context, input) {
|
|
|
173
217
|
});
|
|
174
218
|
return { ok: true, run: flowRunSummary(requireRunById(context, project.id, runId)), index };
|
|
175
219
|
}
|
|
176
|
-
function snapshotVnextExecutionProfile(projectRoot) {
|
|
177
|
-
const profile =
|
|
220
|
+
function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput, now = new Date().toISOString()) {
|
|
221
|
+
const { profile, bytes } = loadVnextExecutionProfileSnapshot(projectRoot);
|
|
222
|
+
if (profile.operational_decision)
|
|
223
|
+
assertOperationalDecisionSource(profile.operational_decision, projectRoot, now);
|
|
178
224
|
const sourcePath = executionProfilePath(projectRoot);
|
|
179
|
-
|
|
225
|
+
const frozen = freezeRunExecutionRouting(ddFlowHome, {
|
|
180
226
|
schema_id: "dd-flow/run-execution-profile@1",
|
|
181
227
|
source_path: sourcePath,
|
|
182
|
-
source_checksum: crypto.createHash("sha256").update(
|
|
228
|
+
source_checksum: crypto.createHash("sha256").update(bytes).digest("hex"),
|
|
229
|
+
...(profile.operational_decision ? { operational_decision: profile.operational_decision } : {}),
|
|
183
230
|
settings: {
|
|
184
231
|
stage_session_mode: profile.stage_session_mode,
|
|
185
232
|
plan_review_mode: profile.plan_review_mode,
|
|
@@ -188,9 +235,47 @@ function snapshotVnextExecutionProfile(projectRoot) {
|
|
|
188
235
|
merge_delivery: profile.merge_delivery,
|
|
189
236
|
merge_cleanup: profile.merge_cleanup,
|
|
190
237
|
stop_target: profile.stop_target,
|
|
191
|
-
code_bootstrap: profile.code_bootstrap
|
|
238
|
+
...(profile.code_bootstrap ? { code_bootstrap: profile.code_bootstrap } : {}),
|
|
239
|
+
...(profile.execution ? { execution: profile.execution } : {})
|
|
192
240
|
}
|
|
193
|
-
};
|
|
241
|
+
}, routingInput);
|
|
242
|
+
if (frozen.settings.merge_mode === "same_session" && frozen.settings.stop_target === "merge_completed"
|
|
243
|
+
&& fs.existsSync(path.join(projectRoot, ".memory-bank", "dd-flow", "project-workspace.json"))
|
|
244
|
+
&& loadVnextWorkspacePolicy(projectRoot).workspace.route === "feature_worktree") {
|
|
245
|
+
throw new AppError("merge_session_workspace_conflict", "Feature-worktree MERGE targets the integration checkout; use server MERGE or a qualified new Session", 2, { project_root: projectRoot });
|
|
246
|
+
}
|
|
247
|
+
return frozen;
|
|
248
|
+
}
|
|
249
|
+
/** Fresh RUN and fresh stage-entry experiments freeze through the same resolver. */
|
|
250
|
+
export function freezeRunExecutionRouting(ddFlowHome, base, input) {
|
|
251
|
+
const execution = input?.execution ?? base.settings.execution;
|
|
252
|
+
const profiles = execution ? snapshotAgentProfiles(ddFlowHome, execution) : undefined;
|
|
253
|
+
if (execution && profiles && base.settings.merge_mode === "same_session" && base.settings.stop_target === "merge_completed") {
|
|
254
|
+
const mode = base.settings.code_review_mode;
|
|
255
|
+
assertSameSessionMerge({ ddFlowHome, policy: execution, profiles, previousStages: mode === "off" ? ["code"] : mode === "auto" ? ["code", "code-review"] : ["code-review"] });
|
|
256
|
+
}
|
|
257
|
+
return { ...base, ...(profiles ? { agent_profiles: profiles } : {}),
|
|
258
|
+
...(input ? { routing_input: { source_path: input.source_path, source_checksum: input.source_checksum, artifact: "execution-routing.input.json" } } : {}),
|
|
259
|
+
settings: { ...base.settings, ...(execution ? { execution } : {}) } };
|
|
260
|
+
}
|
|
261
|
+
/** Called only after a stage-entry import has archived and detached source owners. */
|
|
262
|
+
export function importFlowRunExecutionProfile(context, input) {
|
|
263
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
264
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
265
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
266
|
+
const index = authoritativeIndex(run);
|
|
267
|
+
const lineage = JSON.parse(fs.readFileSync(input.lineageFile, "utf8"));
|
|
268
|
+
if (lineage.schema_id !== "dd-flow/snapshot-runtime-lineage@1" || lineage.run_id !== run.id || path.resolve(lineage.restored_runtime ?? "") !== path.resolve(context.ddFlowHome)
|
|
269
|
+
|| !vnextStages.some(stage => stage.id === input.stage)
|
|
270
|
+
|| (context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_controllers'") && context.db.get("SELECT 1 FROM run_controllers WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id]))
|
|
271
|
+
|| index.stage_runs.some(stage => stage.stage === input.stage || !["done", "skipped"].includes(stage.status))
|
|
272
|
+
|| context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? AND status IN ('active','idle') LIMIT 1", [project.id, run.id])) {
|
|
273
|
+
throw new AppError("snapshot_execution_active", "Execution policy can be selected only at a detached stage-entry import", 1);
|
|
274
|
+
}
|
|
275
|
+
index.execution_profile = { ...input.profile, stage_entry_import: { stage: input.stage, lineage_file: input.lineageFile } };
|
|
276
|
+
index.updated_at = context.now();
|
|
277
|
+
persistRunState(context, project, run, index);
|
|
278
|
+
appendRunTimeline(context, project.id, run.id, { at: index.updated_at, type: "execution_policy_imported", stage: input.stage, lineage_file: input.lineageFile, source_checksum: input.profile.routing_input?.source_checksum });
|
|
194
279
|
}
|
|
195
280
|
/**
|
|
196
281
|
* A vNext route may materialize a feature checkout after its semantic
|
|
@@ -200,19 +285,82 @@ function snapshotVnextExecutionProfile(projectRoot) {
|
|
|
200
285
|
export function rebindFlowRunWorkspace(context, input) {
|
|
201
286
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
202
287
|
const run = resolveRun(context, project.id, input.runId);
|
|
288
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id, input.expectedGeneration);
|
|
203
289
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot);
|
|
204
290
|
const index = authoritativeIndex(run);
|
|
291
|
+
const receipt = input.routeReceipt ? { ...input.routeReceipt, policy: { ...input.routeReceipt.policy,
|
|
292
|
+
worktree_path: input.routeReceipt.policy.worktree_path ? resolveWorkspaceRoot(input.routeReceipt.policy.worktree_path) : null } } : undefined;
|
|
293
|
+
if (index.execution_bundle && !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)) {
|
|
294
|
+
if (workspaceRoot === run.workspace_root)
|
|
295
|
+
return { ok: true, changed: false };
|
|
296
|
+
throw new AppError("workspace_route_invalid", "Only SDLC may provision a different RUN workspace", 1, { effect: "no_effect" });
|
|
297
|
+
}
|
|
298
|
+
if (workspaceRoot === run.workspace_root) {
|
|
299
|
+
if (receipt && index.workspace_route && hybridDigest(retainedWorkspaceRoute(context, run).receipt) !== hybridDigest(receipt)) {
|
|
300
|
+
throw new AppError("workspace_route_conflict", "Committed workspace route differs from the repeated request", 1, { effect: "no_effect" });
|
|
301
|
+
}
|
|
302
|
+
publishRunProjections(context, project.id, run.id);
|
|
303
|
+
return { ok: true, changed: false, workspace_root: workspaceRoot };
|
|
304
|
+
}
|
|
305
|
+
if (index.workspace_route)
|
|
306
|
+
throw new AppError("workspace_route_conflict", "RUN already has a committed workspace route", 1, { effect: "no_effect" });
|
|
307
|
+
if (input.expectedRevision !== undefined && (index.runtime_revision ?? 0) !== input.expectedRevision)
|
|
308
|
+
throw new AppError("run_revision_changed", "RUN changed while its workspace was provisioned", 1, { effect: "no_effect", expected_revision: input.expectedRevision, actual_revision: index.runtime_revision ?? 0 });
|
|
309
|
+
if (index.execution_bundle && !receipt)
|
|
310
|
+
throw new AppError("workspace_route_missing", "Bundled SDLC rebind requires its prepared route receipt", 1, { effect: "no_effect" });
|
|
311
|
+
if (receipt && (run.workspace_root !== project.root || receipt.schema_id !== "dd-flow/vnext-workspace-route@1" || receipt.policy.route !== "feature_worktree"
|
|
312
|
+
|| !receipt.policy.provisioned || receipt.policy.worktree_path !== workspaceRoot || !receipt.created || !receipt.bootstrap)) {
|
|
313
|
+
throw new AppError("workspace_route_invalid", "Prepared workspace route does not match this RUN", 1, { effect: "no_effect" });
|
|
314
|
+
}
|
|
205
315
|
const now = context.now();
|
|
206
316
|
const facts = gitFacts(workspaceRoot);
|
|
317
|
+
if (receipt && (facts.branch !== receipt.policy.feature_branch || !receipt.policy.base_ref || !facts.head
|
|
318
|
+
|| spawnSync("git", ["-C", workspaceRoot, "merge-base", "--is-ancestor", receipt.policy.base_ref, facts.head]).status !== 0)) {
|
|
319
|
+
throw new AppError("workspace_route_invalid", "Prepared checkout differs from the feature branch/base receipt", 1, { effect: "no_effect", run_id: run.id });
|
|
320
|
+
}
|
|
207
321
|
index.workspace = { ...index.workspace, workspace_path: workspaceRoot, branch: facts.branch ?? "unknown", base: facts.head ?? "unknown", manager: "official-worktrunk" };
|
|
208
322
|
index.execution = { ...index.execution, project_root: project.root, workspace_root: workspaceRoot, git: facts };
|
|
209
323
|
index.updated_at = now;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
324
|
+
const route = receipt ? { schema_id: "dd-flow/run-workspace-route@1", run_id: run.id, project_id: project.id, project_root: project.root,
|
|
325
|
+
generation, previous_workspace: run.workspace_root, workspace_root: workspaceRoot, revision: (index.runtime_revision ?? 0) + 1,
|
|
326
|
+
git: facts, receipt, receipt_sha256: hybridDigest(receipt) } : undefined;
|
|
327
|
+
if (route)
|
|
328
|
+
index.workspace_route = { revision: route.revision, sha256: hybridDigest(route) };
|
|
329
|
+
context.db.writeTransaction(() => {
|
|
330
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
331
|
+
const latest = resolveRun(context, project.id, run.id);
|
|
332
|
+
if (latest.workspace_root !== run.workspace_root)
|
|
333
|
+
throw new AppError("workspace_route_changed", "RUN route changed during preparation", 1, { effect: "no_effect" });
|
|
334
|
+
context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
|
|
335
|
+
persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
|
|
336
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, ...(route ? { route } : {}) } });
|
|
337
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
|
|
338
|
+
}, "runs.rebindWorkspace");
|
|
214
339
|
return { ok: true, run_id: run.id, project_root: project.root, workspace_root: workspaceRoot, git: facts };
|
|
215
340
|
}
|
|
341
|
+
function retainedWorkspaceRoute(context, run) {
|
|
342
|
+
const index = authoritativeIndex(run), binding = index.workspace_route;
|
|
343
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.workspace_rebound' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.route.revision') = ?", [run.project_id, run.id, binding?.revision ?? -1]);
|
|
344
|
+
const original = rows.length === 1 ? JSON.parse(rows[0].payload_json).route : undefined;
|
|
345
|
+
if (!binding || !original || original.schema_id !== "dd-flow/run-workspace-route@1" || hybridDigest(original) !== binding.sha256 || hybridDigest(original.receipt) !== original.receipt_sha256) {
|
|
346
|
+
throw new AppError("workspace_route_invalid", "RUN committed workspace route evidence changed", 1, { effect: "no_effect", run_id: run.id });
|
|
347
|
+
}
|
|
348
|
+
// Relocation receipts are admitted by snapshot import; never rewrite bundle
|
|
349
|
+
// or source route authority merely because a physical checkout moved.
|
|
350
|
+
const route = JSON.parse(projectPacketLocators(JSON.stringify(original), runContextRelocations(context, run.project_id, run.id)));
|
|
351
|
+
if (route.receipt_sha256 !== original.receipt_sha256
|
|
352
|
+
|| route.run_id !== run.id || route.project_id !== run.project_id || route.project_root !== run.project_root || route.workspace_root !== run.workspace_root
|
|
353
|
+
|| index.workspace.workspace_path !== run.workspace_root || index.execution?.workspace_root !== run.workspace_root || index.execution?.project_root !== run.project_root
|
|
354
|
+
|| index.workspace.manager !== "official-worktrunk" || index.workspace.branch !== route.receipt.policy.feature_branch
|
|
355
|
+
// execution.git is the latest observation (stage completion/refresh updates
|
|
356
|
+
// it). The immutable review base is workspace.base, not that moving HEAD.
|
|
357
|
+
|| index.workspace.base !== route.git.head
|
|
358
|
+
|| !Number.isInteger(route.generation) || route.generation < 0
|
|
359
|
+
|| route.receipt.policy.worktree_path !== run.workspace_root) {
|
|
360
|
+
throw new AppError("workspace_route_invalid", "RUN has no exact coherent committed workspace route", 1, { effect: "no_effect", run_id: run.id });
|
|
361
|
+
}
|
|
362
|
+
return route;
|
|
363
|
+
}
|
|
216
364
|
function loadUpgradeFlowContract(context, projectRoot) {
|
|
217
365
|
const canon = resolveCanonRoot(context, context.env.DD_MEMORYBANK ? { explicitRoot: context.env.DD_MEMORYBANK } : {});
|
|
218
366
|
if (!canon.ok || !canon.canon) {
|
|
@@ -224,12 +372,57 @@ function loadUpgradeFlowContract(context, projectRoot) {
|
|
|
224
372
|
}
|
|
225
373
|
return loadCanonicalFlowContract(canon.canon.flow_root);
|
|
226
374
|
}
|
|
227
|
-
export function getFlowRunStatus(context, input) {
|
|
375
|
+
export function getFlowRunStatus(context, input, preparedBundle) {
|
|
228
376
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
229
377
|
const run = resolveRun(context, project.id, input.runId);
|
|
378
|
+
if (preparedBundle)
|
|
379
|
+
assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, preparedBundle);
|
|
230
380
|
const index = authoritativeIndex(run);
|
|
231
381
|
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
232
|
-
return { ok: true, run: flowRunSummary(run), index, ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
382
|
+
return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index, preparedBundle), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
383
|
+
}
|
|
384
|
+
/** A read-only projection of the next legal lifecycle action.
|
|
385
|
+
* It is derived from the persisted RUN/Work facts so controllers never infer a
|
|
386
|
+
* transition from a model's prose or a hard-coded stage array. */
|
|
387
|
+
function runContinuation(context, run, index, preparedBundle) {
|
|
388
|
+
if (["done", "failed", "cancelled"].includes(run.status))
|
|
389
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
390
|
+
const control = runControlDirective(context, run.project_id, run.id);
|
|
391
|
+
if (control)
|
|
392
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "operator_control", command: null, control };
|
|
393
|
+
const active = index.stage_runs.find((stage) => !["done", "skipped"].includes(stage.status));
|
|
394
|
+
if (active) {
|
|
395
|
+
const runningWork = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND status = 'running' ORDER BY updated_at DESC LIMIT 1", [run.project_id, run.id]);
|
|
396
|
+
const kind = active.status === "running" ? "continue_stage" : active.status === "paused" || active.status === "waiting_for_user" ? "paused" : "blocked";
|
|
397
|
+
return { kind, stage: active.stage, attempt: active.attempt ?? null, work_id: active.pause?.work_id ?? runningWork?.work_id ?? null, reason: active.status, command: null };
|
|
398
|
+
}
|
|
399
|
+
if (run.status === "waiting_for_user" || run.status === "paused")
|
|
400
|
+
return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
401
|
+
if (run.status === "blocked")
|
|
402
|
+
return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
403
|
+
if (index.execution_bundle) {
|
|
404
|
+
const bundle = preparedBundle ?? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
|
|
405
|
+
const next = bundledNextStage(bundle.descriptor, index);
|
|
406
|
+
if (!next)
|
|
407
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
|
|
408
|
+
const command = bundle.engine().command(context.ddFlowHome, ["stage", "start", run.id, "--stage", next, "--project-root", run.project_root, "--json"]);
|
|
409
|
+
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command,
|
|
410
|
+
execution: { status: "resolved", ...bundle.execution.stage(next) } };
|
|
411
|
+
}
|
|
412
|
+
if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
|
|
413
|
+
const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
|
|
414
|
+
const next = completed ? vnextStages.find((stage) => stage.id === completed.stage)?.next.find((candidate) => candidate !== "plan-review" || index.settings?.plan_review?.mode !== "off") : "specify";
|
|
415
|
+
if (!next)
|
|
416
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
|
|
417
|
+
const command = `${flowCommand(context)} stage start ${run.id} --stage ${next} --project-root ${JSON.stringify(run.project_root)} --json`;
|
|
418
|
+
const profile = index.execution_profile;
|
|
419
|
+
const execution = profile?.settings.execution ? profile.agent_profiles
|
|
420
|
+
? { status: "resolved", ...resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy: profile.settings.execution, profiles: profile.agent_profiles, projectRoot: run.project_root, workspaceRoot: run.workspace_root, ...(completed ? { fromStage: completed.stage } : {}), stage: next, stageSessionMode: profile.settings.stage_session_mode, mergeMode: profile.settings.merge_mode }) }
|
|
421
|
+
: { status: "unfrozen", reason: "execution_profile_not_frozen" }
|
|
422
|
+
: undefined;
|
|
423
|
+
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command, ...(execution ? { execution } : {}) };
|
|
424
|
+
}
|
|
425
|
+
return { kind: "wait", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: "no_vnext_continuation", command: null };
|
|
233
426
|
}
|
|
234
427
|
export function getFlowRunConfig(context, input) {
|
|
235
428
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -250,9 +443,12 @@ export function getFlowRunVariables(context, input) {
|
|
|
250
443
|
return { ok: true, run_id: run.id, variables };
|
|
251
444
|
}
|
|
252
445
|
export function setFlowRunVariable(context, input) {
|
|
446
|
+
return setRunVariable(context, input, input.value, prepareFlowRunVariable(context, input));
|
|
447
|
+
}
|
|
448
|
+
export function prepareFlowRunVariable(context, input) {
|
|
253
449
|
if (!input.key.startsWith("user."))
|
|
254
450
|
throw new AppError("validation", "Only user.* RUN variables may be set directly", 2, { key: input.key });
|
|
255
|
-
return
|
|
451
|
+
return prepareRunVariable(context, input);
|
|
256
452
|
}
|
|
257
453
|
/** Controllers own policy.* and runtime.* values; agents never call this directly. */
|
|
258
454
|
export function setRuntimeRunVariable(context, input) {
|
|
@@ -261,20 +457,7 @@ export function setRuntimeRunVariable(context, input) {
|
|
|
261
457
|
return setRunVariable(context, input, input.value);
|
|
262
458
|
}
|
|
263
459
|
export function setFlowRunConfig(context, input) {
|
|
264
|
-
|
|
265
|
-
throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
|
|
266
|
-
}
|
|
267
|
-
if (!["auto", "off", "standard", "deep"].includes(input.value)) {
|
|
268
|
-
throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
|
|
269
|
-
}
|
|
270
|
-
const reason = requiredPlain(input.reason, "reason");
|
|
271
|
-
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
272
|
-
const run = resolveRun(context, project.id, input.runId);
|
|
273
|
-
const index = authoritativeIndex(run);
|
|
274
|
-
const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
|
|
275
|
-
if (index.stage_runs.some((stage) => stage.stage === stageName)) {
|
|
276
|
-
throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
|
|
277
|
-
}
|
|
460
|
+
const { reason, project, run, index } = prepareFlowRunConfig(context, input);
|
|
278
461
|
const now = context.now();
|
|
279
462
|
const settings = runSettings(index);
|
|
280
463
|
const previous = input.key === "plan_review.mode" ? settings.plan_review : settings.code_review;
|
|
@@ -289,13 +472,32 @@ export function setFlowRunConfig(context, input) {
|
|
|
289
472
|
index.updated_at = now;
|
|
290
473
|
persistRunState(context, project, run, index);
|
|
291
474
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.config_set", payload: { run_id: run.id, key: input.key, value: mode, source, reason } });
|
|
292
|
-
appendRunTimeline(
|
|
475
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "run_config_set", run_id: run.id, key: input.key, value: mode, source, reason });
|
|
293
476
|
return { ok: true, run_id: run.id, settings: runSettings(index), idempotent: false };
|
|
294
477
|
}
|
|
478
|
+
export function prepareFlowRunConfig(context, input) {
|
|
479
|
+
if (input.key !== "plan_review.mode" && input.key !== "code_review.mode") {
|
|
480
|
+
throw new AppError("validation", `Unknown RUN config key: ${input.key}`, 2);
|
|
481
|
+
}
|
|
482
|
+
if (!["auto", "off", "standard", "deep"].includes(input.value)) {
|
|
483
|
+
throw new AppError("validation", "--value for review mode must be auto, off, standard, or deep", 2);
|
|
484
|
+
}
|
|
485
|
+
const reason = requiredPlain(input.reason, "reason");
|
|
486
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
487
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
488
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
489
|
+
const index = authoritativeIndex(run);
|
|
490
|
+
const stageName = input.key === "plan_review.mode" ? "plan-review" : "code-review";
|
|
491
|
+
if (index.stage_runs.some((stage) => stage.stage === stageName)) {
|
|
492
|
+
throw new AppError("invalid_run_config_state", `${input.key} is frozen after ${stageName} starts`, 2, { run_id: run.id });
|
|
493
|
+
}
|
|
494
|
+
return { reason, project, run, index };
|
|
495
|
+
}
|
|
295
496
|
/** Freeze an automatic review request once from already accepted policy or PLAN facts. */
|
|
296
497
|
export function freezeFlowRunReviewMode(context, input) {
|
|
297
498
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
298
499
|
const run = resolveRun(context, project.id, input.runId);
|
|
500
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
299
501
|
const index = authoritativeIndex(run);
|
|
300
502
|
const settings = runSettings(index);
|
|
301
503
|
const key = input.review === "plan" ? "plan_review" : "code_review";
|
|
@@ -312,8 +514,8 @@ export function getFlowRunFlagsStatus(context, input) {
|
|
|
312
514
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
313
515
|
const run = resolveRun(context, project.id, input.runId);
|
|
314
516
|
const index = authoritativeIndex(run);
|
|
315
|
-
const
|
|
316
|
-
const flags =
|
|
517
|
+
const legacyRuntime = index.flow_flags ? null : readRuntimeSnapshot(run.runtime_path);
|
|
518
|
+
const flags = index.flow_flags ?? legacyRuntime?.flow_flags;
|
|
317
519
|
if (!flags) {
|
|
318
520
|
return {
|
|
319
521
|
ok: true,
|
|
@@ -331,29 +533,31 @@ export function getFlowRunFlagsStatus(context, input) {
|
|
|
331
533
|
snapshot_revision: flags.snapshot_revision,
|
|
332
534
|
snapshot_checksum: flags.snapshot_checksum,
|
|
333
535
|
flow_flags: flags,
|
|
334
|
-
runtime_revision:
|
|
536
|
+
runtime_revision: index.runtime_revision ?? legacyRuntime?.runtime_revision ?? null
|
|
335
537
|
};
|
|
336
538
|
}
|
|
337
539
|
export function reviseFlowRunFlags(context, input) {
|
|
338
540
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
339
541
|
const run = resolveRun(context, project.id, input.runId);
|
|
542
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
340
543
|
const idempotencyKey = requiredPlain(input.idempotencyKey, "idempotency-key");
|
|
341
|
-
context.db.
|
|
342
|
-
|
|
343
|
-
|
|
544
|
+
const priorMutation = context.db.get("SELECT result_json FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? AND idempotency_key = ?", [project.id, run.id, idempotencyKey]);
|
|
545
|
+
if (priorMutation)
|
|
546
|
+
return { ...JSON.parse(priorMutation.result_json), idempotent: true };
|
|
547
|
+
// Canonical contract bytes are prepared before taking the writer. Revision
|
|
548
|
+
// and idempotency are compared again against SQL authority under the lock.
|
|
549
|
+
const contract = loadProjectFlowContract(run.project_root);
|
|
550
|
+
return context.db.writeTransaction(() => {
|
|
344
551
|
const existingMutation = context.db.get("SELECT result_json FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? AND idempotency_key = ?", [project.id, run.id, idempotencyKey]);
|
|
345
552
|
if (existingMutation) {
|
|
346
553
|
const result = { ...JSON.parse(existingMutation.result_json), idempotent: true };
|
|
347
|
-
context.db.exec("COMMIT");
|
|
348
|
-
committed = true;
|
|
349
554
|
return result;
|
|
350
555
|
}
|
|
351
556
|
// Read the authority only after the write lock: two callers with the same
|
|
352
557
|
// expected revision must not both pass the compare-and-set check.
|
|
353
558
|
const lockedRun = requireRunById(context, project.id, run.id);
|
|
354
559
|
const index = authoritativeIndex(lockedRun);
|
|
355
|
-
const
|
|
356
|
-
const current = runtime?.flow_flags ?? index.flow_flags;
|
|
560
|
+
const current = index.flow_flags;
|
|
357
561
|
if (!current) {
|
|
358
562
|
throw new AppError("validation", "Legacy RUN has no flow-flag snapshot; revise is unavailable", 2, { run_id: run.id });
|
|
359
563
|
}
|
|
@@ -370,7 +574,6 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
370
574
|
});
|
|
371
575
|
}
|
|
372
576
|
const now = context.now();
|
|
373
|
-
const contract = loadProjectFlowContract(lockedRun.project_root);
|
|
374
577
|
const next = resolveFlowFlags(contract, {
|
|
375
578
|
flowKind: index.flow_kind,
|
|
376
579
|
preset: input.preset ?? current.preset.applied,
|
|
@@ -426,14 +629,12 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
426
629
|
context.db.run(`INSERT INTO flow_run_flag_mutations
|
|
427
630
|
(project_id, run_id, idempotency_key, expected_revision, resulting_revision, result_json, created_at)
|
|
428
631
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, [project.id, run.id, idempotencyKey, input.expectedRevision, next.snapshot_revision, JSON.stringify(result), now]);
|
|
429
|
-
context.db.exec("COMMIT");
|
|
430
|
-
committed = true;
|
|
431
632
|
appendAudit(context, {
|
|
432
633
|
projectId: project.id,
|
|
433
634
|
eventType: "flow_run.flags_revised",
|
|
434
635
|
payload: { run_id: run.id, snapshot_revision: next.snapshot_revision, snapshot_checksum: next.snapshot_checksum, ...(reason ? { reason } : {}) }
|
|
435
636
|
});
|
|
436
|
-
appendRunTimeline(
|
|
637
|
+
appendRunTimeline(context, project.id, run.id, {
|
|
437
638
|
at: now,
|
|
438
639
|
type: "flow_flags_revised",
|
|
439
640
|
run_id: run.id,
|
|
@@ -443,12 +644,7 @@ export function reviseFlowRunFlags(context, input) {
|
|
|
443
644
|
...(reason ? { reason } : {})
|
|
444
645
|
});
|
|
445
646
|
return result;
|
|
446
|
-
}
|
|
447
|
-
catch (error) {
|
|
448
|
-
if (!committed)
|
|
449
|
-
context.db.exec("ROLLBACK");
|
|
450
|
-
throw error;
|
|
451
|
-
}
|
|
647
|
+
}, "runs.reviseFlowFlags");
|
|
452
648
|
}
|
|
453
649
|
export function listFlowRuns(context, input) {
|
|
454
650
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -459,113 +655,268 @@ export function listFlowRuns(context, input) {
|
|
|
459
655
|
.map(flowRunSummary);
|
|
460
656
|
return { ok: true, project, runs };
|
|
461
657
|
}
|
|
462
|
-
export function
|
|
658
|
+
export function prepareFlowRunStageAttachment(context, input, archive = false) {
|
|
463
659
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
464
660
|
const run = resolveRun(context, project.id, input.runId);
|
|
465
|
-
const
|
|
466
|
-
const now = context.now();
|
|
661
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
467
662
|
const stage = requiredPlain(input.stage, "stage");
|
|
468
|
-
const dir =
|
|
663
|
+
const dir = prepareStageDirectory(input.dir);
|
|
664
|
+
const index = authoritativeIndex(run);
|
|
469
665
|
const status = parseStageStatus(input.status);
|
|
470
666
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
dir
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
667
|
+
const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : null;
|
|
668
|
+
if (bundle)
|
|
669
|
+
assertBundledStageStart(bundle.descriptor, index, stage, dir, status, existing);
|
|
670
|
+
else
|
|
671
|
+
assertVnextStageStart(run, index, stage, status, existing);
|
|
672
|
+
if (archive) {
|
|
673
|
+
const runRoot = runArtifactRoot(run);
|
|
674
|
+
if (fs.existsSync(path.join(runRoot, `.archive-${dir}.json`)) || status === "running" && existing) {
|
|
675
|
+
archiveExistingStageAttempt(context, project.id, run.id, runRoot, dir);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return { project, run, index, stage, dir, status, existing, generation, bundle, attempt: status === "running" ? nextAttempt(existing) : existing?.attempt ?? "try-001", git: status === "running" ? gitFacts(run.workspace_root) : undefined };
|
|
679
|
+
}
|
|
680
|
+
export function attachFlowRunStage(context, input, prepared = prepareFlowRunStageAttachment(context, input, true)) {
|
|
681
|
+
// Filesystem archive and Git inspection are preparation, never writer work.
|
|
682
|
+
const { project, run, stage, dir, status, generation, git } = prepared;
|
|
683
|
+
const attached = context.db.writeTransaction(() => {
|
|
684
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
685
|
+
const latest = requireRunById(context, project.id, run.id);
|
|
686
|
+
const index = authoritativeIndex(latest);
|
|
687
|
+
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
688
|
+
if ((index.runtime_revision ?? 0) !== (prepared.index.runtime_revision ?? 0) || existing?.attempt !== prepared.existing?.attempt) {
|
|
689
|
+
throw new AppError("stage_revision_changed", "Stage changed during start preparation", 1, { run_id: run.id, stage });
|
|
690
|
+
}
|
|
691
|
+
if (prepared.bundle) {
|
|
692
|
+
if (index.execution_bundle?.manifest_sha256 !== prepared.bundle.sha256)
|
|
693
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle changed during stage preparation", 1);
|
|
694
|
+
assertBundledStageStart(prepared.bundle.descriptor, index, stage, dir, status, existing);
|
|
695
|
+
}
|
|
696
|
+
else
|
|
697
|
+
assertVnextStageStart(latest, index, stage, status, existing);
|
|
698
|
+
const now = context.now();
|
|
699
|
+
const stageRun = {
|
|
700
|
+
...(existing ?? { order: index.stage_runs.length + 1 }),
|
|
701
|
+
stage,
|
|
702
|
+
dir,
|
|
703
|
+
status,
|
|
704
|
+
...(input.inputSha256 ? { input_sha256: input.inputSha256 } : {}),
|
|
705
|
+
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : existing?.data_schema_id ? { data_schema_id: existing.data_schema_id } : {}),
|
|
706
|
+
updated_at: now
|
|
707
|
+
};
|
|
708
|
+
if (status === "running") {
|
|
709
|
+
stageRun.started_at = now;
|
|
710
|
+
delete stageRun.completed_at;
|
|
711
|
+
stageRun.duration_ms = null;
|
|
712
|
+
if (git)
|
|
713
|
+
stageRun.git = git;
|
|
714
|
+
stageRun.attempt = prepared.attempt;
|
|
715
|
+
}
|
|
716
|
+
upsertStage(index, stageRun);
|
|
717
|
+
index.current_stage = stage;
|
|
718
|
+
index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
|
|
719
|
+
index.updated_at = now;
|
|
720
|
+
persistRunState(context, project, run, index);
|
|
721
|
+
appendAudit(context, {
|
|
722
|
+
projectId: project.id,
|
|
723
|
+
eventType: "flow_run.stage_attached",
|
|
724
|
+
payload: { run_id: run.id, stage, dir, status }
|
|
725
|
+
});
|
|
726
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_attached", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
|
|
727
|
+
context.db.afterCommit(() => reconcileRunStageReports(context, project.id, run.id, stage));
|
|
728
|
+
// A stage transition must not rewrite every Session in the RUN. A RUN may
|
|
729
|
+
// contain independent reviewer/worker sessions; their stage is set by the
|
|
730
|
+
// stage/work binding that actually owns that Session.
|
|
731
|
+
if (status === "running")
|
|
732
|
+
context.db.afterCommit(() => refreshRunSessionProjection(context, project.id, run.id));
|
|
733
|
+
return { stageRun, index };
|
|
734
|
+
}, "stage.attach");
|
|
506
735
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
507
|
-
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
736
|
+
return { ok: true, run: flowRunSummary(updatedRun), stage_run: attached.stageRun, index: attached.index, flow_guidance: guidanceForRun(context, updatedRun, attached.index) };
|
|
737
|
+
}
|
|
738
|
+
/** Keep synchronous stage settlement atomic; files are projections, SQLite is authoritative. */
|
|
739
|
+
export function withStageSettlement(context, operation) {
|
|
740
|
+
return context.db.writeTransaction(operation, "stage.settle");
|
|
508
741
|
}
|
|
509
|
-
export function
|
|
742
|
+
export function prepareFlowRunStageCompletion(context, input, admittedBundle) {
|
|
743
|
+
for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
|
|
744
|
+
if (value !== undefined)
|
|
745
|
+
requiredPlain(value, name);
|
|
746
|
+
}
|
|
747
|
+
if (input.aliases !== undefined) {
|
|
748
|
+
if (!Array.isArray(input.aliases))
|
|
749
|
+
throw new AppError("validation", "artifact aliases must be an array", 2);
|
|
750
|
+
for (const alias of input.aliases)
|
|
751
|
+
requiredPlain(alias, "artifact alias");
|
|
752
|
+
}
|
|
510
753
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
511
754
|
const run = resolveRun(context, project.id, input.runId);
|
|
755
|
+
if (["done", "failed", "skipped"].includes(input.status))
|
|
756
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.stage);
|
|
757
|
+
else
|
|
758
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
512
759
|
const index = authoritativeIndex(run);
|
|
513
|
-
const now = context.now();
|
|
514
760
|
const stage = requiredPlain(input.stage, "stage");
|
|
515
761
|
const status = parseStageStatus(input.status);
|
|
516
762
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
517
763
|
if (!existing) {
|
|
518
764
|
throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
|
|
519
765
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
};
|
|
533
|
-
upsertStage(index, stageRun);
|
|
534
|
-
if (status === "waiting_for_user") {
|
|
535
|
-
index.status = "waiting_for_user";
|
|
536
|
-
index.verdict = "waiting_for_user";
|
|
537
|
-
}
|
|
538
|
-
else if (status === "blocked" || status === "failed") {
|
|
539
|
-
index.status = status;
|
|
540
|
-
index.verdict = status;
|
|
766
|
+
// Bound completion must admit the same immutable descriptor/engine authority
|
|
767
|
+
// as stage entry. Keep filesystem admission outside the SQLite writer.
|
|
768
|
+
const bundle = admittedBundle === undefined
|
|
769
|
+
? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : admittedBundle;
|
|
770
|
+
if (bundle && !index.execution_bundle)
|
|
771
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared after finish admission", 1);
|
|
772
|
+
if (bundle)
|
|
773
|
+
assertCompletionBundleAuthority(context, run, index);
|
|
774
|
+
if (index.execution_bundle && (!bundle || bundle.sha256 !== index.execution_bundle.manifest_sha256
|
|
775
|
+
|| bundle.manifest.descriptor.sha256 !== index.execution_bundle.descriptor_sha256
|
|
776
|
+
|| hybridDigest(bundleAuthority(bundle.admission())) !== index.execution_bundle.admission_sha256)) {
|
|
777
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared finish bundle does not match authoritative RUN admission", 1);
|
|
541
778
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
index.
|
|
779
|
+
if (bundle) {
|
|
780
|
+
const declared = bundle.descriptor.stage(stage);
|
|
781
|
+
if (declared.directory !== existing.dir || index.current_stage !== stage) {
|
|
782
|
+
throw new AppError("flow_stage_completion_invalid", "Completion does not belong to the active descriptor stage", 2, { phase: "prepare", effect: "no_effect", run_id: run.id, stage });
|
|
783
|
+
}
|
|
545
784
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
785
|
+
const dataPath = input.data ? path.join(runArtifactRoot(run), existing.dir, input.data) : null;
|
|
786
|
+
const dataSha256 = input.dataSha256 ?? (dataPath && fs.existsSync(dataPath) ? crypto.createHash("sha256").update(fs.readFileSync(dataPath)).digest("hex") : null);
|
|
787
|
+
if (dataSha256 && !/^[a-f0-9]{64}$/.test(dataSha256))
|
|
788
|
+
throw new AppError("validation", "Stage data SHA-256 is invalid", 2);
|
|
789
|
+
return { project, run, index, stage, status, existing, bundle, dataSha256, git: input.git ?? gitFacts(run.workspace_root), generation: ["done", "failed", "skipped"].includes(status) ? assertStageSettlementAllowed(context, project.id, run.id, stage) : assertRunMutationAllowed(context, project.id, run.id) };
|
|
790
|
+
}
|
|
791
|
+
/** Raw historical control is not a finish-sealing bypass for a descriptor-bound RUN. */
|
|
792
|
+
export function prepareRawFlowRunStageCompletion(context, input) {
|
|
793
|
+
const prepared = prepareFlowRunStageCompletion(context, input);
|
|
794
|
+
if (prepared.bundle)
|
|
795
|
+
throw new AppError("stage_finish_required", "Descriptor-bound stages must settle through their registered stage finish handler", 2, { phase: "prepare", effect: "no_effect", run_id: prepared.run.id, stage: prepared.stage });
|
|
796
|
+
return prepared;
|
|
797
|
+
}
|
|
798
|
+
function stageUsageProjection(context, run, stage, startedAt) {
|
|
799
|
+
if (!startedAt)
|
|
800
|
+
return { status: "unavailable", input_tokens: null, cached_input_tokens: null, output_tokens: null, total_tokens: null, session_count: 0, observed_at: context.now() };
|
|
801
|
+
const value = recalculateRunUsage(context, { projectId: run.project_id, runId: run.id, stage: { name: stage, startedAt, completedAt: context.now() } });
|
|
802
|
+
const totals = value.totals, coverage = value.coverage;
|
|
803
|
+
const count = (name) => typeof coverage?.[name] === "number" ? coverage[name] : 0;
|
|
804
|
+
const measured = count("measured"), other = Object.values(coverage ?? {}).reduce((total, item) => total + (typeof item === "number" ? item : 0), 0) - measured;
|
|
805
|
+
const tokens = (name) => typeof totals?.[name] === "number" ? totals[name] : null;
|
|
806
|
+
return { status: measured === 0 ? "unavailable" : other > 0 || ["input_tokens", "cache_read_input_tokens", "output_tokens", "total_tokens"].some(name => tokens(name) === null) ? "partial" : "measured", input_tokens: tokens("input_tokens"), cached_input_tokens: tokens("cache_read_input_tokens"), output_tokens: tokens("output_tokens"), total_tokens: tokens("total_tokens"), session_count: Array.isArray(value.sessions) ? value.sessions.length : 0, observed_at: typeof value.observed_at === "string" ? value.observed_at : context.now() };
|
|
807
|
+
}
|
|
808
|
+
export function completeFlowRunStage(context, input, prepared = prepareFlowRunStageCompletion(context, input)) {
|
|
809
|
+
const { project, run, stage, status, dataSha256, git, generation } = prepared;
|
|
810
|
+
const completed = context.db.writeTransaction(() => {
|
|
811
|
+
if (["done", "failed", "skipped"].includes(status))
|
|
812
|
+
assertStageSettlementAllowed(context, project.id, run.id, stage, generation);
|
|
813
|
+
else
|
|
814
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
815
|
+
const latest = requireRunById(context, project.id, run.id);
|
|
816
|
+
const index = authoritativeIndex(latest);
|
|
817
|
+
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
818
|
+
if (!existing || existing.attempt !== prepared.existing.attempt || index.runtime_revision !== prepared.index.runtime_revision) {
|
|
819
|
+
throw new AppError("stage_revision_changed", "Stage attempt changed during finish preparation", 1, { run_id: run.id, stage, expected_attempt: prepared.existing.attempt, actual_attempt: existing?.attempt ?? null });
|
|
820
|
+
}
|
|
821
|
+
if (hybridDigest(index.execution_bundle ?? null) !== hybridDigest(prepared.index.execution_bundle ?? null)) {
|
|
822
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle authority changed during finish preparation", 1);
|
|
823
|
+
}
|
|
824
|
+
if (prepared.bundle && (index.current_stage !== stage || existing.dir !== prepared.bundle.descriptor.stage(stage).directory)) {
|
|
825
|
+
throw new AppError("flow_stage_completion_invalid", "Active descriptor stage changed during finish preparation", 1);
|
|
826
|
+
}
|
|
827
|
+
if (prepared.bundle)
|
|
828
|
+
assertCompletionBundleAuthority(context, latest, index);
|
|
829
|
+
const now = context.now();
|
|
830
|
+
const stageRun = {
|
|
831
|
+
...existing,
|
|
832
|
+
status,
|
|
833
|
+
...(input.stageReport ? { stage_report: input.stageReport } : {}),
|
|
834
|
+
...(input.data ? { data: input.data } : {}),
|
|
835
|
+
...(dataSha256 ? { data_sha256: dataSha256 } : {}),
|
|
836
|
+
...(input.dataSchemaId ? { data_schema_id: input.dataSchemaId } : {}),
|
|
837
|
+
...(input.report ? { report: input.report } : {}),
|
|
838
|
+
usage: input.usage ?? stageUsageProjection(context, latest, stage, existing.started_at),
|
|
839
|
+
...(input.aliases && input.aliases.length > 0 ? { artifact_aliases: input.aliases } : {}),
|
|
840
|
+
updated_at: now,
|
|
841
|
+
...(status === "done" || status === "waiting_for_user" || status === "blocked" || status === "failed" || status === "skipped"
|
|
842
|
+
? { completed_at: now, duration_ms: durationMs(existing.started_at ?? null, now), git }
|
|
843
|
+
: {})
|
|
844
|
+
};
|
|
845
|
+
upsertStage(index, stageRun);
|
|
846
|
+
if (status === "waiting_for_user") {
|
|
847
|
+
index.status = "waiting_for_user";
|
|
848
|
+
index.verdict = "waiting_for_user";
|
|
849
|
+
}
|
|
850
|
+
else if (status === "blocked" || status === "failed") {
|
|
851
|
+
index.status = status;
|
|
852
|
+
index.verdict = status;
|
|
853
|
+
}
|
|
854
|
+
else if (status === "done" && index.status === "waiting_for_user") {
|
|
855
|
+
index.status = "running";
|
|
856
|
+
index.verdict = "running";
|
|
857
|
+
}
|
|
858
|
+
index.current_stage = stage;
|
|
859
|
+
index.attempts = index.stage_runs.map((item) => ({ stage: item.stage, attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1, root: item.dir, archive: null, status: item.status, started_at: item.started_at ?? now, finished_at: item.completed_at ?? null }));
|
|
860
|
+
index.updated_at = now;
|
|
861
|
+
index.execution = {
|
|
862
|
+
project_root: index.execution?.project_root ?? run.project_root,
|
|
863
|
+
workspace_root: index.execution?.workspace_root ?? run.workspace_root,
|
|
864
|
+
git
|
|
865
|
+
};
|
|
866
|
+
persistRunState(context, project, latest, index);
|
|
867
|
+
context.db.afterCommit(() => refreshRunSessionProjection(context, project.id, run.id));
|
|
868
|
+
appendAudit(context, {
|
|
869
|
+
projectId: project.id,
|
|
870
|
+
eventType: "flow_run.stage_completed",
|
|
871
|
+
payload: { run_id: run.id, stage, status, stage_report: input.stageReport ?? null, data: input.data ?? null }
|
|
872
|
+
});
|
|
873
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_completed", run_id: run.id, stage, status, attempt: stageRun.attempt ?? null });
|
|
874
|
+
return { index, stageRun };
|
|
875
|
+
}, "runs.completeFlowRunStage");
|
|
562
876
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
563
|
-
return { ok: true, run: flowRunSummary(updatedRun), stage_run: stageRun, index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
877
|
+
return { ok: true, run: flowRunSummary(updatedRun), stage_run: completed.stageRun, index: completed.index, flow_guidance: guidanceForRun(context, updatedRun, completed.index) };
|
|
878
|
+
}
|
|
879
|
+
/** Writer-safe readback of independently retained SQL authority; no filesystem admission. */
|
|
880
|
+
function assertCompletionBundleAuthority(context, run, index) {
|
|
881
|
+
const anchor = index.execution_bundle;
|
|
882
|
+
if (!anchor)
|
|
883
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared during finish", 1);
|
|
884
|
+
assertRunBundleSqlAuthority(context, { projectId: run.project_id, runId: run.id }, anchor);
|
|
885
|
+
}
|
|
886
|
+
/** Metadata-only fencing shared by lifecycle ownership and the settlement writer.
|
|
887
|
+
* False means genuine historical authority, never a lost/orphaned bundle anchor.
|
|
888
|
+
* Physical descriptor/engine admission remains an independent outside-writer check.
|
|
889
|
+
*/
|
|
890
|
+
export function assertRunBundleSqlAuthority(context, scope, anchor) {
|
|
891
|
+
const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
|
|
892
|
+
const row = hasAuthority ? context.db.get("SELECT manifest_sha256, admission_json FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [scope.projectId, scope.runId]) : undefined;
|
|
893
|
+
if (!anchor) {
|
|
894
|
+
if (row)
|
|
895
|
+
throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || typeof anchor.flow_id !== "string"
|
|
899
|
+
|| [anchor.manifest_sha256, anchor.descriptor_sha256, anchor.admission_sha256].some(hash => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash)))
|
|
900
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
|
|
901
|
+
if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
|
|
902
|
+
throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority disappeared during finish", 1);
|
|
903
|
+
let valid = false;
|
|
904
|
+
try {
|
|
905
|
+
if (Buffer.byteLength(row.admission_json, "utf8") <= 1024 * 1024) {
|
|
906
|
+
const retained = JSON.parse(row.admission_json);
|
|
907
|
+
valid = retained.runId === scope.runId && hybridDigest(retained) === anchor.admission_sha256;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch { /* Corrupt authority cannot be replaced by a prepared object. */ }
|
|
911
|
+
if (!valid)
|
|
912
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed during finish", 1);
|
|
913
|
+
return true;
|
|
564
914
|
}
|
|
565
915
|
/** Pause one live stage without completing it or creating a new attempt. */
|
|
566
916
|
export function pauseFlowRunStage(context, input) {
|
|
567
917
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
568
918
|
const run = resolveRun(context, project.id, input.runId);
|
|
919
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
569
920
|
const index = authoritativeIndex(run);
|
|
570
921
|
const stage = requiredPlain(input.stage, "stage");
|
|
571
922
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -594,13 +945,14 @@ export function pauseFlowRunStage(context, input) {
|
|
|
594
945
|
persistRunState(context, project, run, index);
|
|
595
946
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
596
947
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_paused", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, reason: "waiting_for_user" } });
|
|
597
|
-
appendRunTimeline(
|
|
948
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_waiting_for_user", run_id: run.id, stage, work_id: input.workId, pause_id: input.pauseId, question_path: input.questionPath });
|
|
598
949
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
599
950
|
}
|
|
600
951
|
/** Resume the same stage, Work and attempt after a user answer is recorded. */
|
|
601
952
|
export function resumeFlowRunStage(context, input) {
|
|
602
953
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
603
954
|
const run = resolveRun(context, project.id, input.runId);
|
|
955
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
604
956
|
const index = authoritativeIndex(run);
|
|
605
957
|
const stage = requiredPlain(input.stage, "stage");
|
|
606
958
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
@@ -624,19 +976,28 @@ export function resumeFlowRunStage(context, input) {
|
|
|
624
976
|
persistRunState(context, project, run, index);
|
|
625
977
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
626
978
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_resumed", payload: { run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath } });
|
|
627
|
-
appendRunTimeline(
|
|
628
|
-
appendRunTimeline(
|
|
979
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "user_answer_received", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, answer_path: input.answerPath });
|
|
980
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_resumed", run_id: run.id, stage, work_id: input.workId, pause_id: pause.id, paused_ms: pausedMs });
|
|
629
981
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
630
982
|
}
|
|
631
983
|
/** Pause the current stage for a non-user runtime blocker. */
|
|
632
|
-
export function
|
|
984
|
+
export function prepareFlowRunStageBlock(context, input) {
|
|
985
|
+
if (!["engine", "harness", "environment"].includes(input.kind) || typeof input.retryable !== "boolean")
|
|
986
|
+
throw new AppError("validation", "stage block requires a valid kind and boolean retryable", 2);
|
|
987
|
+
requiredPlain(input.code, "code");
|
|
988
|
+
requiredPlain(input.summary, "summary");
|
|
633
989
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
634
990
|
const run = resolveRun(context, project.id, input.runId);
|
|
991
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
635
992
|
const index = authoritativeIndex(run);
|
|
636
993
|
const stage = requiredPlain(input.stage, "stage");
|
|
637
994
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
638
995
|
if (!stageRun || stageRun.status !== "running")
|
|
639
996
|
throw new AppError("invalid_stage_state", "Only a running stage can be blocked", 1, { run_id: run.id, stage, status: stageRun?.status ?? "missing" });
|
|
997
|
+
return { project, run, index, stage, stageRun };
|
|
998
|
+
}
|
|
999
|
+
export function blockFlowRunStage(context, input, prepared = prepareFlowRunStageBlock(context, input)) {
|
|
1000
|
+
const { project, run, index, stage, stageRun } = prepared;
|
|
640
1001
|
const now = context.now();
|
|
641
1002
|
stageRun.status = "blocked";
|
|
642
1003
|
stageRun.updated_at = now;
|
|
@@ -650,20 +1011,24 @@ export function blockFlowRunStage(context, input) {
|
|
|
650
1011
|
persistRunState(context, project, run, index);
|
|
651
1012
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
652
1013
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_blocked", payload: { run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, retryable: input.retryable } });
|
|
653
|
-
appendRunTimeline(
|
|
1014
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_blocked", run_id: run.id, stage, work_id: input.workId, kind: input.kind, code: input.code, summary: input.summary, retryable: input.retryable });
|
|
654
1015
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
655
1016
|
}
|
|
656
1017
|
/** Continue the same stage after its non-user blocker was repaired externally. */
|
|
657
|
-
export function
|
|
1018
|
+
export function prepareFlowRunStageUnblock(context, input) {
|
|
658
1019
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
659
1020
|
const run = resolveRun(context, project.id, input.runId);
|
|
1021
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
660
1022
|
const index = authoritativeIndex(run);
|
|
661
1023
|
const stage = requiredPlain(input.stage, "stage");
|
|
662
1024
|
const stageRun = index.stage_runs.find((item) => item.stage === stage);
|
|
663
1025
|
if (!stageRun || stageRun.status !== "blocked" || !stageRun.blocker || stageRun.blocker.work_id !== input.workId)
|
|
664
1026
|
throw new AppError("invalid_stage_state", "Stage unblock requires its matching runtime blocker", 1, { run_id: run.id, stage, work_id: input.workId, status: stageRun?.status ?? "missing" });
|
|
1027
|
+
return { project, run, index, stage, stageRun, blocker: stageRun.blocker };
|
|
1028
|
+
}
|
|
1029
|
+
export function unblockFlowRunStage(context, input, prepared = prepareFlowRunStageUnblock(context, input)) {
|
|
1030
|
+
const { project, run, index, stage, stageRun, blocker } = prepared;
|
|
665
1031
|
const now = context.now();
|
|
666
|
-
const blocker = stageRun.blocker;
|
|
667
1032
|
const blockedMs = durationMs(blocker.blocked_at, now) ?? 0;
|
|
668
1033
|
stageRun.status = "running";
|
|
669
1034
|
stageRun.updated_at = now;
|
|
@@ -678,7 +1043,7 @@ export function unblockFlowRunStage(context, input) {
|
|
|
678
1043
|
persistRunState(context, project, run, index);
|
|
679
1044
|
refreshRunSessionProjection(context, project.id, run.id);
|
|
680
1045
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.stage_unblocked", payload: { run_id: run.id, stage, work_id: input.workId, code: blocker.code } });
|
|
681
|
-
appendRunTimeline(
|
|
1046
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "stage_unblocked", run_id: run.id, stage, work_id: input.workId, code: blocker.code, blocked_ms: blockedMs });
|
|
682
1047
|
return { run: flowRunSummary(requireRunById(context, project.id, run.id)), stage_run: stageRun, index };
|
|
683
1048
|
}
|
|
684
1049
|
function operationalRunStatus(context, projectId, runId) {
|
|
@@ -687,11 +1052,76 @@ function operationalRunStatus(context, projectId, runId) {
|
|
|
687
1052
|
const leaves = unfinished.filter((work) => !parents.has(work.work_id));
|
|
688
1053
|
return leaves.length > 0 && leaves.every((work) => work.status === "paused") ? "paused" : "running";
|
|
689
1054
|
}
|
|
690
|
-
|
|
691
|
-
|
|
1055
|
+
/** Keep the logical RUN projection aligned with recovery authority. Stage/Work
|
|
1056
|
+
* evidence and verdicts survive a reversible stop; terminal runs never reopen. */
|
|
1057
|
+
export function setFlowRunRecoveryPaused(context, projectId, runId, paused) {
|
|
1058
|
+
const run = resolveRun(context, projectId, runId);
|
|
1059
|
+
if (!["running", "paused"].includes(run.status))
|
|
1060
|
+
return;
|
|
1061
|
+
const index = authoritativeIndex(run);
|
|
1062
|
+
const status = paused || index.stage_runs.some(stage => ["paused", "waiting_for_user", "blocked"].includes(stage.status))
|
|
1063
|
+
? "paused" : operationalRunStatus(context, projectId, runId);
|
|
1064
|
+
if (run.status === status && index.status === status)
|
|
1065
|
+
return;
|
|
1066
|
+
index.status = status;
|
|
1067
|
+
index.updated_at = context.now();
|
|
1068
|
+
const project = requireProjectByRoot(context, run.project_root);
|
|
1069
|
+
persistRunState(context, project, run, index);
|
|
1070
|
+
}
|
|
1071
|
+
/** Reconciliation repairs projections under the same revision CAS as normal
|
|
1072
|
+
* lifecycle writes. Unknown physical ownership never closes child records. */
|
|
1073
|
+
export function reconcileStoredFlowRun(context, expected, health, transition, childrenSettled = false) {
|
|
1074
|
+
const run = resolveRun(context, expected.project_id, expected.id);
|
|
1075
|
+
if (run.index_json !== expected.index_json || run.status !== expected.status)
|
|
1076
|
+
throw new AppError("run_revision_changed", "RUN changed during health observation", 1);
|
|
1077
|
+
const index = authoritativeIndex(run), now = context.now();
|
|
1078
|
+
index.health = health;
|
|
1079
|
+
if (transition && !["done", "failed", "cancelled", "discarded"].includes(run.status)) {
|
|
1080
|
+
index.status = transition;
|
|
1081
|
+
if (transition === "failed") {
|
|
1082
|
+
index.verdict = "failed";
|
|
1083
|
+
// Observation time is not the historical exit time. Without a retained
|
|
1084
|
+
// finish receipt, keep timing unknown rather than manufacturing days of work.
|
|
1085
|
+
delete index.completed_at;
|
|
1086
|
+
delete index.finished_at;
|
|
1087
|
+
index.duration_ms = null;
|
|
1088
|
+
index.timing_status = 'legacy_incomplete';
|
|
1089
|
+
closeOpenStagesForOverride(index, "failed", now, false);
|
|
1090
|
+
closeOpenWorksForOverride(context, run.project_id, run.id, "failed", now, false);
|
|
1091
|
+
}
|
|
1092
|
+
else if (childrenSettled && health.state === "owner_lost") {
|
|
1093
|
+
for (const stage of index.stage_runs)
|
|
1094
|
+
if (stage.status === "running") {
|
|
1095
|
+
stage.status = "paused";
|
|
1096
|
+
stage.updated_at = now;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (childrenSettled && ["failed", "cancelled", "discarded"].includes(index.status)) {
|
|
1101
|
+
closeOpenStagesForOverride(index, index.status === "failed" ? "failed" : "cancelled", now, false);
|
|
1102
|
+
closeOpenWorksForOverride(context, run.project_id, run.id, index.status === "failed" ? "failed" : "cancelled", now, false);
|
|
1103
|
+
context.db.run("UPDATE sessions SET status = 'stopped', stop_reason = ?, updated_at = ? WHERE project_id = ? AND run_id = ? AND status IN ('pending','active','waiting_user','blocked','stopping')", [`run_${index.status}`, now, run.project_id, run.id]);
|
|
1104
|
+
context.db.run("UPDATE check_receipts SET status = 'aborted' WHERE project_id = ? AND run_id = ? AND status = 'running'", [run.project_id, run.id]);
|
|
1105
|
+
}
|
|
1106
|
+
// Health observation is not semantic progress or a new completion timestamp.
|
|
1107
|
+
const project = requireProjectByRoot(context, run.project_root);
|
|
1108
|
+
persistRunState(context, project, run, index, { healthOnly: true });
|
|
1109
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.health_reconciled", payload: { run_id: run.id, previous_status: run.status, status: index.status, health } });
|
|
1110
|
+
}
|
|
1111
|
+
export function prepareFlowRunCompletion(context, input) {
|
|
1112
|
+
if (input.manualOverrideReason !== undefined) {
|
|
1113
|
+
requiredPlain(input.manualOverrideReason, "override reason");
|
|
1114
|
+
if (!["cancelled", "failed"].includes(input.status))
|
|
1115
|
+
throw new AppError("validation", "Manual RUN override may only cancel or fail a RUN", 2);
|
|
1116
|
+
}
|
|
1117
|
+
if (input.verdict !== undefined)
|
|
1118
|
+
requiredPlain(input.verdict, "verdict");
|
|
1119
|
+
if (input.nextAction !== undefined)
|
|
1120
|
+
requiredPlain(input.nextAction, "next action");
|
|
1121
|
+
const project = requireProjectByRoot(context, input.manualOverrideReason ? canonicalPath(input.projectRoot, false) : resolveProjectRoot(input.projectRoot));
|
|
692
1122
|
const run = resolveRun(context, project.id, input.runId);
|
|
1123
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
693
1124
|
const index = authoritativeIndex(run);
|
|
694
|
-
const now = context.now();
|
|
695
1125
|
const status = parseRunStatus(input.status);
|
|
696
1126
|
if (!['done', 'cancelled', 'failed'].includes(status)) {
|
|
697
1127
|
throw new AppError('validation', 'completeFlowRun accepts only done, cancelled, or failed; use stage block/unblock for a recoverable blocker', 2, { status });
|
|
@@ -701,6 +1131,11 @@ export function completeFlowRun(context, input) {
|
|
|
701
1131
|
if (activeWork)
|
|
702
1132
|
throw new AppError("active_work", "RUN cannot complete while Work remains active", 2, { run_id: run.id, work_id: activeWork.work_id, status: activeWork.status });
|
|
703
1133
|
}
|
|
1134
|
+
return { project, run, index, status };
|
|
1135
|
+
}
|
|
1136
|
+
export function completeFlowRun(context, input) {
|
|
1137
|
+
const { project, run, index, status } = prepareFlowRunCompletion(context, input);
|
|
1138
|
+
const now = context.now();
|
|
704
1139
|
if (input.manualOverrideReason && (status === "cancelled" || status === "failed")) {
|
|
705
1140
|
closeOpenStagesForOverride(index, status, now);
|
|
706
1141
|
closeOpenWorksForOverride(context, project.id, run.id, status, now);
|
|
@@ -734,25 +1169,65 @@ export function completeFlowRun(context, input) {
|
|
|
734
1169
|
eventType: input.manualOverrideReason ? "flow_run.manual_override" : "flow_run.completed",
|
|
735
1170
|
payload: { run_id: run.id, status, verdict: index.verdict, next_action: index.next_action, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) }
|
|
736
1171
|
});
|
|
737
|
-
appendRunTimeline(
|
|
1172
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: input.manualOverrideReason ? "run_manual_override" : "run_completed", run_id: run.id, status, ...(input.manualOverrideReason ? { reason: input.manualOverrideReason } : {}) });
|
|
738
1173
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
739
1174
|
return { ok: true, run: flowRunSummary(updatedRun), index, usage, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
740
1175
|
}
|
|
1176
|
+
/** SQL-only terminal writer for an independently prepared descriptor handler.
|
|
1177
|
+
* This closes runtime state, not semantic or Git acceptance. The caller owns
|
|
1178
|
+
* the outer transaction, root settlement and the handler-specific proof fence.
|
|
1179
|
+
*/
|
|
1180
|
+
export function completeDescriptorRunState(context, scope, bundle, git) {
|
|
1181
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
1182
|
+
assertRunMutationAllowed(context, scope.projectId, scope.runId);
|
|
1183
|
+
const run = requireRunById(context, scope.projectId, scope.runId);
|
|
1184
|
+
const index = authoritativeIndex(run);
|
|
1185
|
+
const terminal = index.stage_runs.at(-1);
|
|
1186
|
+
if (run.status !== "running" || !terminal || terminal.status !== "done"
|
|
1187
|
+
|| !bundle.descriptor.descriptor.terminal_stages.includes(terminal.stage)
|
|
1188
|
+
|| index.stage_runs.some(stage => ["running", "paused", "waiting_for_user", "blocked", "pending"].includes(stage.status))
|
|
1189
|
+
|| context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND status IN ('created','running','paused') LIMIT 1", [scope.projectId, scope.runId])
|
|
1190
|
+
|| context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id WHERE w.project_id=? AND w.run_id=? AND ws.status='running' LIMIT 1", [scope.projectId, scope.runId])) {
|
|
1191
|
+
throw new AppError("descriptor_run_settlement_invalid", "Descriptor RUN is not ready for terminal runtime settlement", 1, { effect: "no_effect" });
|
|
1192
|
+
}
|
|
1193
|
+
const project = context.db.get("SELECT * FROM projects WHERE id=?", [scope.projectId]);
|
|
1194
|
+
if (!project)
|
|
1195
|
+
throw new AppError("not_found", "Project is not registered", 1);
|
|
1196
|
+
const now = context.now();
|
|
1197
|
+
index.status = "done";
|
|
1198
|
+
index.verdict = "accepted";
|
|
1199
|
+
index.next_action = null;
|
|
1200
|
+
index.updated_at = now;
|
|
1201
|
+
index.completed_at = now;
|
|
1202
|
+
index.finished_at = now;
|
|
1203
|
+
index.duration_ms = durationMs(index.started_at ?? run.created_at, now);
|
|
1204
|
+
index.execution = { project_root: run.project_root, workspace_root: run.workspace_root, git: { ...git } };
|
|
1205
|
+
persistRunState(context, project, run, index);
|
|
1206
|
+
closeRunOrchestrators(context, scope.projectId, run, "done", now);
|
|
1207
|
+
refreshRunSessionProjection(context, scope.projectId, scope.runId);
|
|
1208
|
+
appendAudit(context, { projectId: scope.projectId, eventType: "flow_run.completed", payload: { run_id: scope.runId, status: "done", verdict: "accepted", next_action: null } });
|
|
1209
|
+
appendRunTimeline(context, scope.projectId, scope.runId, { at: now, type: "run_completed", run_id: scope.runId, status: "done" });
|
|
1210
|
+
}
|
|
741
1211
|
/** Update a live RUN between stages without manufacturing terminal evidence. */
|
|
742
1212
|
export function advanceFlowRun(context, input) {
|
|
743
1213
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
744
1214
|
const run = resolveRun(context, project.id, input.runId);
|
|
745
1215
|
const index = authoritativeIndex(run);
|
|
1216
|
+
if (input.settlementStage && input.status === "running" && index.current_stage === input.settlementStage && index.stage_runs.some(stage => stage.stage === input.settlementStage && ["done", "skipped"].includes(stage.status)))
|
|
1217
|
+
assertStageSettlementAllowed(context, project.id, run.id, input.settlementStage);
|
|
1218
|
+
else
|
|
1219
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
1220
|
+
const control = runControlDirective(context, project.id, run.id);
|
|
746
1221
|
const now = context.now();
|
|
747
1222
|
index.status = input.status;
|
|
748
1223
|
index.verdict = input.verdict;
|
|
749
|
-
index.next_action = input.nextAction ?? null;
|
|
1224
|
+
index.next_action = control ? null : input.nextAction ?? null;
|
|
750
1225
|
index.updated_at = now;
|
|
751
1226
|
persistRunState(context, project, run, index);
|
|
752
|
-
appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status:
|
|
753
|
-
appendRunTimeline(
|
|
1227
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.progressed", payload: { run_id: run.id, status: index.status, verdict: input.verdict, next_action: index.next_action } });
|
|
1228
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "run_progressed", run_id: run.id, status: index.status, next_action: index.next_action });
|
|
754
1229
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
755
|
-
return { ok: true, run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
1230
|
+
return { ok: true, ...(control ? { control } : {}), run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
756
1231
|
}
|
|
757
1232
|
/**
|
|
758
1233
|
* The only legal backward edge in vNext: an integration gate exposed a
|
|
@@ -762,7 +1237,11 @@ export function advanceFlowRun(context, input) {
|
|
|
762
1237
|
export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
763
1238
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
764
1239
|
const run = resolveRun(context, project.id, input.runId);
|
|
1240
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
765
1241
|
const index = authoritativeIndex(run);
|
|
1242
|
+
const priorRepair = index.variables?.["merge.source_repair"];
|
|
1243
|
+
if (priorRepair?.merge_request_id === input.mergeRequestId && priorRepair.cycle)
|
|
1244
|
+
return { run_id: run.id, cycle: priorRepair.cycle };
|
|
766
1245
|
const code = index.stage_runs.find((item) => item.stage === "code");
|
|
767
1246
|
const review = index.stage_runs.find((item) => item.stage === "code-review");
|
|
768
1247
|
const merge = index.stage_runs.find((item) => item.stage === "merge");
|
|
@@ -776,7 +1255,7 @@ export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
|
776
1255
|
}
|
|
777
1256
|
const root = runArtifactRoot(run);
|
|
778
1257
|
for (const stage of ["code", "code-review", "merge"])
|
|
779
|
-
archiveExistingStageAttempt(root, vnextStageDirectory(stage));
|
|
1258
|
+
archiveExistingStageAttempt(context, project.id, run.id, root, vnextStageDirectory(stage));
|
|
780
1259
|
const now = context.now();
|
|
781
1260
|
const restarted = {
|
|
782
1261
|
...code,
|
|
@@ -811,18 +1290,24 @@ export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
|
811
1290
|
}));
|
|
812
1291
|
persistRunState(context, project, run, index);
|
|
813
1292
|
appendAudit(context, { projectId: project.id, eventType: "flow_run.merge_source_repair_prepared", payload: { run_id: run.id, merge_request_id: input.mergeRequestId, cycle } });
|
|
814
|
-
appendRunTimeline(
|
|
1293
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
|
|
815
1294
|
return { run_id: run.id, cycle };
|
|
816
1295
|
}
|
|
817
|
-
function closeOpenStagesForOverride(index, status, now) {
|
|
1296
|
+
function closeOpenStagesForOverride(index, status, now, timingKnown = true) {
|
|
818
1297
|
const stageStatus = status === "cancelled" ? "skipped" : "failed";
|
|
819
1298
|
for (const stage of index.stage_runs) {
|
|
820
|
-
if (stage.status !== "running" && stage.status !== "
|
|
1299
|
+
if (stage.status !== "running" && stage.status !== "paused" && stage.status !== "waiting_for_user" && stage.status !== "blocked")
|
|
821
1300
|
continue;
|
|
822
1301
|
stage.status = stageStatus;
|
|
823
1302
|
stage.updated_at = now;
|
|
824
|
-
|
|
825
|
-
|
|
1303
|
+
if (timingKnown) {
|
|
1304
|
+
stage.completed_at = now;
|
|
1305
|
+
stage.duration_ms = durationMs(stage.started_at ?? null, now);
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
delete stage.completed_at;
|
|
1309
|
+
stage.duration_ms = null;
|
|
1310
|
+
}
|
|
826
1311
|
}
|
|
827
1312
|
index.attempts = index.stage_runs.map((stage) => ({
|
|
828
1313
|
stage: stage.stage,
|
|
@@ -830,20 +1315,20 @@ function closeOpenStagesForOverride(index, status, now) {
|
|
|
830
1315
|
root: stage.dir,
|
|
831
1316
|
archive: null,
|
|
832
1317
|
status: stage.status,
|
|
833
|
-
started_at: stage.started_at ??
|
|
1318
|
+
started_at: stage.started_at ?? null,
|
|
834
1319
|
finished_at: stage.completed_at ?? null
|
|
835
1320
|
}));
|
|
836
1321
|
}
|
|
837
|
-
function closeOpenWorksForOverride(context, projectId, runId, status, now) {
|
|
1322
|
+
function closeOpenWorksForOverride(context, projectId, runId, status, now, timingKnown = true) {
|
|
838
1323
|
const workStatus = status === "cancelled" ? "cancelled" : "failed";
|
|
839
1324
|
context.db.run(`UPDATE works
|
|
840
|
-
SET status = ?, updated_at = ?, completed_at = ?
|
|
1325
|
+
SET status = ?, updated_at = ?, completed_at = CASE WHEN ? = 1 THEN ? ELSE completed_at END
|
|
841
1326
|
WHERE project_id = ? AND run_id = ?
|
|
842
|
-
AND status IN ('created', 'running', 'paused')`, [workStatus, now, now, projectId, runId]);
|
|
1327
|
+
AND status IN ('created', 'running', 'paused')`, [workStatus, now, timingKnown ? 1 : 0, now, projectId, runId]);
|
|
843
1328
|
context.db.run(`UPDATE work_sessions
|
|
844
|
-
SET status = ?, updated_at = ?, completed_at = ?
|
|
1329
|
+
SET status = ?, updated_at = ?, completed_at = CASE WHEN ? = 1 THEN ? ELSE completed_at END
|
|
845
1330
|
WHERE work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)
|
|
846
|
-
AND status <> 'completed'`, [workStatus, now, now, projectId, runId]);
|
|
1331
|
+
AND status <> 'completed'`, [workStatus, now, timingKnown ? 1 : 0, now, projectId, runId]);
|
|
847
1332
|
}
|
|
848
1333
|
function closeRunOrchestrators(context, projectId, run, status, now) {
|
|
849
1334
|
if (!["done", "blocked", "cancelled", "failed"].includes(status))
|
|
@@ -860,7 +1345,7 @@ function closeRunOrchestrators(context, projectId, run, status, now) {
|
|
|
860
1345
|
context.db.run(`UPDATE flow_session_segments SET ended_at = ?
|
|
861
1346
|
WHERE project_id = ? AND run_id = ? AND session_id IN (${active.map(() => "?").join(", ")}) AND ended_at IS NULL`, [now, projectId, run.id, ...active.map((session) => session.session_id)]);
|
|
862
1347
|
for (const session of active) {
|
|
863
|
-
appendRunTimeline(
|
|
1348
|
+
appendRunTimeline(context, projectId, run.id, {
|
|
864
1349
|
at: now,
|
|
865
1350
|
type: "session_stopped",
|
|
866
1351
|
run_id: run.id,
|
|
@@ -983,7 +1468,7 @@ export function getFlowRunSessions(context, input) {
|
|
|
983
1468
|
}
|
|
984
1469
|
export function appendFlowRunTimelineEvent(context, projectId, runId, event) {
|
|
985
1470
|
const run = requireRunById(context, projectId, runId);
|
|
986
|
-
appendRunTimeline(
|
|
1471
|
+
appendRunTimeline(context, projectId, run.id, { run_id: run.id, ...event });
|
|
987
1472
|
}
|
|
988
1473
|
function guidanceForRun(context, run, index) {
|
|
989
1474
|
if (run.flow_kind === "vnext_specify")
|
|
@@ -1078,32 +1563,33 @@ function vnextSpecifyGuidance(run, index) {
|
|
|
1078
1563
|
};
|
|
1079
1564
|
}
|
|
1080
1565
|
function persistRunState(context, project, run, index, options = {}) {
|
|
1566
|
+
// Fenced productive admission is a logical pause, even while physical work
|
|
1567
|
+
// drains. Late admitted Stage results must not revive a stopped RUN.
|
|
1568
|
+
if (index.status === "running" && (!options.healthOnly || context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'run_recovery_guards'")) && ["draining", "sealed", "resuming"].includes(recoveryGuard(context, project.id, run.id)?.status ?? ""))
|
|
1569
|
+
index.status = "paused";
|
|
1570
|
+
if (!options.healthOnly)
|
|
1571
|
+
delete index.health;
|
|
1081
1572
|
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, project.id, run.id);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
index.status,
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
index.updated_at,
|
|
1103
|
-
index.completed_at ?? null,
|
|
1104
|
-
project.id,
|
|
1105
|
-
run.id
|
|
1106
|
-
]);
|
|
1573
|
+
context.db.writeTransaction(() => {
|
|
1574
|
+
const stored = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [project.id, run.id]);
|
|
1575
|
+
if (!stored)
|
|
1576
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: run.id });
|
|
1577
|
+
const previous = JSON.parse(stored.index_json);
|
|
1578
|
+
const actualRevision = previous.runtime_revision ?? 0;
|
|
1579
|
+
const expectedRevision = index.runtime_revision ?? 0;
|
|
1580
|
+
if (actualRevision !== expectedRevision)
|
|
1581
|
+
throw new AppError("run_revision_changed", "RUN changed during preparation", 1, { run_id: run.id, expected_revision: expectedRevision, actual_revision: actualRevision });
|
|
1582
|
+
index.runtime_revision = actualRevision + 1;
|
|
1583
|
+
const history = [...(previous.flag_revision_history ?? [])];
|
|
1584
|
+
if (options.flagRevision)
|
|
1585
|
+
history.push(options.flagRevision);
|
|
1586
|
+
index.flag_revision_history = history.slice(-32);
|
|
1587
|
+
const updated = options.healthOnly ? context.db.run("UPDATE runs SET status = ?, verdict = ?, next_action = ?, index_json = ?, updated_at = ?, completed_at = ? WHERE project_id = ? AND id = ? AND index_json = ?", [index.status, index.verdict, index.next_action, JSON.stringify(persistedVnextIndex(index)), index.updated_at, index.completed_at ?? null, project.id, run.id, stored.index_json]) : context.db.run(`UPDATE runs SET status = ?, verdict = ?, next_action = ?, index_json = ?, runtime_path = ?, run_index_path = ?, updated_at = ?, completed_at = ?
|
|
1588
|
+
WHERE project_id = ? AND id = ? AND index_json = ?`, [index.status, index.verdict, index.next_action, JSON.stringify(persistedVnextIndex(index)), runtimePath, runtimePath, index.updated_at, index.completed_at ?? null, project.id, run.id, stored.index_json]);
|
|
1589
|
+
if (updated.changes !== 1)
|
|
1590
|
+
throw new AppError("run_revision_changed", "RUN changed during preparation", 1, { run_id: run.id });
|
|
1591
|
+
context.db.afterCommit(() => publishRunProjections(context, project.id, run.id));
|
|
1592
|
+
}, "runs.persistRunState");
|
|
1107
1593
|
}
|
|
1108
1594
|
/** vNext stores derived stage/work state only once: stage_runs, Works and Sessions. */
|
|
1109
1595
|
function persistedVnextIndex(index) {
|
|
@@ -1145,35 +1631,75 @@ function materializedRunVariables(index) {
|
|
|
1145
1631
|
...(index.variables ?? {})
|
|
1146
1632
|
};
|
|
1147
1633
|
}
|
|
1148
|
-
function
|
|
1634
|
+
function prepareRunVariable(context, input) {
|
|
1149
1635
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1150
1636
|
const run = resolveRun(context, project.id, input.runId);
|
|
1637
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
1151
1638
|
const index = authoritativeIndex(run);
|
|
1639
|
+
return { project, run, index };
|
|
1640
|
+
}
|
|
1641
|
+
function setRunVariable(context, input, value, prepared = prepareRunVariable(context, input)) {
|
|
1642
|
+
const { project, run, index } = prepared;
|
|
1152
1643
|
if (index.variables?.[input.key] === value)
|
|
1153
1644
|
return { ok: true, run_id: run.id, key: input.key, value, idempotent: true };
|
|
1154
1645
|
index.variables = { ...(index.variables ?? {}), [input.key]: value };
|
|
1155
1646
|
index.updated_at = context.now();
|
|
1156
1647
|
persistRunState(context, project, run, index);
|
|
1157
|
-
appendRunTimeline(
|
|
1648
|
+
appendRunTimeline(context, project.id, run.id, { at: index.updated_at, type: "run_variable_set", run_id: run.id, key: input.key });
|
|
1158
1649
|
return { ok: true, run_id: run.id, key: input.key, value, idempotent: false };
|
|
1159
1650
|
}
|
|
1160
|
-
function appendRunTimeline(
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1651
|
+
function appendRunTimeline(context, projectId, runId, event) {
|
|
1652
|
+
context.db.writeTransaction(() => {
|
|
1653
|
+
const last = context.db.get("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.timeline' AND json_extract(payload_json, '$.run_id') = ? ORDER BY id DESC LIMIT 1", [projectId, runId]);
|
|
1654
|
+
const sequence = last ? Number(JSON.parse(last.payload_json).sequence) + 1 : 1;
|
|
1655
|
+
const safeEvent = sanitizeTimelineValue(event);
|
|
1656
|
+
const enriched = { event_id: crypto.randomUUID(), sequence, at: typeof safeEvent.at === "string" ? safeEvent.at : context.now(), ...safeEvent };
|
|
1657
|
+
appendAudit(context, { projectId, eventType: "flow_run.timeline", payload: { run_id: runId, sequence, event: enriched } });
|
|
1658
|
+
context.db.afterCommit(() => publishRunProjections(context, projectId, runId));
|
|
1659
|
+
}, "runs.appendRunTimeline");
|
|
1660
|
+
}
|
|
1661
|
+
/** Rebuild projections from committed SQL after taking a per-RUN filesystem lock. */
|
|
1662
|
+
export function publishRunProjections(context, projectId, runId) {
|
|
1663
|
+
const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1664
|
+
if (!run)
|
|
1665
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
1666
|
+
const runtimePath = run.runtime_path || projectRunJsonPath(context.ddFlowHome, projectId, runId);
|
|
1667
|
+
ensureDir(path.dirname(runtimePath));
|
|
1668
|
+
withProjectionLock(`${runtimePath}.publish-lock`, () => {
|
|
1669
|
+
// A delayed publisher must observe the latest commit after acquiring its lock.
|
|
1670
|
+
const latest = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
1671
|
+
if (!latest)
|
|
1672
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
1673
|
+
const index = authoritativeIndex(latest);
|
|
1674
|
+
if (index.workspace_route) {
|
|
1675
|
+
const route = retainedWorkspaceRoute(context, latest);
|
|
1676
|
+
const file = path.join(runArtifactRoot(latest), "02-protocolize", "workspace-route.json");
|
|
1677
|
+
ensureDir(path.dirname(file));
|
|
1678
|
+
try {
|
|
1679
|
+
writeJsonFile(file, route.receipt);
|
|
1680
|
+
}
|
|
1681
|
+
catch (error) {
|
|
1682
|
+
throw new AppError("workspace_route_publication_failed", "Committed route projection could not be published", 1, { effect: "committed", run_id: runId, route_sha256: index.workspace_route.sha256, cause: String(error) });
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
const mutations = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'flow_run_flag_mutations'") ? context.db.all("SELECT resulting_revision, result_json, created_at, idempotency_key FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? ORDER BY resulting_revision DESC LIMIT 32", [projectId, runId]) : [];
|
|
1686
|
+
const history = mutations.reverse().map((row) => ({
|
|
1687
|
+
revision: row.resulting_revision,
|
|
1688
|
+
checksum: String(JSON.parse(row.result_json).snapshot_checksum ?? ""),
|
|
1689
|
+
at: row.created_at,
|
|
1690
|
+
idempotency_key: row.idempotency_key
|
|
1691
|
+
}));
|
|
1692
|
+
writeJsonFile(runtimePath, runtimeSnapshotForIndex(index, index.runtime_revision ?? 1, history.length ? history : index.flag_revision_history ?? []));
|
|
1693
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.timeline' AND json_extract(payload_json, '$.run_id') = ? ORDER BY id", [projectId, runId]);
|
|
1694
|
+
if (latest.run_root) {
|
|
1695
|
+
const timelinePath = path.join(latest.run_root, "timeline.jsonl");
|
|
1696
|
+
ensureDir(path.dirname(timelinePath));
|
|
1697
|
+
const bytes = rows.map((row) => JSON.stringify(JSON.parse(row.payload_json).event)).join("\n");
|
|
1698
|
+
const temporary = `${timelinePath}.tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
1699
|
+
fs.writeFileSync(temporary, bytes ? `${bytes}\n` : "");
|
|
1700
|
+
fs.renameSync(temporary, timelinePath);
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1177
1703
|
}
|
|
1178
1704
|
function runtimeSnapshotForIndex(index, runtimeRevision, flagRevisionHistory = []) {
|
|
1179
1705
|
const rest = { ...index };
|
|
@@ -1199,11 +1725,188 @@ function readRuntimeSnapshot(file) {
|
|
|
1199
1725
|
function authoritativeIndex(run) {
|
|
1200
1726
|
try {
|
|
1201
1727
|
const state = JSON.parse(run.index_json);
|
|
1202
|
-
|
|
1728
|
+
if (!state || typeof state !== "object" || Array.isArray(state) || !Array.isArray(state.stage_runs))
|
|
1729
|
+
throw new Error("RUN index must contain stage_runs");
|
|
1730
|
+
const derivedStage = ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)
|
|
1731
|
+
? [...state.stage_runs].sort((left, right) => right.order - left.order)[0]?.stage : undefined;
|
|
1732
|
+
return { ...state, schema_id: runSchemaId, ...(state.current_stage === undefined && derivedStage ? { current_stage: derivedStage } : {}) };
|
|
1203
1733
|
}
|
|
1204
1734
|
catch (error) {
|
|
1205
|
-
throw new AppError("
|
|
1735
|
+
throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
function assertBundleWorkspaceIdentity(context, run, bundle) {
|
|
1739
|
+
const scope = currentBundleScope(context, run, bundle);
|
|
1740
|
+
const route = authoritativeIndex(run).workspace_route ? retainedWorkspaceRoute(context, run) : undefined;
|
|
1741
|
+
if (scope.workspace === run.workspace_root)
|
|
1742
|
+
return true;
|
|
1743
|
+
if (bundle.descriptor.descriptor.id !== "sdlc" || !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) || scope.workspace !== run.project_root)
|
|
1744
|
+
return false;
|
|
1745
|
+
if (!route)
|
|
1746
|
+
return false;
|
|
1747
|
+
return route.previous_workspace === scope.workspace && route.receipt.policy.route === "feature_worktree"
|
|
1748
|
+
&& route.receipt.policy.base_ref !== null && route.receipt.policy.feature_branch !== null;
|
|
1749
|
+
}
|
|
1750
|
+
function currentBundleScope(context, run, bundle) {
|
|
1751
|
+
return JSON.parse(projectPacketLocators(JSON.stringify(bundle.execution.snapshot().scope), runContextRelocations(context, run.project_id, run.id)));
|
|
1752
|
+
}
|
|
1753
|
+
function assertBundleRunIdentity(context, run, bundle) {
|
|
1754
|
+
const scope = currentBundleScope(context, run, bundle);
|
|
1755
|
+
const flowId = bundle.descriptor.descriptor.id;
|
|
1756
|
+
const matchesFlow = flowId === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flowId === run.flow_kind;
|
|
1757
|
+
if (bundle.manifest.run_id !== run.id || scope.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle) || !matchesFlow) {
|
|
1758
|
+
throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
|
|
1206
1759
|
}
|
|
1760
|
+
const blobs = new Map(bundle.manifest.inventory.map(ref => [ref.path, bundle.blob(ref.path)]));
|
|
1761
|
+
// File-owned handler inventories can never authorize executable code.
|
|
1762
|
+
admitBundledFlow(bundle.blob(bundle.manifest.descriptor.path), bundle.manifest.descriptor.sha256, blobs);
|
|
1763
|
+
}
|
|
1764
|
+
function bundleAuthority(admission) {
|
|
1765
|
+
const authority = { runId: admission.runId, executionScope: structuredClone(admission.executionScope),
|
|
1766
|
+
receipts: structuredClone([...(admission.receipts ?? [])].sort(([left], [right]) => left.localeCompare(right, "en"))) };
|
|
1767
|
+
for (const key of ["descriptorSha256", "engineChecksum", "engineBindingSha256", "engineManifestSha256", "bundleProjectRoot", "bundleProvenance", "bootstrapIdentity"]) {
|
|
1768
|
+
if (admission[key] !== undefined)
|
|
1769
|
+
Object.assign(authority, { [key]: structuredClone(admission[key]) });
|
|
1770
|
+
}
|
|
1771
|
+
return authority;
|
|
1772
|
+
}
|
|
1773
|
+
/** Copies the same SQL authority representation used by legacy bundle binding. */
|
|
1774
|
+
export function runBundleAuthorityMetadata(bundle) {
|
|
1775
|
+
if (!(bundle instanceof RetainedExecutionBundle))
|
|
1776
|
+
throw new AppError("run_bundle_anchor_invalid", "Bundle must be admitted before RUN allocation", 1);
|
|
1777
|
+
const admissionJson = JSON.stringify(bundleAuthority(bundle.admission())), admissionSha256 = hybridDigest(JSON.parse(admissionJson));
|
|
1778
|
+
if (Buffer.byteLength(admissionJson) > 1024 * 1024)
|
|
1779
|
+
throw new AppError("run_bundle_authority_invalid", "RUN admission exceeds its bound", 1);
|
|
1780
|
+
return { admissionJson, anchor: { schema_id: "dd-flow/run-execution-bundle@1",
|
|
1781
|
+
manifest_sha256: bundle.sha256, descriptor_sha256: bundle.manifest.descriptor.sha256,
|
|
1782
|
+
admission_sha256: admissionSha256, flow_id: bundle.manifest.flow_id } };
|
|
1783
|
+
}
|
|
1784
|
+
/** Recheck SQL authority and compiled descriptor admission. Schema loading
|
|
1785
|
+
* belongs outside SQL writers; use the explicit SQL-only fence inside them.
|
|
1786
|
+
*/
|
|
1787
|
+
export function assertPreparedRunBundleAuthority(context, scope, bundle) {
|
|
1788
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
1789
|
+
assertBundleRunIdentity(context, run, bundle);
|
|
1790
|
+
}
|
|
1791
|
+
/** SQL-only fence after the caller has admitted compiled handlers outside the
|
|
1792
|
+
* writer. Descriptor schema loading is deliberately excluded from this fence.
|
|
1793
|
+
*/
|
|
1794
|
+
export function assertPreparedRunBundleSqlAuthority(context, scope, bundle) {
|
|
1795
|
+
if (!(bundle instanceof RetainedExecutionBundle))
|
|
1796
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared bundle is not admitted", 1);
|
|
1797
|
+
const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [scope.projectId, scope.runId]);
|
|
1798
|
+
if (!run)
|
|
1799
|
+
throw new AppError("not_found", "Prepared bundle RUN is unavailable", 1);
|
|
1800
|
+
const index = authoritativeIndex(run), anchor = index.execution_bundle;
|
|
1801
|
+
if (!assertRunBundleSqlAuthority(context, scope, anchor) || !anchor || anchor.manifest_sha256 !== bundle.sha256
|
|
1802
|
+
|| anchor.descriptor_sha256 !== bundle.manifest.descriptor.sha256 || anchor.flow_id !== bundle.manifest.flow_id
|
|
1803
|
+
|| anchor.admission_sha256 !== hybridDigest(bundleAuthority(bundle.admission())))
|
|
1804
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared bundle differs from authoritative RUN", 1);
|
|
1805
|
+
const execution = currentBundleScope(context, run, bundle);
|
|
1806
|
+
const flow = bundle.descriptor.descriptor.id;
|
|
1807
|
+
if (bundle.manifest.run_id !== run.id || execution.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle)
|
|
1808
|
+
|| !(flow === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flow === run.flow_kind)) {
|
|
1809
|
+
throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { effect: "no_effect", run_id: run.id });
|
|
1810
|
+
}
|
|
1811
|
+
return run;
|
|
1812
|
+
}
|
|
1813
|
+
export function readFlowRunExecutionBundle(context, input) {
|
|
1814
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1815
|
+
const run = resolveRun(context, project.id, input.runId), anchor = authoritativeIndex(run).execution_bundle;
|
|
1816
|
+
if (!anchor) {
|
|
1817
|
+
// Genuine historical RUNs have neither authority nor anchor. Lost new-RUN
|
|
1818
|
+
// metadata must not silently switch their execution to the legacy engine path.
|
|
1819
|
+
const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
|
|
1820
|
+
if (hasAuthority && context.db.get("SELECT 1 FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [project.id, run.id])) {
|
|
1821
|
+
throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
|
|
1822
|
+
}
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || !/^[a-f0-9]{64}$/.test(anchor.manifest_sha256)
|
|
1826
|
+
|| !/^[a-f0-9]{64}$/.test(anchor.descriptor_sha256) || !/^[a-f0-9]{64}$/.test(anchor.admission_sha256))
|
|
1827
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
|
|
1828
|
+
const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
|
|
1829
|
+
const row = hasAuthority ? context.db.get("SELECT admission_json, manifest_sha256 FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [project.id, run.id]) : undefined;
|
|
1830
|
+
if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
|
|
1831
|
+
throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority is unavailable", 1);
|
|
1832
|
+
if (Buffer.byteLength(row.admission_json) > 1024 * 1024)
|
|
1833
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
|
|
1834
|
+
let retained;
|
|
1835
|
+
try {
|
|
1836
|
+
retained = JSON.parse(row.admission_json);
|
|
1837
|
+
}
|
|
1838
|
+
catch {
|
|
1839
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority is invalid", 1);
|
|
1840
|
+
}
|
|
1841
|
+
if (hybridDigest(retained) !== anchor.admission_sha256 || !Array.isArray(retained.receipts)
|
|
1842
|
+
|| input.admission && hybridDigest(bundleAuthority(input.admission)) !== anchor.admission_sha256) {
|
|
1843
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed", 1);
|
|
1844
|
+
}
|
|
1845
|
+
const admission = { ...retained, receipts: new Map(retained.receipts), descriptorContext: bundledHandlerContext(new Map()) };
|
|
1846
|
+
const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission);
|
|
1847
|
+
assertBundleRunIdentity(context, run, bundle);
|
|
1848
|
+
if (bundle.manifest.descriptor.sha256 !== anchor.descriptor_sha256 || bundle.manifest.flow_id !== anchor.flow_id) {
|
|
1849
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle descriptor identity changed", 1);
|
|
1850
|
+
}
|
|
1851
|
+
return bundle;
|
|
1852
|
+
}
|
|
1853
|
+
/** Freeze once before physical owners exist. Publication precedes the fenced SQLite anchor. */
|
|
1854
|
+
export function bindFlowRunExecutionBundle(context, input) {
|
|
1855
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1856
|
+
const run = resolveRun(context, project.id, input.runId), index = authoritativeIndex(run);
|
|
1857
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
1858
|
+
assertBundleRunIdentity(context, run, input.bundle);
|
|
1859
|
+
if (input.bundle.manifest.flow_id !== "sdlc") {
|
|
1860
|
+
throw new AppError("run_bundle_start_required", "Memory RUN and its bundle authority must be allocated together through the request-bound start", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
|
|
1861
|
+
}
|
|
1862
|
+
if (index.execution_bundle) {
|
|
1863
|
+
if (index.execution_bundle.manifest_sha256 !== input.bundle.sha256)
|
|
1864
|
+
throw new AppError("run_bundle_immutable", "RUN bundle cannot be replaced", 1);
|
|
1865
|
+
readFlowRunExecutionBundle(context, { ...input, admission: input.bundle.admission() });
|
|
1866
|
+
publishRunProjections(context, project.id, run.id);
|
|
1867
|
+
return { changed: false, sha256: input.bundle.sha256 };
|
|
1868
|
+
}
|
|
1869
|
+
const assertUnstarted = (state) => {
|
|
1870
|
+
if (state.status !== "running" || state.stage_runs.length || state.sessions.length
|
|
1871
|
+
|| context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])
|
|
1872
|
+
|| context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])) {
|
|
1873
|
+
throw new AppError("run_bundle_active", "Execution bundle must be frozen before stages, Works or Sessions exist", 1);
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
assertUnstarted(index);
|
|
1877
|
+
const authority = bundleAuthority(input.bundle.admission());
|
|
1878
|
+
// Hash the JSON representation actually retained, not optional undefined fields.
|
|
1879
|
+
const authorityJson = JSON.stringify(authority), authoritySha256 = hybridDigest(JSON.parse(authorityJson));
|
|
1880
|
+
if (Buffer.byteLength(authorityJson) > 1024 * 1024)
|
|
1881
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
|
|
1882
|
+
input.bundle.materialize(runArtifactRoot(run));
|
|
1883
|
+
try {
|
|
1884
|
+
context.db.writeTransaction(() => {
|
|
1885
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
1886
|
+
const latest = resolveRun(context, project.id, run.id), state = authoritativeIndex(latest);
|
|
1887
|
+
assertUnstarted(state);
|
|
1888
|
+
if (state.runtime_revision !== index.runtime_revision || state.execution_bundle || latest.workspace_root !== run.workspace_root
|
|
1889
|
+
|| latest.project_root !== run.project_root || latest.flow_kind !== run.flow_kind || latest.run_root !== run.run_root) {
|
|
1890
|
+
throw new AppError("run_revision_changed", "RUN changed during bundle publication", 1);
|
|
1891
|
+
}
|
|
1892
|
+
state.execution_bundle = { schema_id: "dd-flow/run-execution-bundle@1", manifest_sha256: input.bundle.sha256,
|
|
1893
|
+
descriptor_sha256: input.bundle.manifest.descriptor.sha256, admission_sha256: authoritySha256, flow_id: input.bundle.manifest.flow_id };
|
|
1894
|
+
context.db.execSchema(`CREATE TABLE IF NOT EXISTS run_bundle_authorities (
|
|
1895
|
+
project_id TEXT NOT NULL, run_id TEXT NOT NULL, manifest_sha256 TEXT NOT NULL, admission_json TEXT NOT NULL,
|
|
1896
|
+
PRIMARY KEY(project_id, run_id), FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id)
|
|
1897
|
+
)`);
|
|
1898
|
+
context.db.run("INSERT INTO run_bundle_authorities (project_id, run_id, manifest_sha256, admission_json) VALUES (?, ?, ?, ?)", [project.id, run.id, input.bundle.sha256, authorityJson]);
|
|
1899
|
+
state.updated_at = context.now();
|
|
1900
|
+
persistRunState(context, project, latest, state);
|
|
1901
|
+
}, "runs.bindExecutionBundle");
|
|
1902
|
+
}
|
|
1903
|
+
catch (error) {
|
|
1904
|
+
throw new AppError("run_bundle_binding_incomplete", "Bundle retained but RUN binding requires recovery", 1, {
|
|
1905
|
+
phase: "publication", effect: "partial", run_id: run.id, sha256: input.bundle.sha256,
|
|
1906
|
+
reason: error instanceof AppError ? error.code : "binding_io_failure",
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
return { changed: true, sha256: input.bundle.sha256 };
|
|
1207
1910
|
}
|
|
1208
1911
|
function sanitizeTimelineValue(value, key) {
|
|
1209
1912
|
if (key && /(secret|password|authorization|cookie|api[_-]?key|private[_-]?key|access[_-]?token|refresh[_-]?token|transcript|prompt|tool[_-]?output|document[_-]?content)/i.test(key)) {
|
|
@@ -1231,6 +1934,24 @@ function nextAttempt(existing) {
|
|
|
1231
1934
|
const number = Number(existing.attempt.replace("try-", ""));
|
|
1232
1935
|
return `try-${String(Number.isFinite(number) ? number + 1 : 1).padStart(3, "0")}`;
|
|
1233
1936
|
}
|
|
1937
|
+
function bundledNextStage(flow, index) {
|
|
1938
|
+
const completed = index.stage_runs.filter(stage => ["done", "skipped"].includes(stage.status)).sort((left, right) => right.order - left.order)[0];
|
|
1939
|
+
if (!completed)
|
|
1940
|
+
return flow.descriptor.entry_stage;
|
|
1941
|
+
// Optional SDLC review routing remains a flow-specific policy, not a generic stage list.
|
|
1942
|
+
const outcome = flow.descriptor.id === "sdlc" && completed.stage === "plan" && index.settings?.plan_review?.mode === "off" ? "not-applicable" : "accepted";
|
|
1943
|
+
return flow.successor(completed.stage, outcome);
|
|
1944
|
+
}
|
|
1945
|
+
function assertBundledStageStart(flow, index, stage, dir, status, existing) {
|
|
1946
|
+
const selected = flow.stage(stage);
|
|
1947
|
+
if (selected.directory !== dir)
|
|
1948
|
+
throw new AppError("flow_stage_directory_invalid", "Stage directory differs from the frozen descriptor", 1, { stage });
|
|
1949
|
+
if (status !== "running")
|
|
1950
|
+
return;
|
|
1951
|
+
if (existing || index.stage_runs.some(candidate => !["done", "skipped"].includes(candidate.status)) || bundledNextStage(flow, index) !== stage) {
|
|
1952
|
+
throw new AppError("illegal_stage_transition", "Stage does not follow the frozen descriptor or an attempt already exists", 2, { run_id: index.run_id, stage });
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1234
1955
|
function assertVnextStageStart(run, index, stage, status, existing) {
|
|
1235
1956
|
if (status !== "running" || (run.flow_kind !== "vnext_specify" && run.flow_kind !== "vnext_protocolize"))
|
|
1236
1957
|
return;
|
|
@@ -1263,7 +1984,7 @@ function upsertStage(index, stageRun) {
|
|
|
1263
1984
|
}
|
|
1264
1985
|
index.stage_runs.sort((a, b) => a.order - b.order);
|
|
1265
1986
|
}
|
|
1266
|
-
function resolveRun(context, projectId, idOrAlias) {
|
|
1987
|
+
export function resolveRun(context, projectId, idOrAlias) {
|
|
1267
1988
|
if (isFullEntityId(idOrAlias)) {
|
|
1268
1989
|
return requireRunById(context, projectId, idOrAlias);
|
|
1269
1990
|
}
|
|
@@ -1289,19 +2010,6 @@ function requireRunById(context, projectId, runId) {
|
|
|
1289
2010
|
}
|
|
1290
2011
|
return run;
|
|
1291
2012
|
}
|
|
1292
|
-
function nextRunId(context, projectId, slug) {
|
|
1293
|
-
const rows = context.db.all("SELECT id FROM runs WHERE project_id = ? AND id LIKE 'RUN-%'", [projectId]);
|
|
1294
|
-
const max = rows.reduce((value, row) => {
|
|
1295
|
-
try {
|
|
1296
|
-
const parsed = parseFullEntityId(row.id);
|
|
1297
|
-
return parsed.type === runIdType ? Math.max(value, Number(parsed.shortId.slice(4))) : value;
|
|
1298
|
-
}
|
|
1299
|
-
catch {
|
|
1300
|
-
return value;
|
|
1301
|
-
}
|
|
1302
|
-
}, 0);
|
|
1303
|
-
return formatFullId(runIdType, max + 1, slug);
|
|
1304
|
-
}
|
|
1305
2013
|
function flowRunSummary(run) {
|
|
1306
2014
|
return {
|
|
1307
2015
|
id: run.id,
|
|
@@ -1357,7 +2065,7 @@ function parseStageStatus(value) {
|
|
|
1357
2065
|
}
|
|
1358
2066
|
return value;
|
|
1359
2067
|
}
|
|
1360
|
-
function
|
|
2068
|
+
export function prepareStageDirectory(value) {
|
|
1361
2069
|
const dir = requiredPlain(value, "dir");
|
|
1362
2070
|
if (!/^\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/.test(dir)) {
|
|
1363
2071
|
throw new AppError("validation", "--dir must use NN-stage-slug format", 2, { dir });
|
|
@@ -1365,6 +2073,8 @@ function requiredStageDir(value) {
|
|
|
1365
2073
|
return dir;
|
|
1366
2074
|
}
|
|
1367
2075
|
function requiredPlain(value, label) {
|
|
2076
|
+
if (typeof value !== "string")
|
|
2077
|
+
throw new AppError("validation", `${label} must be a string`, 2);
|
|
1368
2078
|
const trimmed = value.trim();
|
|
1369
2079
|
if (!trimmed) {
|
|
1370
2080
|
throw new AppError("validation", `${label} must not be empty`, 2);
|
|
@@ -1372,6 +2082,8 @@ function requiredPlain(value, label) {
|
|
|
1372
2082
|
return trimmed;
|
|
1373
2083
|
}
|
|
1374
2084
|
function normalizeSlug(value) {
|
|
2085
|
+
if (typeof value !== "string")
|
|
2086
|
+
throw new AppError("validation", "--slug must be a string", 2);
|
|
1375
2087
|
const slug = value
|
|
1376
2088
|
.normalize("NFKD")
|
|
1377
2089
|
.replace(/[\u0300-\u036f]/g, "")
|
|
@@ -1389,26 +2101,84 @@ function resolveWorkspaceRoot(workspaceRoot) {
|
|
|
1389
2101
|
if (!fs.existsSync(absolute)) {
|
|
1390
2102
|
throw new AppError("not_found", `Workspace root does not exist: ${absolute}`, 1);
|
|
1391
2103
|
}
|
|
2104
|
+
if (!fs.statSync(absolute).isDirectory())
|
|
2105
|
+
throw new AppError("validation", "Workspace root must be a directory", 2, { workspace_root: absolute });
|
|
1392
2106
|
return fs.realpathSync(absolute);
|
|
1393
2107
|
}
|
|
1394
|
-
|
|
2108
|
+
/** Move one completed stage attempt and update only known durable path columns.
|
|
2109
|
+
* Receipt bytes remain immutable; readers resolve their new location through the DB. */
|
|
2110
|
+
export function archiveExistingStageAttempt(context, projectId, runId, runRoot, stageDir) {
|
|
1395
2111
|
const currentStageDir = path.join(runRoot, stageDir);
|
|
1396
2112
|
if (!fs.existsSync(currentStageDir) || !fs.statSync(currentStageDir).isDirectory()) {
|
|
1397
2113
|
return;
|
|
1398
2114
|
}
|
|
1399
|
-
const
|
|
1400
|
-
|
|
2115
|
+
const journal = path.join(runRoot, `.archive-${stageDir}.json`);
|
|
2116
|
+
const pending = fs.existsSync(journal) ? JSON.parse(fs.readFileSync(journal, "utf8")) : null;
|
|
2117
|
+
const row = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
2118
|
+
const prior = row ? JSON.parse(row.index_json) : null;
|
|
2119
|
+
const live = prior?.stage_runs.find((stage) => stage.dir === stageDir);
|
|
2120
|
+
const generation = row ? assertRunMutationAllowed(context, projectId, runId) : 0;
|
|
2121
|
+
if (pending && (pending.project_id && pending.project_id !== projectId || pending.run_id && pending.run_id !== runId))
|
|
2122
|
+
throw new AppError("archive_owner_changed", "Archive journal belongs to another RUN", 1, { journal });
|
|
2123
|
+
if (pending?.generation !== undefined && pending.generation !== generation)
|
|
2124
|
+
throw new AppError("archive_owner_changed", "RUN generation changed while an archive was pending", 1, { journal, expected_generation: pending.generation, generation });
|
|
2125
|
+
if (pending?.prior_attempt && live?.attempt !== pending.prior_attempt && live?.attempt !== pending.next_attempt)
|
|
2126
|
+
throw new AppError("archive_owner_changed", "Stage attempt changed while an archive was pending", 1, { journal, prior_attempt: pending.prior_attempt, actual_attempt: live?.attempt ?? null });
|
|
2127
|
+
const entries = pending?.entries ?? fs.readdirSync(currentStageDir).filter((entry) => !/^try-\d{3}$/.test(entry));
|
|
2128
|
+
if (entries.length === 0 && !pending) {
|
|
1401
2129
|
return;
|
|
1402
2130
|
}
|
|
1403
2131
|
let attempt = 1;
|
|
1404
2132
|
while (fs.existsSync(path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`))) {
|
|
1405
2133
|
attempt += 1;
|
|
1406
2134
|
}
|
|
1407
|
-
const archiveDir = path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
2135
|
+
const archiveDir = pending?.archiveDir ?? path.join(currentStageDir, `try-${String(attempt).padStart(3, "0")}`);
|
|
2136
|
+
const intent = pending ?? { archiveDir, entries, stage_run: live ?? null, project_id: projectId, run_id: runId, prior_attempt: live?.attempt ?? null, next_attempt: nextAttempt(live), runtime_revision: prior?.runtime_revision ?? 0, generation };
|
|
2137
|
+
if (!pending) {
|
|
2138
|
+
writeJsonFile(journal, intent);
|
|
2139
|
+
}
|
|
1408
2140
|
ensureDir(archiveDir);
|
|
1409
2141
|
for (const entry of entries) {
|
|
1410
|
-
|
|
2142
|
+
const source = path.join(currentStageDir, entry);
|
|
2143
|
+
const target = path.join(archiveDir, entry);
|
|
2144
|
+
if (fs.existsSync(source)) {
|
|
2145
|
+
if (fs.existsSync(target))
|
|
2146
|
+
throw new AppError("archive_conflict", "Both live and archived artifact exist; preserve both for recovery", 2, { source, target });
|
|
2147
|
+
fs.renameSync(source, target);
|
|
2148
|
+
}
|
|
2149
|
+
else if (!fs.existsSync(target))
|
|
2150
|
+
throw new AppError("archive_artifact_missing", "Artifact is absent from both archive locations", 2, { source, target });
|
|
1411
2151
|
}
|
|
2152
|
+
context.db.writeTransaction(() => {
|
|
2153
|
+
const current = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
2154
|
+
const currentIndex = current ? JSON.parse(current.index_json) : null;
|
|
2155
|
+
const currentAttempt = currentIndex?.stage_runs.find((stage) => stage.dir === stageDir)?.attempt;
|
|
2156
|
+
if (currentIndex && intent.runtime_revision !== undefined && (currentIndex.runtime_revision ?? 0) !== intent.runtime_revision && currentAttempt !== intent.next_attempt)
|
|
2157
|
+
throw new AppError("archive_owner_changed", "RUN revision changed while an archive was pending", 1, { journal });
|
|
2158
|
+
if (intent.prior_attempt && currentAttempt !== intent.prior_attempt && currentAttempt !== intent.next_attempt)
|
|
2159
|
+
throw new AppError("archive_owner_changed", "Stage attempt changed while an archive was pending", 1, { journal });
|
|
2160
|
+
if (row && generation !== assertRunMutationAllowed(context, projectId, runId))
|
|
2161
|
+
throw new AppError("archive_owner_changed", "RUN generation changed while an archive was pending", 1, { journal });
|
|
2162
|
+
if (intent.next_attempt === undefined || currentAttempt !== intent.next_attempt) {
|
|
2163
|
+
const oldPrefix = `${currentStageDir}${path.sep}`;
|
|
2164
|
+
const newPrefix = `${archiveDir}${path.sep}`;
|
|
2165
|
+
const rewrite = (table, column, where, values) => {
|
|
2166
|
+
context.db.run(`UPDATE ${table} SET ${column} = ? || substr(${column}, ?) WHERE ${column} LIKE ? AND substr(${column}, ?, 4) != 'try-' AND ${where}`, [newPrefix, oldPrefix.length + 1, `${oldPrefix}%`, oldPrefix.length + 1, ...values]);
|
|
2167
|
+
};
|
|
2168
|
+
const workScope = "work_id IN (SELECT work_id FROM works WHERE project_id = ? AND run_id = ?)";
|
|
2169
|
+
rewrite("work_sessions", "prompt_path", workScope, [projectId, runId]);
|
|
2170
|
+
rewrite("work_sessions", "result_path", workScope, [projectId, runId]);
|
|
2171
|
+
const receiptScope = "project_id = ? AND run_id = ?";
|
|
2172
|
+
for (const column of ["stdout_path", "stderr_path", "receipt_path"])
|
|
2173
|
+
rewrite("check_receipts", column, receiptScope, [projectId, runId]);
|
|
2174
|
+
}
|
|
2175
|
+
context.db.afterCommit(() => {
|
|
2176
|
+
if (!fs.existsSync(journal))
|
|
2177
|
+
return;
|
|
2178
|
+
// Retain original Stage facts with the immutable attempt after SQL paths commit.
|
|
2179
|
+
fs.renameSync(journal, path.join(archiveDir, "archive.json"));
|
|
2180
|
+
});
|
|
2181
|
+
});
|
|
1412
2182
|
}
|
|
1413
2183
|
function writeJsonFile(file, value) {
|
|
1414
2184
|
const tmpFile = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
@@ -1433,3 +2203,5 @@ export function gitFacts(workspaceRoot) {
|
|
|
1433
2203
|
status: status === null ? "unavailable" : status.length > 0 ? "dirty" : "clean"
|
|
1434
2204
|
};
|
|
1435
2205
|
}
|
|
2206
|
+
import { assertRunMutationAllowed, assertStageSettlementAllowed, recoveryGuard } from "./run-recovery.js";
|
|
2207
|
+
import { runControlDirective } from "./run-control.js";
|