@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +197 -0
  7. package/dist/cli/command-option-descriptions.js +191 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/error-output.js +52 -0
  11. package/dist/cli/executor-onboarding.js +76 -0
  12. package/dist/cli/flows.js +29 -0
  13. package/dist/cli/harnesses.js +84 -0
  14. package/dist/cli/help.js +240 -6
  15. package/dist/cli/input-preparation.js +54 -5
  16. package/dist/cli/memory-flow-admission.js +44 -0
  17. package/dist/cli/memory-init-invocation.js +78 -0
  18. package/dist/cli/memory-init.js +240 -0
  19. package/dist/cli/memory-storage-admission.js +53 -0
  20. package/dist/cli/models.js +39 -0
  21. package/dist/cli/native-hook-ingress.js +1 -1
  22. package/dist/cli/operator-profiles.js +61 -0
  23. package/dist/cli/output-options.js +35 -0
  24. package/dist/cli/response-file.js +71 -0
  25. package/dist/cli/run-cli.js +283 -160
  26. package/dist/cli/setup.js +149 -0
  27. package/dist/cli.js +54 -2
  28. package/dist/domain/stage-handler-identities.js +20 -0
  29. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  30. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  31. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  33. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  34. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  35. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  36. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  37. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
  38. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +9 -9
  39. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  40. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  41. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  42. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  43. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  44. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  45. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  46. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  47. package/dist/harness-runtime/lib/opencode-plugin.mjs +2 -0
  48. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  49. package/dist/runtime/context.js +1 -1
  50. package/dist/schemas/discovery-result.schema.json +184 -0
  51. package/dist/schemas/execution-bundle.schema.json +205 -0
  52. package/dist/schemas/execution-settings.schema.json +162 -0
  53. package/dist/schemas/flow-descriptor.schema.json +54 -0
  54. package/dist/schemas/flow-run.schema.json +22 -0
  55. package/dist/schemas/harness-catalog.schema.json +80 -0
  56. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  57. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  58. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  59. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  60. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  61. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  62. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  63. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  64. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  65. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  66. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  67. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  68. package/dist/schemas/model-catalog.schema.json +153 -0
  69. package/dist/schemas/profile-settings.schema.json +122 -0
  70. package/dist/schemas/release-manifest.schema.json +291 -0
  71. package/dist/schemas/run-engine-binding.schema.json +1 -1
  72. package/dist/services/check-runner-evidence.js +51 -4
  73. package/dist/services/cli-operation-classifier.js +8 -47
  74. package/dist/services/cli-upgrade-allowlist.js +27 -0
  75. package/dist/services/code-checks.js +387 -34
  76. package/dist/services/controller-execution.js +88 -0
  77. package/dist/services/controller-fanout.js +71 -12
  78. package/dist/services/dashboard.js +8 -2
  79. package/dist/services/descriptor-coordinator-session.js +35 -0
  80. package/dist/services/descriptor-root-work.js +53 -0
  81. package/dist/services/descriptor-run-settlement.js +105 -0
  82. package/dist/services/descriptor-run-start.js +449 -0
  83. package/dist/services/descriptor-stage-coordinator.js +197 -0
  84. package/dist/services/descriptor-stage-settlement.js +222 -0
  85. package/dist/services/descriptor-stage-start.js +139 -0
  86. package/dist/services/discovery/agy-discovery.js +2 -0
  87. package/dist/services/discovery/codex-discovery.js +2 -0
  88. package/dist/services/discovery/droid-discovery.js +2 -0
  89. package/dist/services/discovery/grok-discovery.js +2 -0
  90. package/dist/services/discovery/opencode-discovery.js +2 -0
  91. package/dist/services/discovery/static.js +31 -0
  92. package/dist/services/discovery/zcode-discovery.js +2 -0
  93. package/dist/services/engines.js +39 -22
  94. package/dist/services/eval-snapshots.js +83 -25
  95. package/dist/services/execution-bundles.js +263 -0
  96. package/dist/services/execution-policy.js +20 -16
  97. package/dist/services/executor-onboarding.js +56 -0
  98. package/dist/services/external-work-launch.js +91 -11
  99. package/dist/services/flow-catalog.js +21 -0
  100. package/dist/services/flow-descriptors.js +82 -0
  101. package/dist/services/frozen-engine.js +242 -0
  102. package/dist/services/generic-execution.js +164 -0
  103. package/dist/services/harness-adapter.js +28 -2
  104. package/dist/services/harness-discovery.js +29 -0
  105. package/dist/services/harness-doctor.js +75 -0
  106. package/dist/services/harness-probe-effects.js +51 -0
  107. package/dist/services/harness-version-probe.js +100 -0
  108. package/dist/services/hooks.js +24 -6
  109. package/dist/services/hybrid-contracts.js +483 -0
  110. package/dist/services/ids.js +29 -1
  111. package/dist/services/installation-lock.js +95 -0
  112. package/dist/services/installed-flow-source.js +183 -0
  113. package/dist/services/lifecycle-invocations.js +257 -78
  114. package/dist/services/memory-accepted-composition.js +77 -0
  115. package/dist/services/memory-baseline-preservation.js +65 -0
  116. package/dist/services/memory-completed-correction-wave.js +320 -0
  117. package/dist/services/memory-compose-handler.js +258 -0
  118. package/dist/services/memory-composition-acceptance.js +199 -0
  119. package/dist/services/memory-composition-graph.js +134 -0
  120. package/dist/services/memory-composition-registration.js +146 -0
  121. package/dist/services/memory-composition-work-custody.js +151 -0
  122. package/dist/services/memory-composition-work-results.js +96 -0
  123. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  124. package/dist/services/memory-corrected-review-batch.js +30 -0
  125. package/dist/services/memory-corrected-review-findings.js +106 -0
  126. package/dist/services/memory-corrected-review-inputs.js +210 -0
  127. package/dist/services/memory-corrected-review-projection.js +78 -0
  128. package/dist/services/memory-corrected-review-receipt.js +170 -0
  129. package/dist/services/memory-corrected-review-registration.js +191 -0
  130. package/dist/services/memory-correction-check-receipt.js +219 -0
  131. package/dist/services/memory-correction-input-owner.js +6 -0
  132. package/dist/services/memory-discover-handler.js +104 -0
  133. package/dist/services/memory-finish-evidence.js +435 -0
  134. package/dist/services/memory-finish-handler.js +215 -0
  135. package/dist/services/memory-finish-integration.js +280 -0
  136. package/dist/services/memory-finish-readiness.js +122 -0
  137. package/dist/services/memory-git-integration.js +151 -0
  138. package/dist/services/memory-historical-corrected-review.js +206 -0
  139. package/dist/services/memory-historical-original-review.js +130 -0
  140. package/dist/services/memory-historical-repair-registration.js +99 -0
  141. package/dist/services/memory-historical-second-review.js +128 -0
  142. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  143. package/dist/services/memory-init-contexts.js +89 -0
  144. package/dist/services/memory-init-controller-start.js +84 -0
  145. package/dist/services/memory-init-engine-release.js +223 -0
  146. package/dist/services/memory-init-git-policy.js +136 -0
  147. package/dist/services/memory-init-lint-capability.js +185 -0
  148. package/dist/services/memory-init-native-capacity.js +241 -0
  149. package/dist/services/memory-init-preflight.js +312 -0
  150. package/dist/services/memory-init-project-bootstrap.js +599 -0
  151. package/dist/services/memory-init-request-candidate.js +157 -0
  152. package/dist/services/memory-init-request-input.js +66 -0
  153. package/dist/services/memory-init-resource-storage.js +125 -0
  154. package/dist/services/memory-init-run-allocation.js +81 -0
  155. package/dist/services/memory-init-run-bundle.js +98 -0
  156. package/dist/services/memory-init-start.js +175 -0
  157. package/dist/services/memory-init-startup-custody.js +80 -0
  158. package/dist/services/memory-init-startup-input.js +240 -0
  159. package/dist/services/memory-init-storage.js +81 -0
  160. package/dist/services/memory-init-stores.js +359 -0
  161. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  162. package/dist/services/memory-layer-publication.js +611 -0
  163. package/dist/services/memory-layer.js +156 -0
  164. package/dist/services/memory-lint-config.js +85 -0
  165. package/dist/services/memory-native-review-copy.js +165 -0
  166. package/dist/services/memory-normal-flow-entry.js +75 -0
  167. package/dist/services/memory-ordinary-defaults.js +112 -0
  168. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  169. package/dist/services/memory-preflight-handler.js +259 -0
  170. package/dist/services/memory-repair-current-view.js +299 -0
  171. package/dist/services/memory-repair-document-delta.js +451 -0
  172. package/dist/services/memory-repair-history.js +491 -0
  173. package/dist/services/memory-repair-physical-binding.js +312 -0
  174. package/dist/services/memory-repair-publication-files.js +152 -0
  175. package/dist/services/memory-repair-publication-intent.js +27 -0
  176. package/dist/services/memory-repair-publication-plan.js +160 -0
  177. package/dist/services/memory-repair-publication-source.js +225 -0
  178. package/dist/services/memory-repair-publication.js +794 -0
  179. package/dist/services/memory-repair-registration.js +239 -0
  180. package/dist/services/memory-repair-work-baseline.js +413 -0
  181. package/dist/services/memory-repair-work-completion.js +97 -0
  182. package/dist/services/memory-repair-work-custody.js +73 -0
  183. package/dist/services/memory-repair-work-results.js +89 -0
  184. package/dist/services/memory-repair-workspace.js +252 -0
  185. package/dist/services/memory-repair-writer-copy.js +735 -0
  186. package/dist/services/memory-result-binding.js +173 -0
  187. package/dist/services/memory-retained-review-authority.js +129 -0
  188. package/dist/services/memory-retained-review-results.js +70 -0
  189. package/dist/services/memory-retained-second-baseline.js +102 -0
  190. package/dist/services/memory-retained-second-repairs.js +156 -0
  191. package/dist/services/memory-review-acceptance.js +214 -0
  192. package/dist/services/memory-review-correction-plan.js +171 -0
  193. package/dist/services/memory-review-handler.js +585 -0
  194. package/dist/services/memory-review-inputs.js +200 -0
  195. package/dist/services/memory-review-receipt.js +180 -0
  196. package/dist/services/memory-review-registration.js +108 -0
  197. package/dist/services/memory-review-work-custody.js +124 -0
  198. package/dist/services/memory-review-work-input.js +72 -0
  199. package/dist/services/memory-review-work-results.js +73 -0
  200. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  201. package/dist/services/memory-second-correction-plan.js +76 -0
  202. package/dist/services/memory-second-repair-baseline.js +91 -0
  203. package/dist/services/memory-second-repair-launch.js +614 -0
  204. package/dist/services/memory-second-repair-registration.js +88 -0
  205. package/dist/services/memory-second-repair-source.js +187 -0
  206. package/dist/services/memory-second-review-acceptance.js +115 -0
  207. package/dist/services/memory-source-observations.js +117 -0
  208. package/dist/services/memory-stage-dispatch.js +28 -0
  209. package/dist/services/memory-stage-results.js +199 -0
  210. package/dist/services/memory-verification-acceptance.js +135 -0
  211. package/dist/services/memory-verification-checks.js +252 -0
  212. package/dist/services/memory-verification-receipt.js +159 -0
  213. package/dist/services/memory-verify-handler.js +135 -0
  214. package/dist/services/memory-work-result-preparation.js +63 -0
  215. package/dist/services/model-catalog.js +109 -0
  216. package/dist/services/model-discovery.js +46 -0
  217. package/dist/services/operator-profiles.js +236 -0
  218. package/dist/services/projects.js +17 -2
  219. package/dist/services/recovery-snapshot-database.js +416 -1
  220. package/dist/services/release-archive-producer.js +52 -0
  221. package/dist/services/release-archive.js +143 -0
  222. package/dist/services/release-cache.js +142 -0
  223. package/dist/services/release-compatibility.js +18 -0
  224. package/dist/services/release-download.js +67 -0
  225. package/dist/services/release-envelope.js +104 -0
  226. package/dist/services/release-manifest.js +45 -0
  227. package/dist/services/release-payload.js +114 -0
  228. package/dist/services/release-producer.js +63 -0
  229. package/dist/services/release-provenance.js +160 -0
  230. package/dist/services/release-registry.js +66 -0
  231. package/dist/services/release-source.js +194 -0
  232. package/dist/services/repair-intents.js +1 -1
  233. package/dist/services/review-copy.js +10 -2
  234. package/dist/services/run-control.js +80 -9
  235. package/dist/services/run-controller-capture.js +10 -3
  236. package/dist/services/run-controller-process.js +21 -0
  237. package/dist/services/run-controller-recovery.js +3 -1
  238. package/dist/services/run-controller-state.js +1 -1
  239. package/dist/services/run-controller.js +101 -40
  240. package/dist/services/run-recovery-guard.js +2 -0
  241. package/dist/services/run-recovery.js +145 -5
  242. package/dist/services/run-start-custody.js +177 -0
  243. package/dist/services/runs.js +319 -25
  244. package/dist/services/setup.js +377 -0
  245. package/dist/services/stage-context.js +9 -1
  246. package/dist/services/stage-handlers.js +74 -0
  247. package/dist/services/stage-report-renderer.js +63 -0
  248. package/dist/services/stage-work-graph.js +17 -0
  249. package/dist/services/vnext-code.js +3 -3
  250. package/dist/services/vnext-execution-profile.js +2 -2
  251. package/dist/services/vnext-fanout.js +70 -9
  252. package/dist/services/vnext-merge.js +8 -3
  253. package/dist/services/vnext-plan-review.js +6 -4
  254. package/dist/services/work-registry.js +616 -71
  255. package/dist/services/workspace-files.js +1 -1
  256. package/dist/storage/database.js +84 -6
  257. package/dist/storage/writer-contract.js +11 -7
  258. package/package.json +13 -3
  259. package/tools/create-canon-release.mjs +33 -0
  260. package/tools/produce-hybrid-release.mjs +41 -0
@@ -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
- const value = prepareJsonText(bytes.toString("utf8"), { file: sourcePath, label: "Stage context" }).value;
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) {
@@ -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 a frozen bootstrap command in the RUN execution profile", 1, { run_id: run.id });
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;
@@ -93,8 +93,8 @@ export function validateVnextExecutionProfile(value) {
93
93
  || profile.merge_delivery?.strategy !== "local"
94
94
  || !["retain", "delete_after_success"].includes(String(profile.merge_cleanup?.source))
95
95
  || !["code_completed", "code_review_completed", "merge_completed"].includes(String(profile.stop_target))
96
- || !profile.code_bootstrap || typeof profile.code_bootstrap.command !== "string" || !profile.code_bootstrap.command.trim()
97
- || typeof profile.code_bootstrap.policy_ref !== "string" || !profile.code_bootstrap.policy_ref.trim()
96
+ || (profile.code_bootstrap !== undefined && (!profile.code_bootstrap || typeof profile.code_bootstrap.command !== "string" || !profile.code_bootstrap.command.trim()
97
+ || typeof profile.code_bootstrap.policy_ref !== "string" || !profile.code_bootstrap.policy_ref.trim()))
98
98
  || (profile.execution === undefined ? profile.schema_id === "dd-flow/project-execution@3" : !isExecutionRouting(profile.execution))) {
99
99
  throw new AppError("execution_profile_invalid", "Project execution profile does not match its supported dd-flow schema", 1);
100
100
  }