@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.138
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 +4 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +9 -9
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- 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/opencode-plugin.mjs +2 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +22 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +71 -12
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +175 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +101 -40
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/setup.js +377 -0
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-report-renderer.js +63 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +70 -9
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
package/dist/services/runs.js
CHANGED
|
@@ -4,9 +4,10 @@ import path from "node:path";
|
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { withProjectionLock } from "../storage/projection-lock.js";
|
|
6
6
|
import { reconcileRunStageReports } from "./stage-report-renderer.js";
|
|
7
|
-
import {
|
|
7
|
+
import { isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
|
|
8
8
|
import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadCanonicalFlowContract, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
|
|
9
9
|
import { AppError } from "../shared/errors.js";
|
|
10
|
+
import { nextRunId } from "./ids.js";
|
|
10
11
|
import { ensureDir, canonicalPath, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
|
|
11
12
|
import { appendAudit } from "./audit.js";
|
|
12
13
|
import { registerProject, requireProjectByRoot } from "./projects.js";
|
|
@@ -22,9 +23,11 @@ import { assertSameSessionMerge, loadExecutionRoutingInput, resolveStageTransiti
|
|
|
22
23
|
import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
|
|
23
24
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
24
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";
|
|
25
29
|
const runSchemaId = "dd-flow/flow-run@3";
|
|
26
30
|
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
27
|
-
const runIdType = "RUN";
|
|
28
31
|
const allowedRunFlowKinds = [
|
|
29
32
|
"mb_sdlc",
|
|
30
33
|
"coding",
|
|
@@ -231,7 +234,7 @@ function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput, no
|
|
|
231
234
|
merge_delivery: profile.merge_delivery,
|
|
232
235
|
merge_cleanup: profile.merge_cleanup,
|
|
233
236
|
stop_target: profile.stop_target,
|
|
234
|
-
code_bootstrap: profile.code_bootstrap,
|
|
237
|
+
...(profile.code_bootstrap ? { code_bootstrap: profile.code_bootstrap } : {}),
|
|
235
238
|
...(profile.execution ? { execution: profile.execution } : {})
|
|
236
239
|
}
|
|
237
240
|
}, routingInput);
|
|
@@ -306,17 +309,19 @@ function loadUpgradeFlowContract(context, projectRoot) {
|
|
|
306
309
|
}
|
|
307
310
|
return loadCanonicalFlowContract(canon.canon.flow_root);
|
|
308
311
|
}
|
|
309
|
-
export function getFlowRunStatus(context, input) {
|
|
312
|
+
export function getFlowRunStatus(context, input, preparedBundle) {
|
|
310
313
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
311
314
|
const run = resolveRun(context, project.id, input.runId);
|
|
315
|
+
if (preparedBundle)
|
|
316
|
+
assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, preparedBundle);
|
|
312
317
|
const index = authoritativeIndex(run);
|
|
313
318
|
const runtime = readRuntimeSnapshot(run.runtime_path);
|
|
314
|
-
return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
319
|
+
return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index, preparedBundle), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
315
320
|
}
|
|
316
321
|
/** A read-only projection of the next legal lifecycle action.
|
|
317
322
|
* It is derived from the persisted RUN/Work facts so controllers never infer a
|
|
318
323
|
* transition from a model's prose or a hard-coded stage array. */
|
|
319
|
-
function runContinuation(context, run, index) {
|
|
324
|
+
function runContinuation(context, run, index, preparedBundle) {
|
|
320
325
|
if (["done", "failed", "cancelled"].includes(run.status))
|
|
321
326
|
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
322
327
|
const control = runControlDirective(context, run.project_id, run.id);
|
|
@@ -332,6 +337,15 @@ function runContinuation(context, run, index) {
|
|
|
332
337
|
return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
333
338
|
if (run.status === "blocked")
|
|
334
339
|
return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
340
|
+
if (index.execution_bundle) {
|
|
341
|
+
const bundle = preparedBundle ?? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
|
|
342
|
+
const next = bundledNextStage(bundle.descriptor, index);
|
|
343
|
+
if (!next)
|
|
344
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
|
|
345
|
+
const command = bundle.engine().command(context.ddFlowHome, ["stage", "start", run.id, "--stage", next, "--project-root", run.project_root, "--json"]);
|
|
346
|
+
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command,
|
|
347
|
+
execution: { status: "resolved", ...bundle.execution.stage(next) } };
|
|
348
|
+
}
|
|
335
349
|
if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
|
|
336
350
|
const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
|
|
337
351
|
const next = completed ? vnextStages.find((stage) => stage.id === completed.stage)?.next.find((candidate) => candidate !== "plan-review" || index.settings?.plan_review?.mode !== "off") : "specify";
|
|
@@ -587,14 +601,18 @@ export function prepareFlowRunStageAttachment(context, input, archive = false) {
|
|
|
587
601
|
const index = authoritativeIndex(run);
|
|
588
602
|
const status = parseStageStatus(input.status);
|
|
589
603
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
590
|
-
|
|
604
|
+
const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : null;
|
|
605
|
+
if (bundle)
|
|
606
|
+
assertBundledStageStart(bundle.descriptor, index, stage, dir, status, existing);
|
|
607
|
+
else
|
|
608
|
+
assertVnextStageStart(run, index, stage, status, existing);
|
|
591
609
|
if (archive) {
|
|
592
610
|
const runRoot = runArtifactRoot(run);
|
|
593
611
|
if (fs.existsSync(path.join(runRoot, `.archive-${dir}.json`)) || status === "running" && existing) {
|
|
594
612
|
archiveExistingStageAttempt(context, project.id, run.id, runRoot, dir);
|
|
595
613
|
}
|
|
596
614
|
}
|
|
597
|
-
return { project, run, index, stage, dir, status, existing, generation, attempt: status === "running" ? nextAttempt(existing) : existing?.attempt ?? "try-001", git: status === "running" ? gitFacts(run.workspace_root) : undefined };
|
|
615
|
+
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 };
|
|
598
616
|
}
|
|
599
617
|
export function attachFlowRunStage(context, input, prepared = prepareFlowRunStageAttachment(context, input, true)) {
|
|
600
618
|
// Filesystem archive and Git inspection are preparation, never writer work.
|
|
@@ -607,7 +625,13 @@ export function attachFlowRunStage(context, input, prepared = prepareFlowRunStag
|
|
|
607
625
|
if ((index.runtime_revision ?? 0) !== (prepared.index.runtime_revision ?? 0) || existing?.attempt !== prepared.existing?.attempt) {
|
|
608
626
|
throw new AppError("stage_revision_changed", "Stage changed during start preparation", 1, { run_id: run.id, stage });
|
|
609
627
|
}
|
|
610
|
-
|
|
628
|
+
if (prepared.bundle) {
|
|
629
|
+
if (index.execution_bundle?.manifest_sha256 !== prepared.bundle.sha256)
|
|
630
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle changed during stage preparation", 1);
|
|
631
|
+
assertBundledStageStart(prepared.bundle.descriptor, index, stage, dir, status, existing);
|
|
632
|
+
}
|
|
633
|
+
else
|
|
634
|
+
assertVnextStageStart(latest, index, stage, status, existing);
|
|
611
635
|
const now = context.now();
|
|
612
636
|
const stageRun = {
|
|
613
637
|
...(existing ?? { order: index.stage_runs.length + 1 }),
|
|
@@ -652,7 +676,7 @@ export function attachFlowRunStage(context, input, prepared = prepareFlowRunStag
|
|
|
652
676
|
export function withStageSettlement(context, operation) {
|
|
653
677
|
return context.db.writeTransaction(operation, "stage.settle");
|
|
654
678
|
}
|
|
655
|
-
export function prepareFlowRunStageCompletion(context, input) {
|
|
679
|
+
export function prepareFlowRunStageCompletion(context, input, admittedBundle) {
|
|
656
680
|
for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
|
|
657
681
|
if (value !== undefined)
|
|
658
682
|
requiredPlain(value, name);
|
|
@@ -676,11 +700,37 @@ export function prepareFlowRunStageCompletion(context, input) {
|
|
|
676
700
|
if (!existing) {
|
|
677
701
|
throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
|
|
678
702
|
}
|
|
703
|
+
// Bound completion must admit the same immutable descriptor/engine authority
|
|
704
|
+
// as stage entry. Keep filesystem admission outside the SQLite writer.
|
|
705
|
+
const bundle = admittedBundle === undefined
|
|
706
|
+
? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : admittedBundle;
|
|
707
|
+
if (bundle && !index.execution_bundle)
|
|
708
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared after finish admission", 1);
|
|
709
|
+
if (bundle)
|
|
710
|
+
assertCompletionBundleAuthority(context, run, index);
|
|
711
|
+
if (index.execution_bundle && (!bundle || bundle.sha256 !== index.execution_bundle.manifest_sha256
|
|
712
|
+
|| bundle.manifest.descriptor.sha256 !== index.execution_bundle.descriptor_sha256
|
|
713
|
+
|| hybridDigest(bundleAuthority(bundle.admission())) !== index.execution_bundle.admission_sha256)) {
|
|
714
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared finish bundle does not match authoritative RUN admission", 1);
|
|
715
|
+
}
|
|
716
|
+
if (bundle) {
|
|
717
|
+
const declared = bundle.descriptor.stage(stage);
|
|
718
|
+
if (declared.directory !== existing.dir || index.current_stage !== stage) {
|
|
719
|
+
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 });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
679
722
|
const dataPath = input.data ? path.join(runArtifactRoot(run), existing.dir, input.data) : null;
|
|
680
723
|
const dataSha256 = input.dataSha256 ?? (dataPath && fs.existsSync(dataPath) ? crypto.createHash("sha256").update(fs.readFileSync(dataPath)).digest("hex") : null);
|
|
681
724
|
if (dataSha256 && !/^[a-f0-9]{64}$/.test(dataSha256))
|
|
682
725
|
throw new AppError("validation", "Stage data SHA-256 is invalid", 2);
|
|
683
|
-
return { project, run, index, stage, status, existing, 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) };
|
|
726
|
+
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) };
|
|
727
|
+
}
|
|
728
|
+
/** Raw historical control is not a finish-sealing bypass for a descriptor-bound RUN. */
|
|
729
|
+
export function prepareRawFlowRunStageCompletion(context, input) {
|
|
730
|
+
const prepared = prepareFlowRunStageCompletion(context, input);
|
|
731
|
+
if (prepared.bundle)
|
|
732
|
+
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 });
|
|
733
|
+
return prepared;
|
|
684
734
|
}
|
|
685
735
|
function stageUsageProjection(context, run, stage, startedAt) {
|
|
686
736
|
if (!startedAt)
|
|
@@ -705,6 +755,14 @@ export function completeFlowRunStage(context, input, prepared = prepareFlowRunSt
|
|
|
705
755
|
if (!existing || existing.attempt !== prepared.existing.attempt || index.runtime_revision !== prepared.index.runtime_revision) {
|
|
706
756
|
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 });
|
|
707
757
|
}
|
|
758
|
+
if (hybridDigest(index.execution_bundle ?? null) !== hybridDigest(prepared.index.execution_bundle ?? null)) {
|
|
759
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle authority changed during finish preparation", 1);
|
|
760
|
+
}
|
|
761
|
+
if (prepared.bundle && (index.current_stage !== stage || existing.dir !== prepared.bundle.descriptor.stage(stage).directory)) {
|
|
762
|
+
throw new AppError("flow_stage_completion_invalid", "Active descriptor stage changed during finish preparation", 1);
|
|
763
|
+
}
|
|
764
|
+
if (prepared.bundle)
|
|
765
|
+
assertCompletionBundleAuthority(context, latest, index);
|
|
708
766
|
const now = context.now();
|
|
709
767
|
const stageRun = {
|
|
710
768
|
...existing,
|
|
@@ -755,6 +813,42 @@ export function completeFlowRunStage(context, input, prepared = prepareFlowRunSt
|
|
|
755
813
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
756
814
|
return { ok: true, run: flowRunSummary(updatedRun), stage_run: completed.stageRun, index: completed.index, flow_guidance: guidanceForRun(context, updatedRun, completed.index) };
|
|
757
815
|
}
|
|
816
|
+
/** Writer-safe readback of independently retained SQL authority; no filesystem admission. */
|
|
817
|
+
function assertCompletionBundleAuthority(context, run, index) {
|
|
818
|
+
const anchor = index.execution_bundle;
|
|
819
|
+
if (!anchor)
|
|
820
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared during finish", 1);
|
|
821
|
+
assertRunBundleSqlAuthority(context, { projectId: run.project_id, runId: run.id }, anchor);
|
|
822
|
+
}
|
|
823
|
+
/** Metadata-only fencing shared by lifecycle ownership and the settlement writer.
|
|
824
|
+
* False means genuine historical authority, never a lost/orphaned bundle anchor.
|
|
825
|
+
* Physical descriptor/engine admission remains an independent outside-writer check.
|
|
826
|
+
*/
|
|
827
|
+
export function assertRunBundleSqlAuthority(context, scope, anchor) {
|
|
828
|
+
const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
|
|
829
|
+
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;
|
|
830
|
+
if (!anchor) {
|
|
831
|
+
if (row)
|
|
832
|
+
throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || typeof anchor.flow_id !== "string"
|
|
836
|
+
|| [anchor.manifest_sha256, anchor.descriptor_sha256, anchor.admission_sha256].some(hash => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash)))
|
|
837
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
|
|
838
|
+
if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
|
|
839
|
+
throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority disappeared during finish", 1);
|
|
840
|
+
let valid = false;
|
|
841
|
+
try {
|
|
842
|
+
if (Buffer.byteLength(row.admission_json, "utf8") <= 1024 * 1024) {
|
|
843
|
+
const retained = JSON.parse(row.admission_json);
|
|
844
|
+
valid = retained.runId === scope.runId && hybridDigest(retained) === anchor.admission_sha256;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
catch { /* Corrupt authority cannot be replaced by a prepared object. */ }
|
|
848
|
+
if (!valid)
|
|
849
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed during finish", 1);
|
|
850
|
+
return true;
|
|
851
|
+
}
|
|
758
852
|
/** Pause one live stage without completing it or creating a new attempt. */
|
|
759
853
|
export function pauseFlowRunStage(context, input) {
|
|
760
854
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -1016,6 +1110,41 @@ export function completeFlowRun(context, input) {
|
|
|
1016
1110
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
1017
1111
|
return { ok: true, run: flowRunSummary(updatedRun), index, usage, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
1018
1112
|
}
|
|
1113
|
+
/** SQL-only terminal writer for an independently prepared descriptor handler.
|
|
1114
|
+
* This closes runtime state, not semantic or Git acceptance. The caller owns
|
|
1115
|
+
* the outer transaction, root settlement and the handler-specific proof fence.
|
|
1116
|
+
*/
|
|
1117
|
+
export function completeDescriptorRunState(context, scope, bundle, git) {
|
|
1118
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
1119
|
+
assertRunMutationAllowed(context, scope.projectId, scope.runId);
|
|
1120
|
+
const run = requireRunById(context, scope.projectId, scope.runId);
|
|
1121
|
+
const index = authoritativeIndex(run);
|
|
1122
|
+
const terminal = index.stage_runs.at(-1);
|
|
1123
|
+
if (run.status !== "running" || !terminal || terminal.status !== "done"
|
|
1124
|
+
|| !bundle.descriptor.descriptor.terminal_stages.includes(terminal.stage)
|
|
1125
|
+
|| index.stage_runs.some(stage => ["running", "paused", "waiting_for_user", "blocked", "pending"].includes(stage.status))
|
|
1126
|
+
|| 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])
|
|
1127
|
+
|| 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])) {
|
|
1128
|
+
throw new AppError("descriptor_run_settlement_invalid", "Descriptor RUN is not ready for terminal runtime settlement", 1, { effect: "no_effect" });
|
|
1129
|
+
}
|
|
1130
|
+
const project = context.db.get("SELECT * FROM projects WHERE id=?", [scope.projectId]);
|
|
1131
|
+
if (!project)
|
|
1132
|
+
throw new AppError("not_found", "Project is not registered", 1);
|
|
1133
|
+
const now = context.now();
|
|
1134
|
+
index.status = "done";
|
|
1135
|
+
index.verdict = "accepted";
|
|
1136
|
+
index.next_action = null;
|
|
1137
|
+
index.updated_at = now;
|
|
1138
|
+
index.completed_at = now;
|
|
1139
|
+
index.finished_at = now;
|
|
1140
|
+
index.duration_ms = durationMs(index.started_at ?? run.created_at, now);
|
|
1141
|
+
index.execution = { project_root: run.project_root, workspace_root: run.workspace_root, git: { ...git } };
|
|
1142
|
+
persistRunState(context, project, run, index);
|
|
1143
|
+
closeRunOrchestrators(context, scope.projectId, run, "done", now);
|
|
1144
|
+
refreshRunSessionProjection(context, scope.projectId, scope.runId);
|
|
1145
|
+
appendAudit(context, { projectId: scope.projectId, eventType: "flow_run.completed", payload: { run_id: scope.runId, status: "done", verdict: "accepted", next_action: null } });
|
|
1146
|
+
appendRunTimeline(context, scope.projectId, scope.runId, { at: now, type: "run_completed", run_id: scope.runId, status: "done" });
|
|
1147
|
+
}
|
|
1019
1148
|
/** Update a live RUN between stages without manufacturing terminal evidence. */
|
|
1020
1149
|
export function advanceFlowRun(context, input) {
|
|
1021
1150
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -1524,12 +1653,172 @@ function authoritativeIndex(run) {
|
|
|
1524
1653
|
const state = JSON.parse(run.index_json);
|
|
1525
1654
|
if (!state || typeof state !== "object" || Array.isArray(state) || !Array.isArray(state.stage_runs))
|
|
1526
1655
|
throw new Error("RUN index must contain stage_runs");
|
|
1527
|
-
|
|
1656
|
+
const derivedStage = ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)
|
|
1657
|
+
? [...state.stage_runs].sort((left, right) => right.order - left.order)[0]?.stage : undefined;
|
|
1658
|
+
return { ...state, schema_id: runSchemaId, ...(state.current_stage === undefined && derivedStage ? { current_stage: derivedStage } : {}) };
|
|
1528
1659
|
}
|
|
1529
1660
|
catch (error) {
|
|
1530
1661
|
throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
|
|
1531
1662
|
}
|
|
1532
1663
|
}
|
|
1664
|
+
function assertBundleRunIdentity(run, bundle) {
|
|
1665
|
+
const scope = bundle.execution.snapshot().scope;
|
|
1666
|
+
const flowId = bundle.descriptor.descriptor.id;
|
|
1667
|
+
const matchesFlow = flowId === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flowId === run.flow_kind;
|
|
1668
|
+
if (bundle.manifest.run_id !== run.id || scope.projectRoot !== run.project_root || scope.workspace !== run.workspace_root || !matchesFlow) {
|
|
1669
|
+
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 });
|
|
1670
|
+
}
|
|
1671
|
+
const blobs = new Map(bundle.manifest.inventory.map(ref => [ref.path, bundle.blob(ref.path)]));
|
|
1672
|
+
// File-owned handler inventories can never authorize executable code.
|
|
1673
|
+
admitBundledFlow(bundle.blob(bundle.manifest.descriptor.path), bundle.manifest.descriptor.sha256, blobs);
|
|
1674
|
+
}
|
|
1675
|
+
function bundleAuthority(admission) {
|
|
1676
|
+
const authority = { runId: admission.runId, executionScope: structuredClone(admission.executionScope),
|
|
1677
|
+
receipts: structuredClone([...(admission.receipts ?? [])].sort(([left], [right]) => left.localeCompare(right, "en"))) };
|
|
1678
|
+
for (const key of ["descriptorSha256", "engineChecksum", "engineBindingSha256", "engineManifestSha256", "bundleProjectRoot", "bundleProvenance", "bootstrapIdentity"]) {
|
|
1679
|
+
if (admission[key] !== undefined)
|
|
1680
|
+
Object.assign(authority, { [key]: structuredClone(admission[key]) });
|
|
1681
|
+
}
|
|
1682
|
+
return authority;
|
|
1683
|
+
}
|
|
1684
|
+
/** Copies the same SQL authority representation used by legacy bundle binding. */
|
|
1685
|
+
export function runBundleAuthorityMetadata(bundle) {
|
|
1686
|
+
if (!(bundle instanceof RetainedExecutionBundle))
|
|
1687
|
+
throw new AppError("run_bundle_anchor_invalid", "Bundle must be admitted before RUN allocation", 1);
|
|
1688
|
+
const admissionJson = JSON.stringify(bundleAuthority(bundle.admission())), admissionSha256 = hybridDigest(JSON.parse(admissionJson));
|
|
1689
|
+
if (Buffer.byteLength(admissionJson) > 1024 * 1024)
|
|
1690
|
+
throw new AppError("run_bundle_authority_invalid", "RUN admission exceeds its bound", 1);
|
|
1691
|
+
return { admissionJson, anchor: { schema_id: "dd-flow/run-execution-bundle@1",
|
|
1692
|
+
manifest_sha256: bundle.sha256, descriptor_sha256: bundle.manifest.descriptor.sha256,
|
|
1693
|
+
admission_sha256: admissionSha256, flow_id: bundle.manifest.flow_id } };
|
|
1694
|
+
}
|
|
1695
|
+
/** Recheck SQL authority and compiled descriptor admission. Schema loading
|
|
1696
|
+
* belongs outside SQL writers; use the explicit SQL-only fence inside them.
|
|
1697
|
+
*/
|
|
1698
|
+
export function assertPreparedRunBundleAuthority(context, scope, bundle) {
|
|
1699
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
1700
|
+
assertBundleRunIdentity(run, bundle);
|
|
1701
|
+
}
|
|
1702
|
+
/** SQL-only fence after the caller has admitted compiled handlers outside the
|
|
1703
|
+
* writer. Descriptor schema loading is deliberately excluded from this fence.
|
|
1704
|
+
*/
|
|
1705
|
+
export function assertPreparedRunBundleSqlAuthority(context, scope, bundle) {
|
|
1706
|
+
if (!(bundle instanceof RetainedExecutionBundle))
|
|
1707
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared bundle is not admitted", 1);
|
|
1708
|
+
const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [scope.projectId, scope.runId]);
|
|
1709
|
+
if (!run)
|
|
1710
|
+
throw new AppError("not_found", "Prepared bundle RUN is unavailable", 1);
|
|
1711
|
+
const index = authoritativeIndex(run), anchor = index.execution_bundle;
|
|
1712
|
+
if (!assertRunBundleSqlAuthority(context, scope, anchor) || !anchor || anchor.manifest_sha256 !== bundle.sha256
|
|
1713
|
+
|| anchor.descriptor_sha256 !== bundle.manifest.descriptor.sha256 || anchor.flow_id !== bundle.manifest.flow_id
|
|
1714
|
+
|| anchor.admission_sha256 !== hybridDigest(bundleAuthority(bundle.admission())))
|
|
1715
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared bundle differs from authoritative RUN", 1);
|
|
1716
|
+
const execution = bundle.execution.snapshot().scope;
|
|
1717
|
+
const flow = bundle.descriptor.descriptor.id;
|
|
1718
|
+
if (bundle.manifest.run_id !== run.id || execution.projectRoot !== run.project_root || execution.workspace !== run.workspace_root
|
|
1719
|
+
|| !(flow === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flow === run.flow_kind)) {
|
|
1720
|
+
throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { effect: "no_effect", run_id: run.id });
|
|
1721
|
+
}
|
|
1722
|
+
return run;
|
|
1723
|
+
}
|
|
1724
|
+
export function readFlowRunExecutionBundle(context, input) {
|
|
1725
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1726
|
+
const run = resolveRun(context, project.id, input.runId), anchor = authoritativeIndex(run).execution_bundle;
|
|
1727
|
+
if (!anchor) {
|
|
1728
|
+
// Genuine historical RUNs have neither authority nor anchor. Lost new-RUN
|
|
1729
|
+
// metadata must not silently switch their execution to the legacy engine path.
|
|
1730
|
+
const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
|
|
1731
|
+
if (hasAuthority && context.db.get("SELECT 1 FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [project.id, run.id])) {
|
|
1732
|
+
throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
|
|
1733
|
+
}
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || !/^[a-f0-9]{64}$/.test(anchor.manifest_sha256)
|
|
1737
|
+
|| !/^[a-f0-9]{64}$/.test(anchor.descriptor_sha256) || !/^[a-f0-9]{64}$/.test(anchor.admission_sha256))
|
|
1738
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
|
|
1739
|
+
const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
|
|
1740
|
+
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;
|
|
1741
|
+
if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
|
|
1742
|
+
throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority is unavailable", 1);
|
|
1743
|
+
if (Buffer.byteLength(row.admission_json) > 1024 * 1024)
|
|
1744
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
|
|
1745
|
+
let retained;
|
|
1746
|
+
try {
|
|
1747
|
+
retained = JSON.parse(row.admission_json);
|
|
1748
|
+
}
|
|
1749
|
+
catch {
|
|
1750
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority is invalid", 1);
|
|
1751
|
+
}
|
|
1752
|
+
if (hybridDigest(retained) !== anchor.admission_sha256 || !Array.isArray(retained.receipts)
|
|
1753
|
+
|| input.admission && hybridDigest(bundleAuthority(input.admission)) !== anchor.admission_sha256) {
|
|
1754
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed", 1);
|
|
1755
|
+
}
|
|
1756
|
+
const admission = { ...retained, receipts: new Map(retained.receipts), descriptorContext: bundledHandlerContext(new Map()) };
|
|
1757
|
+
const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission);
|
|
1758
|
+
assertBundleRunIdentity(run, bundle);
|
|
1759
|
+
if (bundle.manifest.descriptor.sha256 !== anchor.descriptor_sha256 || bundle.manifest.flow_id !== anchor.flow_id) {
|
|
1760
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle descriptor identity changed", 1);
|
|
1761
|
+
}
|
|
1762
|
+
return bundle;
|
|
1763
|
+
}
|
|
1764
|
+
/** Freeze once before physical owners exist. Publication precedes the fenced SQLite anchor. */
|
|
1765
|
+
export function bindFlowRunExecutionBundle(context, input) {
|
|
1766
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1767
|
+
const run = resolveRun(context, project.id, input.runId), index = authoritativeIndex(run);
|
|
1768
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
1769
|
+
assertBundleRunIdentity(run, input.bundle);
|
|
1770
|
+
if (input.bundle.manifest.flow_id !== "sdlc") {
|
|
1771
|
+
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 });
|
|
1772
|
+
}
|
|
1773
|
+
if (index.execution_bundle) {
|
|
1774
|
+
if (index.execution_bundle.manifest_sha256 !== input.bundle.sha256)
|
|
1775
|
+
throw new AppError("run_bundle_immutable", "RUN bundle cannot be replaced", 1);
|
|
1776
|
+
readFlowRunExecutionBundle(context, { ...input, admission: input.bundle.admission() });
|
|
1777
|
+
publishRunProjections(context, project.id, run.id);
|
|
1778
|
+
return { changed: false, sha256: input.bundle.sha256 };
|
|
1779
|
+
}
|
|
1780
|
+
const assertUnstarted = (state) => {
|
|
1781
|
+
if (state.status !== "running" || state.stage_runs.length || state.sessions.length
|
|
1782
|
+
|| context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])
|
|
1783
|
+
|| context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])) {
|
|
1784
|
+
throw new AppError("run_bundle_active", "Execution bundle must be frozen before stages, Works or Sessions exist", 1);
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
assertUnstarted(index);
|
|
1788
|
+
const authority = bundleAuthority(input.bundle.admission());
|
|
1789
|
+
// Hash the JSON representation actually retained, not optional undefined fields.
|
|
1790
|
+
const authorityJson = JSON.stringify(authority), authoritySha256 = hybridDigest(JSON.parse(authorityJson));
|
|
1791
|
+
if (Buffer.byteLength(authorityJson) > 1024 * 1024)
|
|
1792
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
|
|
1793
|
+
input.bundle.materialize(runArtifactRoot(run));
|
|
1794
|
+
try {
|
|
1795
|
+
context.db.writeTransaction(() => {
|
|
1796
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
1797
|
+
const latest = resolveRun(context, project.id, run.id), state = authoritativeIndex(latest);
|
|
1798
|
+
assertUnstarted(state);
|
|
1799
|
+
if (state.runtime_revision !== index.runtime_revision || state.execution_bundle || latest.workspace_root !== run.workspace_root
|
|
1800
|
+
|| latest.project_root !== run.project_root || latest.flow_kind !== run.flow_kind || latest.run_root !== run.run_root) {
|
|
1801
|
+
throw new AppError("run_revision_changed", "RUN changed during bundle publication", 1);
|
|
1802
|
+
}
|
|
1803
|
+
state.execution_bundle = { schema_id: "dd-flow/run-execution-bundle@1", manifest_sha256: input.bundle.sha256,
|
|
1804
|
+
descriptor_sha256: input.bundle.manifest.descriptor.sha256, admission_sha256: authoritySha256, flow_id: input.bundle.manifest.flow_id };
|
|
1805
|
+
context.db.execSchema(`CREATE TABLE IF NOT EXISTS run_bundle_authorities (
|
|
1806
|
+
project_id TEXT NOT NULL, run_id TEXT NOT NULL, manifest_sha256 TEXT NOT NULL, admission_json TEXT NOT NULL,
|
|
1807
|
+
PRIMARY KEY(project_id, run_id), FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id)
|
|
1808
|
+
)`);
|
|
1809
|
+
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]);
|
|
1810
|
+
state.updated_at = context.now();
|
|
1811
|
+
persistRunState(context, project, latest, state);
|
|
1812
|
+
}, "runs.bindExecutionBundle");
|
|
1813
|
+
}
|
|
1814
|
+
catch (error) {
|
|
1815
|
+
throw new AppError("run_bundle_binding_incomplete", "Bundle retained but RUN binding requires recovery", 1, {
|
|
1816
|
+
phase: "publication", effect: "partial", run_id: run.id, sha256: input.bundle.sha256,
|
|
1817
|
+
reason: error instanceof AppError ? error.code : "binding_io_failure",
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
return { changed: true, sha256: input.bundle.sha256 };
|
|
1821
|
+
}
|
|
1533
1822
|
function sanitizeTimelineValue(value, key) {
|
|
1534
1823
|
if (key && /(secret|password|authorization|cookie|api[_-]?key|private[_-]?key|access[_-]?token|refresh[_-]?token|transcript|prompt|tool[_-]?output|document[_-]?content)/i.test(key)) {
|
|
1535
1824
|
return "[REDACTED]";
|
|
@@ -1556,6 +1845,24 @@ function nextAttempt(existing) {
|
|
|
1556
1845
|
const number = Number(existing.attempt.replace("try-", ""));
|
|
1557
1846
|
return `try-${String(Number.isFinite(number) ? number + 1 : 1).padStart(3, "0")}`;
|
|
1558
1847
|
}
|
|
1848
|
+
function bundledNextStage(flow, index) {
|
|
1849
|
+
const completed = index.stage_runs.filter(stage => ["done", "skipped"].includes(stage.status)).sort((left, right) => right.order - left.order)[0];
|
|
1850
|
+
if (!completed)
|
|
1851
|
+
return flow.descriptor.entry_stage;
|
|
1852
|
+
// Optional SDLC review routing remains a flow-specific policy, not a generic stage list.
|
|
1853
|
+
const outcome = flow.descriptor.id === "sdlc" && completed.stage === "plan" && index.settings?.plan_review?.mode === "off" ? "not-applicable" : "accepted";
|
|
1854
|
+
return flow.successor(completed.stage, outcome);
|
|
1855
|
+
}
|
|
1856
|
+
function assertBundledStageStart(flow, index, stage, dir, status, existing) {
|
|
1857
|
+
const selected = flow.stage(stage);
|
|
1858
|
+
if (selected.directory !== dir)
|
|
1859
|
+
throw new AppError("flow_stage_directory_invalid", "Stage directory differs from the frozen descriptor", 1, { stage });
|
|
1860
|
+
if (status !== "running")
|
|
1861
|
+
return;
|
|
1862
|
+
if (existing || index.stage_runs.some(candidate => !["done", "skipped"].includes(candidate.status)) || bundledNextStage(flow, index) !== stage) {
|
|
1863
|
+
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 });
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1559
1866
|
function assertVnextStageStart(run, index, stage, status, existing) {
|
|
1560
1867
|
if (status !== "running" || (run.flow_kind !== "vnext_specify" && run.flow_kind !== "vnext_protocolize"))
|
|
1561
1868
|
return;
|
|
@@ -1614,19 +1921,6 @@ function requireRunById(context, projectId, runId) {
|
|
|
1614
1921
|
}
|
|
1615
1922
|
return run;
|
|
1616
1923
|
}
|
|
1617
|
-
function nextRunId(context, projectId, slug) {
|
|
1618
|
-
const rows = context.db.all("SELECT id FROM runs WHERE project_id = ? AND id LIKE 'RUN-%'", [projectId]);
|
|
1619
|
-
const max = rows.reduce((value, row) => {
|
|
1620
|
-
try {
|
|
1621
|
-
const parsed = parseFullEntityId(row.id);
|
|
1622
|
-
return parsed.type === runIdType ? Math.max(value, Number(parsed.shortId.slice(4))) : value;
|
|
1623
|
-
}
|
|
1624
|
-
catch {
|
|
1625
|
-
return value;
|
|
1626
|
-
}
|
|
1627
|
-
}, 0);
|
|
1628
|
-
return formatFullId(runIdType, max + 1, slug);
|
|
1629
|
-
}
|
|
1630
1924
|
function flowRunSummary(run) {
|
|
1631
1925
|
return {
|
|
1632
1926
|
id: run.id,
|