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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +197 -0
  7. package/dist/cli/command-option-descriptions.js +191 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/error-output.js +52 -0
  11. package/dist/cli/executor-onboarding.js +76 -0
  12. package/dist/cli/flows.js +29 -0
  13. package/dist/cli/harnesses.js +84 -0
  14. package/dist/cli/help.js +240 -6
  15. package/dist/cli/input-preparation.js +54 -5
  16. package/dist/cli/memory-flow-admission.js +44 -0
  17. package/dist/cli/memory-init-invocation.js +78 -0
  18. package/dist/cli/memory-init.js +240 -0
  19. package/dist/cli/memory-storage-admission.js +53 -0
  20. package/dist/cli/models.js +39 -0
  21. package/dist/cli/native-hook-ingress.js +1 -1
  22. package/dist/cli/operator-profiles.js +61 -0
  23. package/dist/cli/output-options.js +35 -0
  24. package/dist/cli/response-file.js +71 -0
  25. package/dist/cli/run-cli.js +283 -160
  26. package/dist/cli/runtime-process.js +1 -1
  27. package/dist/cli/setup.js +149 -0
  28. package/dist/cli.js +56 -2
  29. package/dist/domain/stage-handler-identities.js +20 -0
  30. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  32. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +53 -22
  33. package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
  34. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +12 -6
  35. package/dist/harness-runtime/lib/dd-codex.mjs +24 -12
  36. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  37. package/dist/harness-runtime/lib/dd-droid.mjs +32 -9
  38. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +11 -3
  39. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +100 -15
  40. package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
  41. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +22 -10
  42. package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
  43. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  44. package/dist/harness-runtime/lib/delegation-instructions.mjs +16 -3
  45. package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
  46. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  47. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  48. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  49. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  50. package/dist/harness-runtime/lib/native-children.d.mts +2 -0
  51. package/dist/harness-runtime/lib/native-children.mjs +20 -1
  52. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  53. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  54. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  55. package/dist/harness-runtime/lib/opencode-plugin.mjs +32 -9
  56. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  57. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
  58. package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
  59. package/dist/runtime/context.js +1 -1
  60. package/dist/schemas/discovery-result.schema.json +184 -0
  61. package/dist/schemas/execution-bundle.schema.json +205 -0
  62. package/dist/schemas/execution-settings.schema.json +162 -0
  63. package/dist/schemas/flow-descriptor.schema.json +54 -0
  64. package/dist/schemas/flow-run.schema.json +26 -0
  65. package/dist/schemas/harness-catalog.schema.json +80 -0
  66. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  67. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  68. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  69. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  70. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  71. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  72. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  73. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  74. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  75. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  76. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  77. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  78. package/dist/schemas/model-catalog.schema.json +153 -0
  79. package/dist/schemas/profile-settings.schema.json +122 -0
  80. package/dist/schemas/release-manifest.schema.json +291 -0
  81. package/dist/schemas/run-engine-binding.schema.json +1 -1
  82. package/dist/services/check-runner-evidence.js +51 -4
  83. package/dist/services/cli-operation-classifier.js +8 -47
  84. package/dist/services/cli-upgrade-allowlist.js +27 -0
  85. package/dist/services/code-checks.js +387 -34
  86. package/dist/services/controller-child-capacity.js +13 -0
  87. package/dist/services/controller-child-observer.js +18 -2
  88. package/dist/services/controller-execution.js +88 -0
  89. package/dist/services/controller-fanout.js +71 -12
  90. package/dist/services/dashboard.js +8 -2
  91. package/dist/services/descriptor-coordinator-session.js +35 -0
  92. package/dist/services/descriptor-root-work.js +53 -0
  93. package/dist/services/descriptor-run-settlement.js +105 -0
  94. package/dist/services/descriptor-run-start.js +449 -0
  95. package/dist/services/descriptor-stage-coordinator.js +197 -0
  96. package/dist/services/descriptor-stage-settlement.js +222 -0
  97. package/dist/services/descriptor-stage-start.js +139 -0
  98. package/dist/services/discovery/agy-discovery.js +2 -0
  99. package/dist/services/discovery/codex-discovery.js +2 -0
  100. package/dist/services/discovery/droid-discovery.js +2 -0
  101. package/dist/services/discovery/grok-discovery.js +2 -0
  102. package/dist/services/discovery/opencode-discovery.js +2 -0
  103. package/dist/services/discovery/static.js +31 -0
  104. package/dist/services/discovery/zcode-discovery.js +2 -0
  105. package/dist/services/engines.js +39 -22
  106. package/dist/services/eval-snapshots.js +83 -25
  107. package/dist/services/execution-bundles.js +263 -0
  108. package/dist/services/execution-policy.js +20 -16
  109. package/dist/services/executor-onboarding.js +56 -0
  110. package/dist/services/external-work-launch.js +93 -15
  111. package/dist/services/flow-catalog.js +21 -0
  112. package/dist/services/flow-descriptors.js +82 -0
  113. package/dist/services/frozen-engine.js +250 -0
  114. package/dist/services/generic-execution.js +164 -0
  115. package/dist/services/harness-adapter.js +28 -2
  116. package/dist/services/harness-discovery.js +29 -0
  117. package/dist/services/harness-doctor.js +75 -0
  118. package/dist/services/harness-probe-effects.js +51 -0
  119. package/dist/services/harness-version-probe.js +100 -0
  120. package/dist/services/hooks.js +24 -6
  121. package/dist/services/hybrid-contracts.js +483 -0
  122. package/dist/services/ids.js +29 -1
  123. package/dist/services/installation-lock.js +95 -0
  124. package/dist/services/installed-flow-source.js +183 -0
  125. package/dist/services/lifecycle-invocations.js +257 -78
  126. package/dist/services/managed-processes.js +31 -10
  127. package/dist/services/memory-accepted-composition.js +77 -0
  128. package/dist/services/memory-baseline-preservation.js +65 -0
  129. package/dist/services/memory-completed-correction-wave.js +320 -0
  130. package/dist/services/memory-compose-handler.js +258 -0
  131. package/dist/services/memory-composition-acceptance.js +199 -0
  132. package/dist/services/memory-composition-graph.js +134 -0
  133. package/dist/services/memory-composition-registration.js +146 -0
  134. package/dist/services/memory-composition-work-custody.js +151 -0
  135. package/dist/services/memory-composition-work-results.js +96 -0
  136. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  137. package/dist/services/memory-corrected-review-batch.js +30 -0
  138. package/dist/services/memory-corrected-review-findings.js +106 -0
  139. package/dist/services/memory-corrected-review-inputs.js +210 -0
  140. package/dist/services/memory-corrected-review-projection.js +78 -0
  141. package/dist/services/memory-corrected-review-receipt.js +170 -0
  142. package/dist/services/memory-corrected-review-registration.js +191 -0
  143. package/dist/services/memory-correction-check-receipt.js +219 -0
  144. package/dist/services/memory-correction-input-owner.js +6 -0
  145. package/dist/services/memory-discover-handler.js +104 -0
  146. package/dist/services/memory-finish-evidence.js +435 -0
  147. package/dist/services/memory-finish-handler.js +215 -0
  148. package/dist/services/memory-finish-integration.js +280 -0
  149. package/dist/services/memory-finish-readiness.js +122 -0
  150. package/dist/services/memory-git-integration.js +151 -0
  151. package/dist/services/memory-historical-corrected-review.js +206 -0
  152. package/dist/services/memory-historical-original-review.js +130 -0
  153. package/dist/services/memory-historical-repair-registration.js +99 -0
  154. package/dist/services/memory-historical-second-review.js +128 -0
  155. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  156. package/dist/services/memory-init-contexts.js +89 -0
  157. package/dist/services/memory-init-controller-start.js +84 -0
  158. package/dist/services/memory-init-engine-release.js +223 -0
  159. package/dist/services/memory-init-git-policy.js +136 -0
  160. package/dist/services/memory-init-lint-capability.js +185 -0
  161. package/dist/services/memory-init-native-capacity.js +296 -0
  162. package/dist/services/memory-init-preflight.js +330 -0
  163. package/dist/services/memory-init-project-bootstrap.js +599 -0
  164. package/dist/services/memory-init-request-candidate.js +157 -0
  165. package/dist/services/memory-init-request-input.js +66 -0
  166. package/dist/services/memory-init-resource-storage.js +125 -0
  167. package/dist/services/memory-init-run-allocation.js +81 -0
  168. package/dist/services/memory-init-run-bundle.js +98 -0
  169. package/dist/services/memory-init-start.js +181 -0
  170. package/dist/services/memory-init-startup-custody.js +80 -0
  171. package/dist/services/memory-init-startup-input.js +240 -0
  172. package/dist/services/memory-init-storage.js +81 -0
  173. package/dist/services/memory-init-stores.js +359 -0
  174. package/dist/services/memory-init-worktrunk-capability.js +178 -0
  175. package/dist/services/memory-layer-publication.js +611 -0
  176. package/dist/services/memory-layer.js +156 -0
  177. package/dist/services/memory-lint-config.js +85 -0
  178. package/dist/services/memory-native-review-copy.js +165 -0
  179. package/dist/services/memory-normal-flow-entry.js +75 -0
  180. package/dist/services/memory-ordinary-defaults.js +112 -0
  181. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  182. package/dist/services/memory-preflight-handler.js +259 -0
  183. package/dist/services/memory-repair-current-view.js +299 -0
  184. package/dist/services/memory-repair-document-delta.js +451 -0
  185. package/dist/services/memory-repair-history.js +491 -0
  186. package/dist/services/memory-repair-physical-binding.js +312 -0
  187. package/dist/services/memory-repair-publication-files.js +152 -0
  188. package/dist/services/memory-repair-publication-intent.js +27 -0
  189. package/dist/services/memory-repair-publication-plan.js +160 -0
  190. package/dist/services/memory-repair-publication-source.js +225 -0
  191. package/dist/services/memory-repair-publication.js +794 -0
  192. package/dist/services/memory-repair-registration.js +239 -0
  193. package/dist/services/memory-repair-work-baseline.js +413 -0
  194. package/dist/services/memory-repair-work-completion.js +97 -0
  195. package/dist/services/memory-repair-work-custody.js +73 -0
  196. package/dist/services/memory-repair-work-results.js +89 -0
  197. package/dist/services/memory-repair-workspace.js +252 -0
  198. package/dist/services/memory-repair-writer-copy.js +735 -0
  199. package/dist/services/memory-result-binding.js +173 -0
  200. package/dist/services/memory-retained-review-authority.js +129 -0
  201. package/dist/services/memory-retained-review-results.js +70 -0
  202. package/dist/services/memory-retained-second-baseline.js +102 -0
  203. package/dist/services/memory-retained-second-repairs.js +156 -0
  204. package/dist/services/memory-review-acceptance.js +214 -0
  205. package/dist/services/memory-review-correction-plan.js +171 -0
  206. package/dist/services/memory-review-handler.js +585 -0
  207. package/dist/services/memory-review-inputs.js +200 -0
  208. package/dist/services/memory-review-receipt.js +180 -0
  209. package/dist/services/memory-review-registration.js +108 -0
  210. package/dist/services/memory-review-work-custody.js +124 -0
  211. package/dist/services/memory-review-work-input.js +72 -0
  212. package/dist/services/memory-review-work-results.js +73 -0
  213. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  214. package/dist/services/memory-second-correction-plan.js +76 -0
  215. package/dist/services/memory-second-repair-baseline.js +91 -0
  216. package/dist/services/memory-second-repair-launch.js +614 -0
  217. package/dist/services/memory-second-repair-registration.js +88 -0
  218. package/dist/services/memory-second-repair-source.js +187 -0
  219. package/dist/services/memory-second-review-acceptance.js +115 -0
  220. package/dist/services/memory-source-observations.js +117 -0
  221. package/dist/services/memory-stage-dispatch.js +28 -0
  222. package/dist/services/memory-stage-results.js +199 -0
  223. package/dist/services/memory-verification-acceptance.js +135 -0
  224. package/dist/services/memory-verification-checks.js +252 -0
  225. package/dist/services/memory-verification-receipt.js +159 -0
  226. package/dist/services/memory-verify-handler.js +135 -0
  227. package/dist/services/memory-work-result-preparation.js +63 -0
  228. package/dist/services/merge-server.js +2 -4
  229. package/dist/services/model-catalog.js +109 -0
  230. package/dist/services/model-discovery.js +46 -0
  231. package/dist/services/operator-profiles.js +236 -0
  232. package/dist/services/projects.js +17 -2
  233. package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
  234. package/dist/services/recovery-snapshot-database.js +417 -2
  235. package/dist/services/release-archive-producer.js +52 -0
  236. package/dist/services/release-archive.js +143 -0
  237. package/dist/services/release-cache.js +142 -0
  238. package/dist/services/release-compatibility.js +18 -0
  239. package/dist/services/release-download.js +67 -0
  240. package/dist/services/release-envelope.js +104 -0
  241. package/dist/services/release-manifest.js +45 -0
  242. package/dist/services/release-payload.js +114 -0
  243. package/dist/services/release-producer.js +63 -0
  244. package/dist/services/release-provenance.js +160 -0
  245. package/dist/services/release-registry.js +66 -0
  246. package/dist/services/release-source.js +194 -0
  247. package/dist/services/repair-intents.js +1 -1
  248. package/dist/services/review-copy.js +10 -2
  249. package/dist/services/run-control.js +80 -9
  250. package/dist/services/run-controller-adapter.js +1 -1
  251. package/dist/services/run-controller-capture.js +10 -3
  252. package/dist/services/run-controller-process.js +21 -0
  253. package/dist/services/run-controller-recovery.js +5 -5
  254. package/dist/services/run-controller-state.js +1 -1
  255. package/dist/services/run-controller.js +158 -64
  256. package/dist/services/run-recovery-guard.js +5 -3
  257. package/dist/services/run-recovery-runtime.js +3 -3
  258. package/dist/services/run-recovery.js +145 -5
  259. package/dist/services/run-start-custody.js +177 -0
  260. package/dist/services/runs.js +413 -30
  261. package/dist/services/setup.js +377 -0
  262. package/dist/services/stage-context.js +9 -1
  263. package/dist/services/stage-handlers.js +74 -0
  264. package/dist/services/stage-report-renderer.js +63 -0
  265. package/dist/services/stage-work-graph.js +17 -0
  266. package/dist/services/vnext-code-review.js +1 -1
  267. package/dist/services/vnext-code.js +6 -6
  268. package/dist/services/vnext-execution-profile.js +2 -2
  269. package/dist/services/vnext-fanout.js +73 -11
  270. package/dist/services/vnext-merge.js +8 -3
  271. package/dist/services/vnext-plan-review.js +7 -5
  272. package/dist/services/vnext-plan.js +3 -3
  273. package/dist/services/vnext-protocolize.js +20 -6
  274. package/dist/services/vnext-workspace-policy.js +7 -2
  275. package/dist/services/work-registry.js +616 -71
  276. package/dist/services/workspace-files.js +1 -1
  277. package/dist/storage/database.js +84 -6
  278. package/dist/storage/writer-contract.js +11 -7
  279. package/package.json +13 -3
  280. package/tools/create-canon-release.mjs +33 -0
  281. package/tools/produce-hybrid-release.mjs +41 -0
  282. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
  283. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
@@ -4,9 +4,10 @@ import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { withProjectionLock } from "../storage/projection-lock.js";
6
6
  import { reconcileRunStageReports } from "./stage-report-renderer.js";
7
- import { formatFullId, isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
7
+ import { isFullEntityId, isShortEntityId, parseFullEntityId } from "../domain/entity-ids.js";
8
8
  import { checksumForFlowFlagValues, isFlowFlagDowngrade, loadCanonicalFlowContract, loadProjectFlowContract, resolveFlowFlags } from "../domain/flow-contract.js";
9
9
  import { AppError } from "../shared/errors.js";
10
+ import { nextRunId } from "./ids.js";
10
11
  import { ensureDir, canonicalPath, projectRunHome, projectRunJsonPath, resolveProjectRoot } from "../storage/paths.js";
11
12
  import { appendAudit } from "./audit.js";
12
13
  import { registerProject, requireProjectByRoot } from "./projects.js";
@@ -22,9 +23,12 @@ import { assertSameSessionMerge, loadExecutionRoutingInput, resolveStageTransiti
22
23
  import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
23
24
  import { publicSessionIdentity } from "./session-identity.js";
24
25
  import { flowCommand } from "./stage-pause.js";
26
+ import { RetainedExecutionBundle } from "./execution-bundles.js";
27
+ import { admitBundledFlow, bundledHandlerContext } from "./stage-handlers.js";
28
+ import { hybridDigest } from "./hybrid-contracts.js";
29
+ import { projectPacketLocators, runContextRelocations } from "./stage-context.js";
25
30
  const runSchemaId = "dd-flow/flow-run@3";
26
31
  const runtimeSchemaId = "dd-flow/flow-run@3";
27
- const runIdType = "RUN";
28
32
  const allowedRunFlowKinds = [
29
33
  "mb_sdlc",
30
34
  "coding",
@@ -231,7 +235,7 @@ function snapshotVnextExecutionProfile(projectRoot, ddFlowHome, routingInput, no
231
235
  merge_delivery: profile.merge_delivery,
232
236
  merge_cleanup: profile.merge_cleanup,
233
237
  stop_target: profile.stop_target,
234
- code_bootstrap: profile.code_bootstrap,
238
+ ...(profile.code_bootstrap ? { code_bootstrap: profile.code_bootstrap } : {}),
235
239
  ...(profile.execution ? { execution: profile.execution } : {})
236
240
  }
237
241
  }, routingInput);
@@ -281,20 +285,82 @@ export function importFlowRunExecutionProfile(context, input) {
281
285
  export function rebindFlowRunWorkspace(context, input) {
282
286
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
283
287
  const run = resolveRun(context, project.id, input.runId);
284
- assertRunMutationAllowed(context, project.id, run.id);
288
+ const generation = assertRunMutationAllowed(context, project.id, run.id, input.expectedGeneration);
285
289
  const workspaceRoot = resolveWorkspaceRoot(input.workspaceRoot);
286
290
  const index = authoritativeIndex(run);
291
+ const receipt = input.routeReceipt ? { ...input.routeReceipt, policy: { ...input.routeReceipt.policy,
292
+ worktree_path: input.routeReceipt.policy.worktree_path ? resolveWorkspaceRoot(input.routeReceipt.policy.worktree_path) : null } } : undefined;
293
+ if (index.execution_bundle && !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)) {
294
+ if (workspaceRoot === run.workspace_root)
295
+ return { ok: true, changed: false };
296
+ throw new AppError("workspace_route_invalid", "Only SDLC may provision a different RUN workspace", 1, { effect: "no_effect" });
297
+ }
298
+ if (workspaceRoot === run.workspace_root) {
299
+ if (receipt && index.workspace_route && hybridDigest(retainedWorkspaceRoute(context, run).receipt) !== hybridDigest(receipt)) {
300
+ throw new AppError("workspace_route_conflict", "Committed workspace route differs from the repeated request", 1, { effect: "no_effect" });
301
+ }
302
+ publishRunProjections(context, project.id, run.id);
303
+ return { ok: true, changed: false, workspace_root: workspaceRoot };
304
+ }
305
+ if (index.workspace_route)
306
+ throw new AppError("workspace_route_conflict", "RUN already has a committed workspace route", 1, { effect: "no_effect" });
307
+ if (input.expectedRevision !== undefined && (index.runtime_revision ?? 0) !== input.expectedRevision)
308
+ throw new AppError("run_revision_changed", "RUN changed while its workspace was provisioned", 1, { effect: "no_effect", expected_revision: input.expectedRevision, actual_revision: index.runtime_revision ?? 0 });
309
+ if (index.execution_bundle && !receipt)
310
+ throw new AppError("workspace_route_missing", "Bundled SDLC rebind requires its prepared route receipt", 1, { effect: "no_effect" });
311
+ if (receipt && (run.workspace_root !== project.root || receipt.schema_id !== "dd-flow/vnext-workspace-route@1" || receipt.policy.route !== "feature_worktree"
312
+ || !receipt.policy.provisioned || receipt.policy.worktree_path !== workspaceRoot || !receipt.created || !receipt.bootstrap)) {
313
+ throw new AppError("workspace_route_invalid", "Prepared workspace route does not match this RUN", 1, { effect: "no_effect" });
314
+ }
287
315
  const now = context.now();
288
316
  const facts = gitFacts(workspaceRoot);
317
+ if (receipt && (facts.branch !== receipt.policy.feature_branch || !receipt.policy.base_ref || !facts.head
318
+ || spawnSync("git", ["-C", workspaceRoot, "merge-base", "--is-ancestor", receipt.policy.base_ref, facts.head]).status !== 0)) {
319
+ throw new AppError("workspace_route_invalid", "Prepared checkout differs from the feature branch/base receipt", 1, { effect: "no_effect", run_id: run.id });
320
+ }
289
321
  index.workspace = { ...index.workspace, workspace_path: workspaceRoot, branch: facts.branch ?? "unknown", base: facts.head ?? "unknown", manager: "official-worktrunk" };
290
322
  index.execution = { ...index.execution, project_root: project.root, workspace_root: workspaceRoot, git: facts };
291
323
  index.updated_at = now;
292
- context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
293
- persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
294
- appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason } });
295
- appendRunTimeline(context, project.id, run.id, { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
324
+ const route = receipt ? { schema_id: "dd-flow/run-workspace-route@1", run_id: run.id, project_id: project.id, project_root: project.root,
325
+ generation, previous_workspace: run.workspace_root, workspace_root: workspaceRoot, revision: (index.runtime_revision ?? 0) + 1,
326
+ git: facts, receipt, receipt_sha256: hybridDigest(receipt) } : undefined;
327
+ if (route)
328
+ index.workspace_route = { revision: route.revision, sha256: hybridDigest(route) };
329
+ context.db.writeTransaction(() => {
330
+ assertRunMutationAllowed(context, project.id, run.id, generation);
331
+ const latest = resolveRun(context, project.id, run.id);
332
+ if (latest.workspace_root !== run.workspace_root)
333
+ throw new AppError("workspace_route_changed", "RUN route changed during preparation", 1, { effect: "no_effect" });
334
+ context.db.run("UPDATE runs SET workspace_root = ? WHERE project_id = ? AND id = ?", [workspaceRoot, project.id, run.id]);
335
+ persistRunState(context, project, { ...run, workspace_root: workspaceRoot }, index);
336
+ appendAudit(context, { projectId: project.id, eventType: "flow_run.workspace_rebound", payload: { run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, ...(route ? { route } : {}) } });
337
+ appendRunTimeline(context, project.id, run.id, { at: now, type: "workspace_rebound", run_id: run.id, workspace_root: workspaceRoot, reason: input.reason, git: facts });
338
+ }, "runs.rebindWorkspace");
296
339
  return { ok: true, run_id: run.id, project_root: project.root, workspace_root: workspaceRoot, git: facts };
297
340
  }
341
+ function retainedWorkspaceRoute(context, run) {
342
+ const index = authoritativeIndex(run), binding = index.workspace_route;
343
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.workspace_rebound' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.route.revision') = ?", [run.project_id, run.id, binding?.revision ?? -1]);
344
+ const original = rows.length === 1 ? JSON.parse(rows[0].payload_json).route : undefined;
345
+ if (!binding || !original || original.schema_id !== "dd-flow/run-workspace-route@1" || hybridDigest(original) !== binding.sha256 || hybridDigest(original.receipt) !== original.receipt_sha256) {
346
+ throw new AppError("workspace_route_invalid", "RUN committed workspace route evidence changed", 1, { effect: "no_effect", run_id: run.id });
347
+ }
348
+ // Relocation receipts are admitted by snapshot import; never rewrite bundle
349
+ // or source route authority merely because a physical checkout moved.
350
+ const route = JSON.parse(projectPacketLocators(JSON.stringify(original), runContextRelocations(context, run.project_id, run.id)));
351
+ if (route.receipt_sha256 !== original.receipt_sha256
352
+ || route.run_id !== run.id || route.project_id !== run.project_id || route.project_root !== run.project_root || route.workspace_root !== run.workspace_root
353
+ || index.workspace.workspace_path !== run.workspace_root || index.execution?.workspace_root !== run.workspace_root || index.execution?.project_root !== run.project_root
354
+ || index.workspace.manager !== "official-worktrunk" || index.workspace.branch !== route.receipt.policy.feature_branch
355
+ // execution.git is the latest observation (stage completion/refresh updates
356
+ // it). The immutable review base is workspace.base, not that moving HEAD.
357
+ || index.workspace.base !== route.git.head
358
+ || !Number.isInteger(route.generation) || route.generation < 0
359
+ || route.receipt.policy.worktree_path !== run.workspace_root) {
360
+ throw new AppError("workspace_route_invalid", "RUN has no exact coherent committed workspace route", 1, { effect: "no_effect", run_id: run.id });
361
+ }
362
+ return route;
363
+ }
298
364
  function loadUpgradeFlowContract(context, projectRoot) {
299
365
  const canon = resolveCanonRoot(context, context.env.DD_MEMORYBANK ? { explicitRoot: context.env.DD_MEMORYBANK } : {});
300
366
  if (!canon.ok || !canon.canon) {
@@ -306,17 +372,19 @@ function loadUpgradeFlowContract(context, projectRoot) {
306
372
  }
307
373
  return loadCanonicalFlowContract(canon.canon.flow_root);
308
374
  }
309
- export function getFlowRunStatus(context, input) {
375
+ export function getFlowRunStatus(context, input, preparedBundle) {
310
376
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
311
377
  const run = resolveRun(context, project.id, input.runId);
378
+ if (preparedBundle)
379
+ assertPreparedRunBundleSqlAuthority(context, { projectId: project.id, runId: run.id }, preparedBundle);
312
380
  const index = authoritativeIndex(run);
313
381
  const runtime = readRuntimeSnapshot(run.runtime_path);
314
- return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
382
+ return { ok: true, run: flowRunSummary(run), index, continuation: runContinuation(context, run, index, preparedBundle), ...(runtime ? { runtime } : {}), flow_guidance: guidanceForRun(context, run, index) };
315
383
  }
316
384
  /** A read-only projection of the next legal lifecycle action.
317
385
  * It is derived from the persisted RUN/Work facts so controllers never infer a
318
386
  * transition from a model's prose or a hard-coded stage array. */
319
- function runContinuation(context, run, index) {
387
+ function runContinuation(context, run, index, preparedBundle) {
320
388
  if (["done", "failed", "cancelled"].includes(run.status))
321
389
  return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: run.status, command: null };
322
390
  const control = runControlDirective(context, run.project_id, run.id);
@@ -332,6 +400,15 @@ function runContinuation(context, run, index) {
332
400
  return { kind: "paused", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
333
401
  if (run.status === "blocked")
334
402
  return { kind: "blocked", stage: index.current_stage ?? null, attempt: null, work_id: null, reason: run.status, command: null };
403
+ if (index.execution_bundle) {
404
+ const bundle = preparedBundle ?? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
405
+ const next = bundledNextStage(bundle.descriptor, index);
406
+ if (!next)
407
+ return { kind: "terminal", stage: null, attempt: null, work_id: null, reason: "flow_complete", command: null };
408
+ const command = bundle.engine().command(context.ddFlowHome, ["stage", "start", run.id, "--stage", next, "--project-root", run.project_root, "--json"]);
409
+ return { kind: "start_stage", stage: next, attempt: null, work_id: null, reason: "legal_successor", command,
410
+ execution: { status: "resolved", ...bundle.execution.stage(next) } };
411
+ }
335
412
  if (run.flow_kind === "vnext_specify" || run.flow_kind === "vnext_protocolize") {
336
413
  const completed = index.stage_runs.filter((stage) => stage.status === "done" || stage.status === "skipped").sort((left, right) => right.order - left.order)[0];
337
414
  const next = completed ? vnextStages.find((stage) => stage.id === completed.stage)?.next.find((candidate) => candidate !== "plan-review" || index.settings?.plan_review?.mode !== "off") : "specify";
@@ -587,14 +664,18 @@ export function prepareFlowRunStageAttachment(context, input, archive = false) {
587
664
  const index = authoritativeIndex(run);
588
665
  const status = parseStageStatus(input.status);
589
666
  const existing = index.stage_runs.find((item) => item.stage === stage);
590
- assertVnextStageStart(run, index, stage, status, existing);
667
+ const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : null;
668
+ if (bundle)
669
+ assertBundledStageStart(bundle.descriptor, index, stage, dir, status, existing);
670
+ else
671
+ assertVnextStageStart(run, index, stage, status, existing);
591
672
  if (archive) {
592
673
  const runRoot = runArtifactRoot(run);
593
674
  if (fs.existsSync(path.join(runRoot, `.archive-${dir}.json`)) || status === "running" && existing) {
594
675
  archiveExistingStageAttempt(context, project.id, run.id, runRoot, dir);
595
676
  }
596
677
  }
597
- return { project, run, index, stage, dir, status, existing, generation, attempt: status === "running" ? nextAttempt(existing) : existing?.attempt ?? "try-001", git: status === "running" ? gitFacts(run.workspace_root) : undefined };
678
+ return { project, run, index, stage, dir, status, existing, generation, bundle, attempt: status === "running" ? nextAttempt(existing) : existing?.attempt ?? "try-001", git: status === "running" ? gitFacts(run.workspace_root) : undefined };
598
679
  }
599
680
  export function attachFlowRunStage(context, input, prepared = prepareFlowRunStageAttachment(context, input, true)) {
600
681
  // Filesystem archive and Git inspection are preparation, never writer work.
@@ -607,7 +688,13 @@ export function attachFlowRunStage(context, input, prepared = prepareFlowRunStag
607
688
  if ((index.runtime_revision ?? 0) !== (prepared.index.runtime_revision ?? 0) || existing?.attempt !== prepared.existing?.attempt) {
608
689
  throw new AppError("stage_revision_changed", "Stage changed during start preparation", 1, { run_id: run.id, stage });
609
690
  }
610
- assertVnextStageStart(latest, index, stage, status, existing);
691
+ if (prepared.bundle) {
692
+ if (index.execution_bundle?.manifest_sha256 !== prepared.bundle.sha256)
693
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle changed during stage preparation", 1);
694
+ assertBundledStageStart(prepared.bundle.descriptor, index, stage, dir, status, existing);
695
+ }
696
+ else
697
+ assertVnextStageStart(latest, index, stage, status, existing);
611
698
  const now = context.now();
612
699
  const stageRun = {
613
700
  ...(existing ?? { order: index.stage_runs.length + 1 }),
@@ -652,7 +739,7 @@ export function attachFlowRunStage(context, input, prepared = prepareFlowRunStag
652
739
  export function withStageSettlement(context, operation) {
653
740
  return context.db.writeTransaction(operation, "stage.settle");
654
741
  }
655
- export function prepareFlowRunStageCompletion(context, input) {
742
+ export function prepareFlowRunStageCompletion(context, input, admittedBundle) {
656
743
  for (const [name, value] of Object.entries({ stageReport: input.stageReport, data: input.data, dataSchemaId: input.dataSchemaId, report: input.report })) {
657
744
  if (value !== undefined)
658
745
  requiredPlain(value, name);
@@ -676,11 +763,37 @@ export function prepareFlowRunStageCompletion(context, input) {
676
763
  if (!existing) {
677
764
  throw new AppError("not_found", `Run stage is not attached: ${stage}`, 1, { run_id: run.id, stage });
678
765
  }
766
+ // Bound completion must admit the same immutable descriptor/engine authority
767
+ // as stage entry. Keep filesystem admission outside the SQLite writer.
768
+ const bundle = admittedBundle === undefined
769
+ ? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : admittedBundle;
770
+ if (bundle && !index.execution_bundle)
771
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared after finish admission", 1);
772
+ if (bundle)
773
+ assertCompletionBundleAuthority(context, run, index);
774
+ if (index.execution_bundle && (!bundle || bundle.sha256 !== index.execution_bundle.manifest_sha256
775
+ || bundle.manifest.descriptor.sha256 !== index.execution_bundle.descriptor_sha256
776
+ || hybridDigest(bundleAuthority(bundle.admission())) !== index.execution_bundle.admission_sha256)) {
777
+ throw new AppError("run_bundle_anchor_invalid", "Prepared finish bundle does not match authoritative RUN admission", 1);
778
+ }
779
+ if (bundle) {
780
+ const declared = bundle.descriptor.stage(stage);
781
+ if (declared.directory !== existing.dir || index.current_stage !== stage) {
782
+ throw new AppError("flow_stage_completion_invalid", "Completion does not belong to the active descriptor stage", 2, { phase: "prepare", effect: "no_effect", run_id: run.id, stage });
783
+ }
784
+ }
679
785
  const dataPath = input.data ? path.join(runArtifactRoot(run), existing.dir, input.data) : null;
680
786
  const dataSha256 = input.dataSha256 ?? (dataPath && fs.existsSync(dataPath) ? crypto.createHash("sha256").update(fs.readFileSync(dataPath)).digest("hex") : null);
681
787
  if (dataSha256 && !/^[a-f0-9]{64}$/.test(dataSha256))
682
788
  throw new AppError("validation", "Stage data SHA-256 is invalid", 2);
683
- return { project, run, index, stage, status, existing, dataSha256, git: input.git ?? gitFacts(run.workspace_root), generation: ["done", "failed", "skipped"].includes(status) ? assertStageSettlementAllowed(context, project.id, run.id, stage) : assertRunMutationAllowed(context, project.id, run.id) };
789
+ return { project, run, index, stage, status, existing, bundle, dataSha256, git: input.git ?? gitFacts(run.workspace_root), generation: ["done", "failed", "skipped"].includes(status) ? assertStageSettlementAllowed(context, project.id, run.id, stage) : assertRunMutationAllowed(context, project.id, run.id) };
790
+ }
791
+ /** Raw historical control is not a finish-sealing bypass for a descriptor-bound RUN. */
792
+ export function prepareRawFlowRunStageCompletion(context, input) {
793
+ const prepared = prepareFlowRunStageCompletion(context, input);
794
+ if (prepared.bundle)
795
+ throw new AppError("stage_finish_required", "Descriptor-bound stages must settle through their registered stage finish handler", 2, { phase: "prepare", effect: "no_effect", run_id: prepared.run.id, stage: prepared.stage });
796
+ return prepared;
684
797
  }
685
798
  function stageUsageProjection(context, run, stage, startedAt) {
686
799
  if (!startedAt)
@@ -705,6 +818,14 @@ export function completeFlowRunStage(context, input, prepared = prepareFlowRunSt
705
818
  if (!existing || existing.attempt !== prepared.existing.attempt || index.runtime_revision !== prepared.index.runtime_revision) {
706
819
  throw new AppError("stage_revision_changed", "Stage attempt changed during finish preparation", 1, { run_id: run.id, stage, expected_attempt: prepared.existing.attempt, actual_attempt: existing?.attempt ?? null });
707
820
  }
821
+ if (hybridDigest(index.execution_bundle ?? null) !== hybridDigest(prepared.index.execution_bundle ?? null)) {
822
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle authority changed during finish preparation", 1);
823
+ }
824
+ if (prepared.bundle && (index.current_stage !== stage || existing.dir !== prepared.bundle.descriptor.stage(stage).directory)) {
825
+ throw new AppError("flow_stage_completion_invalid", "Active descriptor stage changed during finish preparation", 1);
826
+ }
827
+ if (prepared.bundle)
828
+ assertCompletionBundleAuthority(context, latest, index);
708
829
  const now = context.now();
709
830
  const stageRun = {
710
831
  ...existing,
@@ -755,6 +876,42 @@ export function completeFlowRunStage(context, input, prepared = prepareFlowRunSt
755
876
  const updatedRun = requireRunById(context, project.id, run.id);
756
877
  return { ok: true, run: flowRunSummary(updatedRun), stage_run: completed.stageRun, index: completed.index, flow_guidance: guidanceForRun(context, updatedRun, completed.index) };
757
878
  }
879
+ /** Writer-safe readback of independently retained SQL authority; no filesystem admission. */
880
+ function assertCompletionBundleAuthority(context, run, index) {
881
+ const anchor = index.execution_bundle;
882
+ if (!anchor)
883
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor disappeared during finish", 1);
884
+ assertRunBundleSqlAuthority(context, { projectId: run.project_id, runId: run.id }, anchor);
885
+ }
886
+ /** Metadata-only fencing shared by lifecycle ownership and the settlement writer.
887
+ * False means genuine historical authority, never a lost/orphaned bundle anchor.
888
+ * Physical descriptor/engine admission remains an independent outside-writer check.
889
+ */
890
+ export function assertRunBundleSqlAuthority(context, scope, anchor) {
891
+ const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
892
+ const row = hasAuthority ? context.db.get("SELECT manifest_sha256, admission_json FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [scope.projectId, scope.runId]) : undefined;
893
+ if (!anchor) {
894
+ if (row)
895
+ throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
896
+ return false;
897
+ }
898
+ if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || typeof anchor.flow_id !== "string"
899
+ || [anchor.manifest_sha256, anchor.descriptor_sha256, anchor.admission_sha256].some(hash => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash)))
900
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
901
+ if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
902
+ throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority disappeared during finish", 1);
903
+ let valid = false;
904
+ try {
905
+ if (Buffer.byteLength(row.admission_json, "utf8") <= 1024 * 1024) {
906
+ const retained = JSON.parse(row.admission_json);
907
+ valid = retained.runId === scope.runId && hybridDigest(retained) === anchor.admission_sha256;
908
+ }
909
+ }
910
+ catch { /* Corrupt authority cannot be replaced by a prepared object. */ }
911
+ if (!valid)
912
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed during finish", 1);
913
+ return true;
914
+ }
758
915
  /** Pause one live stage without completing it or creating a new attempt. */
759
916
  export function pauseFlowRunStage(context, input) {
760
917
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -1016,6 +1173,41 @@ export function completeFlowRun(context, input) {
1016
1173
  const updatedRun = requireRunById(context, project.id, run.id);
1017
1174
  return { ok: true, run: flowRunSummary(updatedRun), index, usage, flow_guidance: guidanceForRun(context, updatedRun, index) };
1018
1175
  }
1176
+ /** SQL-only terminal writer for an independently prepared descriptor handler.
1177
+ * This closes runtime state, not semantic or Git acceptance. The caller owns
1178
+ * the outer transaction, root settlement and the handler-specific proof fence.
1179
+ */
1180
+ export function completeDescriptorRunState(context, scope, bundle, git) {
1181
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
1182
+ assertRunMutationAllowed(context, scope.projectId, scope.runId);
1183
+ const run = requireRunById(context, scope.projectId, scope.runId);
1184
+ const index = authoritativeIndex(run);
1185
+ const terminal = index.stage_runs.at(-1);
1186
+ if (run.status !== "running" || !terminal || terminal.status !== "done"
1187
+ || !bundle.descriptor.descriptor.terminal_stages.includes(terminal.stage)
1188
+ || index.stage_runs.some(stage => ["running", "paused", "waiting_for_user", "blocked", "pending"].includes(stage.status))
1189
+ || context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND status IN ('created','running','paused') LIMIT 1", [scope.projectId, scope.runId])
1190
+ || context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id WHERE w.project_id=? AND w.run_id=? AND ws.status='running' LIMIT 1", [scope.projectId, scope.runId])) {
1191
+ throw new AppError("descriptor_run_settlement_invalid", "Descriptor RUN is not ready for terminal runtime settlement", 1, { effect: "no_effect" });
1192
+ }
1193
+ const project = context.db.get("SELECT * FROM projects WHERE id=?", [scope.projectId]);
1194
+ if (!project)
1195
+ throw new AppError("not_found", "Project is not registered", 1);
1196
+ const now = context.now();
1197
+ index.status = "done";
1198
+ index.verdict = "accepted";
1199
+ index.next_action = null;
1200
+ index.updated_at = now;
1201
+ index.completed_at = now;
1202
+ index.finished_at = now;
1203
+ index.duration_ms = durationMs(index.started_at ?? run.created_at, now);
1204
+ index.execution = { project_root: run.project_root, workspace_root: run.workspace_root, git: { ...git } };
1205
+ persistRunState(context, project, run, index);
1206
+ closeRunOrchestrators(context, scope.projectId, run, "done", now);
1207
+ refreshRunSessionProjection(context, scope.projectId, scope.runId);
1208
+ appendAudit(context, { projectId: scope.projectId, eventType: "flow_run.completed", payload: { run_id: scope.runId, status: "done", verdict: "accepted", next_action: null } });
1209
+ appendRunTimeline(context, scope.projectId, scope.runId, { at: now, type: "run_completed", run_id: scope.runId, status: "done" });
1210
+ }
1019
1211
  /** Update a live RUN between stages without manufacturing terminal evidence. */
1020
1212
  export function advanceFlowRun(context, input) {
1021
1213
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -1479,6 +1671,17 @@ export function publishRunProjections(context, projectId, runId) {
1479
1671
  if (!latest)
1480
1672
  throw new AppError("not_found", "RUN is not registered", 1, { run_id: runId });
1481
1673
  const index = authoritativeIndex(latest);
1674
+ if (index.workspace_route) {
1675
+ const route = retainedWorkspaceRoute(context, latest);
1676
+ const file = path.join(runArtifactRoot(latest), "02-protocolize", "workspace-route.json");
1677
+ ensureDir(path.dirname(file));
1678
+ try {
1679
+ writeJsonFile(file, route.receipt);
1680
+ }
1681
+ catch (error) {
1682
+ throw new AppError("workspace_route_publication_failed", "Committed route projection could not be published", 1, { effect: "committed", run_id: runId, route_sha256: index.workspace_route.sha256, cause: String(error) });
1683
+ }
1684
+ }
1482
1685
  const mutations = context.db.get("SELECT 1 FROM sqlite_schema WHERE name = 'flow_run_flag_mutations'") ? context.db.all("SELECT resulting_revision, result_json, created_at, idempotency_key FROM flow_run_flag_mutations WHERE project_id = ? AND run_id = ? ORDER BY resulting_revision DESC LIMIT 32", [projectId, runId]) : [];
1483
1686
  const history = mutations.reverse().map((row) => ({
1484
1687
  revision: row.resulting_revision,
@@ -1524,12 +1727,187 @@ function authoritativeIndex(run) {
1524
1727
  const state = JSON.parse(run.index_json);
1525
1728
  if (!state || typeof state !== "object" || Array.isArray(state) || !Array.isArray(state.stage_runs))
1526
1729
  throw new Error("RUN index must contain stage_runs");
1527
- return { ...state, schema_id: runSchemaId };
1730
+ const derivedStage = ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind)
1731
+ ? [...state.stage_runs].sort((left, right) => right.order - left.order)[0]?.stage : undefined;
1732
+ return { ...state, schema_id: runSchemaId, ...(state.current_stage === undefined && derivedStage ? { current_stage: derivedStage } : {}) };
1528
1733
  }
1529
1734
  catch (error) {
1530
1735
  throw new AppError("runtime_state_invalid", "RUN mechanical state in SQLite is missing or invalid", 1, { run_id: run.id, cause: String(error) });
1531
1736
  }
1532
1737
  }
1738
+ function assertBundleWorkspaceIdentity(context, run, bundle) {
1739
+ const scope = currentBundleScope(context, run, bundle);
1740
+ const route = authoritativeIndex(run).workspace_route ? retainedWorkspaceRoute(context, run) : undefined;
1741
+ if (scope.workspace === run.workspace_root)
1742
+ return true;
1743
+ if (bundle.descriptor.descriptor.id !== "sdlc" || !["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) || scope.workspace !== run.project_root)
1744
+ return false;
1745
+ if (!route)
1746
+ return false;
1747
+ return route.previous_workspace === scope.workspace && route.receipt.policy.route === "feature_worktree"
1748
+ && route.receipt.policy.base_ref !== null && route.receipt.policy.feature_branch !== null;
1749
+ }
1750
+ function currentBundleScope(context, run, bundle) {
1751
+ return JSON.parse(projectPacketLocators(JSON.stringify(bundle.execution.snapshot().scope), runContextRelocations(context, run.project_id, run.id)));
1752
+ }
1753
+ function assertBundleRunIdentity(context, run, bundle) {
1754
+ const scope = currentBundleScope(context, run, bundle);
1755
+ const flowId = bundle.descriptor.descriptor.id;
1756
+ const matchesFlow = flowId === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flowId === run.flow_kind;
1757
+ if (bundle.manifest.run_id !== run.id || scope.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle) || !matchesFlow) {
1758
+ throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
1759
+ }
1760
+ const blobs = new Map(bundle.manifest.inventory.map(ref => [ref.path, bundle.blob(ref.path)]));
1761
+ // File-owned handler inventories can never authorize executable code.
1762
+ admitBundledFlow(bundle.blob(bundle.manifest.descriptor.path), bundle.manifest.descriptor.sha256, blobs);
1763
+ }
1764
+ function bundleAuthority(admission) {
1765
+ const authority = { runId: admission.runId, executionScope: structuredClone(admission.executionScope),
1766
+ receipts: structuredClone([...(admission.receipts ?? [])].sort(([left], [right]) => left.localeCompare(right, "en"))) };
1767
+ for (const key of ["descriptorSha256", "engineChecksum", "engineBindingSha256", "engineManifestSha256", "bundleProjectRoot", "bundleProvenance", "bootstrapIdentity"]) {
1768
+ if (admission[key] !== undefined)
1769
+ Object.assign(authority, { [key]: structuredClone(admission[key]) });
1770
+ }
1771
+ return authority;
1772
+ }
1773
+ /** Copies the same SQL authority representation used by legacy bundle binding. */
1774
+ export function runBundleAuthorityMetadata(bundle) {
1775
+ if (!(bundle instanceof RetainedExecutionBundle))
1776
+ throw new AppError("run_bundle_anchor_invalid", "Bundle must be admitted before RUN allocation", 1);
1777
+ const admissionJson = JSON.stringify(bundleAuthority(bundle.admission())), admissionSha256 = hybridDigest(JSON.parse(admissionJson));
1778
+ if (Buffer.byteLength(admissionJson) > 1024 * 1024)
1779
+ throw new AppError("run_bundle_authority_invalid", "RUN admission exceeds its bound", 1);
1780
+ return { admissionJson, anchor: { schema_id: "dd-flow/run-execution-bundle@1",
1781
+ manifest_sha256: bundle.sha256, descriptor_sha256: bundle.manifest.descriptor.sha256,
1782
+ admission_sha256: admissionSha256, flow_id: bundle.manifest.flow_id } };
1783
+ }
1784
+ /** Recheck SQL authority and compiled descriptor admission. Schema loading
1785
+ * belongs outside SQL writers; use the explicit SQL-only fence inside them.
1786
+ */
1787
+ export function assertPreparedRunBundleAuthority(context, scope, bundle) {
1788
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
1789
+ assertBundleRunIdentity(context, run, bundle);
1790
+ }
1791
+ /** SQL-only fence after the caller has admitted compiled handlers outside the
1792
+ * writer. Descriptor schema loading is deliberately excluded from this fence.
1793
+ */
1794
+ export function assertPreparedRunBundleSqlAuthority(context, scope, bundle) {
1795
+ if (!(bundle instanceof RetainedExecutionBundle))
1796
+ throw new AppError("run_bundle_anchor_invalid", "Prepared bundle is not admitted", 1);
1797
+ const run = context.db.get("SELECT * FROM runs WHERE project_id = ? AND id = ?", [scope.projectId, scope.runId]);
1798
+ if (!run)
1799
+ throw new AppError("not_found", "Prepared bundle RUN is unavailable", 1);
1800
+ const index = authoritativeIndex(run), anchor = index.execution_bundle;
1801
+ if (!assertRunBundleSqlAuthority(context, scope, anchor) || !anchor || anchor.manifest_sha256 !== bundle.sha256
1802
+ || anchor.descriptor_sha256 !== bundle.manifest.descriptor.sha256 || anchor.flow_id !== bundle.manifest.flow_id
1803
+ || anchor.admission_sha256 !== hybridDigest(bundleAuthority(bundle.admission())))
1804
+ throw new AppError("run_bundle_anchor_invalid", "Prepared bundle differs from authoritative RUN", 1);
1805
+ const execution = currentBundleScope(context, run, bundle);
1806
+ const flow = bundle.descriptor.descriptor.id;
1807
+ if (bundle.manifest.run_id !== run.id || execution.projectRoot !== run.project_root || !assertBundleWorkspaceIdentity(context, run, bundle)
1808
+ || !(flow === "sdlc" ? ["vnext_specify", "vnext_protocolize"].includes(run.flow_kind) : flow === run.flow_kind)) {
1809
+ throw new AppError("run_bundle_identity_invalid", "Execution bundle belongs to another RUN or workspace", 1, { effect: "no_effect", run_id: run.id });
1810
+ }
1811
+ return run;
1812
+ }
1813
+ export function readFlowRunExecutionBundle(context, input) {
1814
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
1815
+ const run = resolveRun(context, project.id, input.runId), anchor = authoritativeIndex(run).execution_bundle;
1816
+ if (!anchor) {
1817
+ // Genuine historical RUNs have neither authority nor anchor. Lost new-RUN
1818
+ // metadata must not silently switch their execution to the legacy engine path.
1819
+ const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
1820
+ if (hasAuthority && context.db.get("SELECT 1 FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [project.id, run.id])) {
1821
+ throw new AppError("run_bundle_anchor_invalid", "Retained RUN authority exists without its execution anchor", 1);
1822
+ }
1823
+ return null;
1824
+ }
1825
+ if (anchor.schema_id !== "dd-flow/run-execution-bundle@1" || !/^[a-f0-9]{64}$/.test(anchor.manifest_sha256)
1826
+ || !/^[a-f0-9]{64}$/.test(anchor.descriptor_sha256) || !/^[a-f0-9]{64}$/.test(anchor.admission_sha256))
1827
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle anchor is invalid", 1);
1828
+ const hasAuthority = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'");
1829
+ const row = hasAuthority ? context.db.get("SELECT admission_json, manifest_sha256 FROM run_bundle_authorities WHERE project_id = ? AND run_id = ?", [project.id, run.id]) : undefined;
1830
+ if (!row || row.manifest_sha256 !== anchor.manifest_sha256)
1831
+ throw new AppError("run_bundle_authority_missing", "RUN bundle admission authority is unavailable", 1);
1832
+ if (Buffer.byteLength(row.admission_json) > 1024 * 1024)
1833
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
1834
+ let retained;
1835
+ try {
1836
+ retained = JSON.parse(row.admission_json);
1837
+ }
1838
+ catch {
1839
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority is invalid", 1);
1840
+ }
1841
+ if (hybridDigest(retained) !== anchor.admission_sha256 || !Array.isArray(retained.receipts)
1842
+ || input.admission && hybridDigest(bundleAuthority(input.admission)) !== anchor.admission_sha256) {
1843
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority changed", 1);
1844
+ }
1845
+ const admission = { ...retained, receipts: new Map(retained.receipts), descriptorContext: bundledHandlerContext(new Map()) };
1846
+ const bundle = RetainedExecutionBundle.read(path.join(runArtifactRoot(run), "execution-bundle"), anchor.manifest_sha256, admission);
1847
+ assertBundleRunIdentity(context, run, bundle);
1848
+ if (bundle.manifest.descriptor.sha256 !== anchor.descriptor_sha256 || bundle.manifest.flow_id !== anchor.flow_id) {
1849
+ throw new AppError("run_bundle_anchor_invalid", "RUN bundle descriptor identity changed", 1);
1850
+ }
1851
+ return bundle;
1852
+ }
1853
+ /** Freeze once before physical owners exist. Publication precedes the fenced SQLite anchor. */
1854
+ export function bindFlowRunExecutionBundle(context, input) {
1855
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
1856
+ const run = resolveRun(context, project.id, input.runId), index = authoritativeIndex(run);
1857
+ const generation = assertRunMutationAllowed(context, project.id, run.id);
1858
+ assertBundleRunIdentity(context, run, input.bundle);
1859
+ if (input.bundle.manifest.flow_id !== "sdlc") {
1860
+ throw new AppError("run_bundle_start_required", "Memory RUN and its bundle authority must be allocated together through the request-bound start", 1, { phase: "prepare", effect: "no_effect", run_id: run.id });
1861
+ }
1862
+ if (index.execution_bundle) {
1863
+ if (index.execution_bundle.manifest_sha256 !== input.bundle.sha256)
1864
+ throw new AppError("run_bundle_immutable", "RUN bundle cannot be replaced", 1);
1865
+ readFlowRunExecutionBundle(context, { ...input, admission: input.bundle.admission() });
1866
+ publishRunProjections(context, project.id, run.id);
1867
+ return { changed: false, sha256: input.bundle.sha256 };
1868
+ }
1869
+ const assertUnstarted = (state) => {
1870
+ if (state.status !== "running" || state.stage_runs.length || state.sessions.length
1871
+ || context.db.get("SELECT 1 FROM sessions WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])
1872
+ || context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? LIMIT 1", [project.id, run.id])) {
1873
+ throw new AppError("run_bundle_active", "Execution bundle must be frozen before stages, Works or Sessions exist", 1);
1874
+ }
1875
+ };
1876
+ assertUnstarted(index);
1877
+ const authority = bundleAuthority(input.bundle.admission());
1878
+ // Hash the JSON representation actually retained, not optional undefined fields.
1879
+ const authorityJson = JSON.stringify(authority), authoritySha256 = hybridDigest(JSON.parse(authorityJson));
1880
+ if (Buffer.byteLength(authorityJson) > 1024 * 1024)
1881
+ throw new AppError("run_bundle_authority_invalid", "RUN bundle admission authority exceeds its bound", 1);
1882
+ input.bundle.materialize(runArtifactRoot(run));
1883
+ try {
1884
+ context.db.writeTransaction(() => {
1885
+ assertRunMutationAllowed(context, project.id, run.id, generation);
1886
+ const latest = resolveRun(context, project.id, run.id), state = authoritativeIndex(latest);
1887
+ assertUnstarted(state);
1888
+ if (state.runtime_revision !== index.runtime_revision || state.execution_bundle || latest.workspace_root !== run.workspace_root
1889
+ || latest.project_root !== run.project_root || latest.flow_kind !== run.flow_kind || latest.run_root !== run.run_root) {
1890
+ throw new AppError("run_revision_changed", "RUN changed during bundle publication", 1);
1891
+ }
1892
+ state.execution_bundle = { schema_id: "dd-flow/run-execution-bundle@1", manifest_sha256: input.bundle.sha256,
1893
+ descriptor_sha256: input.bundle.manifest.descriptor.sha256, admission_sha256: authoritySha256, flow_id: input.bundle.manifest.flow_id };
1894
+ context.db.execSchema(`CREATE TABLE IF NOT EXISTS run_bundle_authorities (
1895
+ project_id TEXT NOT NULL, run_id TEXT NOT NULL, manifest_sha256 TEXT NOT NULL, admission_json TEXT NOT NULL,
1896
+ PRIMARY KEY(project_id, run_id), FOREIGN KEY(project_id, run_id) REFERENCES runs(project_id, id)
1897
+ )`);
1898
+ context.db.run("INSERT INTO run_bundle_authorities (project_id, run_id, manifest_sha256, admission_json) VALUES (?, ?, ?, ?)", [project.id, run.id, input.bundle.sha256, authorityJson]);
1899
+ state.updated_at = context.now();
1900
+ persistRunState(context, project, latest, state);
1901
+ }, "runs.bindExecutionBundle");
1902
+ }
1903
+ catch (error) {
1904
+ throw new AppError("run_bundle_binding_incomplete", "Bundle retained but RUN binding requires recovery", 1, {
1905
+ phase: "publication", effect: "partial", run_id: run.id, sha256: input.bundle.sha256,
1906
+ reason: error instanceof AppError ? error.code : "binding_io_failure",
1907
+ });
1908
+ }
1909
+ return { changed: true, sha256: input.bundle.sha256 };
1910
+ }
1533
1911
  function sanitizeTimelineValue(value, key) {
1534
1912
  if (key && /(secret|password|authorization|cookie|api[_-]?key|private[_-]?key|access[_-]?token|refresh[_-]?token|transcript|prompt|tool[_-]?output|document[_-]?content)/i.test(key)) {
1535
1913
  return "[REDACTED]";
@@ -1556,6 +1934,24 @@ function nextAttempt(existing) {
1556
1934
  const number = Number(existing.attempt.replace("try-", ""));
1557
1935
  return `try-${String(Number.isFinite(number) ? number + 1 : 1).padStart(3, "0")}`;
1558
1936
  }
1937
+ function bundledNextStage(flow, index) {
1938
+ const completed = index.stage_runs.filter(stage => ["done", "skipped"].includes(stage.status)).sort((left, right) => right.order - left.order)[0];
1939
+ if (!completed)
1940
+ return flow.descriptor.entry_stage;
1941
+ // Optional SDLC review routing remains a flow-specific policy, not a generic stage list.
1942
+ const outcome = flow.descriptor.id === "sdlc" && completed.stage === "plan" && index.settings?.plan_review?.mode === "off" ? "not-applicable" : "accepted";
1943
+ return flow.successor(completed.stage, outcome);
1944
+ }
1945
+ function assertBundledStageStart(flow, index, stage, dir, status, existing) {
1946
+ const selected = flow.stage(stage);
1947
+ if (selected.directory !== dir)
1948
+ throw new AppError("flow_stage_directory_invalid", "Stage directory differs from the frozen descriptor", 1, { stage });
1949
+ if (status !== "running")
1950
+ return;
1951
+ if (existing || index.stage_runs.some(candidate => !["done", "skipped"].includes(candidate.status)) || bundledNextStage(flow, index) !== stage) {
1952
+ throw new AppError("illegal_stage_transition", "Stage does not follow the frozen descriptor or an attempt already exists", 2, { run_id: index.run_id, stage });
1953
+ }
1954
+ }
1559
1955
  function assertVnextStageStart(run, index, stage, status, existing) {
1560
1956
  if (status !== "running" || (run.flow_kind !== "vnext_specify" && run.flow_kind !== "vnext_protocolize"))
1561
1957
  return;
@@ -1614,19 +2010,6 @@ function requireRunById(context, projectId, runId) {
1614
2010
  }
1615
2011
  return run;
1616
2012
  }
1617
- function nextRunId(context, projectId, slug) {
1618
- const rows = context.db.all("SELECT id FROM runs WHERE project_id = ? AND id LIKE 'RUN-%'", [projectId]);
1619
- const max = rows.reduce((value, row) => {
1620
- try {
1621
- const parsed = parseFullEntityId(row.id);
1622
- return parsed.type === runIdType ? Math.max(value, Number(parsed.shortId.slice(4))) : value;
1623
- }
1624
- catch {
1625
- return value;
1626
- }
1627
- }, 0);
1628
- return formatFullId(runIdType, max + 1, slug);
1629
- }
1630
2013
  function flowRunSummary(run) {
1631
2014
  return {
1632
2015
  id: run.id,