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