@deksden-com/dd-flow-cli 0.9.0-beta.136 → 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 (230) hide show
  1. package/CHANGELOG.md +12 -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 +56 -3
  7. package/dist/cli/command-option-descriptions.js +5 -0
  8. package/dist/cli/executor-onboarding.js +8 -3
  9. package/dist/cli/flows.js +29 -0
  10. package/dist/cli/help.js +240 -6
  11. package/dist/cli/input-preparation.js +54 -5
  12. package/dist/cli/memory-flow-admission.js +44 -0
  13. package/dist/cli/memory-init-invocation.js +78 -0
  14. package/dist/cli/memory-init.js +240 -0
  15. package/dist/cli/memory-storage-admission.js +53 -0
  16. package/dist/cli/native-hook-ingress.js +1 -1
  17. package/dist/cli/response-file.js +2 -0
  18. package/dist/cli/run-cli.js +283 -160
  19. package/dist/cli/setup.js +9 -7
  20. package/dist/cli.js +54 -2
  21. package/dist/domain/stage-handler-identities.js +20 -0
  22. package/dist/harness-runtime/DD-ZCODE.md +13 -0
  23. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
  24. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  25. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  27. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  28. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  29. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  30. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  31. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
  35. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  36. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  37. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  39. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  40. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  41. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  42. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  43. package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
  44. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  45. package/dist/runtime/context.js +1 -1
  46. package/dist/schemas/discovery-result.schema.json +184 -0
  47. package/dist/schemas/execution-bundle.schema.json +205 -0
  48. package/dist/schemas/execution-settings.schema.json +162 -0
  49. package/dist/schemas/flow-descriptor.schema.json +54 -0
  50. package/dist/schemas/flow-run.schema.json +22 -0
  51. package/dist/schemas/harness-catalog.schema.json +80 -0
  52. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  53. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  54. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  55. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  56. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  57. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  58. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  59. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  60. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  61. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  62. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  63. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  64. package/dist/schemas/model-catalog.schema.json +153 -0
  65. package/dist/schemas/profile-settings.schema.json +122 -0
  66. package/dist/schemas/release-manifest.schema.json +291 -0
  67. package/dist/schemas/run-engine-binding.schema.json +1 -1
  68. package/dist/services/check-runner-evidence.js +51 -4
  69. package/dist/services/cli-operation-classifier.js +8 -47
  70. package/dist/services/code-checks.js +387 -34
  71. package/dist/services/controller-child-observer.js +99 -0
  72. package/dist/services/controller-execution.js +88 -0
  73. package/dist/services/controller-fanout.js +79 -13
  74. package/dist/services/dashboard.js +8 -2
  75. package/dist/services/descriptor-coordinator-session.js +35 -0
  76. package/dist/services/descriptor-root-work.js +53 -0
  77. package/dist/services/descriptor-run-settlement.js +105 -0
  78. package/dist/services/descriptor-run-start.js +449 -0
  79. package/dist/services/descriptor-stage-coordinator.js +197 -0
  80. package/dist/services/descriptor-stage-settlement.js +222 -0
  81. package/dist/services/descriptor-stage-start.js +139 -0
  82. package/dist/services/engines.js +39 -22
  83. package/dist/services/eval-snapshots.js +83 -25
  84. package/dist/services/execution-bundles.js +263 -0
  85. package/dist/services/execution-policy.js +20 -16
  86. package/dist/services/external-work-launch.js +91 -11
  87. package/dist/services/flow-catalog.js +21 -0
  88. package/dist/services/flow-descriptors.js +82 -0
  89. package/dist/services/frozen-engine.js +242 -0
  90. package/dist/services/generic-execution.js +164 -0
  91. package/dist/services/harness-adapter.js +28 -2
  92. package/dist/services/hooks.js +24 -6
  93. package/dist/services/hybrid-contracts.js +31 -3
  94. package/dist/services/ids.js +29 -1
  95. package/dist/services/installation-lock.js +7 -4
  96. package/dist/services/installed-flow-source.js +183 -0
  97. package/dist/services/lifecycle-invocations.js +257 -78
  98. package/dist/services/memory-accepted-composition.js +77 -0
  99. package/dist/services/memory-baseline-preservation.js +65 -0
  100. package/dist/services/memory-completed-correction-wave.js +320 -0
  101. package/dist/services/memory-compose-handler.js +258 -0
  102. package/dist/services/memory-composition-acceptance.js +199 -0
  103. package/dist/services/memory-composition-graph.js +134 -0
  104. package/dist/services/memory-composition-registration.js +146 -0
  105. package/dist/services/memory-composition-work-custody.js +151 -0
  106. package/dist/services/memory-composition-work-results.js +96 -0
  107. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  108. package/dist/services/memory-corrected-review-batch.js +30 -0
  109. package/dist/services/memory-corrected-review-findings.js +106 -0
  110. package/dist/services/memory-corrected-review-inputs.js +210 -0
  111. package/dist/services/memory-corrected-review-projection.js +78 -0
  112. package/dist/services/memory-corrected-review-receipt.js +170 -0
  113. package/dist/services/memory-corrected-review-registration.js +191 -0
  114. package/dist/services/memory-correction-check-receipt.js +219 -0
  115. package/dist/services/memory-correction-input-owner.js +6 -0
  116. package/dist/services/memory-discover-handler.js +104 -0
  117. package/dist/services/memory-finish-evidence.js +435 -0
  118. package/dist/services/memory-finish-handler.js +215 -0
  119. package/dist/services/memory-finish-integration.js +280 -0
  120. package/dist/services/memory-finish-readiness.js +122 -0
  121. package/dist/services/memory-git-integration.js +151 -0
  122. package/dist/services/memory-historical-corrected-review.js +206 -0
  123. package/dist/services/memory-historical-original-review.js +130 -0
  124. package/dist/services/memory-historical-repair-registration.js +99 -0
  125. package/dist/services/memory-historical-second-review.js +128 -0
  126. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  127. package/dist/services/memory-init-contexts.js +89 -0
  128. package/dist/services/memory-init-controller-start.js +84 -0
  129. package/dist/services/memory-init-engine-release.js +223 -0
  130. package/dist/services/memory-init-git-policy.js +136 -0
  131. package/dist/services/memory-init-lint-capability.js +185 -0
  132. package/dist/services/memory-init-native-capacity.js +241 -0
  133. package/dist/services/memory-init-preflight.js +312 -0
  134. package/dist/services/memory-init-project-bootstrap.js +599 -0
  135. package/dist/services/memory-init-request-candidate.js +157 -0
  136. package/dist/services/memory-init-request-input.js +66 -0
  137. package/dist/services/memory-init-resource-storage.js +125 -0
  138. package/dist/services/memory-init-run-allocation.js +81 -0
  139. package/dist/services/memory-init-run-bundle.js +98 -0
  140. package/dist/services/memory-init-start.js +175 -0
  141. package/dist/services/memory-init-startup-custody.js +80 -0
  142. package/dist/services/memory-init-startup-input.js +240 -0
  143. package/dist/services/memory-init-storage.js +81 -0
  144. package/dist/services/memory-init-stores.js +359 -0
  145. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  146. package/dist/services/memory-layer-publication.js +611 -0
  147. package/dist/services/memory-layer.js +156 -0
  148. package/dist/services/memory-lint-config.js +85 -0
  149. package/dist/services/memory-native-review-copy.js +165 -0
  150. package/dist/services/memory-normal-flow-entry.js +75 -0
  151. package/dist/services/memory-ordinary-defaults.js +112 -0
  152. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  153. package/dist/services/memory-preflight-handler.js +259 -0
  154. package/dist/services/memory-repair-current-view.js +299 -0
  155. package/dist/services/memory-repair-document-delta.js +451 -0
  156. package/dist/services/memory-repair-history.js +491 -0
  157. package/dist/services/memory-repair-physical-binding.js +312 -0
  158. package/dist/services/memory-repair-publication-files.js +152 -0
  159. package/dist/services/memory-repair-publication-intent.js +27 -0
  160. package/dist/services/memory-repair-publication-plan.js +160 -0
  161. package/dist/services/memory-repair-publication-source.js +225 -0
  162. package/dist/services/memory-repair-publication.js +794 -0
  163. package/dist/services/memory-repair-registration.js +239 -0
  164. package/dist/services/memory-repair-work-baseline.js +413 -0
  165. package/dist/services/memory-repair-work-completion.js +97 -0
  166. package/dist/services/memory-repair-work-custody.js +73 -0
  167. package/dist/services/memory-repair-work-results.js +89 -0
  168. package/dist/services/memory-repair-workspace.js +252 -0
  169. package/dist/services/memory-repair-writer-copy.js +735 -0
  170. package/dist/services/memory-result-binding.js +173 -0
  171. package/dist/services/memory-retained-review-authority.js +129 -0
  172. package/dist/services/memory-retained-review-results.js +70 -0
  173. package/dist/services/memory-retained-second-baseline.js +102 -0
  174. package/dist/services/memory-retained-second-repairs.js +156 -0
  175. package/dist/services/memory-review-acceptance.js +214 -0
  176. package/dist/services/memory-review-correction-plan.js +171 -0
  177. package/dist/services/memory-review-handler.js +585 -0
  178. package/dist/services/memory-review-inputs.js +200 -0
  179. package/dist/services/memory-review-receipt.js +180 -0
  180. package/dist/services/memory-review-registration.js +108 -0
  181. package/dist/services/memory-review-work-custody.js +124 -0
  182. package/dist/services/memory-review-work-input.js +72 -0
  183. package/dist/services/memory-review-work-results.js +73 -0
  184. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  185. package/dist/services/memory-second-correction-plan.js +76 -0
  186. package/dist/services/memory-second-repair-baseline.js +91 -0
  187. package/dist/services/memory-second-repair-launch.js +614 -0
  188. package/dist/services/memory-second-repair-registration.js +88 -0
  189. package/dist/services/memory-second-repair-source.js +187 -0
  190. package/dist/services/memory-second-review-acceptance.js +115 -0
  191. package/dist/services/memory-source-observations.js +117 -0
  192. package/dist/services/memory-stage-dispatch.js +28 -0
  193. package/dist/services/memory-stage-results.js +199 -0
  194. package/dist/services/memory-verification-acceptance.js +135 -0
  195. package/dist/services/memory-verification-checks.js +252 -0
  196. package/dist/services/memory-verification-receipt.js +159 -0
  197. package/dist/services/memory-verify-handler.js +135 -0
  198. package/dist/services/memory-work-result-preparation.js +63 -0
  199. package/dist/services/projects.js +17 -2
  200. package/dist/services/recovery-snapshot-database.js +416 -1
  201. package/dist/services/repair-intents.js +1 -1
  202. package/dist/services/review-copy.js +10 -2
  203. package/dist/services/run-control.js +80 -9
  204. package/dist/services/run-controller-capture.js +10 -3
  205. package/dist/services/run-controller-process.js +21 -0
  206. package/dist/services/run-controller-recovery.js +3 -1
  207. package/dist/services/run-controller-state.js +1 -1
  208. package/dist/services/run-controller.js +184 -48
  209. package/dist/services/run-recovery-guard.js +2 -0
  210. package/dist/services/run-recovery.js +145 -5
  211. package/dist/services/run-start-custody.js +177 -0
  212. package/dist/services/runs.js +319 -25
  213. package/dist/services/stage-context.js +9 -1
  214. package/dist/services/stage-handlers.js +74 -0
  215. package/dist/services/stage-lifecycle.js +2 -1
  216. package/dist/services/stage-report-renderer.js +76 -0
  217. package/dist/services/stage-work-graph.js +17 -0
  218. package/dist/services/vnext-code.js +3 -3
  219. package/dist/services/vnext-execution-profile.js +2 -2
  220. package/dist/services/vnext-fanout.js +136 -10
  221. package/dist/services/vnext-merge.js +8 -3
  222. package/dist/services/vnext-plan-review.js +6 -4
  223. package/dist/services/vnext-protocolize.js +60 -2
  224. package/dist/services/work-registry.js +616 -71
  225. package/dist/services/workspace-files.js +1 -1
  226. package/dist/storage/database.js +84 -6
  227. package/dist/storage/writer-contract.js +11 -7
  228. package/package.json +13 -3
  229. package/tools/create-canon-release.mjs +33 -0
  230. package/tools/produce-hybrid-release.mjs +41 -0
@@ -5,6 +5,8 @@ import { AppError } from "../shared/errors.js";
5
5
  import { harnessKey } from "./harness-config.js";
6
6
  import { vnextStages } from "../domain/stage-catalog.js";
7
7
  import { prepareFileBytes, prepareJsonText } from "../cli/input-preparation.js";
8
+ import { operatorSettingsPath, readOperatorProfiles, readOperatorProfileInput, resolveOperatorProfile } from "./operator-profiles.js";
9
+ import { selectOnboardingExecutor } from "./executor-onboarding.js";
8
10
  /** Read once so the frozen policy and its provenance describe the same bytes. */
9
11
  export function loadExecutionRoutingInput(file) {
10
12
  const { file: source_path, bytes } = prepareFileBytes(file, "execution routing");
@@ -48,23 +50,20 @@ export function isExecutionRouting(value) {
48
50
  export function loadAgentProfile(ddFlowHome, id) {
49
51
  if (typeof id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id))
50
52
  throw new AppError("agent_profile_invalid", "Agent profile id is invalid", 2, { id });
53
+ if (fs.existsSync(operatorSettingsPath(ddFlowHome)))
54
+ return resolveOperatorProfile(readOperatorProfiles(ddFlowHome), id);
51
55
  const file = path.join(ddFlowHome, "agent-profiles", `${id}.json`);
52
- let value;
56
+ if (!fs.existsSync(file))
57
+ throw new AppError("agent_profile_missing", "Agent profile is unavailable", 1, { id });
58
+ let profile;
53
59
  try {
54
- value = JSON.parse(fs.readFileSync(file, "utf8"));
60
+ profile = readOperatorProfileInput(file);
55
61
  }
56
- catch (cause) {
57
- throw new AppError("agent_profile_missing", "Agent profile is unavailable", 1, { id, file, cause: String(cause) });
62
+ catch {
63
+ throw new AppError("agent_profile_invalid", "Agent profile must be a bounded valid regular JSON file", 2, { id });
58
64
  }
59
- if (!value || typeof value !== "object" || Array.isArray(value))
60
- throw new AppError("agent_profile_invalid", "Agent profile must be an object", 2, { id, file });
61
- const profile = value;
62
- if (profile.schema_id !== "dd-flow/agent-profile@1" || profile.id !== id
63
- || Object.keys(profile).some(key => !["schema_id", "id", "harness", "provider", "model", "reasoning", "mode", "permission"].includes(key))
64
- || !["codex", "zcode", "agy", "opencode", "grok", "droid"].includes(String(profile.harness))
65
- || !nonempty(profile.provider) || !nonempty(profile.model) || !nonempty(profile.reasoning) || !nonempty(profile.mode)
66
- || (profile.permission !== "allow" && profile.permission !== "deny")) {
67
- throw new AppError("agent_profile_invalid", "Agent profile does not match dd-flow/agent-profile@1", 2, { id, file });
65
+ if (profile.id !== id) {
66
+ throw new AppError("agent_profile_invalid", "Agent profile does not match its selected ID", 2, { id });
68
67
  }
69
68
  // Only execution settings enter RUN evidence; never copy arbitrary profile fields.
70
69
  return { schema_id: profile.schema_id, id, harness: profile.harness, provider: profile.provider, model: profile.model, reasoning: profile.reasoning, mode: profile.mode, permission: profile.permission };
@@ -73,10 +72,16 @@ export function snapshotAgentProfiles(ddFlowHome, policy) {
73
72
  if (!isExecutionRouting(policy))
74
73
  throw new AppError("execution_profile_invalid", "Execution routing contains an invalid stage, profile or delegation policy", 2);
75
74
  const profiles = Object.create(null);
75
+ const settings = fs.existsSync(operatorSettingsPath(ddFlowHome)) ? readOperatorProfiles(ddFlowHome) : undefined;
76
76
  for (const override of [policy, ...Object.values(policy.stage_overrides ?? {})]) {
77
77
  for (const id of [override.agent_profile_id, override.delegation?.agent_profile_id]) {
78
- if (id !== undefined && !Object.hasOwn(profiles, id))
79
- profiles[id] = loadAgentProfile(ddFlowHome, id);
78
+ if (id !== undefined && !Object.hasOwn(profiles, id)) {
79
+ const profile = settings ? selectOnboardingExecutor({}, () => settings, { allowSkip: false, projectProfileId: id }).profile : loadAgentProfile(ddFlowHome, id);
80
+ profiles[id] = profile;
81
+ // Routing labels may be aliases. Default worker resolution uses the
82
+ // complete coordinator's canonical ID, which must also be frozen.
83
+ profiles[profile.id] = profile;
84
+ }
80
85
  }
81
86
  }
82
87
  // Validate every declared route now, including stages not reached by this run.
@@ -177,5 +182,4 @@ export function validateExecutionOverride(value) {
177
182
  if (override.delegation !== undefined)
178
183
  validateDelegation(override.delegation);
179
184
  }
180
- function nonempty(value) { return typeof value === "string" && Boolean(value.trim()); }
181
185
  function digest(value) { return crypto.createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
@@ -18,6 +18,10 @@ import { qualifyAgyHooks } from "../harness-runtime/lib/dd-agy-daemon.mjs";
18
18
  import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
19
19
  import { continueCodexCapacity } from "./codex-capacity.js";
20
20
  import { errorRecord } from "./run-controller-state.js";
21
+ import { readFlowRunExecutionBundle } from "./runs.js";
22
+ import { stageWorkIds } from "./stage-work-graph.js";
23
+ import { hybridDigest } from "./hybrid-contracts.js";
24
+ import { prepareMemoryRepairDispatchFromStage, prepareMemorySecondRepairDispatchFromStage, prepareExternalMemoryRepairCopyFromStage, prepareMemorySecondRepairLaunchFromStage } from "./memory-review-handler.js";
21
25
  /**
22
26
  * Creates an external provider root for one already-created Work. The launch
23
27
  * is not a Work claim: only the worker's trusted `work start` hook binds it.
@@ -38,8 +42,6 @@ export async function launchExternalWork(context, input) {
38
42
  throw new AppError("invalid_run_state", "External Work dispatch requires a running RUN", 2, { run_id: work.run_id, status: run.status });
39
43
  const generation = assertRunMutationAllowed(context, work.project_id, work.run_id);
40
44
  const snapshot = JSON.parse(run.index_json);
41
- if (!snapshot.execution_profile?.agent_profiles)
42
- throw new AppError("execution_profile_not_frozen", "External dispatch requires a RUN with frozen agent profiles; recreate the RUN with the current engine", 1, { work_id: work.work_id });
43
45
  const override = workOverride(work.payload_json);
44
46
  const policy = snapshot.execution_profile?.settings?.execution ?? {};
45
47
  const stage = snapshot.current_stage ?? [...(snapshot.stage_runs ?? [])].reverse().find((entry) => entry.status === "running")?.stage;
@@ -47,12 +49,66 @@ export async function launchExternalWork(context, input) {
47
49
  throw new AppError("execution_stage_mismatch", "Requested stage does not match the RUN's current stage", 2, { requested_stage: input.stage, current_stage: stage ?? null });
48
50
  if (!stage && Object.keys(policy.stage_overrides ?? {}).length)
49
51
  throw new AppError("execution_stage_missing", "Stage overrides require an authoritative RUN stage", 1, { run_id: work.run_id });
50
- const resolved = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy, profiles: snapshot.execution_profile.agent_profiles, ...(stage ? { stage } : {}), ...(override ? { work: override } : {}) });
52
+ const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: work.run_id });
53
+ let resolved;
54
+ if (bundle) {
55
+ if (!stage)
56
+ throw new AppError("execution_stage_missing", "Bound Work requires an authoritative RUN stage", 1);
57
+ if (snapshot.current_stage !== stage || !snapshot.stage_runs?.some(entry => entry.stage === stage && entry.status === "running")
58
+ || !stageWorkIds(context, work.project_id, work.run_id, stage).has(work.work_id)) {
59
+ throw new AppError("execution_stage_mismatch", "Bound Work does not belong to the active running stage", 2);
60
+ }
61
+ if (override?.delegation !== undefined)
62
+ throw new AppError("generic_execution_invalid", "Work cannot replace the RUN's frozen delegation", 2);
63
+ const route = bundle.execution.work(stage, override?.agent_profile_id);
64
+ const selected = { coordinator: route.coordinator, worker: route.worker,
65
+ delegation: { mode: route.delegation.mode, max_parallel: route.delegation.mode === "external" ? route.delegation.max_parallel : null } };
66
+ resolved = { ...selected, profile_hash: crypto.createHash("sha256").update(JSON.stringify(selected)).digest("hex") };
67
+ }
68
+ else {
69
+ if (!snapshot.execution_profile?.agent_profiles)
70
+ throw new AppError("execution_profile_not_frozen", "External dispatch requires a RUN with frozen agent profiles; recreate the RUN with the current engine", 1, { work_id: work.work_id });
71
+ resolved = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy, profiles: snapshot.execution_profile.agent_profiles, ...(stage ? { stage } : {}), ...(override ? { work: override } : {}) });
72
+ }
51
73
  if (resolved.delegation.mode !== "external")
52
74
  throw new AppError("external_launch_not_selected", "This Work's frozen policy selects native delegation", 2, { work_id: work.work_id });
75
+ let payload = null;
76
+ try {
77
+ payload = work.payload_json ? JSON.parse(work.payload_json) : null;
78
+ }
79
+ catch { /* Legacy non-memory payload handling remains owned by the Work registry. */ }
80
+ const repairWave = payload?.kind === "memory-repair" ? payload.memory_repair?.wave : undefined;
81
+ if (payload?.kind === "memory-repair" && (payload.read_only !== false || ![1, 2].includes(repairWave ?? 0)
82
+ || bundle?.descriptor.descriptor.id !== "mb-init" || stage !== "review")) {
83
+ throw new AppError("memory_repair_launch_invalid", "Repair dispatch requires its registered memory review wave", 2, { phase: "prepare", effect: "no_effect" });
84
+ }
85
+ const repairScope = { projectRoot: run.project_root, runId: work.run_id, workId: work.work_id };
86
+ const prepareRepairPacket = repairWave === 1 ? prepareMemoryRepairDispatchFromStage : prepareMemorySecondRepairDispatchFromStage;
87
+ const repairPacket = repairWave ? prepareRepairPacket(context, repairScope) : null;
88
+ const assertRepairPacket = (effect = "no_effect") => {
89
+ if (!repairPacket)
90
+ return;
91
+ try {
92
+ const expected = { coordinator: resolved.coordinator, worker: resolved.worker, delegation: resolved.delegation };
93
+ const actual = { coordinator: repairPacket.profile.coordinator, worker: repairPacket.profile.worker, delegation: repairPacket.profile.delegation };
94
+ if (repairPacket.work_id !== work.work_id || repairPacket.profile.profile_hash !== resolved.profile_hash
95
+ || hybridDigest(actual) !== hybridDigest(expected) || hybridDigest(prepareRepairPacket(context, repairScope)) !== hybridDigest(repairPacket)) {
96
+ throw new AppError("memory_repair_launch_invalid", "Repair ready copy or frozen route changed before dispatch", 2, { phase: "prepare", effect: "no_effect" });
97
+ }
98
+ }
99
+ catch (error) {
100
+ if (effect === "partial")
101
+ throw new AppError("memory_repair_launch_pending", "Repair launch files are retained but dispatch custody changed", 1, { effect, work_id: work.work_id, operation_id: operationId, state_dir: root, next_action: "reconcile_exact_request", cause: errorRecord(error) });
102
+ throw error;
103
+ }
104
+ };
105
+ assertRepairPacket();
53
106
  const agyWorker = runtimeHarness(resolved.worker) === "antigravity-cli";
107
+ if (agyWorker && repairPacket)
108
+ throw new AppError("memory_repair_launch_invalid", "Repair copy hook qualification is required before Antigravity dispatch", 2, { phase: "prepare", effect: "no_effect" });
54
109
  if (agyWorker)
55
110
  await qualifyAgyHooks(run.workspace_root);
111
+ assertRepairPacket();
56
112
  const operationId = `OPR-${crypto.randomUUID()}`;
57
113
  const root = path.join(run.run_root, "external-launches", operationId);
58
114
  const journal = path.join(root, "adapter.events.jsonl");
@@ -76,8 +132,10 @@ export async function launchExternalWork(context, input) {
76
132
  reviewCopy = { root: path.relative(run.run_root, isolatedWorkspace).split(path.sep).join("/"), sha256: reviewExecutionCopyHash(isolatedWorkspace, run.workspace_root, true, reviewInputs, reviewBase), source_sha256: after };
77
133
  }
78
134
  const now = context.now();
79
- const flowExecutable = prepareHarnessFlowExecutable(context, root);
80
- const env = { ...context.env, DD_FLOW_BIN: flowExecutable, DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined, ...(isolatedWorkspace ? { DD_FLOW_READ_ONLY_WORKSPACE: isolatedWorkspace } : {}) };
135
+ const flowExecutable = prepareHarnessFlowExecutable(context, root, bundle?.engine());
136
+ const env = { ...context.env, DD_FLOW_BIN: flowExecutable, DD_FLOW_INVOCATION_SCOPE: undefined, DD_FLOW_CURRENT_INVOCATION: undefined,
137
+ DD_FLOW_MEMORY_REPAIR_WORKSPACE: repairPacket?.copy_root, ...(repairPacket ? { DD_FLOW_READ_ONLY_WORKSPACE: undefined } : {}),
138
+ ...(isolatedWorkspace ? { DD_FLOW_READ_ONLY_WORKSPACE: isolatedWorkspace } : {}) };
81
139
  // An external Codex root is a fresh thread, so it does not inherit the
82
140
  // coordinator's managed PreToolUse hook. Its first `work start` must emit
83
141
  // that hook event to bind the Work to the physical session.
@@ -89,8 +147,9 @@ export async function launchExternalWork(context, input) {
89
147
  let runtimeContext = { ...context, env };
90
148
  const runtimeOwner = { schema_id: "dd-flow/runtime-owner@1", owner_id: `run:${work.run_id}:work:${work.work_id}`, role: "worker", project_id: work.project_id, run_id: work.run_id, work_id: work.work_id, operation_id: operationId, state_dir: root, dd_flow_home: context.ddFlowHome, dd_flow_bin: flowExecutable, resource_home: context.env.DD_FLOW_RESOURCE_HOME ?? context.ddFlowHome, budget: runtimeBudgetForRun(context, work.project_id, work.run_id) };
91
149
  const executable = resolveHarnessCommand(context.ddFlowHome, runtimeHarness(resolved.worker), "adapter");
92
- context.db.run("INSERT INTO work_launches (operation_id, work_id, profile_json, profile_hash, status, state_dir, journal_path, provider_session_id, adapter_receipt_json, created_at, updated_at) VALUES (?, ?, ?, ?, 'launching', ?, ?, NULL, NULL, ?, ?)", [operationId, work.work_id, JSON.stringify({ ...resolved, runtime_adapter: { executable, owner: runtimeOwner }, ...(reviewCopy ? { review_copy: reviewCopy } : {}) }), resolved.profile_hash, root, journal, now, now]);
93
- const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", isolatedWorkspace ?? run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
150
+ assertRepairPacket("partial");
151
+ context.db.run("INSERT INTO work_launches (operation_id, work_id, profile_json, profile_hash, status, state_dir, journal_path, provider_session_id, adapter_receipt_json, created_at, updated_at) VALUES (?, ?, ?, ?, 'launching', ?, ?, NULL, NULL, ?, ?)", [operationId, work.work_id, JSON.stringify({ ...resolved, runtime_adapter: { executable, owner: runtimeOwner }, ...(reviewCopy ? { review_copy: reviewCopy } : {}), ...(repairPacket ? { memory_repair_copy: repairPacket.profile.memory_repair_copy } : {}) }), resolved.profile_hash, root, journal, now, now]);
152
+ const common = ["--dd-flow-bin", flowExecutable, "--state-dir", root, "--journal", journal, "--cwd", repairPacket?.copy_root ?? isolatedWorkspace ?? run.workspace_root, "--provider", resolved.worker.provider, "--model", resolved.worker.model, "--reasoning", resolved.worker.reasoning, "--mode", resolved.worker.mode, "--permission", resolved.worker.permission, "--project-root", run.project_root, "--dd-flow-home", context.ddFlowHome, ...harnessRuntimeArguments(context.ddFlowHome, runtimeHarness(resolved.worker)), "--json"];
94
153
  const evidence = (value) => fs.appendFileSync(journal, `${JSON.stringify({ type: "adapter_call", at: context.now(), ...value })}\n`);
95
154
  try {
96
155
  const call = (args, timeoutMs, id = `${operationId}:${args[0]}.${args[1]}`) => runHarnessAdapter({ executable, args, harness: runtimeHarness(resolved.worker), env: runtimeContext.env, operationId: id, runtimeOwner, ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(input.signal ? { signal: input.signal } : {}), ...(input.progress ? { progress: input.progress } : {}), onEvidence: evidence, onNativeOutcome: value => {
@@ -111,7 +170,15 @@ export async function launchExternalWork(context, input) {
111
170
  runtimeContext = managedInvocationContext(runtimeContext, { harness: runtimeHarness(resolved.worker), stateDir: root, sessionId,
112
171
  projectRoot: run.project_root, runId: work.run_id, generation });
113
172
  assertRunMutationAllowed(context, work.project_id, work.run_id, generation);
114
- assertWorkLaunchReady(context, work.work_id);
173
+ assertWorkLaunchReady(runtimeContext, work.work_id);
174
+ if (repairPacket) {
175
+ const owner = repairWave === 1
176
+ ? prepareExternalMemoryRepairCopyFromStage(context, { ...repairScope, workspaceRoot: repairPacket.copy_root })
177
+ : prepareMemorySecondRepairLaunchFromStage(context, repairScope);
178
+ if (owner.root() !== repairPacket.copy_root)
179
+ throw new AppError("memory_repair_launch_invalid", "Physical repair launch differs from the prepared copy", 1, { effect: "partial", next_action: "reconcile_exact_request" });
180
+ owner.revalidate(context);
181
+ }
115
182
  if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
116
183
  throw new AppError("read_only_input_changed", "Authoritative review workspace changed after the execution copy was made", 2, { work_id: work.work_id });
117
184
  const promptFile = path.join(root, "launch.md");
@@ -178,8 +245,15 @@ export async function launchExternalWork(context, input) {
178
245
  }
179
246
  catch (error) {
180
247
  const saved = context.db.get("SELECT adapter_receipt_json FROM work_launches WHERE operation_id = ?", [operationId]);
181
- context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ ...(saved?.adapter_receipt_json ? JSON.parse(saved.adapter_receipt_json) : {}), error: errorRecord(error) }), context.now(), operationId]);
182
- throw error;
248
+ let failure = error;
249
+ if (repairPacket) {
250
+ const physical = context.db.get("SELECT provider_session_id FROM work_launches WHERE operation_id = ?", [operationId]);
251
+ failure = new AppError("memory_repair_launch_pending", "Repair launch retains runtime effects; reconcile the exact request before continuing", 1, { effect: error instanceof AppError && error.details.effect === "unknown" ? "unknown" : "partial",
252
+ work_id: work.work_id, operation_id: operationId, provider_session_id: physical?.provider_session_id ?? null,
253
+ next_action: "reconcile_exact_request", cause: errorRecord(error) });
254
+ }
255
+ context.db.run("UPDATE work_launches SET status = 'failed', adapter_receipt_json = ?, updated_at = ? WHERE operation_id = ?", [JSON.stringify({ ...(saved?.adapter_receipt_json ? JSON.parse(saved.adapter_receipt_json) : {}), error: errorRecord(failure) }), context.now(), operationId]);
256
+ throw failure;
183
257
  }
184
258
  }
185
259
  export function writeReviewGitEvidence(source, copy, base, explicitInputs = []) {
@@ -192,7 +266,13 @@ export function writeReviewGitEvidence(source, copy, base, explicitInputs = [])
192
266
  if (!base || base === "unknown")
193
267
  throw new AppError("review_git_evidence_missing", "Git review source has no frozen base commit", 1, { workspace_root: source });
194
268
  const paths = (reviewSourcePaths(source, explicitInputs, base) ?? []).map(file => `:(literal)${file}`);
195
- for (const [name, args] of [["status.txt", ["status", "--short", "--untracked-files=all"]], ["diff.patch", ["diff", "--no-ext-diff", "--no-renames", "--binary", base]]]) {
269
+ const commands = [["status.txt", ["status", "--short", "--untracked-files=all"]],
270
+ ["diff.patch", ["diff", "--no-ext-diff", "--no-renames", "--binary", ...(base === "unborn" ? ["--cached"] : [base])]]];
271
+ // No revision exists in an unborn repository. Retain both index additions
272
+ // and subsequent working edits; untracked bytes are in the filtered copy.
273
+ if (base === "unborn")
274
+ commands.push(["diff-working.patch", ["diff", "--no-ext-diff", "--no-renames", "--binary"]]);
275
+ for (const [name, args] of commands) {
196
276
  const result = paths.length ? spawnSync("git", ["-C", source, ...args, "--", ...paths], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }) : { status: 0, stdout: "", stderr: "", error: undefined };
197
277
  if (result.status !== 0 || result.error)
198
278
  throw new AppError("review_git_evidence_failed", "Could not collect read-only Git evidence for reviewer", 1, { workspace_root: source, command: name, cause: result.error?.message ?? result.stderr });
@@ -0,0 +1,21 @@
1
+ import { vnextStages } from "../domain/stage-catalog.js";
2
+ import { sdlcHandlerIdentities, memoryInitHandlerIdentities } from "../domain/stage-handler-identities.js";
3
+ import { AppError } from "../shared/errors.js";
4
+ /** Engine-owned catalog, not descriptor discovery or permission to execute a RUN. */
5
+ export function listBundledFlows() {
6
+ return [
7
+ { id: "sdlc", handler_status: "registered", entry_stage: "specify", terminal_stages: ["merge"],
8
+ stages: vnextStages.map(stage => ({ id: stage.id, directory: stage.directory,
9
+ entry_handler: sdlcHandlerIdentities[stage.id].entryId, finish_handler: sdlcHandlerIdentities[stage.id].finishId })),
10
+ edges: vnextStages.flatMap(stage => stage.next.map((to, index) => ({ from: stage.id, to, on: index ? "not-applicable" : "accepted" }))) },
11
+ { id: "mb-init", handler_status: "registered", entry_stage: "preflight", terminal_stages: ["finish"],
12
+ stages: Object.values(memoryInitHandlerIdentities).map((handler, index) => ({ id: handler.stage, directory: `${String(index + 1).padStart(2, "0")}-${handler.stage}`, entry_handler: handler.entryId, finish_handler: handler.finishId })),
13
+ edges: ["preflight", "discover", "compose", "verify", "review"].flatMap((from, index) => [{ from, to: ["discover", "compose", "verify", "review", "finish"][index], on: "accepted" }, ...(from === "verify" ? [{ from, to: "review", on: "needs_repair" }] : [])]) }
14
+ ];
15
+ }
16
+ export function showBundledFlow(id) {
17
+ const flow = listBundledFlows().find(candidate => candidate.id === id);
18
+ if (!flow)
19
+ throw new AppError("flow_not_found", "Unknown bundled flow", 2, { effect: "no_effect", flow_id: id });
20
+ return flow;
21
+ }
@@ -0,0 +1,82 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { validateFlowDescriptor, validateHybridShape } from "./hybrid-contracts.js";
4
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
5
+ const blobLimit = 1024 * 1024;
6
+ function freeze(value) {
7
+ if (value !== null && typeof value === "object") {
8
+ for (const child of Object.values(value))
9
+ freeze(child);
10
+ Object.freeze(value);
11
+ }
12
+ }
13
+ /** Admitted retained bytes only. This is not a RUN/storage/dispatch permission. */
14
+ export class ResolvedFlowDescriptor {
15
+ sha256;
16
+ descriptor;
17
+ #bytes;
18
+ #blobs;
19
+ constructor(bytes, descriptor, blobs) {
20
+ this.sha256 = digest(bytes);
21
+ this.#bytes = bytes;
22
+ this.#blobs = blobs;
23
+ freeze(descriptor);
24
+ this.descriptor = descriptor;
25
+ Object.freeze(this);
26
+ }
27
+ /** Context comes from the engine materializer, never descriptor-owned allowlists. */
28
+ static admit(input, expectedSha256, context) {
29
+ const details = { phase: "prepare", effect: "no_effect" };
30
+ if (input.byteLength > blobLimit || !/^[a-f0-9]{64}$/.test(expectedSha256)) {
31
+ throw new AppError("flow_descriptor_invalid", "Flow descriptor identity or size is invalid", 2, details);
32
+ }
33
+ const bytes = Buffer.from(input);
34
+ if (digest(bytes) !== expectedSha256)
35
+ throw new AppError("flow_descriptor_integrity", "Flow descriptor bytes do not match the selected identity", 1, details);
36
+ let value;
37
+ try {
38
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
39
+ }
40
+ catch {
41
+ throw new AppError("flow_descriptor_invalid", "Flow descriptor must be valid UTF-8 JSON", 2, details);
42
+ }
43
+ const shape = validateHybridShape("flow-descriptor", value);
44
+ if (shape.length)
45
+ throw new AppError("flow_descriptor_invalid", "Flow descriptor shape is invalid", 2, { ...details, issues: shape });
46
+ const blobs = new Map();
47
+ let total = 0;
48
+ for (const [name, source] of context.blobs) {
49
+ total += source.byteLength;
50
+ if (source.byteLength > blobLimit || total > 32 * blobLimit)
51
+ throw new AppError("flow_descriptor_invalid", "Flow descriptor blobs exceed the bounded inventory", 2, details);
52
+ blobs.set(name, Buffer.from(source));
53
+ }
54
+ const issues = validateFlowDescriptor(value, { ...context, blobs });
55
+ if (issues.length)
56
+ throw new AppError("flow_descriptor_invalid", "Flow descriptor context is invalid", 2, { ...details, issues });
57
+ return new ResolvedFlowDescriptor(bytes, value, blobs);
58
+ }
59
+ stage(id) {
60
+ const stage = this.descriptor.stages.find(candidate => candidate.id === id);
61
+ if (!stage)
62
+ throw new AppError("flow_stage_invalid", "Stage does not belong to the resolved flow", 2, { flow_id: this.descriptor.id, stage: id, effect: "no_effect" });
63
+ return stage;
64
+ }
65
+ successor(id, outcome) {
66
+ this.stage(id);
67
+ if (outcome !== "accepted" && outcome !== "not-applicable" && outcome !== "needs_repair") {
68
+ throw new AppError("flow_transition_invalid", "Unknown stage outcome", 2, { flow_id: this.descriptor.id, stage: id, outcome, effect: "no_effect" });
69
+ }
70
+ const edge = this.descriptor.edges.find(candidate => candidate.from === id && candidate.on === outcome);
71
+ if (edge)
72
+ return edge.to;
73
+ if (this.descriptor.terminal_stages.includes(id))
74
+ return null;
75
+ throw new AppError("flow_transition_invalid", "No declared successor for this stage outcome", 2, { flow_id: this.descriptor.id, stage: id, outcome, effect: "no_effect" });
76
+ }
77
+ descriptorBytes() { return Buffer.from(this.#bytes); }
78
+ stageBlob(id, kind) {
79
+ const reference = this.stage(id)[kind];
80
+ return Buffer.from(this.#blobs.get(reference.path));
81
+ }
82
+ }
@@ -0,0 +1,242 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { Ajv } from "ajv/dist/ajv.js";
6
+ import { AppError } from "../shared/errors.js";
7
+ import { engineSnapshotChecksum } from "./engines.js";
8
+ import { readBoundedReleaseFile } from "./release-source.js";
9
+ import { shellArgument } from "./runtime-command.js";
10
+ import { hybridDigest } from "./hybrid-contracts.js";
11
+ import { isMemoryInitResourceStorageFacts } from "./memory-init-resource-storage.js";
12
+ import { memoryInitStartupInputSchemas } from "./memory-init-startup-custody.js";
13
+ function storageProbeRefuse(reason) {
14
+ throw new AppError("frozen_engine_storage_incompatible", "Selected frozen engine cannot admit memory-init storage", 1, { reason, effect: "no_effect" });
15
+ }
16
+ const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
17
+ function refuse(reason) {
18
+ throw new AppError("frozen_engine_invalid", "Frozen engine cannot be admitted", 1, { reason, phase: "prepare", effect: "no_effect" });
19
+ }
20
+ function json(bytes) {
21
+ try {
22
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
23
+ }
24
+ catch {
25
+ return refuse("invalid_utf8_json");
26
+ }
27
+ }
28
+ function shape(name, value, property) {
29
+ const schema = JSON.parse(fs.readFileSync(new URL(`../schemas/${name}.schema.json`, import.meta.url), "utf8"));
30
+ return new Ajv({ strict: false, validateFormats: false }).compile(property ? schema.properties[property] : schema)(value) === true;
31
+ }
32
+ function containedFile(root, relative) {
33
+ if (path.isAbsolute(relative) || relative.split(/[\\/]/).some(part => part === ".." || part === "." || !part))
34
+ refuse("escaping_engine_path");
35
+ const parts = relative.split(/[\\/]/);
36
+ let current = root;
37
+ for (const [index, part] of parts.entries()) {
38
+ current = path.join(current, part);
39
+ const stat = fs.lstatSync(current);
40
+ if (stat.isSymbolicLink() || (index === parts.length - 1 ? !stat.isFile() : !stat.isDirectory()))
41
+ refuse("non_regular_engine_path");
42
+ }
43
+ return current;
44
+ }
45
+ /** New-bundle admission only. It does not replace historical binding or storage admission. */
46
+ export class AdmittedFrozenEngine {
47
+ manifestSha256;
48
+ entrypoint;
49
+ nodeExecutable;
50
+ #manifest;
51
+ #bytes;
52
+ constructor(manifest, bytes, entrypoint) {
53
+ this.#manifest = manifest;
54
+ this.#bytes = bytes;
55
+ this.manifestSha256 = hash(bytes);
56
+ this.entrypoint = entrypoint;
57
+ this.nodeExecutable = fs.realpathSync(process.execPath);
58
+ Object.freeze(this);
59
+ }
60
+ /** Both hashes are independently selected by the RUN/bundle authority. No subprocess probe. */
61
+ static admit(binding, expectedManifestSha256) {
62
+ if (!shape("run-engine-binding", binding) || !/^[a-f0-9]{64}$/.test(expectedManifestSha256))
63
+ refuse("invalid_binding_identity");
64
+ return AdmittedFrozenEngine.fromIdentity(binding.engine, expectedManifestSha256);
65
+ }
66
+ /** Pre-RUN selection: no invented RUN id, binding or allocation authority. */
67
+ static admitSelection(identity, expectedManifestSha256) {
68
+ if (!shape("run-engine-binding", identity, "engine") || !/^[a-f0-9]{64}$/.test(expectedManifestSha256))
69
+ refuse("invalid_selection_identity");
70
+ return AdmittedFrozenEngine.fromIdentity(identity, expectedManifestSha256);
71
+ }
72
+ static fromIdentity(identity, expectedManifestSha256) {
73
+ try {
74
+ const root = path.resolve(identity.snapshot_root);
75
+ if (!path.isAbsolute(identity.snapshot_root) || !fs.lstatSync(root).isDirectory())
76
+ refuse("missing_or_linked_snapshot");
77
+ const bytes = readBoundedReleaseFile(path.join(root, "engine.json"), 1024 * 1024);
78
+ if (hash(bytes) !== expectedManifestSha256)
79
+ refuse("manifest_bytes_changed");
80
+ const value = json(bytes);
81
+ if (!shape("engine-manifest", value))
82
+ refuse("invalid_manifest_shape");
83
+ const manifest = value;
84
+ if (manifest.package_name !== identity.package_name || manifest.package_version !== identity.package_version
85
+ || manifest.engine_version !== identity.engine_version || path.resolve(manifest.snapshot_root) !== root
86
+ || manifest.integrity.mode !== "full_content" || manifest.integrity.checksum !== identity.integrity_checksum)
87
+ refuse("foreign_manifest_identity");
88
+ if (engineSnapshotChecksum(root) !== identity.integrity_checksum)
89
+ refuse("executable_snapshot_changed");
90
+ const entrypoint = containedFile(root, manifest.entrypoint);
91
+ if (!manifest.schema_registry || manifest.schema_registry.entries.length === 0)
92
+ refuse("missing_schema_registry");
93
+ const names = new Set(), paths = new Set();
94
+ for (const entry of manifest.schema_registry.entries) {
95
+ if (names.has(entry.name) || paths.has(entry.path))
96
+ refuse("ambiguous_schema_registry");
97
+ names.add(entry.name);
98
+ paths.add(entry.path);
99
+ const schemaBytes = readBoundedReleaseFile(containedFile(root, entry.path), 1024 * 1024);
100
+ if (hash(schemaBytes) !== entry.checksum || json(schemaBytes)?.$id !== entry.id)
101
+ refuse("schema_registry_identity_changed");
102
+ }
103
+ return new AdmittedFrozenEngine(manifest, bytes, entrypoint);
104
+ }
105
+ catch (error) {
106
+ if (error instanceof AppError && error.code === "frozen_engine_invalid")
107
+ throw error;
108
+ return refuse("snapshot_unavailable_or_unreadable");
109
+ }
110
+ }
111
+ manifest() { return structuredClone(this.#manifest); }
112
+ manifestBytes() { return Buffer.from(this.#bytes); }
113
+ /** Qualify actual descriptor handlers in this immutable executable, not the
114
+ * invoking CLI catalog. No storage or productive operation is admitted. */
115
+ admitMemoryInitFlow(source, startupInputSchema) {
116
+ const fail = (reason) => {
117
+ throw new AppError("frozen_engine_flow_incompatible", "Selected frozen engine cannot admit memory-init handlers", 1, { reason, phase: "prepare", effect: "no_effect" });
118
+ };
119
+ const revalidate = () => {
120
+ try {
121
+ if (hash(readBoundedReleaseFile(path.join(this.#manifest.snapshot_root, "engine.json"), 1024 * 1024)) !== this.manifestSha256
122
+ || engineSnapshotChecksum(this.#manifest.snapshot_root) !== this.#manifest.integrity.checksum)
123
+ fail("engine_changed_during_admission");
124
+ // Historical checksums exclude every engine.json path, including a
125
+ // nested directory. New memory execution must not admit unhashed code;
126
+ // do not change the checksum algorithm used by existing SDLC RUNs.
127
+ if (fs.readdirSync(this.#manifest.snapshot_root, { recursive: true, withFileTypes: true })
128
+ .some(entry => entry.name === "engine.json" && path.resolve(entry.parentPath) !== this.#manifest.snapshot_root))
129
+ fail("unhashed_engine_path");
130
+ }
131
+ catch (error) {
132
+ if (error instanceof AppError && error.code === "frozen_engine_flow_incompatible")
133
+ throw error;
134
+ fail("engine_unavailable_during_admission");
135
+ }
136
+ };
137
+ if (startupInputSchema !== undefined && !memoryInitStartupInputSchemas.includes(startupInputSchema))
138
+ fail("unsupported_startup_input_schema");
139
+ const input = Buffer.from(JSON.stringify({ source: source.snapshot(), source_sha256: source.sha256,
140
+ ...(startupInputSchema === undefined ? {} : { startup_input_schema: startupInputSchema }) }));
141
+ if (input.length > 64 * 1024 * 1024)
142
+ fail("unbounded_source");
143
+ const expected = { schema_id: startupInputSchema === undefined ? "dd-flow/memory-init-engine-flow@1" : "dd-flow/memory-init-engine-flow@2",
144
+ ...(startupInputSchema === undefined ? {} : { startup_input_schema: startupInputSchema }),
145
+ flow_id: "mb-init", source_sha256: source.sha256,
146
+ descriptor_sha256: source.descriptorReference().sha256, stages: source.resolveRegisteredFlow().descriptor.stages.map(stage => stage.id) };
147
+ revalidate();
148
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key) && !key.startsWith("DD_FLOW_")));
149
+ const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "flow", "admit", "--operation", "memory-init", "--json"], { input, cwd: this.#manifest.snapshot_root, env: { ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
150
+ timeout: 15000, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
151
+ if (result.error || result.signal || result.status !== 0)
152
+ fail("selected_engine_probe_failed");
153
+ revalidate();
154
+ let response;
155
+ try {
156
+ response = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(result.stdout));
157
+ }
158
+ catch {
159
+ return fail("invalid_probe_json");
160
+ }
161
+ if (!response || typeof response !== "object" || Array.isArray(response) || Object.keys(response).sort().join() !== "action,facts,ok,sha256"
162
+ || response.ok !== true || response.action !== "runtime_flow_admit" || !response.facts || typeof response.facts !== "object" || Array.isArray(response.facts)
163
+ || hybridDigest(response.facts) !== hybridDigest(expected)
164
+ || response.sha256 !== hybridDigest(expected))
165
+ fail("invalid_probe_response");
166
+ return { facts: structuredClone(expected), sha256: hybridDigest(expected), engine_manifest_sha256: this.manifestSha256,
167
+ engine_sha256: this.#manifest.integrity.checksum };
168
+ }
169
+ /** Execute admission in the selected immutable engine, never in the invoking CLI.
170
+ * This qualifies storage connections only, not provider or flow readiness.
171
+ */
172
+ admitMemoryInitStorage(home, resourceHome) {
173
+ for (const root of [home, resourceHome]) {
174
+ if (!path.isAbsolute(root) || path.resolve(root) !== root || fs.realpathSync(root) !== root)
175
+ storageProbeRefuse("noncanonical_storage_home");
176
+ }
177
+ const revalidateEngine = () => {
178
+ if (hash(readBoundedReleaseFile(path.join(this.#manifest.snapshot_root, "engine.json"), 1024 * 1024)) !== this.manifestSha256
179
+ || engineSnapshotChecksum(this.#manifest.snapshot_root) !== this.#manifest.integrity.checksum)
180
+ storageProbeRefuse("engine_changed_during_admission");
181
+ };
182
+ revalidateEngine();
183
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key)));
184
+ const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "storage", "admit", "--operation", "memory-init", "--json"], { env: { ...env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
185
+ timeout: 15000, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
186
+ if (result.error || result.signal || result.status !== 0)
187
+ storageProbeRefuse("selected_engine_probe_failed");
188
+ revalidateEngine();
189
+ let response;
190
+ try {
191
+ response = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(result.stdout));
192
+ }
193
+ catch {
194
+ return storageProbeRefuse("invalid_probe_json");
195
+ }
196
+ if (!response || typeof response !== "object" || Array.isArray(response) || Object.keys(response).sort().join() !== "action,facts,ok,sha256"
197
+ || response.ok !== true || response.action !== "runtime_storage_admit")
198
+ storageProbeRefuse("invalid_probe_response");
199
+ const facts = response.facts, run = facts?.run;
200
+ if (!facts || Object.keys(facts).sort().join() !== "operation,outcome,resource,route,run,schema_id"
201
+ || facts.schema_id !== "dd-flow/memory-init-engine-storage@1" || facts.operation !== "memory-init"
202
+ || facts.route !== "verified-no-op" || facts.outcome !== "no-op" || response.sha256 !== hybridDigest(facts)
203
+ || !isMemoryInitResourceStorageFacts(facts.resource) || facts.resource.store_path !== path.join(resourceHome, "runtime.sqlite")
204
+ || !run || Object.keys(run).sort().join() !== "executing_writer_version,operation,outcome,route,schema_id,schema_sha256,store_dev,store_ino,store_kind,store_path"
205
+ || run.schema_id !== "dd-flow/memory-init-runtime-storage@1" || run.operation !== "memory-init" || run.store_kind !== "run"
206
+ || run.store_path !== path.join(home, "db.sqlite") || run.route !== "verified-no-op" || run.outcome !== "no-op"
207
+ || run.executing_writer_version !== 2 || typeof run.schema_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(run.schema_sha256))
208
+ storageProbeRefuse("invalid_probe_facts");
209
+ for (const store of [run, facts.resource]) {
210
+ const stat = fs.lstatSync(store.store_path);
211
+ if (!stat.isFile() || stat.nlink !== 1 || stat.dev !== store.store_dev || stat.ino !== store.store_ino)
212
+ storageProbeRefuse("storage_identity_changed");
213
+ }
214
+ return { facts: structuredClone(facts), sha256: response.sha256, engine_manifest_sha256: this.manifestSha256,
215
+ engine_sha256: this.#manifest.integrity.checksum };
216
+ }
217
+ /** Read an exact registered schema from this snapshot, never the mutable CLI. */
218
+ schema(name) {
219
+ try {
220
+ if (!fs.lstatSync(this.#manifest.snapshot_root).isDirectory())
221
+ refuse("missing_or_linked_snapshot");
222
+ const entry = this.#manifest.schema_registry?.entries.find(item => item.name === name);
223
+ if (!entry)
224
+ refuse("schema_not_registered");
225
+ const bytes = readBoundedReleaseFile(containedFile(this.#manifest.snapshot_root, entry.path), 1024 * 1024);
226
+ if (hash(bytes) !== entry.checksum || json(bytes).$id !== entry.id)
227
+ refuse("schema_registry_identity_changed");
228
+ return Buffer.from(bytes);
229
+ }
230
+ catch (error) {
231
+ if (error instanceof AppError && error.code === "frozen_engine_invalid")
232
+ throw error;
233
+ return refuse("schema_unavailable");
234
+ }
235
+ }
236
+ /** No PATH/dd-flow fallback or mutable launcher environment reference. */
237
+ command(home, args) {
238
+ if (!path.isAbsolute(home))
239
+ refuse("runtime_home_not_absolute");
240
+ return `DD_FLOW_HOME=${shellArgument(home)} DD_FLOW_ENGINE_MODE=1 DD_FLOW_ENGINE_HOME=${shellArgument(this.#manifest.snapshot_root)} ${[this.nodeExecutable, this.entrypoint, ...args].map(shellArgument).join(" ")}`;
241
+ }
242
+ }