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

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