@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.139
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 +19 -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/runtime-process.js +1 -1
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +56 -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 +53 -22
- package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +12 -6
- package/dist/harness-runtime/lib/dd-codex.mjs +24 -12
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +32 -9
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +11 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +100 -15
- package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +22 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +16 -3
- package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
- 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-children.d.mts +2 -0
- package/dist/harness-runtime/lib/native-children.mjs +20 -1
- 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 +32 -9
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
- package/dist/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 +26 -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-child-capacity.js +13 -0
- package/dist/services/controller-child-observer.js +18 -2
- 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 +93 -15
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +250 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +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/managed-processes.js +31 -10
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +296 -0
- package/dist/services/memory-init-preflight.js +330 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +181 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +178 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/merge-server.js +2 -4
- 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/{codex-capacity.js → provider-capacity.js} +34 -16
- package/dist/services/recovery-snapshot-database.js +417 -2
- 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-adapter.js +1 -1
- 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 +5 -5
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +158 -64
- package/dist/services/run-recovery-guard.js +5 -3
- package/dist/services/run-recovery-runtime.js +3 -3
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +413 -30
- 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-review.js +1 -1
- package/dist/services/vnext-code.js +6 -6
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +73 -11
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +7 -5
- package/dist/services/vnext-plan.js +3 -3
- package/dist/services/vnext-protocolize.js +20 -6
- package/dist/services/vnext-workspace-policy.js +7 -2
- 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/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
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,12 @@ 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";
|
|
29
|
+
import { projectPacketLocators, runContextRelocations } from "./stage-context.js";
|
|
25
30
|
const runSchemaId = "dd-flow/flow-run@3";
|
|
26
31
|
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
27
|
-
const runIdType = "RUN";
|
|
28
32
|
const allowedRunFlowKinds = [
|
|
29
33
|
"mb_sdlc",
|
|
30
34
|
"coding",
|
|
@@ -231,7 +235,7 @@ function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput, no
|
|
|
231
235
|
merge_delivery: profile.merge_delivery,
|
|
232
236
|
merge_cleanup: profile.merge_cleanup,
|
|
233
237
|
stop_target: profile.stop_target,
|
|
234
|
-
code_bootstrap: profile.code_bootstrap,
|
|
238
|
+
...(profile.code_bootstrap ? { code_bootstrap: profile.code_bootstrap } : {}),
|
|
235
239
|
...(profile.execution ? { execution: profile.execution } : {})
|
|
236
240
|
}
|
|
237
241
|
}, routingInput);
|
|
@@ -281,20 +285,82 @@ export function importFlowRunExecutionProfile(context, input) {
|
|
|
281
285
|
export function rebindFlowRunWorkspace(context, input) {
|
|
282
286
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
283
287
|
const run = resolveRun(context, project.id, input.runId);
|
|
284
|
-
assertRunMutationAllowed(context, project.id, run.id);
|
|
288
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id, input.expectedGeneration);
|
|
285
289
|
const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot);
|
|
286
290
|
const index = authoritativeIndex(run);
|
|
291
|
+
const receipt = input.routeReceipt ? { ...input.routeReceipt, policy: { ...input.routeReceipt.policy,
|
|
292
|
+
worktree_path: input.routeReceipt.policy.worktree_path ? resolveWorkspaceRoot(input.routeReceipt.policy.worktree_path) : null } } : undefined;
|
|
293
|
+
if (index.execution_bundle && !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)) {
|
|
294
|
+
if (workspaceRoot === run.workspace_root)
|
|
295
|
+
return { ok: true, changed: false };
|
|
296
|
+
throw new AppError("workspace_route_invalid", "Only SDLC may provision a different RUN workspace", 1, { effect: "no_effect" });
|
|
297
|
+
}
|
|
298
|
+
if (workspaceRoot === run.workspace_root) {
|
|
299
|
+
if (receipt && index.workspace_route && hybridDigest(retainedWorkspaceRoute(context, run).receipt) !== hybridDigest(receipt)) {
|
|
300
|
+
throw new AppError("workspace_route_conflict", "Committed workspace route differs from the repeated request", 1, { effect: "no_effect" });
|
|
301
|
+
}
|
|
302
|
+
publishRunProjections(context, project.id, run.id);
|
|
303
|
+
return { ok: true, changed: false, workspace_root: workspaceRoot };
|
|
304
|
+
}
|
|
305
|
+
if (index.workspace_route)
|
|
306
|
+
throw new AppError("workspace_route_conflict", "RUN already has a committed workspace route", 1, { effect: "no_effect" });
|
|
307
|
+
if (input.expectedRevision !== undefined && (index.runtime_revision ?? 0) !== input.expectedRevision)
|
|
308
|
+
throw new AppError("run_revision_changed", "RUN changed while its workspace was provisioned", 1, { effect: "no_effect", expected_revision: input.expectedRevision, actual_revision: index.runtime_revision ?? 0 });
|
|
309
|
+
if (index.execution_bundle && !receipt)
|
|
310
|
+
throw new AppError("workspace_route_missing", "Bundled SDLC rebind requires its prepared route receipt", 1, { effect: "no_effect" });
|
|
311
|
+
if (receipt && (run.workspace_root !== project.root || receipt.schema_id !== "dd-flow/vnext-workspace-route@1" || receipt.policy.route !== "feature_worktree"
|
|
312
|
+
|| !receipt.policy.provisioned || receipt.policy.worktree_path !== workspaceRoot || !receipt.created || !receipt.bootstrap)) {
|
|
313
|
+
throw new AppError("workspace_route_invalid", "Prepared workspace route does not match this RUN", 1, { effect: "no_effect" });
|
|
314
|
+
}
|
|
287
315
|
const now = context.now();
|
|
288
316
|
const facts = gitFacts(workspaceRoot);
|
|
317
|
+
if (receipt && (facts.branch !== receipt.policy.feature_branch || !receipt.policy.base_ref || !facts.head
|
|
318
|
+
|| spawnSync("git", ["-C", workspaceRoot, "merge-base", "--is-ancestor", receipt.policy.base_ref, facts.head]).status !== 0)) {
|
|
319
|
+
throw new AppError("workspace_route_invalid", "Prepared checkout differs from the feature branch/base receipt", 1, { effect: "no_effect", run_id: run.id });
|
|
320
|
+
}
|
|
289
321
|
index.workspace = { ...index.workspace, workspace_path: workspaceRoot, branch: facts.branch ?? "unknown", base: facts.head ?? "unknown", manager: "official-worktrunk" };
|
|
290
322
|
index.execution = { ...index.execution, project_root: project.root, workspace_root: workspaceRoot, git: facts };
|
|
291
323
|
index.updated_at = now;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
324
|
+
const route = receipt ? { schema_id: "dd-flow/run-workspace-route@1", run_id: run.id, project_id: project.id, project_root: project.root,
|
|
325
|
+
generation, previous_workspace: run.workspace_root, workspace_root: workspaceRoot, revision: (index.runtime_revision ?? 0) + 1,
|
|
326
|
+
git: facts, receipt, receipt_sha256: hybridDigest(receipt) } : undefined;
|
|
327
|
+
if (route)
|
|
328
|
+
index.workspace_route = { revision: route.revision, sha256: hybridDigest(route) };
|
|
329
|
+
context.db.writeTransaction(() => {
|
|
330
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
331
|
+
const latest = resolveRun(context, project.id, run.id);
|
|
332
|
+
if (latest.workspace_root !== run.workspace_root)
|
|
333
|
+
throw new AppError("workspace_route_changed", "RUN route changed during preparation", 1, { effect: "no_effect" });
|
|
334
|
+
context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
|
|
335
|
+
persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
|
|
336
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, ...(route ? { route } : {}) } });
|
|
337
|
+
appendRunTimeline(context, project.id, run.id, { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
|
|
338
|
+
}, "runs.rebindWorkspace");
|
|
296
339
|
return { ok: true, run_id: run.id, project_root: project.root, workspace_root: workspaceRoot, git: facts };
|
|
297
340
|
}
|
|
341
|
+
function retainedWorkspaceRoute(context, run) {
|
|
342
|
+
const index = authoritativeIndex(run), binding = index.workspace_route;
|
|
343
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.workspace_rebound' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.route.revision') = ?", [run.project_id, run.id, binding?.revision ?? -1]);
|
|
344
|
+
const original = rows.length === 1 ? JSON.parse(rows[0].payload_json).route : undefined;
|
|
345
|
+
if (!binding || !original || original.schema_id !== "dd-flow/run-workspace-route@1" || hybridDigest(original) !== binding.sha256 || hybridDigest(original.receipt) !== original.receipt_sha256) {
|
|
346
|
+
throw new AppError("workspace_route_invalid", "RUN committed workspace route evidence changed", 1, { effect: "no_effect", run_id: run.id });
|
|
347
|
+
}
|
|
348
|
+
// Relocation receipts are admitted by snapshot import; never rewrite bundle
|
|
349
|
+
// or source route authority merely because a physical checkout moved.
|
|
350
|
+
const route = JSON.parse(projectPacketLocators(JSON.stringify(original), runContextRelocations(context, run.project_id, run.id)));
|
|
351
|
+
if (route.receipt_sha256 !== original.receipt_sha256
|
|
352
|
+
|| route.run_id !== run.id || route.project_id !== run.project_id || route.project_root !== run.project_root || route.workspace_root !== run.workspace_root
|
|
353
|
+
|| index.workspace.workspace_path !== run.workspace_root || index.execution?.workspace_root !== run.workspace_root || index.execution?.project_root !== run.project_root
|
|
354
|
+
|| index.workspace.manager !== "official-worktrunk" || index.workspace.branch !== route.receipt.policy.feature_branch
|
|
355
|
+
// execution.git is the latest observation (stage completion/refresh updates
|
|
356
|
+
// it). The immutable review base is workspace.base, not that moving HEAD.
|
|
357
|
+
|| index.workspace.base !== route.git.head
|
|
358
|
+
|| !Number.isInteger(route.generation) || route.generation < 0
|
|
359
|
+
|| route.receipt.policy.worktree_path !== run.workspace_root) {
|
|
360
|
+
throw new AppError("workspace_route_invalid", "RUN has no exact coherent committed workspace route", 1, { effect: "no_effect", run_id: run.id });
|
|
361
|
+
}
|
|
362
|
+
return route;
|
|
363
|
+
}
|
|
298
364
|
function loadUpgradeFlowContract(context, projectRoot) {
|
|
299
365
|
const canon = resolveCanonRoot(context, context.env.DD_MEMORYBANK ? { explicitRoot: context.env.DD_MEMORYBANK } : {});
|
|
300
366
|
if (!canon.ok || !canon.canon) {
|
|
@@ -306,17 +372,19 @@ function loadUpgradeFlowContract(context, projectRoot) {
|
|
|
306
372
|
}
|
|
307
373
|
return loadCanonicalFlowContract(canon.canon.flow_root);
|
|
308
374
|
}
|
|
309
|
-
export function getFlowRunStatus(context, input) {
|
|
375
|
+
export function getFlowRunStatus(context, input, preparedBundle) {
|
|
310
376
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
311
377
|
const run = resolveRun(context, project.id, input.runId);
|
|
378
|
+
if (preparedBundle)
|
|
379
|
+
assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, preparedBundle);
|
|
312
380
|
const index = authoritativeIndex(run);
|
|
313
381
|
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) };
|
|
382
|
+
return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index, preparedBundle), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
|
|
315
383
|
}
|
|
316
384
|
/** A read-only projection of the next legal lifecycle action.
|
|
317
385
|
* It is derived from the persisted RUN/Work facts so controllers never infer a
|
|
318
386
|
* transition from a model's prose or a hard-coded stage array. */
|
|
319
|
-
function runContinuation(context, run, index) {
|
|
387
|
+
function runContinuation(context, run, index, preparedBundle) {
|
|
320
388
|
if (["done", "failed", "cancelled"].includes(run.status))
|
|
321
389
|
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
322
390
|
const control = runControlDirective(context, run.project_id, run.id);
|
|
@@ -332,6 +400,15 @@ function runContinuation(context, run, index) {
|
|
|
332
400
|
return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
333
401
|
if (run.status === "blocked")
|
|
334
402
|
return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
|
|
403
|
+
if (index.execution_bundle) {
|
|
404
|
+
const bundle = preparedBundle ?? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
|
|
405
|
+
const next = bundledNextStage(bundle.descriptor, index);
|
|
406
|
+
if (!next)
|
|
407
|
+
return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
|
|
408
|
+
const command = bundle.engine().command(context.ddFlowHome, ["stage", "start", run.id, "--stage", next, "--project-root", run.project_root, "--json"]);
|
|
409
|
+
return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command,
|
|
410
|
+
execution: { status: "resolved", ...bundle.execution.stage(next) } };
|
|
411
|
+
}
|
|
335
412
|
if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
|
|
336
413
|
const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
|
|
337
414
|
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 +664,18 @@ export function prepareFlowRunStageAttachment(context, input, archive = false) {
|
|
|
587
664
|
const index = authoritativeIndex(run);
|
|
588
665
|
const status = parseStageStatus(input.status);
|
|
589
666
|
const existing = index.stage_runs.find((item) => item.stage === stage);
|
|
590
|
-
|
|
667
|
+
const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : null;
|
|
668
|
+
if (bundle)
|
|
669
|
+
assertBundledStageStart(bundle.descriptor, index, stage, dir, status, existing);
|
|
670
|
+
else
|
|
671
|
+
assertVnextStageStart(run, index, stage, status, existing);
|
|
591
672
|
if (archive) {
|
|
592
673
|
const runRoot = runArtifactRoot(run);
|
|
593
674
|
if (fs.existsSync(path.join(runRoot, `.archive-${dir}.json`)) || status === "running" && existing) {
|
|
594
675
|
archiveExistingStageAttempt(context, project.id, run.id, runRoot, dir);
|
|
595
676
|
}
|
|
596
677
|
}
|
|
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 };
|
|
678
|
+
return { project, run, index, stage, dir, status, existing, generation, bundle, attempt: status === "running" ? nextAttempt(existing) : existing?.attempt ?? "try-001", git: status === "running" ? gitFacts(run.workspace_root) : undefined };
|
|
598
679
|
}
|
|
599
680
|
export function attachFlowRunStage(context, input, prepared = prepareFlowRunStageAttachment(context, input, true)) {
|
|
600
681
|
// Filesystem archive and Git inspection are preparation, never writer work.
|
|
@@ -607,7 +688,13 @@ export function attachFlowRunStage(context, input, prepared = prepareFlowRunStag
|
|
|
607
688
|
if ((index.runtime_revision ?? 0) !== (prepared.index.runtime_revision ?? 0) || existing?.attempt !== prepared.existing?.attempt) {
|
|
608
689
|
throw new AppError("stage_revision_changed", "Stage changed during start preparation", 1, { run_id: run.id, stage });
|
|
609
690
|
}
|
|
610
|
-
|
|
691
|
+
if (prepared.bundle) {
|
|
692
|
+
if (index.execution_bundle?.manifest_sha256 !== prepared.bundle.sha256)
|
|
693
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle changed during stage preparation", 1);
|
|
694
|
+
assertBundledStageStart(prepared.bundle.descriptor, index, stage, dir, status, existing);
|
|
695
|
+
}
|
|
696
|
+
else
|
|
697
|
+
assertVnextStageStart(latest, index, stage, status, existing);
|
|
611
698
|
const now = context.now();
|
|
612
699
|
const stageRun = {
|
|
613
700
|
...(existing ?? { order: index.stage_runs.length + 1 }),
|
|
@@ -652,7 +739,7 @@ export function attachFlowRunStage(context, input, prepared = prepareFlowRunStag
|
|
|
652
739
|
export function withStageSettlement(context, operation) {
|
|
653
740
|
return context.db.writeTransaction(operation, "stage.settle");
|
|
654
741
|
}
|
|
655
|
-
export function prepareFlowRunStageCompletion(context, input) {
|
|
742
|
+
export function prepareFlowRunStageCompletion(context, input, admittedBundle) {
|
|
656
743
|
for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
|
|
657
744
|
if (value !== undefined)
|
|
658
745
|
requiredPlain(value, name);
|
|
@@ -676,11 +763,37 @@ export function prepareFlowRunStageCompletion(context, input) {
|
|
|
676
763
|
if (!existing) {
|
|
677
764
|
throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
|
|
678
765
|
}
|
|
766
|
+
// Bound completion must admit the same immutable descriptor/engine authority
|
|
767
|
+
// as stage entry. Keep filesystem admission outside the SQLite writer.
|
|
768
|
+
const bundle = admittedBundle === undefined
|
|
769
|
+
? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : admittedBundle;
|
|
770
|
+
if (bundle && !index.execution_bundle)
|
|
771
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared after finish admission", 1);
|
|
772
|
+
if (bundle)
|
|
773
|
+
assertCompletionBundleAuthority(context, run, index);
|
|
774
|
+
if (index.execution_bundle && (!bundle || bundle.sha256 !== index.execution_bundle.manifest_sha256
|
|
775
|
+
|| bundle.manifest.descriptor.sha256 !== index.execution_bundle.descriptor_sha256
|
|
776
|
+
|| hybridDigest(bundleAuthority(bundle.admission())) !== index.execution_bundle.admission_sha256)) {
|
|
777
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared finish bundle does not match authoritative RUN admission", 1);
|
|
778
|
+
}
|
|
779
|
+
if (bundle) {
|
|
780
|
+
const declared = bundle.descriptor.stage(stage);
|
|
781
|
+
if (declared.directory !== existing.dir || index.current_stage !== stage) {
|
|
782
|
+
throw new AppError("flow_stage_completion_invalid", "Completion does not belong to the active descriptor stage", 2, { phase: "prepare", effect: "no_effect", run_id: run.id, stage });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
679
785
|
const dataPath = input.data ? path.join(runArtifactRoot(run), existing.dir, input.data) : null;
|
|
680
786
|
const dataSha256 = input.dataSha256 ?? (dataPath && fs.existsSync(dataPath) ? crypto.createHash("sha256").update(fs.readFileSync(dataPath)).digest("hex") : null);
|
|
681
787
|
if (dataSha256 && !/^[a-f0-9]{64}$/.test(dataSha256))
|
|
682
788
|
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) };
|
|
789
|
+
return { project, run, index, stage, status, existing, bundle, dataSha256, git: input.git ?? gitFacts(run.workspace_root), generation: ["done", "failed", "skipped"].includes(status) ? assertStageSettlementAllowed(context, project.id, run.id, stage) : assertRunMutationAllowed(context, project.id, run.id) };
|
|
790
|
+
}
|
|
791
|
+
/** Raw historical control is not a finish-sealing bypass for a descriptor-bound RUN. */
|
|
792
|
+
export function prepareRawFlowRunStageCompletion(context, input) {
|
|
793
|
+
const prepared = prepareFlowRunStageCompletion(context, input);
|
|
794
|
+
if (prepared.bundle)
|
|
795
|
+
throw new AppError("stage_finish_required", "Descriptor-bound stages must settle through their registered stage finish handler", 2, { phase: "prepare", effect: "no_effect", run_id: prepared.run.id, stage: prepared.stage });
|
|
796
|
+
return prepared;
|
|
684
797
|
}
|
|
685
798
|
function stageUsageProjection(context, run, stage, startedAt) {
|
|
686
799
|
if (!startedAt)
|
|
@@ -705,6 +818,14 @@ export function completeFlowRunStage(context, input, prepared = prepareFlowRunSt
|
|
|
705
818
|
if (!existing || existing.attempt !== prepared.existing.attempt || index.runtime_revision !== prepared.index.runtime_revision) {
|
|
706
819
|
throw new AppError("stage_revision_changed", "Stage attempt changed during finish preparation", 1, { run_id: run.id, stage, expected_attempt: prepared.existing.attempt, actual_attempt: existing?.attempt ?? null });
|
|
707
820
|
}
|
|
821
|
+
if (hybridDigest(index.execution_bundle ?? null) !== hybridDigest(prepared.index.execution_bundle ?? null)) {
|
|
822
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle authority changed during finish preparation", 1);
|
|
823
|
+
}
|
|
824
|
+
if (prepared.bundle && (index.current_stage !== stage || existing.dir !== prepared.bundle.descriptor.stage(stage).directory)) {
|
|
825
|
+
throw new AppError("flow_stage_completion_invalid", "Active descriptor stage changed during finish preparation", 1);
|
|
826
|
+
}
|
|
827
|
+
if (prepared.bundle)
|
|
828
|
+
assertCompletionBundleAuthority(context, latest, index);
|
|
708
829
|
const now = context.now();
|
|
709
830
|
const stageRun = {
|
|
710
831
|
...existing,
|
|
@@ -755,6 +876,42 @@ export function completeFlowRunStage(context, input, prepared = prepareFlowRunSt
|
|
|
755
876
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
756
877
|
return { ok: true, run: flowRunSummary(updatedRun), stage_run: completed.stageRun, index: completed.index, flow_guidance: guidanceForRun(context, updatedRun, completed.index) };
|
|
757
878
|
}
|
|
879
|
+
/** Writer-safe readback of independently retained SQL authority; no filesystem admission. */
|
|
880
|
+
function assertCompletionBundleAuthority(context, run, index) {
|
|
881
|
+
const anchor = index.execution_bundle;
|
|
882
|
+
if (!anchor)
|
|
883
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared during finish", 1);
|
|
884
|
+
assertRunBundleSqlAuthority(context, { projectId: run.project_id, runId: run.id }, anchor);
|
|
885
|
+
}
|
|
886
|
+
/** Metadata-only fencing shared by lifecycle ownership and the settlement writer.
|
|
887
|
+
* False means genuine historical authority, never a lost/orphaned bundle anchor.
|
|
888
|
+
* Physical descriptor/engine admission remains an independent outside-writer check.
|
|
889
|
+
*/
|
|
890
|
+
export function assertRunBundleSqlAuthority(context, scope, anchor) {
|
|
891
|
+
const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
|
|
892
|
+
const row = hasAuthority ? context.db.get("SELECT manifest_sha256, admission_json FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [scope.projectId, scope.runId]) : undefined;
|
|
893
|
+
if (!anchor) {
|
|
894
|
+
if (row)
|
|
895
|
+
throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || typeof anchor.flow_id !== "string"
|
|
899
|
+
|| [anchor.manifest_sha256, anchor.descriptor_sha256, anchor.admission_sha256].some(hash => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash)))
|
|
900
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
|
|
901
|
+
if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
|
|
902
|
+
throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority disappeared during finish", 1);
|
|
903
|
+
let valid = false;
|
|
904
|
+
try {
|
|
905
|
+
if (Buffer.byteLength(row.admission_json, "utf8") <= 1024 * 1024) {
|
|
906
|
+
const retained = JSON.parse(row.admission_json);
|
|
907
|
+
valid = retained.runId === scope.runId && hybridDigest(retained) === anchor.admission_sha256;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch { /* Corrupt authority cannot be replaced by a prepared object. */ }
|
|
911
|
+
if (!valid)
|
|
912
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed during finish", 1);
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
758
915
|
/** Pause one live stage without completing it or creating a new attempt. */
|
|
759
916
|
export function pauseFlowRunStage(context, input) {
|
|
760
917
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -1016,6 +1173,41 @@ export function completeFlowRun(context, input) {
|
|
|
1016
1173
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
1017
1174
|
return { ok: true, run: flowRunSummary(updatedRun), index, usage, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
1018
1175
|
}
|
|
1176
|
+
/** SQL-only terminal writer for an independently prepared descriptor handler.
|
|
1177
|
+
* This closes runtime state, not semantic or Git acceptance. The caller owns
|
|
1178
|
+
* the outer transaction, root settlement and the handler-specific proof fence.
|
|
1179
|
+
*/
|
|
1180
|
+
export function completeDescriptorRunState(context, scope, bundle, git) {
|
|
1181
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
1182
|
+
assertRunMutationAllowed(context, scope.projectId, scope.runId);
|
|
1183
|
+
const run = requireRunById(context, scope.projectId, scope.runId);
|
|
1184
|
+
const index = authoritativeIndex(run);
|
|
1185
|
+
const terminal = index.stage_runs.at(-1);
|
|
1186
|
+
if (run.status !== "running" || !terminal || terminal.status !== "done"
|
|
1187
|
+
|| !bundle.descriptor.descriptor.terminal_stages.includes(terminal.stage)
|
|
1188
|
+
|| index.stage_runs.some(stage => ["running", "paused", "waiting_for_user", "blocked", "pending"].includes(stage.status))
|
|
1189
|
+
|| context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND status IN ('created','running','paused') LIMIT 1", [scope.projectId, scope.runId])
|
|
1190
|
+
|| context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id WHERE w.project_id=? AND w.run_id=? AND ws.status='running' LIMIT 1", [scope.projectId, scope.runId])) {
|
|
1191
|
+
throw new AppError("descriptor_run_settlement_invalid", "Descriptor RUN is not ready for terminal runtime settlement", 1, { effect: "no_effect" });
|
|
1192
|
+
}
|
|
1193
|
+
const project = context.db.get("SELECT * FROM projects WHERE id=?", [scope.projectId]);
|
|
1194
|
+
if (!project)
|
|
1195
|
+
throw new AppError("not_found", "Project is not registered", 1);
|
|
1196
|
+
const now = context.now();
|
|
1197
|
+
index.status = "done";
|
|
1198
|
+
index.verdict = "accepted";
|
|
1199
|
+
index.next_action = null;
|
|
1200
|
+
index.updated_at = now;
|
|
1201
|
+
index.completed_at = now;
|
|
1202
|
+
index.finished_at = now;
|
|
1203
|
+
index.duration_ms = durationMs(index.started_at ?? run.created_at, now);
|
|
1204
|
+
index.execution = { project_root: run.project_root, workspace_root: run.workspace_root, git: { ...git } };
|
|
1205
|
+
persistRunState(context, project, run, index);
|
|
1206
|
+
closeRunOrchestrators(context, scope.projectId, run, "done", now);
|
|
1207
|
+
refreshRunSessionProjection(context, scope.projectId, scope.runId);
|
|
1208
|
+
appendAudit(context, { projectId: scope.projectId, eventType: "flow_run.completed", payload: { run_id: scope.runId, status: "done", verdict: "accepted", next_action: null } });
|
|
1209
|
+
appendRunTimeline(context, scope.projectId, scope.runId, { at: now, type: "run_completed", run_id: scope.runId, status: "done" });
|
|
1210
|
+
}
|
|
1019
1211
|
/** Update a live RUN between stages without manufacturing terminal evidence. */
|
|
1020
1212
|
export function advanceFlowRun(context, input) {
|
|
1021
1213
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
@@ -1479,6 +1671,17 @@ export function publishRunProjections(context, projectId, runId) {
|
|
|
1479
1671
|
if (!latest)
|
|
1480
1672
|
throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
|
|
1481
1673
|
const index = authoritativeIndex(latest);
|
|
1674
|
+
if (index.workspace_route) {
|
|
1675
|
+
const route = retainedWorkspaceRoute(context, latest);
|
|
1676
|
+
const file = path.join(runArtifactRoot(latest), "02-protocolize", "workspace-route.json");
|
|
1677
|
+
ensureDir(path.dirname(file));
|
|
1678
|
+
try {
|
|
1679
|
+
writeJsonFile(file, route.receipt);
|
|
1680
|
+
}
|
|
1681
|
+
catch (error) {
|
|
1682
|
+
throw new AppError("workspace_route_publication_failed", "Committed route projection could not be published", 1, { effect: "committed", run_id: runId, route_sha256: index.workspace_route.sha256, cause: String(error) });
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1482
1685
|
const mutations = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'flow_run_flag_mutations'") ? context.db.all("SELECT resulting_revision, result_json, created_at, idempotency_key FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? ORDER BY resulting_revision DESC LIMIT 32", [projectId, runId]) : [];
|
|
1483
1686
|
const history = mutations.reverse().map((row) => ({
|
|
1484
1687
|
revision: row.resulting_revision,
|
|
@@ -1524,12 +1727,187 @@ function authoritativeIndex(run) {
|
|
|
1524
1727
|
const state = JSON.parse(run.index_json);
|
|
1525
1728
|
if (!state || typeof state !== "object" || Array.isArray(state) || !Array.isArray(state.stage_runs))
|
|
1526
1729
|
throw new Error("RUN index must contain stage_runs");
|
|
1527
|
-
|
|
1730
|
+
const derivedStage = ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)
|
|
1731
|
+
? [...state.stage_runs].sort((left, right) => right.order - left.order)[0]?.stage : undefined;
|
|
1732
|
+
return { ...state, schema_id: runSchemaId, ...(state.current_stage === undefined && derivedStage ? { current_stage: derivedStage } : {}) };
|
|
1528
1733
|
}
|
|
1529
1734
|
catch (error) {
|
|
1530
1735
|
throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
|
|
1531
1736
|
}
|
|
1532
1737
|
}
|
|
1738
|
+
function assertBundleWorkspaceIdentity(context, run, bundle) {
|
|
1739
|
+
const scope = currentBundleScope(context, run, bundle);
|
|
1740
|
+
const route = authoritativeIndex(run).workspace_route ? retainedWorkspaceRoute(context, run) : undefined;
|
|
1741
|
+
if (scope.workspace === run.workspace_root)
|
|
1742
|
+
return true;
|
|
1743
|
+
if (bundle.descriptor.descriptor.id !== "sdlc" || !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) || scope.workspace !== run.project_root)
|
|
1744
|
+
return false;
|
|
1745
|
+
if (!route)
|
|
1746
|
+
return false;
|
|
1747
|
+
return route.previous_workspace === scope.workspace && route.receipt.policy.route === "feature_worktree"
|
|
1748
|
+
&& route.receipt.policy.base_ref !== null && route.receipt.policy.feature_branch !== null;
|
|
1749
|
+
}
|
|
1750
|
+
function currentBundleScope(context, run, bundle) {
|
|
1751
|
+
return JSON.parse(projectPacketLocators(JSON.stringify(bundle.execution.snapshot().scope), runContextRelocations(context, run.project_id, run.id)));
|
|
1752
|
+
}
|
|
1753
|
+
function assertBundleRunIdentity(context, run, bundle) {
|
|
1754
|
+
const scope = currentBundleScope(context, run, bundle);
|
|
1755
|
+
const flowId = bundle.descriptor.descriptor.id;
|
|
1756
|
+
const matchesFlow = flowId === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flowId === run.flow_kind;
|
|
1757
|
+
if (bundle.manifest.run_id !== run.id || scope.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle) || !matchesFlow) {
|
|
1758
|
+
throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
|
|
1759
|
+
}
|
|
1760
|
+
const blobs = new Map(bundle.manifest.inventory.map(ref => [ref.path, bundle.blob(ref.path)]));
|
|
1761
|
+
// File-owned handler inventories can never authorize executable code.
|
|
1762
|
+
admitBundledFlow(bundle.blob(bundle.manifest.descriptor.path), bundle.manifest.descriptor.sha256, blobs);
|
|
1763
|
+
}
|
|
1764
|
+
function bundleAuthority(admission) {
|
|
1765
|
+
const authority = { runId: admission.runId, executionScope: structuredClone(admission.executionScope),
|
|
1766
|
+
receipts: structuredClone([...(admission.receipts ?? [])].sort(([left], [right]) => left.localeCompare(right, "en"))) };
|
|
1767
|
+
for (const key of ["descriptorSha256", "engineChecksum", "engineBindingSha256", "engineManifestSha256", "bundleProjectRoot", "bundleProvenance", "bootstrapIdentity"]) {
|
|
1768
|
+
if (admission[key] !== undefined)
|
|
1769
|
+
Object.assign(authority, { [key]: structuredClone(admission[key]) });
|
|
1770
|
+
}
|
|
1771
|
+
return authority;
|
|
1772
|
+
}
|
|
1773
|
+
/** Copies the same SQL authority representation used by legacy bundle binding. */
|
|
1774
|
+
export function runBundleAuthorityMetadata(bundle) {
|
|
1775
|
+
if (!(bundle instanceof RetainedExecutionBundle))
|
|
1776
|
+
throw new AppError("run_bundle_anchor_invalid", "Bundle must be admitted before RUN allocation", 1);
|
|
1777
|
+
const admissionJson = JSON.stringify(bundleAuthority(bundle.admission())), admissionSha256 = hybridDigest(JSON.parse(admissionJson));
|
|
1778
|
+
if (Buffer.byteLength(admissionJson) > 1024 * 1024)
|
|
1779
|
+
throw new AppError("run_bundle_authority_invalid", "RUN admission exceeds its bound", 1);
|
|
1780
|
+
return { admissionJson, anchor: { schema_id: "dd-flow/run-execution-bundle@1",
|
|
1781
|
+
manifest_sha256: bundle.sha256, descriptor_sha256: bundle.manifest.descriptor.sha256,
|
|
1782
|
+
admission_sha256: admissionSha256, flow_id: bundle.manifest.flow_id } };
|
|
1783
|
+
}
|
|
1784
|
+
/** Recheck SQL authority and compiled descriptor admission. Schema loading
|
|
1785
|
+
* belongs outside SQL writers; use the explicit SQL-only fence inside them.
|
|
1786
|
+
*/
|
|
1787
|
+
export function assertPreparedRunBundleAuthority(context, scope, bundle) {
|
|
1788
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
1789
|
+
assertBundleRunIdentity(context, run, bundle);
|
|
1790
|
+
}
|
|
1791
|
+
/** SQL-only fence after the caller has admitted compiled handlers outside the
|
|
1792
|
+
* writer. Descriptor schema loading is deliberately excluded from this fence.
|
|
1793
|
+
*/
|
|
1794
|
+
export function assertPreparedRunBundleSqlAuthority(context, scope, bundle) {
|
|
1795
|
+
if (!(bundle instanceof RetainedExecutionBundle))
|
|
1796
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared bundle is not admitted", 1);
|
|
1797
|
+
const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [scope.projectId, scope.runId]);
|
|
1798
|
+
if (!run)
|
|
1799
|
+
throw new AppError("not_found", "Prepared bundle RUN is unavailable", 1);
|
|
1800
|
+
const index = authoritativeIndex(run), anchor = index.execution_bundle;
|
|
1801
|
+
if (!assertRunBundleSqlAuthority(context, scope, anchor) || !anchor || anchor.manifest_sha256 !== bundle.sha256
|
|
1802
|
+
|| anchor.descriptor_sha256 !== bundle.manifest.descriptor.sha256 || anchor.flow_id !== bundle.manifest.flow_id
|
|
1803
|
+
|| anchor.admission_sha256 !== hybridDigest(bundleAuthority(bundle.admission())))
|
|
1804
|
+
throw new AppError("run_bundle_anchor_invalid", "Prepared bundle differs from authoritative RUN", 1);
|
|
1805
|
+
const execution = currentBundleScope(context, run, bundle);
|
|
1806
|
+
const flow = bundle.descriptor.descriptor.id;
|
|
1807
|
+
if (bundle.manifest.run_id !== run.id || execution.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle)
|
|
1808
|
+
|| !(flow === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flow === run.flow_kind)) {
|
|
1809
|
+
throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { effect: "no_effect", run_id: run.id });
|
|
1810
|
+
}
|
|
1811
|
+
return run;
|
|
1812
|
+
}
|
|
1813
|
+
export function readFlowRunExecutionBundle(context, input) {
|
|
1814
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1815
|
+
const run = resolveRun(context, project.id, input.runId), anchor = authoritativeIndex(run).execution_bundle;
|
|
1816
|
+
if (!anchor) {
|
|
1817
|
+
// Genuine historical RUNs have neither authority nor anchor. Lost new-RUN
|
|
1818
|
+
// metadata must not silently switch their execution to the legacy engine path.
|
|
1819
|
+
const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
|
|
1820
|
+
if (hasAuthority && context.db.get("SELECT 1 FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [project.id, run.id])) {
|
|
1821
|
+
throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
|
|
1822
|
+
}
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || !/^[a-f0-9]{64}$/.test(anchor.manifest_sha256)
|
|
1826
|
+
|| !/^[a-f0-9]{64}$/.test(anchor.descriptor_sha256) || !/^[a-f0-9]{64}$/.test(anchor.admission_sha256))
|
|
1827
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
|
|
1828
|
+
const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
|
|
1829
|
+
const row = hasAuthority ? context.db.get("SELECT admission_json, manifest_sha256 FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [project.id, run.id]) : undefined;
|
|
1830
|
+
if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
|
|
1831
|
+
throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority is unavailable", 1);
|
|
1832
|
+
if (Buffer.byteLength(row.admission_json) > 1024 * 1024)
|
|
1833
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
|
|
1834
|
+
let retained;
|
|
1835
|
+
try {
|
|
1836
|
+
retained = JSON.parse(row.admission_json);
|
|
1837
|
+
}
|
|
1838
|
+
catch {
|
|
1839
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority is invalid", 1);
|
|
1840
|
+
}
|
|
1841
|
+
if (hybridDigest(retained) !== anchor.admission_sha256 || !Array.isArray(retained.receipts)
|
|
1842
|
+
|| input.admission && hybridDigest(bundleAuthority(input.admission)) !== anchor.admission_sha256) {
|
|
1843
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed", 1);
|
|
1844
|
+
}
|
|
1845
|
+
const admission = { ...retained, receipts: new Map(retained.receipts), descriptorContext: bundledHandlerContext(new Map()) };
|
|
1846
|
+
const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission);
|
|
1847
|
+
assertBundleRunIdentity(context, run, bundle);
|
|
1848
|
+
if (bundle.manifest.descriptor.sha256 !== anchor.descriptor_sha256 || bundle.manifest.flow_id !== anchor.flow_id) {
|
|
1849
|
+
throw new AppError("run_bundle_anchor_invalid", "RUN bundle descriptor identity changed", 1);
|
|
1850
|
+
}
|
|
1851
|
+
return bundle;
|
|
1852
|
+
}
|
|
1853
|
+
/** Freeze once before physical owners exist. Publication precedes the fenced SQLite anchor. */
|
|
1854
|
+
export function bindFlowRunExecutionBundle(context, input) {
|
|
1855
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
1856
|
+
const run = resolveRun(context, project.id, input.runId), index = authoritativeIndex(run);
|
|
1857
|
+
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
1858
|
+
assertBundleRunIdentity(context, run, input.bundle);
|
|
1859
|
+
if (input.bundle.manifest.flow_id !== "sdlc") {
|
|
1860
|
+
throw new AppError("run_bundle_start_required", "Memory RUN and its bundle authority must be allocated together through the request-bound start", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
|
|
1861
|
+
}
|
|
1862
|
+
if (index.execution_bundle) {
|
|
1863
|
+
if (index.execution_bundle.manifest_sha256 !== input.bundle.sha256)
|
|
1864
|
+
throw new AppError("run_bundle_immutable", "RUN bundle cannot be replaced", 1);
|
|
1865
|
+
readFlowRunExecutionBundle(context, { ...input, admission: input.bundle.admission() });
|
|
1866
|
+
publishRunProjections(context, project.id, run.id);
|
|
1867
|
+
return { changed: false, sha256: input.bundle.sha256 };
|
|
1868
|
+
}
|
|
1869
|
+
const assertUnstarted = (state) => {
|
|
1870
|
+
if (state.status !== "running" || state.stage_runs.length || state.sessions.length
|
|
1871
|
+
|| context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])
|
|
1872
|
+
|| context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])) {
|
|
1873
|
+
throw new AppError("run_bundle_active", "Execution bundle must be frozen before stages, Works or Sessions exist", 1);
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
assertUnstarted(index);
|
|
1877
|
+
const authority = bundleAuthority(input.bundle.admission());
|
|
1878
|
+
// Hash the JSON representation actually retained, not optional undefined fields.
|
|
1879
|
+
const authorityJson = JSON.stringify(authority), authoritySha256 = hybridDigest(JSON.parse(authorityJson));
|
|
1880
|
+
if (Buffer.byteLength(authorityJson) > 1024 * 1024)
|
|
1881
|
+
throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
|
|
1882
|
+
input.bundle.materialize(runArtifactRoot(run));
|
|
1883
|
+
try {
|
|
1884
|
+
context.db.writeTransaction(() => {
|
|
1885
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
1886
|
+
const latest = resolveRun(context, project.id, run.id), state = authoritativeIndex(latest);
|
|
1887
|
+
assertUnstarted(state);
|
|
1888
|
+
if (state.runtime_revision !== index.runtime_revision || state.execution_bundle || latest.workspace_root !== run.workspace_root
|
|
1889
|
+
|| latest.project_root !== run.project_root || latest.flow_kind !== run.flow_kind || latest.run_root !== run.run_root) {
|
|
1890
|
+
throw new AppError("run_revision_changed", "RUN changed during bundle publication", 1);
|
|
1891
|
+
}
|
|
1892
|
+
state.execution_bundle = { schema_id: "dd-flow/run-execution-bundle@1", manifest_sha256: input.bundle.sha256,
|
|
1893
|
+
descriptor_sha256: input.bundle.manifest.descriptor.sha256, admission_sha256: authoritySha256, flow_id: input.bundle.manifest.flow_id };
|
|
1894
|
+
context.db.execSchema(`CREATE TABLE IF NOT EXISTS run_bundle_authorities (
|
|
1895
|
+
project_id TEXT NOT NULL, run_id TEXT NOT NULL, manifest_sha256 TEXT NOT NULL, admission_json TEXT NOT NULL,
|
|
1896
|
+
PRIMARY KEY(project_id, run_id), FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id)
|
|
1897
|
+
)`);
|
|
1898
|
+
context.db.run("INSERT INTO run_bundle_authorities (project_id, run_id, manifest_sha256, admission_json) VALUES (?, ?, ?, ?)", [project.id, run.id, input.bundle.sha256, authorityJson]);
|
|
1899
|
+
state.updated_at = context.now();
|
|
1900
|
+
persistRunState(context, project, latest, state);
|
|
1901
|
+
}, "runs.bindExecutionBundle");
|
|
1902
|
+
}
|
|
1903
|
+
catch (error) {
|
|
1904
|
+
throw new AppError("run_bundle_binding_incomplete", "Bundle retained but RUN binding requires recovery", 1, {
|
|
1905
|
+
phase: "publication", effect: "partial", run_id: run.id, sha256: input.bundle.sha256,
|
|
1906
|
+
reason: error instanceof AppError ? error.code : "binding_io_failure",
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
return { changed: true, sha256: input.bundle.sha256 };
|
|
1910
|
+
}
|
|
1533
1911
|
function sanitizeTimelineValue(value, key) {
|
|
1534
1912
|
if (key && /(secret|password|authorization|cookie|api[_-]?key|private[_-]?key|access[_-]?token|refresh[_-]?token|transcript|prompt|tool[_-]?output|document[_-]?content)/i.test(key)) {
|
|
1535
1913
|
return "[REDACTED]";
|
|
@@ -1556,6 +1934,24 @@ function nextAttempt(existing) {
|
|
|
1556
1934
|
const number = Number(existing.attempt.replace("try-", ""));
|
|
1557
1935
|
return `try-${String(Number.isFinite(number) ? number + 1 : 1).padStart(3, "0")}`;
|
|
1558
1936
|
}
|
|
1937
|
+
function bundledNextStage(flow, index) {
|
|
1938
|
+
const completed = index.stage_runs.filter(stage => ["done", "skipped"].includes(stage.status)).sort((left, right) => right.order - left.order)[0];
|
|
1939
|
+
if (!completed)
|
|
1940
|
+
return flow.descriptor.entry_stage;
|
|
1941
|
+
// Optional SDLC review routing remains a flow-specific policy, not a generic stage list.
|
|
1942
|
+
const outcome = flow.descriptor.id === "sdlc" && completed.stage === "plan" && index.settings?.plan_review?.mode === "off" ? "not-applicable" : "accepted";
|
|
1943
|
+
return flow.successor(completed.stage, outcome);
|
|
1944
|
+
}
|
|
1945
|
+
function assertBundledStageStart(flow, index, stage, dir, status, existing) {
|
|
1946
|
+
const selected = flow.stage(stage);
|
|
1947
|
+
if (selected.directory !== dir)
|
|
1948
|
+
throw new AppError("flow_stage_directory_invalid", "Stage directory differs from the frozen descriptor", 1, { stage });
|
|
1949
|
+
if (status !== "running")
|
|
1950
|
+
return;
|
|
1951
|
+
if (existing || index.stage_runs.some(candidate => !["done", "skipped"].includes(candidate.status)) || bundledNextStage(flow, index) !== stage) {
|
|
1952
|
+
throw new AppError("illegal_stage_transition", "Stage does not follow the frozen descriptor or an attempt already exists", 2, { run_id: index.run_id, stage });
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1559
1955
|
function assertVnextStageStart(run, index, stage, status, existing) {
|
|
1560
1956
|
if (status !== "running" || (run.flow_kind !== "vnext_specify" && run.flow_kind !== "vnext_protocolize"))
|
|
1561
1957
|
return;
|
|
@@ -1614,19 +2010,6 @@ function requireRunById(context, projectId, runId) {
|
|
|
1614
2010
|
}
|
|
1615
2011
|
return run;
|
|
1616
2012
|
}
|
|
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
2013
|
function flowRunSummary(run) {
|
|
1631
2014
|
return {
|
|
1632
2015
|
id: run.id,
|