@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.
Files changed (283) hide show
  1. package/CHANGELOG.md +19 -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/runtime-process.js +1 -1
  27. package/dist/cli/setup.js +149 -0
  28. package/dist/cli.js +56 -2
  29. package/dist/domain/stage-handler-identities.js +20 -0
  30. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  32. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +53 -22
  33. package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
  34. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +12 -6
  35. package/dist/harness-runtime/lib/dd-codex.mjs +24 -12
  36. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  37. package/dist/harness-runtime/lib/dd-droid.mjs +32 -9
  38. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +11 -3
  39. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +100 -15
  40. package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
  41. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +22 -10
  42. package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
  43. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  44. package/dist/harness-runtime/lib/delegation-instructions.mjs +16 -3
  45. package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
  46. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  47. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  48. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  49. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  50. package/dist/harness-runtime/lib/native-children.d.mts +2 -0
  51. package/dist/harness-runtime/lib/native-children.mjs +20 -1
  52. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  53. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  54. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  55. package/dist/harness-runtime/lib/opencode-plugin.mjs +32 -9
  56. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  57. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
  58. package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
  59. package/dist/runtime/context.js +1 -1
  60. package/dist/schemas/discovery-result.schema.json +184 -0
  61. package/dist/schemas/execution-bundle.schema.json +205 -0
  62. package/dist/schemas/execution-settings.schema.json +162 -0
  63. package/dist/schemas/flow-descriptor.schema.json +54 -0
  64. package/dist/schemas/flow-run.schema.json +26 -0
  65. package/dist/schemas/harness-catalog.schema.json +80 -0
  66. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  67. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  68. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  69. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  70. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  71. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  72. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  73. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  74. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  75. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  76. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  77. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  78. package/dist/schemas/model-catalog.schema.json +153 -0
  79. package/dist/schemas/profile-settings.schema.json +122 -0
  80. package/dist/schemas/release-manifest.schema.json +291 -0
  81. package/dist/schemas/run-engine-binding.schema.json +1 -1
  82. package/dist/services/check-runner-evidence.js +51 -4
  83. package/dist/services/cli-operation-classifier.js +8 -47
  84. package/dist/services/cli-upgrade-allowlist.js +27 -0
  85. package/dist/services/code-checks.js +387 -34
  86. package/dist/services/controller-child-capacity.js +13 -0
  87. package/dist/services/controller-child-observer.js +18 -2
  88. package/dist/services/controller-execution.js +88 -0
  89. package/dist/services/controller-fanout.js +71 -12
  90. package/dist/services/dashboard.js +8 -2
  91. package/dist/services/descriptor-coordinator-session.js +35 -0
  92. package/dist/services/descriptor-root-work.js +53 -0
  93. package/dist/services/descriptor-run-settlement.js +105 -0
  94. package/dist/services/descriptor-run-start.js +449 -0
  95. package/dist/services/descriptor-stage-coordinator.js +197 -0
  96. package/dist/services/descriptor-stage-settlement.js +222 -0
  97. package/dist/services/descriptor-stage-start.js +139 -0
  98. package/dist/services/discovery/agy-discovery.js +2 -0
  99. package/dist/services/discovery/codex-discovery.js +2 -0
  100. package/dist/services/discovery/droid-discovery.js +2 -0
  101. package/dist/services/discovery/grok-discovery.js +2 -0
  102. package/dist/services/discovery/opencode-discovery.js +2 -0
  103. package/dist/services/discovery/static.js +31 -0
  104. package/dist/services/discovery/zcode-discovery.js +2 -0
  105. package/dist/services/engines.js +39 -22
  106. package/dist/services/eval-snapshots.js +83 -25
  107. package/dist/services/execution-bundles.js +263 -0
  108. package/dist/services/execution-policy.js +20 -16
  109. package/dist/services/executor-onboarding.js +56 -0
  110. package/dist/services/external-work-launch.js +93 -15
  111. package/dist/services/flow-catalog.js +21 -0
  112. package/dist/services/flow-descriptors.js +82 -0
  113. package/dist/services/frozen-engine.js +250 -0
  114. package/dist/services/generic-execution.js +164 -0
  115. package/dist/services/harness-adapter.js +28 -2
  116. package/dist/services/harness-discovery.js +29 -0
  117. package/dist/services/harness-doctor.js +75 -0
  118. package/dist/services/harness-probe-effects.js +51 -0
  119. package/dist/services/harness-version-probe.js +100 -0
  120. package/dist/services/hooks.js +24 -6
  121. package/dist/services/hybrid-contracts.js +483 -0
  122. package/dist/services/ids.js +29 -1
  123. package/dist/services/installation-lock.js +95 -0
  124. package/dist/services/installed-flow-source.js +183 -0
  125. package/dist/services/lifecycle-invocations.js +257 -78
  126. package/dist/services/managed-processes.js +31 -10
  127. package/dist/services/memory-accepted-composition.js +77 -0
  128. package/dist/services/memory-baseline-preservation.js +65 -0
  129. package/dist/services/memory-completed-correction-wave.js +320 -0
  130. package/dist/services/memory-compose-handler.js +258 -0
  131. package/dist/services/memory-composition-acceptance.js +199 -0
  132. package/dist/services/memory-composition-graph.js +134 -0
  133. package/dist/services/memory-composition-registration.js +146 -0
  134. package/dist/services/memory-composition-work-custody.js +151 -0
  135. package/dist/services/memory-composition-work-results.js +96 -0
  136. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  137. package/dist/services/memory-corrected-review-batch.js +30 -0
  138. package/dist/services/memory-corrected-review-findings.js +106 -0
  139. package/dist/services/memory-corrected-review-inputs.js +210 -0
  140. package/dist/services/memory-corrected-review-projection.js +78 -0
  141. package/dist/services/memory-corrected-review-receipt.js +170 -0
  142. package/dist/services/memory-corrected-review-registration.js +191 -0
  143. package/dist/services/memory-correction-check-receipt.js +219 -0
  144. package/dist/services/memory-correction-input-owner.js +6 -0
  145. package/dist/services/memory-discover-handler.js +104 -0
  146. package/dist/services/memory-finish-evidence.js +435 -0
  147. package/dist/services/memory-finish-handler.js +215 -0
  148. package/dist/services/memory-finish-integration.js +280 -0
  149. package/dist/services/memory-finish-readiness.js +122 -0
  150. package/dist/services/memory-git-integration.js +151 -0
  151. package/dist/services/memory-historical-corrected-review.js +206 -0
  152. package/dist/services/memory-historical-original-review.js +130 -0
  153. package/dist/services/memory-historical-repair-registration.js +99 -0
  154. package/dist/services/memory-historical-second-review.js +128 -0
  155. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  156. package/dist/services/memory-init-contexts.js +89 -0
  157. package/dist/services/memory-init-controller-start.js +84 -0
  158. package/dist/services/memory-init-engine-release.js +223 -0
  159. package/dist/services/memory-init-git-policy.js +136 -0
  160. package/dist/services/memory-init-lint-capability.js +185 -0
  161. package/dist/services/memory-init-native-capacity.js +296 -0
  162. package/dist/services/memory-init-preflight.js +330 -0
  163. package/dist/services/memory-init-project-bootstrap.js +599 -0
  164. package/dist/services/memory-init-request-candidate.js +157 -0
  165. package/dist/services/memory-init-request-input.js +66 -0
  166. package/dist/services/memory-init-resource-storage.js +125 -0
  167. package/dist/services/memory-init-run-allocation.js +81 -0
  168. package/dist/services/memory-init-run-bundle.js +98 -0
  169. package/dist/services/memory-init-start.js +181 -0
  170. package/dist/services/memory-init-startup-custody.js +80 -0
  171. package/dist/services/memory-init-startup-input.js +240 -0
  172. package/dist/services/memory-init-storage.js +81 -0
  173. package/dist/services/memory-init-stores.js +359 -0
  174. package/dist/services/memory-init-worktrunk-capability.js +178 -0
  175. package/dist/services/memory-layer-publication.js +611 -0
  176. package/dist/services/memory-layer.js +156 -0
  177. package/dist/services/memory-lint-config.js +85 -0
  178. package/dist/services/memory-native-review-copy.js +165 -0
  179. package/dist/services/memory-normal-flow-entry.js +75 -0
  180. package/dist/services/memory-ordinary-defaults.js +112 -0
  181. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  182. package/dist/services/memory-preflight-handler.js +259 -0
  183. package/dist/services/memory-repair-current-view.js +299 -0
  184. package/dist/services/memory-repair-document-delta.js +451 -0
  185. package/dist/services/memory-repair-history.js +491 -0
  186. package/dist/services/memory-repair-physical-binding.js +312 -0
  187. package/dist/services/memory-repair-publication-files.js +152 -0
  188. package/dist/services/memory-repair-publication-intent.js +27 -0
  189. package/dist/services/memory-repair-publication-plan.js +160 -0
  190. package/dist/services/memory-repair-publication-source.js +225 -0
  191. package/dist/services/memory-repair-publication.js +794 -0
  192. package/dist/services/memory-repair-registration.js +239 -0
  193. package/dist/services/memory-repair-work-baseline.js +413 -0
  194. package/dist/services/memory-repair-work-completion.js +97 -0
  195. package/dist/services/memory-repair-work-custody.js +73 -0
  196. package/dist/services/memory-repair-work-results.js +89 -0
  197. package/dist/services/memory-repair-workspace.js +252 -0
  198. package/dist/services/memory-repair-writer-copy.js +735 -0
  199. package/dist/services/memory-result-binding.js +173 -0
  200. package/dist/services/memory-retained-review-authority.js +129 -0
  201. package/dist/services/memory-retained-review-results.js +70 -0
  202. package/dist/services/memory-retained-second-baseline.js +102 -0
  203. package/dist/services/memory-retained-second-repairs.js +156 -0
  204. package/dist/services/memory-review-acceptance.js +214 -0
  205. package/dist/services/memory-review-correction-plan.js +171 -0
  206. package/dist/services/memory-review-handler.js +585 -0
  207. package/dist/services/memory-review-inputs.js +200 -0
  208. package/dist/services/memory-review-receipt.js +180 -0
  209. package/dist/services/memory-review-registration.js +108 -0
  210. package/dist/services/memory-review-work-custody.js +124 -0
  211. package/dist/services/memory-review-work-input.js +72 -0
  212. package/dist/services/memory-review-work-results.js +73 -0
  213. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  214. package/dist/services/memory-second-correction-plan.js +76 -0
  215. package/dist/services/memory-second-repair-baseline.js +91 -0
  216. package/dist/services/memory-second-repair-launch.js +614 -0
  217. package/dist/services/memory-second-repair-registration.js +88 -0
  218. package/dist/services/memory-second-repair-source.js +187 -0
  219. package/dist/services/memory-second-review-acceptance.js +115 -0
  220. package/dist/services/memory-source-observations.js +117 -0
  221. package/dist/services/memory-stage-dispatch.js +28 -0
  222. package/dist/services/memory-stage-results.js +199 -0
  223. package/dist/services/memory-verification-acceptance.js +135 -0
  224. package/dist/services/memory-verification-checks.js +252 -0
  225. package/dist/services/memory-verification-receipt.js +159 -0
  226. package/dist/services/memory-verify-handler.js +135 -0
  227. package/dist/services/memory-work-result-preparation.js +63 -0
  228. package/dist/services/merge-server.js +2 -4
  229. package/dist/services/model-catalog.js +109 -0
  230. package/dist/services/model-discovery.js +46 -0
  231. package/dist/services/operator-profiles.js +236 -0
  232. package/dist/services/projects.js +17 -2
  233. package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
  234. package/dist/services/recovery-snapshot-database.js +417 -2
  235. package/dist/services/release-archive-producer.js +52 -0
  236. package/dist/services/release-archive.js +143 -0
  237. package/dist/services/release-cache.js +142 -0
  238. package/dist/services/release-compatibility.js +18 -0
  239. package/dist/services/release-download.js +67 -0
  240. package/dist/services/release-envelope.js +104 -0
  241. package/dist/services/release-manifest.js +45 -0
  242. package/dist/services/release-payload.js +114 -0
  243. package/dist/services/release-producer.js +63 -0
  244. package/dist/services/release-provenance.js +160 -0
  245. package/dist/services/release-registry.js +66 -0
  246. package/dist/services/release-source.js +194 -0
  247. package/dist/services/repair-intents.js +1 -1
  248. package/dist/services/review-copy.js +10 -2
  249. package/dist/services/run-control.js +80 -9
  250. package/dist/services/run-controller-adapter.js +1 -1
  251. package/dist/services/run-controller-capture.js +10 -3
  252. package/dist/services/run-controller-process.js +21 -0
  253. package/dist/services/run-controller-recovery.js +5 -5
  254. package/dist/services/run-controller-state.js +1 -1
  255. package/dist/services/run-controller.js +158 -64
  256. package/dist/services/run-recovery-guard.js +5 -3
  257. package/dist/services/run-recovery-runtime.js +3 -3
  258. package/dist/services/run-recovery.js +145 -5
  259. package/dist/services/run-start-custody.js +177 -0
  260. package/dist/services/runs.js +413 -30
  261. package/dist/services/setup.js +377 -0
  262. package/dist/services/stage-context.js +9 -1
  263. package/dist/services/stage-handlers.js +74 -0
  264. package/dist/services/stage-report-renderer.js +63 -0
  265. package/dist/services/stage-work-graph.js +17 -0
  266. package/dist/services/vnext-code-review.js +1 -1
  267. package/dist/services/vnext-code.js +6 -6
  268. package/dist/services/vnext-execution-profile.js +2 -2
  269. package/dist/services/vnext-fanout.js +73 -11
  270. package/dist/services/vnext-merge.js +8 -3
  271. package/dist/services/vnext-plan-review.js +7 -5
  272. package/dist/services/vnext-plan.js +3 -3
  273. package/dist/services/vnext-protocolize.js +20 -6
  274. package/dist/services/vnext-workspace-policy.js +7 -2
  275. package/dist/services/work-registry.js +616 -71
  276. package/dist/services/workspace-files.js +1 -1
  277. package/dist/storage/database.js +84 -6
  278. package/dist/storage/writer-contract.js +11 -7
  279. package/package.json +13 -3
  280. package/tools/create-canon-release.mjs +33 -0
  281. package/tools/produce-hybrid-release.mjs +41 -0
  282. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
  283. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
@@ -0,0 +1,2 @@
1
+ import { discoverExecutable } from "./static.js";
2
+ export const discoverZcode = (input) => discoverExecutable("zcode", ["zcode"], input);
@@ -6,31 +6,25 @@ import { spawn } from "node:child_process";
6
6
  import { commandText } from '../harness-runtime/lib/process-json.mjs';
7
7
  import { SETUP_STARTUP_MS, publishSetup } from '../harness-runtime/lib/setup-progress.mjs';
8
8
  import { createRequire } from "node:module";
9
+ import { valid } from "semver";
9
10
  import { getCliBuildInfo } from "./build-info.js";
10
11
  import { AppError } from "../shared/errors.js";
11
12
  import { ensureDir, engineStoreRoot, engineVersionRoot, resolveProjectRoot } from "../storage/paths.js";
12
- import { classifyCliOperation } from "./cli-operation-classifier.js";
13
+ import { classifyCliOperation, isRouterNative } from "./cli-operation-classifier.js";
13
14
  import { requireProtocol } from "./protocols.js";
14
15
  import { resolveCanonRootBeforeContext } from "./canon.js";
15
16
  import { appendAudit } from "./audit.js";
17
+ import { readBoundedReleaseFile } from "./release-source.js";
16
18
  import { findProjectByRoot } from "./projects.js";
17
19
  import { parseCommandArgs } from "./lifecycle-command.js";
18
20
  import { allRunEngineBindings, locateRunRoot, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
19
21
  export const engineManifestSchemaId = "dd-flow/engine-manifest@1";
20
- const routerNativeFamilies = new Set(["engine", "version", "schema"]);
21
22
  const sourceFile = fileURLToPath(import.meta.url);
22
23
  export function isEngineMode(env) {
23
24
  return env.DD_FLOW_ENGINE_MODE === "1";
24
25
  }
25
26
  export function isRouterNativeCommand(args) {
26
- const first = args[0];
27
- if (!first || first === "--version")
28
- return true;
29
- if (first === "--help" || first === "-h")
30
- return true;
31
- if (args.includes("--help") || args.includes("-h"))
32
- return true;
33
- return first === "run" && args[1] === "reconcile" || routerNativeFamilies.has(first) || first === "codex" && args[1] === "hook" && args[2] === "handle";
27
+ return isRouterNative(args);
34
28
  }
35
29
  export function installCurrentEngine(context, input = {}) {
36
30
  const installation = prepareInstallation(context, input);
@@ -56,6 +50,27 @@ function* prepareInstallation(context, input) {
56
50
  if (!packageRoot) {
57
51
  throw new AppError("engine_install_failed", "Cannot locate package root for current dd-flow engine", 1);
58
52
  }
53
+ return yield* installPackageSnapshot(context, packageRoot, build.package_name, build.version, input);
54
+ }
55
+ /** Delivery from an independently selected package, not the invoking CLI.
56
+ * The caller owns release/dependency qualification of expectedChecksum.
57
+ * Existing snapshots are preserved even when not yet bound to a RUN. */
58
+ export function installPackageEngine(context, input) {
59
+ if (!path.isAbsolute(input.packageRoot) || fs.realpathSync(input.packageRoot) !== input.packageRoot
60
+ || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(input.packageName)
61
+ || valid(input.version) !== input.version
62
+ || !/^[a-f0-9]{64}$/.test(input.expectedChecksum)) {
63
+ throw new AppError("engine_install_failed", "Selected package requires a canonical root and independent content checksum", 1, { effect: "no_effect" });
64
+ }
65
+ const metadata = JSON.parse(readBoundedReleaseFile(path.join(input.packageRoot, "package.json"), 1024 * 1024).toString("utf8"));
66
+ if (metadata.name !== input.packageName || metadata.version !== input.version
67
+ || snapshotFilesChecksum(packageSnapshotFiles(input.packageRoot)) !== input.expectedChecksum) {
68
+ throw new AppError("engine_install_failed", "Selected package identity or contents changed before delivery", 1, { effect: "no_effect" });
69
+ }
70
+ return installPackageSnapshot(context, input.packageRoot, input.packageName, input.version, { force: true, forRouting: true }, input.expectedChecksum, input.validateStaged).next().value;
71
+ }
72
+ function* installPackageSnapshot(context, packageRoot, packageName, version, input, expectedChecksum, validateStaged) {
73
+ const build = { package_name: packageName, version };
59
74
  const target = engineVersionRoot(context.ddFlowHome, build.package_name, build.version);
60
75
  const manifestPath = path.join(target, "engine.json");
61
76
  const existing = readManifest(manifestPath);
@@ -90,11 +105,20 @@ function* prepareInstallation(context, input) {
90
105
  copyPackageSnapshot(packageRoot, tmp);
91
106
  const stagedManifest = buildManifest(context, build.package_name, build.version, packageRoot, target, tmp);
92
107
  const stagedChecksum = stagedManifest.integrity.checksum;
108
+ if (expectedChecksum !== undefined && stagedChecksum !== expectedChecksum) {
109
+ throw new AppError("engine_install_failed", "Selected package changed during delivery", 1, { effect: "partial", path: tmp });
110
+ }
111
+ try {
112
+ validateStaged?.(tmp);
113
+ }
114
+ catch (error) {
115
+ throw new AppError("engine_install_failed", "Staged package no longer matches its selected release", 1, { effect: "partial", path: tmp, reason: error instanceof AppError ? error.details.reason ?? error.code : "staged_selection_changed" });
116
+ }
93
117
  const activationTarget = input.force
94
118
  && existing
95
119
  && stagedChecksum
96
120
  && stagedChecksum !== existing.integrity.checksum
97
- && engineIsBound(context.ddFlowHome, existing)
121
+ && (expectedChecksum !== undefined || engineIsBound(context.ddFlowHome, existing))
98
122
  ? `${target}--${stagedChecksum}`
99
123
  : target;
100
124
  const duplicate = stagedChecksum ? findInstalledManifest(context.ddFlowHome, {
@@ -194,7 +218,6 @@ export async function routeArgsThroughEngine(context, args, io, stdin, env, reso
194
218
  const bound = boundRunEngineForArgs(context, args, resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined);
195
219
  if (bound) {
196
220
  const entrypoint = resolveManifestEntrypoint(bound.manifest);
197
- await probeEntrypoint(bound.manifest, undefined, context.ddFlowHome, process.env);
198
221
  return spawnEngine(bound.manifest, entrypoint, args, io, stdin, env, getCliBuildInfo().version);
199
222
  }
200
223
  const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined, env }, { allowCurrentInProcess: true }, classification);
@@ -213,15 +236,8 @@ export async function routeArgsThroughEngine(context, args, io, stdin, env, reso
213
236
  return Promise.resolve({ routed: true, exitCode: 0, inProcess: true });
214
237
  }
215
238
  const entrypoint = resolveManifestEntrypoint(selection.selected);
216
- try {
217
- await probeEntrypoint(selection.selected, undefined, context.ddFlowHome, process.env);
218
- }
219
- catch (cause) {
220
- const error = cause;
221
- if (error.code !== 'engine_probe_failed')
222
- throw error;
223
- throw new AppError('missing_engine', 'Selected engine cannot load its runtime dependencies', 1, { probe: error.details });
224
- }
239
+ // The admitted immutable entrypoint executes the requested operation itself.
240
+ // A separate --version process adds no operation-specific admission.
225
241
  return spawnEngine(selection.selected, entrypoint, args, io, stdin, env, current.version);
226
242
  }
227
243
  export async function prepareRunEngineBind(context, input) {
@@ -694,7 +710,8 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
694
710
  supports_contracts: {
695
711
  compatibility: ["dd-flow/compatibility@1"],
696
712
  version: ["dd-flow/version-report@1"],
697
- status: ["dd-flow/status-report@1"]
713
+ status: ["dd-flow/status-report@1"],
714
+ hybrid: ["dd-flow/flow-descriptor@1", "dd-flow/execution-settings@1", "dd-flow/execution-bundle@1", "dd-flow/memory-bootstrap-receipt@2"]
698
715
  },
699
716
  schema_registry: buildSchemaRegistry(checksumRoot),
700
717
  integrity: {
@@ -16,13 +16,26 @@ import { validateRunOperationalDecision } from "./vnext-execution-profile.js";
16
16
  import { readVnextFlowDefinition } from "./vnext-specify.js";
17
17
  import { captureRecoveryDatabase, readRecoveryResourceEvidence } from "./recovery-snapshot-database.js";
18
18
  import { resourceHome } from "./managed-processes.js";
19
+ import { getResourceDatabase } from "../storage/database.js";
19
20
  import { codexHomeSharedEntries } from "./hooks.js";
20
21
  import { prepareOutputFile } from "../cli/input-preparation.js";
21
22
  import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-files.js";
22
23
  import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
24
+ import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
25
+ import { readSetupStatus } from "./setup.js";
23
26
  import { isObservationOperation, nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
24
27
  /** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
25
28
  export function prepareEvalBootstrapSnapshot(context, input) {
29
+ let source;
30
+ if (input.flow !== undefined && input.flow !== "sdlc") {
31
+ if (input.flow !== "mb-init")
32
+ throw new AppError("snapshot_flow_unsupported", "Bootstrap snapshot flow is not registered", 2, { phase: "prepare", effect: "no_effect" });
33
+ const home = context.env.DD_FLOW_CONFIG_HOME ?? context.ddFlowHome;
34
+ const selected = readSetupStatus(home);
35
+ if (!selected.installation)
36
+ throw new AppError("snapshot_flow_unavailable", "Bootstrap snapshot requires the selected installed flow", 2, { phase: "prepare", effect: "no_effect" });
37
+ source = PreparedInstalledFlowSource.fromInstalledRelease(home, selected.installation.manifest_sha256, input.flow);
38
+ }
26
39
  const projectRoot = resolveProjectRoot(input.projectRoot);
27
40
  const output = path.resolve(input.output);
28
41
  if (fs.existsSync(output))
@@ -40,10 +53,10 @@ export function prepareEvalBootstrapSnapshot(context, input) {
40
53
  throw error;
41
54
  throw new AppError(error.code, error.message, 2, { ...error.details, phase: "prepare", effect: "no_effect", recoverable: true });
42
55
  }
43
- return { projectRoot, output };
56
+ return { projectRoot, output, source };
44
57
  }
45
58
  export function createEvalBootstrapSnapshot(context, input) {
46
- const { projectRoot, output } = prepareEvalBootstrapSnapshot(context, input);
59
+ const { projectRoot, output, source } = prepareEvalBootstrapSnapshot(context, input);
47
60
  const temporary = `${output}.partial-${crypto.randomUUID()}`;
48
61
  const before = bootstrapSourceVersion(projectRoot);
49
62
  try {
@@ -53,8 +66,9 @@ export function createEvalBootstrapSnapshot(context, input) {
53
66
  if (before !== after)
54
67
  throw new AppError("snapshot_source_changed", "Bootstrap source changed during snapshot capture", 1, { phase: "bootstrap", recoverable: true });
55
68
  const manifest = {
56
- schema_id: "dd-eval/bootstrap-snapshot@1",
57
- stage: "specify",
69
+ schema_id: source ? "dd-eval/bootstrap-snapshot@2" : "dd-eval/bootstrap-snapshot@1",
70
+ stage: source?.resolveRegisteredFlow().descriptor.stages[0]?.id ?? "specify",
71
+ ...(source ? { flow: { id: "mb-init", source_sha256: source.sha256, descriptor_sha256: source.descriptorReference().sha256 } } : {}),
58
72
  project_root: projectRoot,
59
73
  project_sha256: treeHash(path.join(temporary, "project")),
60
74
  project_git: snapshotProjectGit(projectRoot, temporary)
@@ -62,6 +76,7 @@ export function createEvalBootstrapSnapshot(context, input) {
62
76
  writeJson(path.join(temporary, "bootstrap.json"), manifest);
63
77
  if (after !== bootstrapSourceVersion(projectRoot))
64
78
  throw new AppError("snapshot_source_changed", "Bootstrap source changed while preserving Git evidence", 1, { phase: "bootstrap", recoverable: true });
79
+ source?.revalidate();
65
80
  syncSnapshotTree(temporary);
66
81
  fs.renameSync(temporary, output);
67
82
  syncPath(path.dirname(output));
@@ -97,7 +112,7 @@ export function restoreEvalBootstrapSnapshot(_context, input, prepared = prepare
97
112
  clearProjectTree(projectRoot, false);
98
113
  restoreProjectGit(snapshot, manifest.project_git, projectRoot);
99
114
  replaceProjectTree(projectRoot, sourceProject);
100
- return { ok: true, schema_id: "dd-eval/bootstrap-restore@1", snapshot, project_root: projectRoot, target_stage: "specify" };
115
+ return { ok: true, schema_id: "dd-eval/bootstrap-restore@1", snapshot, project_root: projectRoot, target_stage: manifest.stage, ...(manifest.flow ? { flow: manifest.flow } : {}) };
101
116
  }
102
117
  /**
103
118
  * Eval fixtures require a dedicated home. Sealed recovery exports instead
@@ -204,7 +219,7 @@ export function prepareEvalRunSnapshotCreation(context, input) {
204
219
  if (isInside(physicalPath(projectRoot), physicalPath(output)) || isInside(physicalPath(workspaceRoot), physicalPath(output)))
205
220
  throw new AppError("snapshot_output_inside_project", "Snapshot output must be outside the project and RUN workspace trees", 1, { output });
206
221
  prepareOutputFile(output);
207
- prepareSnapshotProjectGit(projectRoot);
222
+ prepareSnapshotProjectGit(projectRoot, recovery && status.run.flow_kind === "mb-init");
208
223
  prepareSnapshotWorkspace(workspaceRoot, projectRoot);
209
224
  return { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren };
210
225
  }
@@ -262,7 +277,7 @@ function captureEvalRunSnapshot(context, input) {
262
277
  verbatimSymlinks: true,
263
278
  filter: projectPayloadFilter(projectRoot)
264
279
  });
265
- const projectGit = snapshotProjectGit(projectRoot, output);
280
+ const projectGit = snapshotProjectGit(projectRoot, output, recovery && status.run.flow_kind === "mb-init");
266
281
  const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
267
282
  // Accepted Work results are captured in SQLite. Their file representations
268
283
  // are retained evidence, not a second authority or a checkpoint gate.
@@ -276,7 +291,7 @@ function captureEvalRunSnapshot(context, input) {
276
291
  }
277
292
  }
278
293
  const manifest = {
279
- schema_id: "dd-flow/eval-run-snapshot@5",
294
+ schema_id: projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
280
295
  run_id: input.runId,
281
296
  project_id: project.id,
282
297
  project_root: projectRoot,
@@ -793,27 +808,31 @@ function assertRestoreTargetSeparation(snapshot, targets) {
793
808
  function sourceWorkspaceRoot(manifest) {
794
809
  return manifest.workspace.kind === "project" ? manifest.project_root : manifest.workspace.source_root;
795
810
  }
796
- function prepareSnapshotProjectGit(projectRoot) {
811
+ function prepareSnapshotProjectGit(projectRoot, allowUnborn = false) {
797
812
  const branch = git(projectRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
798
- const head = git(projectRoot, ["rev-parse", "HEAD"]);
799
- if (!branch || !head)
800
- throw new AppError("snapshot_project_git_invalid", "Snapshot requires a project on a named Git branch with a readable HEAD", 1, { project_root: projectRoot });
813
+ const head = git(projectRoot, ["rev-parse", "--verify", "HEAD"]);
814
+ if (!branch || !head && !(allowUnborn && git(projectRoot, ["for-each-ref", "--format=%(refname)"]) === ""))
815
+ throw new AppError("snapshot_project_git_invalid", "Snapshot requires a named Git branch and readable HEAD, or an admitted empty-ref Memory recovery", 1, { project_root: projectRoot });
801
816
  return { branch, head };
802
817
  }
803
- function snapshotProjectGit(projectRoot, output) {
804
- const { branch, head } = prepareSnapshotProjectGit(projectRoot);
818
+ function snapshotProjectGit(projectRoot, output, allowUnborn = false) {
819
+ const { branch, head } = prepareSnapshotProjectGit(projectRoot, allowUnborn);
820
+ if (head === null)
821
+ return { branch, head: null, bundle_sha256: null, state: "unborn", ...snapshotIndex(projectRoot, output, "project") };
805
822
  const bundle = path.join(output, "project.bundle");
806
823
  runGit(projectRoot, ["bundle", "create", bundle, `refs/heads/${branch}`]);
807
824
  return { branch, head, bundle_sha256: fileHash(bundle), ...snapshotIndex(projectRoot, output, "project") };
808
825
  }
809
826
  function restoreProjectGit(snapshot, source, projectRoot) {
810
827
  const bundle = path.join(snapshot, "project.bundle");
811
- if (!fs.existsSync(bundle) || fileHash(bundle) !== source.bundle_sha256)
828
+ if (source.state === "unborn" ? fs.existsSync(bundle) : !fs.existsSync(bundle) || fileHash(bundle) !== source.bundle_sha256)
812
829
  throw new AppError("snapshot_checksum_mismatch", "Snapshot project Git bundle checksum does not match manifest", 1, { snapshot });
813
830
  fs.rmSync(path.join(projectRoot, ".git"), { recursive: true, force: true });
814
831
  runGit(projectRoot, ["init", "-b", source.branch]);
815
- runGit(projectRoot, ["fetch", bundle, `+refs/heads/${source.branch}:refs/remotes/snapshot/${source.branch}`]);
816
- runGit(projectRoot, ["checkout", "-B", source.branch, source.head]);
832
+ if (source.state !== "unborn") {
833
+ runGit(projectRoot, ["fetch", bundle, `+refs/heads/${source.branch}:refs/remotes/snapshot/${source.branch}`]);
834
+ runGit(projectRoot, ["checkout", "-B", source.branch, source.head]);
835
+ }
817
836
  restoreIndex(snapshot, "project", source, projectRoot);
818
837
  }
819
838
  const mergeStateFiles = ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE", "ORIG_HEAD", "AUTO_MERGE"];
@@ -912,7 +931,9 @@ export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true
912
931
  const expectedModes = manifest.payload_modes?.[entry.name];
913
932
  if (checkWorkingState && expectedModes && treeModesHash(entry.root, allowed) !== expectedModes)
914
933
  throw new AppError("recovery_workspace_drift", "Executable file modes changed after recovery capture", 1, { payload: entry.name });
915
- if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "HEAD"]) !== entry.head || git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ? indexEntries(entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
934
+ if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "--verify", "HEAD"]) !== entry.head
935
+ || entry.head === null && git(entry.root, ["for-each-ref", "--format=%(refname)"]) !== ""
936
+ || git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ? indexEntries(entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
916
937
  throw new AppError("recovery_workspace_drift", "Working files or Git state changed after recovery capture", 1, { payload: entry.name });
917
938
  }
918
939
  }
@@ -935,9 +956,23 @@ function verifySnapshotPayload(snapshot, manifest) {
935
956
  const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind === "git_worktree" ? [{ name: "workspace", hash: manifest.workspace.sha256, git: manifest.workspace }] : [])];
936
957
  for (const payload of payloads)
937
958
  verifyGitSnapshotPayload(snapshot, payload);
959
+ if (manifest.schema_id === "dd-flow/eval-run-snapshot@6") {
960
+ const db = new DatabaseSync(path.join(snapshot, "runtime", "db.sqlite"), { readOnly: true });
961
+ try {
962
+ const row = db.prepare("SELECT flow_kind, project_root, workspace_root FROM runs WHERE id=?").get(manifest.run_id);
963
+ if (!row || row.flow_kind !== "mb-init" || row.project_root !== manifest.project_root || row.workspace_root !== manifest.project_root)
964
+ throw new AppError("snapshot_invalid", "Unborn recovery must belong to its captured Memory RUN", 1, { snapshot });
965
+ }
966
+ finally {
967
+ db.close();
968
+ }
969
+ }
938
970
  }
939
971
  function verifyGitSnapshotPayload(snapshot, payload) {
940
- if (treeHash(path.join(snapshot, payload.name)) !== payload.hash || fileHash(path.join(snapshot, `${payload.name}.bundle`)) !== payload.git.bundle_sha256 || payload.git.index_sha256 && fileHash(path.join(snapshot, `${payload.name}.index.patch`)) !== payload.git.index_sha256 || payload.git.index_format === "entries@1" && (!payload.git.index_objects_sha256 || fileHash(path.join(snapshot, `${payload.name}.index.pack`)) !== payload.git.index_objects_sha256))
972
+ const bundle = path.join(snapshot, `${payload.name}.bundle`);
973
+ const invalidBundle = payload.git.state === "unborn" ? payload.git.bundle_sha256 !== null || fs.existsSync(bundle)
974
+ : !fs.existsSync(bundle) || fileHash(bundle) !== payload.git.bundle_sha256;
975
+ if (treeHash(path.join(snapshot, payload.name)) !== payload.hash || invalidBundle || payload.git.index_sha256 && fileHash(path.join(snapshot, `${payload.name}.index.patch`)) !== payload.git.index_sha256 || payload.git.index_format === "entries@1" && (!payload.git.index_objects_sha256 || fileHash(path.join(snapshot, `${payload.name}.index.pack`)) !== payload.git.index_objects_sha256))
941
976
  throw new AppError("snapshot_checksum_mismatch", "Snapshot payload or Git index checksum does not match", 1, { payload: payload.name });
942
977
  }
943
978
  function git(root, args) {
@@ -1073,8 +1108,8 @@ function runtimeSnapshotKind(file, excluded, providerHomes = []) {
1073
1108
  }
1074
1109
  return "ordinary";
1075
1110
  }
1076
- /** Productive writers are fenced separately. Compare exactly the copied
1077
- * source set; SQLite serialization includes committed WAL pages without
1111
+ /** Productive writers are fenced separately. Compare the copied source set
1112
+ * without idle diagnostic appends; SQLite includes committed WAL pages without
1078
1113
  * mistaking a physical checkpoint for new logical content. */
1079
1114
  function snapshotSourceVersion(context, input, retainedDatabases, retainedJournals) {
1080
1115
  const databases = retainedDatabases ?? new Map();
@@ -1085,6 +1120,11 @@ function snapshotSourceVersion(context, input, retainedDatabases, retainedJourna
1085
1120
  if (!run)
1086
1121
  throw new AppError("not_found", "Snapshot RUN is not registered", 1);
1087
1122
  const entries = new Map();
1123
+ const diagnosticLogs = new Set(getResourceDatabase(resourceHome(context), "read_existing").all("SELECT stdout_path,stderr_path,metadata_json FROM managed_processes WHERE project_id=? AND run_id=? AND kind IN ('codex-daemon','zcode-daemon','grok-daemon','agy-daemon','opencode-daemon','droid-daemon')", [project.id, input.runId]).flatMap(row => {
1124
+ const directory = JSON.parse(row.metadata_json).state_dir;
1125
+ const log = typeof directory === "string" && path.isAbsolute(directory) ? path.join(directory, "daemon.log") : null;
1126
+ return log && isInside(context.ddFlowHome, log) && row.stdout_path === log && row.stderr_path === log ? [log] : [];
1127
+ }));
1088
1128
  const excluded = codexHomeSnapshotExclusions(context, project.id, input.runId);
1089
1129
  const providerHomes = input.recoveryId === undefined ? grokProviderHomes(context.ddFlowHome) : [];
1090
1130
  const observations = new Map();
@@ -1132,7 +1172,12 @@ function snapshotSourceVersion(context, input, retainedDatabases, retainedJourna
1132
1172
  }
1133
1173
  entries.set(key, `sqlite:${sqliteSemanticFingerprint(db)}:${stat.mode & 0o111}`);
1134
1174
  }
1135
- else if (stat.isFile() && ["adapter.events.jsonl", "events.jsonl", "model-observations.jsonl", "timeline.jsonl", "daemon.log", "controller.log"].includes(path.basename(file))) {
1175
+ else if (stat.isFile() && diagnosticLogs.has(file)) {
1176
+ // Daemon stdout/stderr can append while an otherwise settled Session is idle.
1177
+ // Keep its identity/mode fenced; the copied bytes remain sealed in the snapshot.
1178
+ entries.set(key, `diagnostic-log:${stat.dev}:${stat.ino}:${stat.mode & 0o111}`);
1179
+ }
1180
+ else if (stat.isFile() && (["adapter.events.jsonl", "events.jsonl", "model-observations.jsonl", "timeline.jsonl", "controller.log"].includes(path.basename(file)) || native && file === path.join(directory, "daemon.log"))) {
1136
1181
  const bytes = fs.readFileSync(file);
1137
1182
  let cut = journals.get(file);
1138
1183
  if (!cut && !retainedJournals) {
@@ -1409,11 +1454,20 @@ function readManifest(snapshot) {
1409
1454
  if (!fs.existsSync(file))
1410
1455
  throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
1411
1456
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
1412
- if (value.schema_id !== "dd-flow/eval-run-snapshot@5" || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
1457
+ if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@6"].includes(String(value.schema_id)) || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
1413
1458
  throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
1414
1459
  if (value.runtime_payload_policy !== undefined && (value.runtime_payload_policy !== "flow-evidence@1" || value.purpose === "recovery"))
1415
1460
  throw new AppError("snapshot_invalid", "Snapshot runtime payload policy is invalid for its purpose", 1, { snapshot });
1416
- if (!value.project_git.branch || !value.project_git.head || !value.project_git.bundle_sha256)
1461
+ const unborn = value.schema_id === "dd-flow/eval-run-snapshot@6";
1462
+ if (!value.project_git.branch || (unborn
1463
+ ? value.purpose !== "recovery" || value.workspace.kind !== "project" || value.project_git.state !== "unborn"
1464
+ || value.project_git.head !== null || value.project_git.bundle_sha256 !== null
1465
+ || value.project_git.index_format !== "entries@1" || !/^[a-f0-9]{64}$/.test(value.project_git.index_sha256 ?? "")
1466
+ || !/^[a-f0-9]{64}$/.test(value.project_git.index_objects_sha256 ?? "")
1467
+ || Object.keys(value.project_git).sort().join() !== "branch,bundle_sha256,head,index_format,index_objects_sha256,index_sha256,state"
1468
+ || typeof value.project_git.branch !== "string"
1469
+ || spawnSync("git", ["check-ref-format", "--branch", value.project_git.branch], { encoding: "utf8" }).status !== 0
1470
+ : value.project_git.state !== undefined || !value.project_git.head || !value.project_git.bundle_sha256))
1417
1471
  throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
1418
1472
  if (value.workspace.kind === "git_worktree" && (!value.workspace.branch || !value.workspace.base_ref || !value.workspace.bundle_sha256))
1419
1473
  throw new AppError("snapshot_invalid", "Snapshot worktree manifest is incomplete", 1, { snapshot });
@@ -1424,7 +1478,11 @@ function readBootstrapManifest(snapshot) {
1424
1478
  if (!fs.existsSync(file))
1425
1479
  throw new AppError("snapshot_missing", "Bootstrap snapshot manifest is missing", 1, { snapshot });
1426
1480
  const value = JSON.parse(fs.readFileSync(file, "utf8"));
1427
- if (value.schema_id !== "dd-eval/bootstrap-snapshot@1" || value.stage !== "specify" || !value.project_root || !value.project_sha256 || !value.project_git?.branch || !value.project_git.head || !value.project_git.bundle_sha256)
1481
+ const validBoundary = value.schema_id === "dd-eval/bootstrap-snapshot@1" ? value.stage === "specify" && value.flow === undefined
1482
+ : value.schema_id === "dd-eval/bootstrap-snapshot@2" && value.flow?.id === "mb-init" && value.stage === "preflight"
1483
+ && Object.keys(value.flow).sort().join() === "descriptor_sha256,id,source_sha256"
1484
+ && /^[a-f0-9]{64}$/.test(value.flow.source_sha256) && /^[a-f0-9]{64}$/.test(value.flow.descriptor_sha256);
1485
+ if (!validBoundary || !value.project_root || !value.project_sha256 || !value.project_git?.branch || !value.project_git.head || !value.project_git.bundle_sha256)
1428
1486
  throw new AppError("snapshot_invalid", "Bootstrap snapshot manifest is invalid", 1, { snapshot });
1429
1487
  return value;
1430
1488
  }
@@ -0,0 +1,263 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { Ajv2020 } from "ajv/dist/2020.js";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { ResolvedFlowDescriptor } from "./flow-descriptors.js";
7
+ import { AdmittedFrozenEngine } from "./frozen-engine.js";
8
+ import { FrozenGenericExecution } from "./generic-execution.js";
9
+ import { hybridDigest, validateHybridContract, validateHybridShape } from "./hybrid-contracts.js";
10
+ import { readBoundedReleaseFile } from "./release-source.js";
11
+ import { acquireInstallationLock } from "./installation-lock.js";
12
+ const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
13
+ const limit = 1024 * 1024;
14
+ function refuse(reason, issues) {
15
+ throw new AppError("execution_bundle_invalid", "Execution bundle cannot be admitted", 1, { reason, phase: "prepare", effect: "no_effect", ...(issues ? { issues } : {}) });
16
+ }
17
+ function json(bytes) {
18
+ try {
19
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
20
+ }
21
+ catch {
22
+ return refuse("invalid_utf8_json");
23
+ }
24
+ }
25
+ function frozen(value) {
26
+ if (value !== null && typeof value === "object") {
27
+ for (const child of Object.values(value))
28
+ frozen(child);
29
+ Object.freeze(value);
30
+ }
31
+ }
32
+ function directory(file) {
33
+ if (!fs.lstatSync(file).isDirectory())
34
+ refuse("non_regular_bundle_directory");
35
+ }
36
+ function fileIn(root, logical, create = false) {
37
+ const parts = logical.split("/");
38
+ let current = root;
39
+ for (const part of parts.slice(0, -1)) {
40
+ current = path.join(current, part);
41
+ if (create && !fs.existsSync(current))
42
+ fs.mkdirSync(current, { mode: 0o700 });
43
+ directory(current);
44
+ }
45
+ return path.join(current, parts.at(-1));
46
+ }
47
+ /** Retained contract data only. RUN generation, physical ownership and writer admission remain separate. */
48
+ export class RetainedExecutionBundle {
49
+ sha256;
50
+ manifest;
51
+ descriptor;
52
+ execution;
53
+ #bytes;
54
+ #blobs;
55
+ #admission;
56
+ #engine;
57
+ constructor(bytes, manifest, blobs, admission, descriptor, execution) {
58
+ frozen(manifest);
59
+ this.manifest = manifest;
60
+ this.sha256 = hash(bytes);
61
+ this.#bytes = bytes;
62
+ this.#blobs = blobs;
63
+ this.#admission = admission;
64
+ this.descriptor = descriptor;
65
+ this.execution = execution;
66
+ Object.freeze(this);
67
+ }
68
+ static admit(input, expectedSha256, sourceBlobs, selected) {
69
+ if (input.byteLength > limit || !/^[a-f0-9]{64}$/.test(expectedSha256))
70
+ refuse("invalid_bundle_identity");
71
+ const bytes = Buffer.from(input);
72
+ if (hash(bytes) !== expectedSha256)
73
+ refuse("manifest_bytes_changed");
74
+ const value = json(bytes), shape = validateHybridShape("execution-bundle", value);
75
+ if (shape.length)
76
+ refuse("invalid_bundle_shape", shape);
77
+ const manifest = value;
78
+ if (manifest.run_id !== selected.runId || selected.executionScope.projectRoot !== selected.bundleProjectRoot)
79
+ refuse("foreign_run_identity");
80
+ const admission = structuredClone(selected);
81
+ const names = manifest.inventory.map(ref => ref.path), folded = new Set();
82
+ for (const name of names) {
83
+ const normalized = name.toLowerCase();
84
+ if (folded.has(normalized) || ["manifest.json", "publication.json"].includes(normalized)
85
+ || names.some(other => other !== name && other.toLowerCase().startsWith(`${normalized}/`)))
86
+ refuse("conflicting_bundle_path");
87
+ folded.add(normalized);
88
+ }
89
+ const blobs = new Map();
90
+ let total = 0;
91
+ for (const reference of manifest.inventory) {
92
+ const source = sourceBlobs.get(reference.path);
93
+ if (!source || source.byteLength === 0 || source.byteLength > limit || (total += source.byteLength) > 32 * limit)
94
+ refuse("missing_or_unbounded_blob");
95
+ blobs.set(reference.path, Buffer.from(source));
96
+ }
97
+ const context = { ...admission, blobs };
98
+ const issues = validateHybridContract("execution-bundle", manifest, context);
99
+ if (issues.length)
100
+ refuse("invalid_bundle_context", issues);
101
+ if (!admission.descriptorContext)
102
+ refuse("missing_handler_context");
103
+ const descriptor = ResolvedFlowDescriptor.admit(blobs.get(manifest.descriptor.path), manifest.descriptor.sha256, { ...admission.descriptorContext, blobs });
104
+ for (const stage of descriptor.descriptor.stages) {
105
+ try {
106
+ new TextDecoder("utf-8", { fatal: true }).decode(descriptor.stageBlob(stage.id, "instruction"));
107
+ }
108
+ catch {
109
+ refuse("invalid_instruction_encoding");
110
+ }
111
+ }
112
+ const profiles = json(blobs.get(manifest.profile_settings.path));
113
+ const snapshot = { schema_id: "dd-flow/frozen-execution@1", scope: admission.executionScope,
114
+ settings: json(blobs.get(manifest.execution_settings.path)), profiles: profiles.profiles };
115
+ const execution = FrozenGenericExecution.restore(snapshot, hybridDigest(snapshot), descriptor, admission.executionScope, admission.receipts);
116
+ const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution);
117
+ admitted.engine();
118
+ return admitted;
119
+ }
120
+ /** Read only the retained inventory. Mutable canon/profile sources are never consulted. */
121
+ static read(root, expectedSha256, admission) {
122
+ try {
123
+ directory(root);
124
+ const bytes = readBoundedReleaseFile(path.join(root, "manifest.json"), limit);
125
+ if (hash(bytes) !== expectedSha256)
126
+ refuse("manifest_bytes_changed");
127
+ const value = json(bytes), issues = validateHybridShape("execution-bundle", value);
128
+ if (issues.length)
129
+ refuse("invalid_bundle_shape", issues);
130
+ const manifest = value, blobs = new Map();
131
+ for (const ref of manifest.inventory)
132
+ blobs.set(ref.path, readBoundedReleaseFile(fileIn(root, ref.path), limit));
133
+ return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission);
134
+ }
135
+ catch (error) {
136
+ if (error instanceof AppError && ["execution_bundle_invalid", "frozen_engine_invalid"].includes(error.code))
137
+ throw error;
138
+ return refuse("retained_bundle_unavailable");
139
+ }
140
+ }
141
+ manifestBytes() { return Buffer.from(this.#bytes); }
142
+ blob(logical) {
143
+ const bytes = this.#blobs.get(logical);
144
+ if (!bytes)
145
+ refuse("unlisted_bundle_blob");
146
+ return Buffer.from(bytes);
147
+ }
148
+ admission() { return structuredClone(this.#admission); }
149
+ engine() {
150
+ // An issued bundle already admitted this exact immutable engine. Fresh
151
+ // read/admit creates a new owner; publication explicitly rechecks below.
152
+ return this.#engine ??= this.#admitEngine();
153
+ }
154
+ #admitEngine() {
155
+ const binding = json(this.blob(this.manifest.engine_binding.path));
156
+ const engine = AdmittedFrozenEngine.admit(binding, this.manifest.engine_manifest.sha256);
157
+ if (!engine.manifestBytes().equals(this.blob(this.manifest.engine_manifest.path)))
158
+ refuse("engine_manifest_bytes_changed");
159
+ const manifest = engine.manifest(), contracts = new Set(Object.values(manifest.supports_contracts).flat());
160
+ for (const id of ["dd-flow/flow-descriptor@1", "dd-flow/execution-settings@1", "dd-flow/execution-bundle@1"]) {
161
+ if (!contracts.has(id))
162
+ refuse("engine_contract_not_supported");
163
+ }
164
+ for (const reference of this.manifest.bootstrap_receipts) {
165
+ const receipt = json(this.blob(reference.path));
166
+ if (receipt.schema_id !== "dd-flow/memory-bootstrap-receipt@2")
167
+ continue;
168
+ if (!contracts.has(receipt.schema_id))
169
+ refuse("engine_contract_not_supported");
170
+ const entries = manifest.schema_registry.entries.filter(entry => entry.id === receipt.schema_id);
171
+ const entry = entries[0];
172
+ if (entries.length !== 1 || !entry)
173
+ refuse("unregistered_bootstrap_schema");
174
+ try {
175
+ const validate = new Ajv2020({ strict: true, validateFormats: false }).compile(json(engine.schema(entry.name)));
176
+ if (!validate(receipt))
177
+ refuse("invalid_frozen_bootstrap_shape", validate.errors);
178
+ }
179
+ catch (error) {
180
+ if (error instanceof AppError)
181
+ throw error;
182
+ refuse("invalid_frozen_bootstrap_schema");
183
+ }
184
+ }
185
+ for (const stage of this.descriptor.descriptor.stages) {
186
+ const bytes = this.descriptor.stageBlob(stage.id, "result_schema"), schema = json(bytes);
187
+ if (!manifest.schema_registry.entries.some(entry => entry.id === schema.$id && entry.checksum === hash(bytes)))
188
+ refuse("unregistered_stage_schema");
189
+ }
190
+ return engine;
191
+ }
192
+ /** Deterministic publication under the RUN's existing root and immutable engine binding. */
193
+ materialize(runRoot) {
194
+ this.#admitEngine();
195
+ directory(runRoot);
196
+ if (hash(readBoundedReleaseFile(path.join(runRoot, "engine-binding.json"), limit)) !== this.manifest.engine_binding.sha256)
197
+ refuse("run_engine_binding_changed");
198
+ const target = path.join(runRoot, "execution-bundle"), pending = path.join(runRoot, `.execution-bundle.pending-${this.sha256}`);
199
+ const lock = acquireInstallationLock(path.join(runRoot, "execution-bundle.lock"), this.sha256);
200
+ let publicationStarted = false;
201
+ try {
202
+ if (fs.existsSync(target)) {
203
+ RetainedExecutionBundle.read(target, this.sha256, this.#admission);
204
+ return { path: target, sha256: this.sha256, changed: false };
205
+ }
206
+ const owner = Buffer.from(JSON.stringify({ schema_id: "dd-flow/bundle-publication@1", sha256: this.sha256, run_id: this.manifest.run_id }));
207
+ publicationStarted = true;
208
+ if (!fs.existsSync(pending)) {
209
+ fs.mkdirSync(pending, { mode: 0o700 });
210
+ fs.writeFileSync(path.join(pending, "publication.json"), owner, { flag: "wx", mode: 0o600 });
211
+ }
212
+ directory(pending);
213
+ if (!readBoundedReleaseFile(path.join(pending, "publication.json"), limit).equals(owner))
214
+ refuse("foreign_pending_publication");
215
+ const write = (logical, bytes) => {
216
+ const file = fileIn(pending, logical, true);
217
+ if (fs.existsSync(file)) {
218
+ if (!readBoundedReleaseFile(file, limit).equals(bytes))
219
+ refuse("retained_blob_changed");
220
+ return;
221
+ }
222
+ const temporary = path.join(pending, `.write-${hash(Buffer.from(logical))}.tmp`);
223
+ if (fs.existsSync(temporary)) {
224
+ const stat = fs.lstatSync(temporary);
225
+ if (!stat.isFile() || stat.nlink !== 1)
226
+ refuse("foreign_pending_write");
227
+ fs.unlinkSync(temporary);
228
+ }
229
+ // This temporary is owned by the exact publication receipt and lock.
230
+ // A crashed partial write is deterministic; no productive operation is retried.
231
+ const fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
232
+ try {
233
+ fs.writeFileSync(fd, bytes);
234
+ fs.fsyncSync(fd);
235
+ }
236
+ finally {
237
+ fs.closeSync(fd);
238
+ }
239
+ fs.renameSync(temporary, file);
240
+ };
241
+ for (const [logical, bytes] of this.#blobs)
242
+ write(logical, bytes);
243
+ write("manifest.json", this.#bytes);
244
+ RetainedExecutionBundle.read(pending, this.sha256, this.#admission);
245
+ if (fs.existsSync(target))
246
+ refuse("concurrent_bundle_publication");
247
+ fs.renameSync(pending, target);
248
+ return { path: target, sha256: this.sha256, changed: true };
249
+ }
250
+ catch (error) {
251
+ if (!publicationStarted)
252
+ throw error;
253
+ throw new AppError("execution_bundle_publication_failed", "Execution bundle publication requires recovery", 1, {
254
+ phase: "publication", effect: "partial", run_id: this.manifest.run_id,
255
+ sha256: this.sha256, pending_path: pending,
256
+ reason: error instanceof AppError ? error.code : "publication_io_failure",
257
+ });
258
+ }
259
+ finally {
260
+ lock.release();
261
+ }
262
+ }
263
+ }