@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
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
|
|
6
|
+
import { admitReleaseManifest } from "./release-manifest.js";
|
|
7
|
+
import { validateReleasePayloadIdentity } from "./release-payload.js";
|
|
8
|
+
import { publishReleaseCache, verifyPublishedReleaseCache } from "./release-cache.js";
|
|
9
|
+
import { acquireInstallationLock } from "./installation-lock.js";
|
|
10
|
+
import { authenticateReleaseEnvelope } from "./release-envelope.js";
|
|
11
|
+
import { prepareOperatorProfile, validateProfileCreationIntent, commitProfileCreation, readOperatorProfiles } from "./operator-profiles.js";
|
|
12
|
+
/** Production ingress authenticates the source before constructing any installation authority. */
|
|
13
|
+
export function installAuthenticatedRelease(home, input) {
|
|
14
|
+
const authenticated = authenticateReleaseEnvelope(input.envelope, input.trustedProducers, input.compatibility);
|
|
15
|
+
return installRelease(home, { requestId: input.requestId, action: input.action,
|
|
16
|
+
manifest: authenticated.manifest, manifestBytes: authenticated.manifestBytes, authority: authenticated.authority,
|
|
17
|
+
canonArchive: input.canonArchive, engineArchive: input.engineArchive,
|
|
18
|
+
...(input.executor !== undefined ? { executor: input.executor } : {}),
|
|
19
|
+
...(input.profileCreation !== undefined ? { profileCreation: input.profileCreation } : {}),
|
|
20
|
+
...(input.deferredOnboarding ? { deferredOnboarding: true } : {}),
|
|
21
|
+
...(input.onCheckpoint ? { onCheckpoint: input.onCheckpoint } : {}) });
|
|
22
|
+
}
|
|
23
|
+
function refuse(reason, effect = "no_effect") {
|
|
24
|
+
throw new AppError("setup_conflict", "Setup request cannot proceed", 1, { reason, phase: "setup", effect, help: "setup", next_action: "dd-flow setup status --json" });
|
|
25
|
+
}
|
|
26
|
+
const bytesDigest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
27
|
+
function manifestFromBytes(bytes) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return refuse("invalid_manifest_bytes");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function regularJson(file) {
|
|
36
|
+
if (!fs.lstatSync(file).isFile())
|
|
37
|
+
refuse("unsafe_setup_file");
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return refuse("invalid_setup_file");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function atomicJson(file, value) {
|
|
46
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
47
|
+
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
48
|
+
try {
|
|
49
|
+
try {
|
|
50
|
+
fs.writeFileSync(fd, JSON.stringify(value));
|
|
51
|
+
fs.fsyncSync(fd);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
fs.closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
fs.renameSync(temporary, file);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (fs.existsSync(temporary))
|
|
60
|
+
fs.unlinkSync(temporary);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function readSetupRequest(file) {
|
|
64
|
+
const value = regularJson(file);
|
|
65
|
+
const required = ["schema_id", "request_id", "identity", "previous_selection", "phase", "installation"];
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
67
|
+
|| required.some(key => !Object.hasOwn(value, key))
|
|
68
|
+
|| Object.keys(value).some(key => ![...required, "executor", "profileCreation", "deferredOnboarding"].includes(key)))
|
|
69
|
+
refuse("invalid_request_record");
|
|
70
|
+
const request = value;
|
|
71
|
+
if (Object.hasOwn(request, "deferredOnboarding") && (request.deferredOnboarding !== true || Object.hasOwn(request, "executor") || Object.hasOwn(request, "profileCreation")))
|
|
72
|
+
refuse("invalid_deferred_onboarding");
|
|
73
|
+
if (request.schema_id !== "dd-flow/setup-request@1"
|
|
74
|
+
|| typeof request.request_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(request.request_id)
|
|
75
|
+
|| typeof request.identity !== "string" || !/^[a-f0-9]{64}$/.test(request.identity)
|
|
76
|
+
|| request.previous_selection !== null && (typeof request.previous_selection !== "string" || !/^[a-f0-9]{64}$/.test(request.previous_selection))
|
|
77
|
+
|| !["prepared", "canon_cached", "engine_cached", "profiles_saved", "committed"].includes(request.phase)
|
|
78
|
+
|| !request.installation || typeof request.installation !== "object" || Array.isArray(request.installation))
|
|
79
|
+
refuse("invalid_request_record");
|
|
80
|
+
if (Object.hasOwn(request, "executor") && request.executor !== null) {
|
|
81
|
+
try {
|
|
82
|
+
prepareOperatorProfile(request.executor);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
refuse("invalid_request_executor");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (Object.hasOwn(request, "profileCreation")) {
|
|
89
|
+
try {
|
|
90
|
+
validateProfileCreationIntent(request.profileCreation);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
refuse("invalid_request_profile_creation");
|
|
94
|
+
}
|
|
95
|
+
if (!request.executor || hybridDigest(request.executor) !== hybridDigest(request.profileCreation.profile))
|
|
96
|
+
refuse("request_profile_executor_mismatch");
|
|
97
|
+
}
|
|
98
|
+
return request;
|
|
99
|
+
}
|
|
100
|
+
/** Read retained executor consent for an exact retry, without installation or profile effects.
|
|
101
|
+
* Full source/installation request identity is still checked by installRelease.
|
|
102
|
+
*/
|
|
103
|
+
export function readRetainedSetupExecutor(home, requestId) {
|
|
104
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(requestId))
|
|
105
|
+
refuse("invalid_request");
|
|
106
|
+
const root = path.resolve(home), directory = path.join(root, "setup-requests");
|
|
107
|
+
for (const candidate of [root, directory])
|
|
108
|
+
if (fs.existsSync(candidate) && !fs.lstatSync(candidate).isDirectory())
|
|
109
|
+
refuse("unsafe_setup_directory");
|
|
110
|
+
const file = path.join(directory, `${requestId}.json`);
|
|
111
|
+
if (!fs.existsSync(file))
|
|
112
|
+
return null;
|
|
113
|
+
const request = readSetupRequest(file);
|
|
114
|
+
if (request.request_id !== requestId)
|
|
115
|
+
refuse("request_identity_conflict");
|
|
116
|
+
if (!Object.hasOwn(request, "executor"))
|
|
117
|
+
return null;
|
|
118
|
+
return { executor: structuredClone(request.executor ?? null),
|
|
119
|
+
...(request.profileCreation ? { profileCreation: structuredClone(request.profileCreation) } : {}) };
|
|
120
|
+
}
|
|
121
|
+
/** A deferred request is immutable installation consent, not an implicit skip. */
|
|
122
|
+
export function hasDeferredSetupOnboarding(home, requestId) {
|
|
123
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(requestId))
|
|
124
|
+
refuse("invalid_request");
|
|
125
|
+
const root = path.resolve(home), directory = path.join(root, "setup-requests");
|
|
126
|
+
for (const candidate of [root, directory])
|
|
127
|
+
if (fs.existsSync(candidate) && !fs.lstatSync(candidate).isDirectory())
|
|
128
|
+
refuse("unsafe_setup_directory");
|
|
129
|
+
const file = path.join(directory, `${requestId}.json`);
|
|
130
|
+
if (!fs.existsSync(file))
|
|
131
|
+
return false;
|
|
132
|
+
const request = readSetupRequest(file);
|
|
133
|
+
if (request.request_id !== requestId)
|
|
134
|
+
refuse("request_identity_conflict");
|
|
135
|
+
return request.deferredOnboarding === true;
|
|
136
|
+
}
|
|
137
|
+
function deferredRequest(home, requestId) {
|
|
138
|
+
if (!hasDeferredSetupOnboarding(home, requestId))
|
|
139
|
+
refuse("onboarding_not_deferred");
|
|
140
|
+
const request = readSetupRequest(path.join(path.resolve(home), "setup-requests", `${requestId}.json`));
|
|
141
|
+
if (request.phase !== "committed")
|
|
142
|
+
refuse("installation_not_committed");
|
|
143
|
+
return request;
|
|
144
|
+
}
|
|
145
|
+
/** Retained consent allows recovery without asking again or importing current defaults. */
|
|
146
|
+
export function readSetupOnboarding(home, requestId) {
|
|
147
|
+
const request = deferredRequest(home, requestId);
|
|
148
|
+
const directory = path.join(path.resolve(home), "setup-onboarding");
|
|
149
|
+
if (fs.existsSync(directory) && !fs.lstatSync(directory).isDirectory())
|
|
150
|
+
refuse("unsafe_onboarding_directory");
|
|
151
|
+
const file = path.join(directory, `${requestId}.json`);
|
|
152
|
+
if (!fs.existsSync(file))
|
|
153
|
+
return null;
|
|
154
|
+
const receipt = regularJson(file);
|
|
155
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)
|
|
156
|
+
|| Object.keys(receipt).some(key => !["schema_id", "request_id", "installation_identity", "phase", "executor", "profileCreation"].includes(key))
|
|
157
|
+
|| receipt.schema_id !== "dd-flow/setup-onboarding@1" || receipt.request_id !== requestId
|
|
158
|
+
|| receipt.installation_identity !== request.identity || !["selected", "committed"].includes(receipt.phase)
|
|
159
|
+
|| !Object.hasOwn(receipt, "executor"))
|
|
160
|
+
refuse("invalid_onboarding_receipt");
|
|
161
|
+
try {
|
|
162
|
+
if (receipt.executor !== null)
|
|
163
|
+
prepareOperatorProfile(receipt.executor);
|
|
164
|
+
if (receipt.profileCreation !== undefined) {
|
|
165
|
+
validateProfileCreationIntent(receipt.profileCreation);
|
|
166
|
+
if (!receipt.executor || hybridDigest(receipt.executor) !== hybridDigest(receipt.profileCreation.profile))
|
|
167
|
+
refuse("onboarding_profile_mismatch");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
refuse("invalid_onboarding_receipt");
|
|
172
|
+
}
|
|
173
|
+
return structuredClone(receipt);
|
|
174
|
+
}
|
|
175
|
+
/** Persist human consent before profile effects; replay never restores later profile edits. */
|
|
176
|
+
export function commitSetupOnboarding(home, requestId, selection) {
|
|
177
|
+
const request = deferredRequest(home, requestId), root = path.resolve(home);
|
|
178
|
+
const executor = selection.executor === null ? null : prepareOperatorProfile(selection.executor);
|
|
179
|
+
if (selection.profileCreation) {
|
|
180
|
+
validateProfileCreationIntent(selection.profileCreation);
|
|
181
|
+
if (!executor || hybridDigest(executor) !== hybridDigest(selection.profileCreation.profile))
|
|
182
|
+
refuse("onboarding_profile_mismatch");
|
|
183
|
+
}
|
|
184
|
+
const candidate = { schema_id: "dd-flow/setup-onboarding@1", request_id: requestId,
|
|
185
|
+
installation_identity: request.identity, phase: "selected", executor,
|
|
186
|
+
...(selection.profileCreation ? { profileCreation: selection.profileCreation } : {}) };
|
|
187
|
+
const lease = acquireInstallationLock(path.join(root, "setup.lock"), hybridDigest({ request_id: requestId, identity: request.identity }));
|
|
188
|
+
let failure, result;
|
|
189
|
+
try {
|
|
190
|
+
const retained = readSetupOnboarding(root, requestId);
|
|
191
|
+
if (retained && hybridDigest({ ...retained, phase: "selected" }) !== hybridDigest(candidate))
|
|
192
|
+
refuse("onboarding_selection_conflict");
|
|
193
|
+
if (retained?.phase === "committed")
|
|
194
|
+
result = retained;
|
|
195
|
+
else {
|
|
196
|
+
if (hybridDigest(readSetupStatus(root).installation) !== hybridDigest(request.installation))
|
|
197
|
+
refuse("onboarding_installation_changed");
|
|
198
|
+
const directory = path.join(root, "setup-onboarding");
|
|
199
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
200
|
+
const file = path.join(directory, `${requestId}.json`);
|
|
201
|
+
if (!retained)
|
|
202
|
+
atomicJson(file, candidate);
|
|
203
|
+
if (candidate.profileCreation)
|
|
204
|
+
commitProfileCreation(root, candidate.profileCreation);
|
|
205
|
+
result = { ...candidate, phase: "committed" };
|
|
206
|
+
atomicJson(file, result);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
failure = error instanceof AppError ? error : new AppError("setup_onboarding_interrupted", "Installed release is retained; executor onboarding requires reconciliation", 1, { phase: "executor_onboarding", effect: "unknown", request_id: requestId, help: "setup", next_action: "Replay the exact setup request" });
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
try {
|
|
214
|
+
lease.release();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
if (failure)
|
|
218
|
+
failure.details.cleanup = "not_confirmed";
|
|
219
|
+
else
|
|
220
|
+
failure = new AppError("setup_cleanup_failed", "Onboarding owner cleanup requires reconciliation", 1, { effect: "unknown", cleanup: "not_confirmed", request_id: requestId, help: "setup" });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (failure)
|
|
224
|
+
throw failure;
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
/** Read-only global readiness. No SQLite connection, migration, project registration or probes. */
|
|
228
|
+
export function readSetupStatus(home) {
|
|
229
|
+
const root = path.resolve(home);
|
|
230
|
+
if (fs.existsSync(root) && !fs.lstatSync(root).isDirectory())
|
|
231
|
+
refuse("unsafe_setup_home");
|
|
232
|
+
const file = path.join(root, "installation.json");
|
|
233
|
+
const installation = fs.existsSync(file) ? regularJson(file) : null;
|
|
234
|
+
if (installation) {
|
|
235
|
+
if (!installation.manifest || typeof installation.manifest_bytes_base64 !== "string" || validateHybridShape("release-manifest", installation.manifest).length ||
|
|
236
|
+
installation.canon_root !== path.join(root, "release-cache", "canon", installation.manifest.canon.version, "payload") ||
|
|
237
|
+
installation.engine_root !== path.join(root, "release-cache", "engine", installation.manifest.engine.version, "payload"))
|
|
238
|
+
refuse("invalid_installation_selection");
|
|
239
|
+
const bytes = Buffer.from(installation.manifest_bytes_base64, "base64");
|
|
240
|
+
if (bytes.toString("base64") !== installation.manifest_bytes_base64 || bytesDigest(bytes) !== installation.manifest_sha256 ||
|
|
241
|
+
hybridDigest(manifestFromBytes(bytes)) !== hybridDigest(installation.manifest))
|
|
242
|
+
refuse("invalid_installation_selection");
|
|
243
|
+
}
|
|
244
|
+
let readiness = installation ? "installed" : "not_installed";
|
|
245
|
+
if (installation) {
|
|
246
|
+
try {
|
|
247
|
+
verifyPublishedReleaseCache(path.dirname(installation.canon_root), installation.manifest.canon.archive);
|
|
248
|
+
verifyPublishedReleaseCache(path.dirname(installation.engine_root), installation.manifest.engine.archive);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
readiness = "degraded";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const existingStorage = fs.existsSync(path.join(root, "db.sqlite"));
|
|
255
|
+
return { installation, installation_readiness: readiness,
|
|
256
|
+
storage_readiness: existingStorage ? "unknown_requires_diagnostic" : "not_initialized",
|
|
257
|
+
runtime_readiness: "unknown_requires_qualification",
|
|
258
|
+
next_action: !installation ? "dd-flow setup --help" : existingStorage ? "dd-flow migration plan --help" : "dd-flow engine doctor --json",
|
|
259
|
+
...(existingStorage ? { storage_continuation: { kind: "storage_admission",
|
|
260
|
+
status: "requires_diagnostic", reason: "existing_storage_not_inspected",
|
|
261
|
+
help_command: "dd-flow migration plan --help", automatic_migration: false } } : {}) };
|
|
262
|
+
}
|
|
263
|
+
/** Exact-request installation transaction. Selected immutable caches do not affect existing RUNs. */
|
|
264
|
+
export function installRelease(home, input) {
|
|
265
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(input.requestId) || !["install", "update"].includes(input.action))
|
|
266
|
+
refuse("invalid_request");
|
|
267
|
+
const executor = input.executor === undefined ? undefined : input.executor === null ? null : prepareOperatorProfile(input.executor);
|
|
268
|
+
const executorBinding = executor === undefined ? {} : { executor: structuredClone(executor) };
|
|
269
|
+
if (input.deferredOnboarding !== undefined && (input.deferredOnboarding !== true || executor !== undefined || input.profileCreation !== undefined))
|
|
270
|
+
refuse("invalid_deferred_onboarding");
|
|
271
|
+
const onboardingBinding = input.deferredOnboarding ? { deferredOnboarding: true } : {};
|
|
272
|
+
if (input.profileCreation !== undefined) {
|
|
273
|
+
validateProfileCreationIntent(input.profileCreation);
|
|
274
|
+
if (!executor || hybridDigest(executor) !== hybridDigest(input.profileCreation.profile))
|
|
275
|
+
refuse("profile_executor_mismatch");
|
|
276
|
+
}
|
|
277
|
+
const profileBinding = input.profileCreation === undefined ? {} : { profileCreation: structuredClone(input.profileCreation) };
|
|
278
|
+
const manifest = admitReleaseManifest(input.manifest, input.authority);
|
|
279
|
+
if (!Buffer.isBuffer(input.manifestBytes) || hybridDigest(manifestFromBytes(input.manifestBytes)) !== hybridDigest(manifest))
|
|
280
|
+
refuse("manifest_bytes_mismatch");
|
|
281
|
+
// Both archives must be valid before even creating the home or publishing the first cache.
|
|
282
|
+
validateReleasePayloadIdentity(manifest, input.canonArchive, input.engineArchive);
|
|
283
|
+
const root = path.resolve(home), requests = path.join(root, "setup-requests"), lock = path.join(root, "setup.lock");
|
|
284
|
+
const installation = { manifest, manifest_sha256: bytesDigest(input.manifestBytes), manifest_bytes_base64: input.manifestBytes.toString("base64"),
|
|
285
|
+
canon_root: path.join(root, "release-cache", "canon", manifest.canon.version, "payload"),
|
|
286
|
+
engine_root: path.join(root, "release-cache", "engine", manifest.engine.version, "payload") };
|
|
287
|
+
const identity = hybridDigest({ action: input.action, installation, ...executorBinding, ...profileBinding, ...onboardingBinding });
|
|
288
|
+
readSetupStatus(root);
|
|
289
|
+
for (const directory of [root, requests]) {
|
|
290
|
+
if (fs.existsSync(directory) && !fs.lstatSync(directory).isDirectory())
|
|
291
|
+
refuse("unsafe_setup_directory");
|
|
292
|
+
}
|
|
293
|
+
if (input.profileCreation) {
|
|
294
|
+
const existingFile = path.join(requests, `${input.requestId}.json`);
|
|
295
|
+
const retained = fs.existsSync(existingFile) ? readSetupRequest(existingFile) : null;
|
|
296
|
+
if (retained?.phase !== "committed") {
|
|
297
|
+
const revision = readOperatorProfiles(root).revision;
|
|
298
|
+
if (revision !== input.profileCreation.before.revision && revision !== input.profileCreation.after.revision)
|
|
299
|
+
refuse("profile_selection_changed");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
fs.mkdirSync(requests, { recursive: true, mode: 0o700 });
|
|
303
|
+
const lease = acquireInstallationLock(lock, hybridDigest({ request_id: input.requestId, identity }));
|
|
304
|
+
let failure;
|
|
305
|
+
const perform = () => {
|
|
306
|
+
const file = path.join(requests, `${input.requestId}.json`);
|
|
307
|
+
const current = readSetupStatus(root).installation;
|
|
308
|
+
let request = { schema_id: "dd-flow/setup-request@1", request_id: input.requestId, identity,
|
|
309
|
+
previous_selection: current ? hybridDigest(current) : null, phase: "prepared", installation, ...executorBinding, ...profileBinding, ...onboardingBinding };
|
|
310
|
+
if (fs.existsSync(file)) {
|
|
311
|
+
request = readSetupRequest(file);
|
|
312
|
+
if (request.schema_id !== "dd-flow/setup-request@1" || request.request_id !== input.requestId || request.identity !== identity || hybridDigest(request.installation) !== hybridDigest(installation))
|
|
313
|
+
refuse("request_identity_conflict");
|
|
314
|
+
if (request.deferredOnboarding !== input.deferredOnboarding)
|
|
315
|
+
refuse("request_onboarding_conflict");
|
|
316
|
+
if (hybridDigest({ executor: request.executor ?? null, present: Object.hasOwn(request, "executor") }) !==
|
|
317
|
+
hybridDigest({ executor: executor ?? null, present: executor !== undefined }))
|
|
318
|
+
refuse("request_executor_conflict");
|
|
319
|
+
if (hybridDigest({ profileCreation: request.profileCreation ?? null }) !== hybridDigest({ profileCreation: input.profileCreation ?? null }))
|
|
320
|
+
refuse("request_profile_creation_conflict");
|
|
321
|
+
if (!["prepared", "canon_cached", "engine_cached", "profiles_saved", "committed"].includes(request.phase))
|
|
322
|
+
refuse("invalid_request_phase");
|
|
323
|
+
}
|
|
324
|
+
// Replay still revalidates retained payload bytes; it never reactivates an older selection.
|
|
325
|
+
if (request.phase === "committed") {
|
|
326
|
+
publishReleaseCache(root, "canon", manifest.canon.version, manifest.canon.archive, input.canonArchive);
|
|
327
|
+
publishReleaseCache(root, "engine", manifest.engine.version, manifest.engine.archive, input.engineArchive);
|
|
328
|
+
return { ok: true, replayed: true, installation: request.installation, ...executorBinding };
|
|
329
|
+
}
|
|
330
|
+
const selectedDigest = current ? hybridDigest(current) : null;
|
|
331
|
+
if (selectedDigest !== request.previous_selection && selectedDigest !== hybridDigest(installation))
|
|
332
|
+
refuse("selection_changed_since_request");
|
|
333
|
+
if (input.action === "install" && current && selectedDigest !== hybridDigest(installation))
|
|
334
|
+
refuse("existing_installation_requires_update");
|
|
335
|
+
const checkpoint = (phase) => {
|
|
336
|
+
request.phase = phase;
|
|
337
|
+
atomicJson(file, request);
|
|
338
|
+
input.onCheckpoint?.(phase);
|
|
339
|
+
};
|
|
340
|
+
checkpoint("prepared");
|
|
341
|
+
publishReleaseCache(root, "canon", manifest.canon.version, manifest.canon.archive, input.canonArchive);
|
|
342
|
+
checkpoint("canon_cached");
|
|
343
|
+
publishReleaseCache(root, "engine", manifest.engine.version, manifest.engine.archive, input.engineArchive);
|
|
344
|
+
checkpoint("engine_cached");
|
|
345
|
+
if (request.profileCreation) {
|
|
346
|
+
commitProfileCreation(root, request.profileCreation);
|
|
347
|
+
checkpoint("profiles_saved");
|
|
348
|
+
}
|
|
349
|
+
atomicJson(path.join(root, "installation.json"), installation);
|
|
350
|
+
checkpoint("committed");
|
|
351
|
+
return { ok: true, replayed: false, installation, ...executorBinding };
|
|
352
|
+
};
|
|
353
|
+
let result;
|
|
354
|
+
try {
|
|
355
|
+
result = perform();
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
failure = error instanceof AppError ? error : new AppError("setup_interrupted", "Setup request requires reconciliation", 1, { phase: "setup", effect: "unknown", request_id: input.requestId, next_action: "Replay the exact setup request", help: "setup" });
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
try {
|
|
362
|
+
lease.release();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Cleanup is secondary to the retained operation result. Never leak a
|
|
366
|
+
// raw filesystem cause or erase the original failure with an unlink error.
|
|
367
|
+
if (failure)
|
|
368
|
+
failure.details.cleanup = "not_confirmed";
|
|
369
|
+
else
|
|
370
|
+
failure = new AppError("setup_cleanup_failed", "Setup owner cleanup requires reconciliation", 1, { phase: "setup", effect: "unknown", cleanup: "not_confirmed", request_id: input.requestId,
|
|
371
|
+
next_action: "Inspect dd-flow setup status --json; replay the exact request after its owner exits", help: "setup" });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (failure)
|
|
375
|
+
throw failure;
|
|
376
|
+
return result;
|
|
377
|
+
}
|
|
@@ -136,7 +136,15 @@ export function loadExternalStageContext(input) {
|
|
|
136
136
|
const materializedSha256 = sha256(bytes);
|
|
137
137
|
if (materializedSha256 !== input.expectedSha256)
|
|
138
138
|
throw new AppError("context_checksum_mismatch", "Stage context checksum does not match --context-sha256", 2, { context_file: sourcePath, expected_sha256: input.expectedSha256, actual_sha256: materializedSha256 });
|
|
139
|
-
|
|
139
|
+
return loadExternalStageContextBytes({ bytes, sourcePath, stage: input.stage });
|
|
140
|
+
}
|
|
141
|
+
/** The same context contract for independently retained Stage input bytes;
|
|
142
|
+
* historical readers must not reopen the original mutable source file. */
|
|
143
|
+
export function loadExternalStageContextBytes(input) {
|
|
144
|
+
if (!(input.bytes instanceof Uint8Array) || !input.bytes.length || input.bytes.length > 1024 * 1024)
|
|
145
|
+
throw new AppError("validation", "Stage context exceeds its byte bound", 2);
|
|
146
|
+
const bytes = Buffer.from(input.bytes), sourcePath = input.sourcePath, materializedSha256 = sha256(bytes);
|
|
147
|
+
const value = prepareJsonText(new TextDecoder("utf-8", { fatal: true }).decode(bytes), { file: sourcePath, label: "Stage context" }).value;
|
|
140
148
|
validateContext(value, input.stage);
|
|
141
149
|
return { value, sourcePath, materializedSha256, bytes };
|
|
142
150
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { finishVnextSpecifyStage, launchVnextSpecify } from "./vnext-specify.js";
|
|
3
|
+
import { finishVnextProtocolize, startVnextProtocolize } from "./vnext-protocolize.js";
|
|
4
|
+
import { finishVnextPlan, startVnextPlan } from "./vnext-plan.js";
|
|
5
|
+
import { finishVnextPlanReview, startVnextPlanReview } from "./vnext-plan-review.js";
|
|
6
|
+
import { finishVnextCode, startVnextCode } from "./vnext-code.js";
|
|
7
|
+
import { finishVnextCodeReview, startVnextCodeReview } from "./vnext-code-review.js";
|
|
8
|
+
import { finishVnextMerge, startVnextMerge } from "./vnext-merge.js";
|
|
9
|
+
import { ResolvedFlowDescriptor } from "./flow-descriptors.js";
|
|
10
|
+
import { vnextStages } from "../domain/stage-catalog.js";
|
|
11
|
+
import { sdlcHandlerIdentities as ids } from "../domain/stage-handler-identities.js";
|
|
12
|
+
import { memoryInitStageHandlers } from "./memory-stage-dispatch.js";
|
|
13
|
+
/** Engine-owned code, not a plugin registration surface or executable descriptor expression. */
|
|
14
|
+
export const sdlcStageHandlers = Object.freeze({
|
|
15
|
+
specify: Object.freeze({ ...ids.specify, entry: launchVnextSpecify, finish: finishVnextSpecifyStage }),
|
|
16
|
+
protocolize: Object.freeze({ ...ids.protocolize, entry: startVnextProtocolize, finish: finishVnextProtocolize }),
|
|
17
|
+
plan: Object.freeze({ ...ids.plan, entry: startVnextPlan, finish: finishVnextPlan }),
|
|
18
|
+
"plan-review": Object.freeze({ ...ids["plan-review"], entry: startVnextPlanReview, finish: finishVnextPlanReview }),
|
|
19
|
+
code: Object.freeze({ ...ids.code, entry: startVnextCode, finish: finishVnextCode }),
|
|
20
|
+
"code-review": Object.freeze({ ...ids["code-review"], entry: startVnextCodeReview, finish: finishVnextCodeReview }),
|
|
21
|
+
merge: Object.freeze({ ...ids.merge, entry: startVnextMerge, finish: finishVnextMerge })
|
|
22
|
+
});
|
|
23
|
+
/** A fresh inventory cannot mutate the registered handler functions. */
|
|
24
|
+
export function bundledHandlerContext(blobs) {
|
|
25
|
+
return {
|
|
26
|
+
entryHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers)].map(handler => handler.entryId)),
|
|
27
|
+
finishHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers)].map(handler => handler.finishId)), blobs
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Handler identity is also stage/flow scoped: a known CODE handler is not a memory handler. */
|
|
31
|
+
export function admitBundledFlow(input, sha256, blobs) {
|
|
32
|
+
const flow = ResolvedFlowDescriptor.admit(input, sha256, bundledHandlerContext(blobs));
|
|
33
|
+
if (flow.descriptor.id === "sdlc") {
|
|
34
|
+
const edges = vnextStages.flatMap(stage => stage.next.map((to, index) => ({ from: stage.id, to, on: index ? "not-applicable" : "accepted" })));
|
|
35
|
+
if (flow.descriptor.entry_stage !== "specify" || flow.descriptor.terminal_stages.length !== 1 || flow.descriptor.terminal_stages[0] !== "merge"
|
|
36
|
+
|| flow.descriptor.stages.length !== vnextStages.length
|
|
37
|
+
|| flow.descriptor.stages.some((stage, index) => stage.id !== vnextStages[index].id || stage.directory !== vnextStages[index].directory)
|
|
38
|
+
|| flow.descriptor.edges.length !== edges.length
|
|
39
|
+
|| edges.some(expected => !flow.descriptor.edges.some(edge => edge.from === expected.from && edge.to === expected.to && edge.on === expected.on))) {
|
|
40
|
+
throw new AppError("flow_handler_scope_invalid", "SDLC handlers require the registered seven-stage layout and transitions", 2, { phase: "prepare", effect: "no_effect" });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (flow.descriptor.id === "mb-init") {
|
|
44
|
+
const stages = Object.keys(memoryInitStageHandlers);
|
|
45
|
+
const edges = stages.slice(0, -1).map((from, index) => ({ from, to: stages[index + 1], on: "accepted" }));
|
|
46
|
+
// Historical packs have only accepted transitions; diagnostic VERIFY uses
|
|
47
|
+
// the same REVIEW handler without granting another route or stage.
|
|
48
|
+
const requiredEdges = flow.descriptor.edges.filter(edge => !(edge.from === "verify" && edge.to === "review" && edge.on === "needs_repair"));
|
|
49
|
+
if (flow.descriptor.entry_stage !== "preflight" || flow.descriptor.terminal_stages.length !== 1 || flow.descriptor.terminal_stages[0] !== "finish"
|
|
50
|
+
|| flow.descriptor.stages.length !== stages.length
|
|
51
|
+
|| flow.descriptor.stages.some((stage, index) => stage.id !== stages[index] || stage.directory !== `${String(index + 1).padStart(2, "0")}-${stage.id}`)
|
|
52
|
+
|| requiredEdges.length !== edges.length
|
|
53
|
+
|| edges.some(expected => !flow.descriptor.edges.some(edge => edge.from === expected.from && edge.to === expected.to && edge.on === expected.on))) {
|
|
54
|
+
throw new AppError("flow_handler_scope_invalid", "Memory init handlers require the registered six-stage layout and transitions", 2, { phase: "prepare", effect: "no_effect" });
|
|
55
|
+
}
|
|
56
|
+
for (const stage of flow.descriptor.stages) {
|
|
57
|
+
let schemaId;
|
|
58
|
+
try {
|
|
59
|
+
schemaId = JSON.parse(flow.stageBlob(stage.id, "result_schema").toString("utf8")).$id;
|
|
60
|
+
}
|
|
61
|
+
catch { /* Invalid schema cannot authorize this stage handler. */ }
|
|
62
|
+
if (schemaId !== `dd-flow/mb-init-${stage.id}-result@1`)
|
|
63
|
+
throw new AppError("flow_handler_scope_invalid", "Memory init stage requires its own registered result schema", 2, { phase: "prepare", effect: "no_effect", stage: stage.id });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const stage of flow.descriptor.stages) {
|
|
67
|
+
const handlers = flow.descriptor.id === "sdlc" ? Object.values(sdlcStageHandlers) : flow.descriptor.id === "mb-init" ? Object.values(memoryInitStageHandlers) : [];
|
|
68
|
+
const handler = handlers.find(candidate => candidate.stage === stage.id);
|
|
69
|
+
if (!handler || stage.entry_handler !== handler.entryId || stage.finish_handler !== handler.finishId) {
|
|
70
|
+
throw new AppError("flow_handler_scope_invalid", "Registered handlers do not belong to this flow stage", 2, { phase: "prepare", effect: "no_effect", flow_id: flow.descriptor.id, stage: stage.id });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return flow;
|
|
74
|
+
}
|
|
@@ -4,6 +4,69 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { withProjectionLock } from "../storage/projection-lock.js";
|
|
6
6
|
import { writeJsonAtomic } from "../storage/paths.js";
|
|
7
|
+
import { assertRunBundleSqlAuthority } from "./runs.js";
|
|
8
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
9
|
+
/** Accepted bytes come from SQL receipts, never from the mutable publication.
|
|
10
|
+
* Stage completion and its independent committed checksum must agree. This is
|
|
11
|
+
* byte custody, not a substitute for the registered handler's semantic gates.
|
|
12
|
+
*/
|
|
13
|
+
export function readAcceptedStageData(context, scope) {
|
|
14
|
+
function refuse() { throw new AppError("stage_data_custody_invalid", "Accepted stage data lacks matching committed SQL byte custody", 1, { run_id: scope.runId, stage: scope.stage }); }
|
|
15
|
+
const run = context.db.get("SELECT index_json, run_root, run_dir FROM runs WHERE project_id = ? AND id = ?", [scope.projectId, scope.runId]);
|
|
16
|
+
if (!run)
|
|
17
|
+
refuse();
|
|
18
|
+
const index = JSON.parse(run.index_json);
|
|
19
|
+
assertRunBundleSqlAuthority(context, scope, index.execution_bundle);
|
|
20
|
+
const stage = index.stage_runs?.find((item) => item.stage === scope.stage);
|
|
21
|
+
const portable = (value) => typeof value === "string" && Boolean(value) && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value) && !value.includes("\\") && !value.includes("\0") && value.split("/").every(part => Boolean(part) && part !== "." && part !== "..");
|
|
22
|
+
if (!stage || stage.status !== "done" || !portable(stage.dir) || !portable(stage.data)
|
|
23
|
+
|| typeof stage.attempt !== "string" || !/^try-[0-9]{3,}$/.test(stage.attempt)
|
|
24
|
+
|| typeof stage.data_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(stage.data_sha256))
|
|
25
|
+
refuse();
|
|
26
|
+
const root = run.run_root ?? run.run_dir;
|
|
27
|
+
if (!path.isAbsolute(root))
|
|
28
|
+
refuse();
|
|
29
|
+
const relative = `${stage.dir}/${stage.data}`, destination = path.resolve(root, relative);
|
|
30
|
+
const rows = context.db.all("SELECT event_type, payload_json FROM audit_events WHERE project_id = ? AND event_type IN ('flow_run.stage_packet', 'flow_run.stage_report') AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.stage') = ? AND json_extract(payload_json, '$.attempt') = ? LIMIT 33", [scope.projectId, scope.runId, scope.stage, stage.attempt]);
|
|
31
|
+
if (rows.length > 32)
|
|
32
|
+
refuse();
|
|
33
|
+
let accepted;
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
if (Buffer.byteLength(row.payload_json) > 8 * 1024 * 1024)
|
|
36
|
+
refuse();
|
|
37
|
+
const receipt = JSON.parse(row.payload_json);
|
|
38
|
+
if (receipt.sha256 !== stage.data_sha256)
|
|
39
|
+
continue;
|
|
40
|
+
const locator = row.event_type === "flow_run.stage_packet" ? receipt.file : typeof receipt.root === "string" ? path.join(receipt.root, "stage-report.json") : null;
|
|
41
|
+
if (typeof locator !== "string" || resolveStagePublicationPath(context, scope.projectId, scope.runId, locator) !== destination)
|
|
42
|
+
continue;
|
|
43
|
+
let bytes;
|
|
44
|
+
if (row.event_type === "flow_run.stage_packet") {
|
|
45
|
+
if (typeof receipt.base64 !== "string")
|
|
46
|
+
refuse();
|
|
47
|
+
bytes = Buffer.from(receipt.base64, "base64");
|
|
48
|
+
if (bytes.toString("base64") !== receipt.base64)
|
|
49
|
+
refuse();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
if (typeof receipt.bytes?.json !== "string")
|
|
53
|
+
refuse();
|
|
54
|
+
bytes = Buffer.from(receipt.bytes.json);
|
|
55
|
+
}
|
|
56
|
+
if (!bytes.length || bytes.length > 1024 * 1024 || crypto.createHash("sha256").update(bytes).digest("hex") !== stage.data_sha256)
|
|
57
|
+
refuse();
|
|
58
|
+
if (accepted && !accepted.equals(bytes))
|
|
59
|
+
refuse();
|
|
60
|
+
accepted = bytes;
|
|
61
|
+
}
|
|
62
|
+
if (!accepted)
|
|
63
|
+
refuse();
|
|
64
|
+
return { bytes: Buffer.from(accepted), result: { path: relative, sha256: stage.data_sha256 },
|
|
65
|
+
input_sha256: stage.input_sha256, data_schema_id: stage.data_schema_id,
|
|
66
|
+
authority_sha256: hybridDigest({ scope, attempt: stage.attempt, directory: stage.dir, data: stage.data,
|
|
67
|
+
input_sha256: stage.input_sha256 ?? null, data_schema_id: stage.data_schema_id ?? null,
|
|
68
|
+
anchor: index.execution_bundle ?? null, data_sha256: stage.data_sha256 }) };
|
|
69
|
+
}
|
|
7
70
|
/** Deterministic JSON -> Markdown/HTML projection shared by every stage. */
|
|
8
71
|
export function writeStageReport(root, report) {
|
|
9
72
|
const json = path.join(root, "stage-report.json");
|
|
@@ -7,6 +7,23 @@ export function stageAttempt(context, projectId, runId, stage) {
|
|
|
7
7
|
return [...stages].reverse().find(item => item.stage === stage)?.attempt ?? "try-001";
|
|
8
8
|
}
|
|
9
9
|
export function bindStageWork(context, input) {
|
|
10
|
+
context.db.writeTransaction(() => bindStageWorkOwned(context, input), "stage-work.bind");
|
|
11
|
+
}
|
|
12
|
+
function bindStageWorkOwned(context, input) {
|
|
13
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
|
|
14
|
+
const index = run ? JSON.parse(run.index_json) : null;
|
|
15
|
+
if (index?.execution_bundle) {
|
|
16
|
+
// Stage attachment already admitted the frozen descriptor outside the writer.
|
|
17
|
+
// Membership checks here consume only authoritative SQLite facts.
|
|
18
|
+
const stage = index.stage_runs.find(item => item.stage === input.stage && item.status === "running");
|
|
19
|
+
const work = context.db.get("SELECT parent_work_id FROM works WHERE project_id = ? AND run_id = ? AND work_id = ?", [input.projectId, input.runId, input.workId]);
|
|
20
|
+
const parentOwned = !work?.parent_work_id || context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND work_id = ?", [input.projectId, input.runId, work.parent_work_id]);
|
|
21
|
+
const currentStage = index.current_stage ?? (index.execution_bundle.flow_id === "sdlc" ? index.stage_runs.at(-1)?.stage : undefined);
|
|
22
|
+
if (!stage || currentStage !== input.stage || !work || !parentOwned || input.attempt !== undefined && input.attempt !== (stage.attempt ?? "try-001")
|
|
23
|
+
|| input.cycle !== undefined && input.cycle !== stageCycle(context, input.projectId, input.runId, input.stage)) {
|
|
24
|
+
throw new AppError("stage_work_scope_invalid", "Work membership does not belong to the active frozen stage scope", 1, { run_id: input.runId, stage: input.stage, work_id: input.workId });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
10
27
|
context.db.run("INSERT OR IGNORE INTO stage_work_memberships (project_id, run_id, work_id, stage, attempt, cycle) VALUES (?, ?, ?, ?, ?, ?)", [input.projectId, input.runId, input.workId, input.stage, input.attempt ?? stageAttempt(context, input.projectId, input.runId, input.stage), input.cycle ?? stageCycle(context, input.projectId, input.runId, input.stage)]);
|
|
11
28
|
}
|
|
12
29
|
export function stageCycle(context, projectId, runId, stage) {
|
|
@@ -42,7 +42,7 @@ export function prepareVnextCodeReviewStart(context, input) {
|
|
|
42
42
|
const run = requireRun(context, project.id, input.runId);
|
|
43
43
|
const home = requireHome(run);
|
|
44
44
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
45
|
-
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
45
|
+
requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
46
46
|
if (!fs.existsSync(path.join(home, "05-code", "stage-report.json")))
|
|
47
47
|
throw new AppError("runtime_artifact_missing", "CODE-REVIEW requires an accepted CODE report", 1);
|
|
48
48
|
const root = path.join(home, stageDir);
|
|
@@ -42,7 +42,7 @@ export async function prepareVnextCodeWorkspace(context, input) {
|
|
|
42
42
|
const project = requireProjectByRoot(context, projectRoot);
|
|
43
43
|
const run = requireRun(context, project.id, input.runId);
|
|
44
44
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
45
|
-
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, stage: input.stage ?? stage });
|
|
45
|
+
requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: requireHome(run), workspaceRoot: run.workspace_root, stage: input.stage ?? stage });
|
|
46
46
|
const root = path.join(requireHome(run), stageDir);
|
|
47
47
|
fs.mkdirSync(root, { recursive: true });
|
|
48
48
|
return ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress, { force: input.stage !== "code-review" });
|
|
@@ -53,7 +53,7 @@ export function prepareVnextCodeStart(context, input) {
|
|
|
53
53
|
const run = requireRun(context, project.id, input.runId);
|
|
54
54
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
55
55
|
const home = requireHome(run);
|
|
56
|
-
requireVnextWorkspaceRoute({ projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
56
|
+
requireVnextWorkspaceRoute(context, { projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
57
57
|
const existingRoot = path.join(home, stageDir);
|
|
58
58
|
const existingPrompt = path.join(existingRoot, "stage-prompt.md");
|
|
59
59
|
const existingStage = stageStatus(run, stage);
|
|
@@ -158,7 +158,7 @@ export async function finishVnextCode(context, input) {
|
|
|
158
158
|
const home = requireHome(run);
|
|
159
159
|
const root = path.join(home, stageDir);
|
|
160
160
|
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
161
|
-
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
161
|
+
requireVnextWorkspaceRoute(context, { projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
162
162
|
if (stageStatus(run, stage) === "done" && fs.existsSync(path.join(root, "stage-report.json"))) {
|
|
163
163
|
resumeAcceptedCodeStage(context, run, projectRoot, stage, JSON.parse(fs.readFileSync(path.join(root, "stage-report.json"), "utf8")), nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))));
|
|
164
164
|
return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: path.join(root, "stage-report.json"), next_action: nextAction(run, changedPathsFromReport(path.join(root, "stage-report.json"))) };
|
|
@@ -498,7 +498,7 @@ export function prepareVnextCodeRepair(context, input) {
|
|
|
498
498
|
export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRepair(context, input)) {
|
|
499
499
|
const { project, run } = prepared;
|
|
500
500
|
if (prepared.kind === "replay")
|
|
501
|
-
return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous), reused: true };
|
|
501
|
+
return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous, { preview: true }), reused: true };
|
|
502
502
|
const { parentWorkId, items, key, receipt } = prepared;
|
|
503
503
|
const create = () => String(addWorkBatch(context, { parentWorkId, prepared: items, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"), ...(input.reviewCycle ? { cycle: input.reviewCycle } : {}) }).work_ids[key]);
|
|
504
504
|
const id = input.registrationOwned ? create() : ensureRepairIntent(context, {
|
|
@@ -513,7 +513,7 @@ export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRe
|
|
|
513
513
|
...(receipt ? { check_receipt_id: receipt.id } : prepared.input.reviewFindings?.length ? { review_finding_ids: prepared.input.reviewFindings.map((finding) => finding.finding_ref) } : { semantic_unresolved: prepared.input.semanticUnresolved })
|
|
514
514
|
});
|
|
515
515
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
516
|
-
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
516
|
+
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work, { preview: true }) };
|
|
517
517
|
}
|
|
518
518
|
/** Retain the causal declaration for worker context; run_at still records its original gate. */
|
|
519
519
|
export function selectRepairChecks(input) {
|
|
@@ -730,7 +730,7 @@ async function ensureCodeWorkspaceReady(context, projectId, run, root, readiness
|
|
|
730
730
|
const profile = JSON.parse(run.index_json);
|
|
731
731
|
const bootstrap = profile.execution_profile?.settings?.code_bootstrap;
|
|
732
732
|
if (!bootstrap || typeof bootstrap.command !== "string" || typeof bootstrap.policy_ref !== "string") {
|
|
733
|
-
throw new AppError("execution_profile_invalid", "CODE requires
|
|
733
|
+
throw new AppError("execution_profile_invalid", "CODE requires an evidenced bootstrap command and policy in the frozen RUN profile; configure code_bootstrap before starting a code-ready RUN", 1, { run_id: run.id });
|
|
734
734
|
}
|
|
735
735
|
const receiptPath = path.join(root, "workspace-readiness.json");
|
|
736
736
|
const ownerId = context.env.DD_FLOW_CODE_READINESS_OWNER;
|