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