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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +197 -0
  7. package/dist/cli/command-option-descriptions.js +191 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/error-output.js +52 -0
  11. package/dist/cli/executor-onboarding.js +76 -0
  12. package/dist/cli/flows.js +29 -0
  13. package/dist/cli/harnesses.js +84 -0
  14. package/dist/cli/help.js +240 -6
  15. package/dist/cli/input-preparation.js +54 -5
  16. package/dist/cli/memory-flow-admission.js +44 -0
  17. package/dist/cli/memory-init-invocation.js +78 -0
  18. package/dist/cli/memory-init.js +240 -0
  19. package/dist/cli/memory-storage-admission.js +53 -0
  20. package/dist/cli/models.js +39 -0
  21. package/dist/cli/native-hook-ingress.js +1 -1
  22. package/dist/cli/operator-profiles.js +61 -0
  23. package/dist/cli/output-options.js +35 -0
  24. package/dist/cli/response-file.js +71 -0
  25. package/dist/cli/run-cli.js +283 -160
  26. package/dist/cli/runtime-process.js +1 -1
  27. package/dist/cli/setup.js +149 -0
  28. package/dist/cli.js +56 -2
  29. package/dist/domain/stage-handler-identities.js +20 -0
  30. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  31. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  32. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +53 -22
  33. package/dist/harness-runtime/lib/dd-agy.mjs +16 -5
  34. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +12 -6
  35. package/dist/harness-runtime/lib/dd-codex.mjs +24 -12
  36. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  37. package/dist/harness-runtime/lib/dd-droid.mjs +32 -9
  38. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +11 -3
  39. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +100 -15
  40. package/dist/harness-runtime/lib/dd-opencode.mjs +51 -20
  41. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +22 -10
  42. package/dist/harness-runtime/lib/dd-zcode.mjs +17 -4
  43. package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
  44. package/dist/harness-runtime/lib/delegation-instructions.mjs +16 -3
  45. package/dist/harness-runtime/lib/lease-renewal.mjs +2 -1
  46. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  47. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  48. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  49. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  50. package/dist/harness-runtime/lib/native-children.d.mts +2 -0
  51. package/dist/harness-runtime/lib/native-children.mjs +20 -1
  52. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  53. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  54. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  55. package/dist/harness-runtime/lib/opencode-plugin.mjs +32 -9
  56. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  57. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +13 -0
  58. package/dist/harness-runtime/lib/provider-overload-policy.mjs +138 -0
  59. package/dist/runtime/context.js +1 -1
  60. package/dist/schemas/discovery-result.schema.json +184 -0
  61. package/dist/schemas/execution-bundle.schema.json +205 -0
  62. package/dist/schemas/execution-settings.schema.json +162 -0
  63. package/dist/schemas/flow-descriptor.schema.json +54 -0
  64. package/dist/schemas/flow-run.schema.json +26 -0
  65. package/dist/schemas/harness-catalog.schema.json +80 -0
  66. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  67. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  68. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  69. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  70. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  71. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  72. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  73. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  74. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  75. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  76. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  77. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  78. package/dist/schemas/model-catalog.schema.json +153 -0
  79. package/dist/schemas/profile-settings.schema.json +122 -0
  80. package/dist/schemas/release-manifest.schema.json +291 -0
  81. package/dist/schemas/run-engine-binding.schema.json +1 -1
  82. package/dist/services/check-runner-evidence.js +51 -4
  83. package/dist/services/cli-operation-classifier.js +8 -47
  84. package/dist/services/cli-upgrade-allowlist.js +27 -0
  85. package/dist/services/code-checks.js +387 -34
  86. package/dist/services/controller-child-capacity.js +13 -0
  87. package/dist/services/controller-child-observer.js +18 -2
  88. package/dist/services/controller-execution.js +88 -0
  89. package/dist/services/controller-fanout.js +71 -12
  90. package/dist/services/dashboard.js +8 -2
  91. package/dist/services/descriptor-coordinator-session.js +35 -0
  92. package/dist/services/descriptor-root-work.js +53 -0
  93. package/dist/services/descriptor-run-settlement.js +105 -0
  94. package/dist/services/descriptor-run-start.js +449 -0
  95. package/dist/services/descriptor-stage-coordinator.js +197 -0
  96. package/dist/services/descriptor-stage-settlement.js +222 -0
  97. package/dist/services/descriptor-stage-start.js +139 -0
  98. package/dist/services/discovery/agy-discovery.js +2 -0
  99. package/dist/services/discovery/codex-discovery.js +2 -0
  100. package/dist/services/discovery/droid-discovery.js +2 -0
  101. package/dist/services/discovery/grok-discovery.js +2 -0
  102. package/dist/services/discovery/opencode-discovery.js +2 -0
  103. package/dist/services/discovery/static.js +31 -0
  104. package/dist/services/discovery/zcode-discovery.js +2 -0
  105. package/dist/services/engines.js +39 -22
  106. package/dist/services/eval-snapshots.js +83 -25
  107. package/dist/services/execution-bundles.js +263 -0
  108. package/dist/services/execution-policy.js +20 -16
  109. package/dist/services/executor-onboarding.js +56 -0
  110. package/dist/services/external-work-launch.js +93 -15
  111. package/dist/services/flow-catalog.js +21 -0
  112. package/dist/services/flow-descriptors.js +82 -0
  113. package/dist/services/frozen-engine.js +250 -0
  114. package/dist/services/generic-execution.js +164 -0
  115. package/dist/services/harness-adapter.js +28 -2
  116. package/dist/services/harness-discovery.js +29 -0
  117. package/dist/services/harness-doctor.js +75 -0
  118. package/dist/services/harness-probe-effects.js +51 -0
  119. package/dist/services/harness-version-probe.js +100 -0
  120. package/dist/services/hooks.js +24 -6
  121. package/dist/services/hybrid-contracts.js +483 -0
  122. package/dist/services/ids.js +29 -1
  123. package/dist/services/installation-lock.js +95 -0
  124. package/dist/services/installed-flow-source.js +183 -0
  125. package/dist/services/lifecycle-invocations.js +257 -78
  126. package/dist/services/managed-processes.js +31 -10
  127. package/dist/services/memory-accepted-composition.js +77 -0
  128. package/dist/services/memory-baseline-preservation.js +65 -0
  129. package/dist/services/memory-completed-correction-wave.js +320 -0
  130. package/dist/services/memory-compose-handler.js +258 -0
  131. package/dist/services/memory-composition-acceptance.js +199 -0
  132. package/dist/services/memory-composition-graph.js +134 -0
  133. package/dist/services/memory-composition-registration.js +146 -0
  134. package/dist/services/memory-composition-work-custody.js +151 -0
  135. package/dist/services/memory-composition-work-results.js +96 -0
  136. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  137. package/dist/services/memory-corrected-review-batch.js +30 -0
  138. package/dist/services/memory-corrected-review-findings.js +106 -0
  139. package/dist/services/memory-corrected-review-inputs.js +210 -0
  140. package/dist/services/memory-corrected-review-projection.js +78 -0
  141. package/dist/services/memory-corrected-review-receipt.js +170 -0
  142. package/dist/services/memory-corrected-review-registration.js +191 -0
  143. package/dist/services/memory-correction-check-receipt.js +219 -0
  144. package/dist/services/memory-correction-input-owner.js +6 -0
  145. package/dist/services/memory-discover-handler.js +104 -0
  146. package/dist/services/memory-finish-evidence.js +435 -0
  147. package/dist/services/memory-finish-handler.js +215 -0
  148. package/dist/services/memory-finish-integration.js +280 -0
  149. package/dist/services/memory-finish-readiness.js +122 -0
  150. package/dist/services/memory-git-integration.js +151 -0
  151. package/dist/services/memory-historical-corrected-review.js +206 -0
  152. package/dist/services/memory-historical-original-review.js +130 -0
  153. package/dist/services/memory-historical-repair-registration.js +99 -0
  154. package/dist/services/memory-historical-second-review.js +128 -0
  155. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  156. package/dist/services/memory-init-contexts.js +89 -0
  157. package/dist/services/memory-init-controller-start.js +84 -0
  158. package/dist/services/memory-init-engine-release.js +223 -0
  159. package/dist/services/memory-init-git-policy.js +136 -0
  160. package/dist/services/memory-init-lint-capability.js +185 -0
  161. package/dist/services/memory-init-native-capacity.js +296 -0
  162. package/dist/services/memory-init-preflight.js +330 -0
  163. package/dist/services/memory-init-project-bootstrap.js +599 -0
  164. package/dist/services/memory-init-request-candidate.js +157 -0
  165. package/dist/services/memory-init-request-input.js +66 -0
  166. package/dist/services/memory-init-resource-storage.js +125 -0
  167. package/dist/services/memory-init-run-allocation.js +81 -0
  168. package/dist/services/memory-init-run-bundle.js +98 -0
  169. package/dist/services/memory-init-start.js +181 -0
  170. package/dist/services/memory-init-startup-custody.js +80 -0
  171. package/dist/services/memory-init-startup-input.js +240 -0
  172. package/dist/services/memory-init-storage.js +81 -0
  173. package/dist/services/memory-init-stores.js +359 -0
  174. package/dist/services/memory-init-worktrunk-capability.js +178 -0
  175. package/dist/services/memory-layer-publication.js +611 -0
  176. package/dist/services/memory-layer.js +156 -0
  177. package/dist/services/memory-lint-config.js +85 -0
  178. package/dist/services/memory-native-review-copy.js +165 -0
  179. package/dist/services/memory-normal-flow-entry.js +75 -0
  180. package/dist/services/memory-ordinary-defaults.js +112 -0
  181. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  182. package/dist/services/memory-preflight-handler.js +259 -0
  183. package/dist/services/memory-repair-current-view.js +299 -0
  184. package/dist/services/memory-repair-document-delta.js +451 -0
  185. package/dist/services/memory-repair-history.js +491 -0
  186. package/dist/services/memory-repair-physical-binding.js +312 -0
  187. package/dist/services/memory-repair-publication-files.js +152 -0
  188. package/dist/services/memory-repair-publication-intent.js +27 -0
  189. package/dist/services/memory-repair-publication-plan.js +160 -0
  190. package/dist/services/memory-repair-publication-source.js +225 -0
  191. package/dist/services/memory-repair-publication.js +794 -0
  192. package/dist/services/memory-repair-registration.js +239 -0
  193. package/dist/services/memory-repair-work-baseline.js +413 -0
  194. package/dist/services/memory-repair-work-completion.js +97 -0
  195. package/dist/services/memory-repair-work-custody.js +73 -0
  196. package/dist/services/memory-repair-work-results.js +89 -0
  197. package/dist/services/memory-repair-workspace.js +252 -0
  198. package/dist/services/memory-repair-writer-copy.js +735 -0
  199. package/dist/services/memory-result-binding.js +173 -0
  200. package/dist/services/memory-retained-review-authority.js +129 -0
  201. package/dist/services/memory-retained-review-results.js +70 -0
  202. package/dist/services/memory-retained-second-baseline.js +102 -0
  203. package/dist/services/memory-retained-second-repairs.js +156 -0
  204. package/dist/services/memory-review-acceptance.js +214 -0
  205. package/dist/services/memory-review-correction-plan.js +171 -0
  206. package/dist/services/memory-review-handler.js +585 -0
  207. package/dist/services/memory-review-inputs.js +200 -0
  208. package/dist/services/memory-review-receipt.js +180 -0
  209. package/dist/services/memory-review-registration.js +108 -0
  210. package/dist/services/memory-review-work-custody.js +124 -0
  211. package/dist/services/memory-review-work-input.js +72 -0
  212. package/dist/services/memory-review-work-results.js +73 -0
  213. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  214. package/dist/services/memory-second-correction-plan.js +76 -0
  215. package/dist/services/memory-second-repair-baseline.js +91 -0
  216. package/dist/services/memory-second-repair-launch.js +614 -0
  217. package/dist/services/memory-second-repair-registration.js +88 -0
  218. package/dist/services/memory-second-repair-source.js +187 -0
  219. package/dist/services/memory-second-review-acceptance.js +115 -0
  220. package/dist/services/memory-source-observations.js +117 -0
  221. package/dist/services/memory-stage-dispatch.js +28 -0
  222. package/dist/services/memory-stage-results.js +199 -0
  223. package/dist/services/memory-verification-acceptance.js +135 -0
  224. package/dist/services/memory-verification-checks.js +252 -0
  225. package/dist/services/memory-verification-receipt.js +159 -0
  226. package/dist/services/memory-verify-handler.js +135 -0
  227. package/dist/services/memory-work-result-preparation.js +63 -0
  228. package/dist/services/merge-server.js +2 -4
  229. package/dist/services/model-catalog.js +109 -0
  230. package/dist/services/model-discovery.js +46 -0
  231. package/dist/services/operator-profiles.js +236 -0
  232. package/dist/services/projects.js +17 -2
  233. package/dist/services/{codex-capacity.js → provider-capacity.js} +34 -16
  234. package/dist/services/recovery-snapshot-database.js +417 -2
  235. package/dist/services/release-archive-producer.js +52 -0
  236. package/dist/services/release-archive.js +143 -0
  237. package/dist/services/release-cache.js +142 -0
  238. package/dist/services/release-compatibility.js +18 -0
  239. package/dist/services/release-download.js +67 -0
  240. package/dist/services/release-envelope.js +104 -0
  241. package/dist/services/release-manifest.js +45 -0
  242. package/dist/services/release-payload.js +114 -0
  243. package/dist/services/release-producer.js +63 -0
  244. package/dist/services/release-provenance.js +160 -0
  245. package/dist/services/release-registry.js +66 -0
  246. package/dist/services/release-source.js +194 -0
  247. package/dist/services/repair-intents.js +1 -1
  248. package/dist/services/review-copy.js +10 -2
  249. package/dist/services/run-control.js +80 -9
  250. package/dist/services/run-controller-adapter.js +1 -1
  251. package/dist/services/run-controller-capture.js +10 -3
  252. package/dist/services/run-controller-process.js +21 -0
  253. package/dist/services/run-controller-recovery.js +5 -5
  254. package/dist/services/run-controller-state.js +1 -1
  255. package/dist/services/run-controller.js +158 -64
  256. package/dist/services/run-recovery-guard.js +5 -3
  257. package/dist/services/run-recovery-runtime.js +3 -3
  258. package/dist/services/run-recovery.js +145 -5
  259. package/dist/services/run-start-custody.js +177 -0
  260. package/dist/services/runs.js +413 -30
  261. package/dist/services/setup.js +377 -0
  262. package/dist/services/stage-context.js +9 -1
  263. package/dist/services/stage-handlers.js +74 -0
  264. package/dist/services/stage-report-renderer.js +63 -0
  265. package/dist/services/stage-work-graph.js +17 -0
  266. package/dist/services/vnext-code-review.js +1 -1
  267. package/dist/services/vnext-code.js +6 -6
  268. package/dist/services/vnext-execution-profile.js +2 -2
  269. package/dist/services/vnext-fanout.js +73 -11
  270. package/dist/services/vnext-merge.js +8 -3
  271. package/dist/services/vnext-plan-review.js +7 -5
  272. package/dist/services/vnext-plan.js +3 -3
  273. package/dist/services/vnext-protocolize.js +20 -6
  274. package/dist/services/vnext-workspace-policy.js +7 -2
  275. package/dist/services/work-registry.js +616 -71
  276. package/dist/services/workspace-files.js +1 -1
  277. package/dist/storage/database.js +84 -6
  278. package/dist/storage/writer-contract.js +11 -7
  279. package/package.json +13 -3
  280. package/tools/create-canon-release.mjs +33 -0
  281. package/tools/produce-hybrid-release.mjs +41 -0
  282. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +0 -10
  283. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +0 -102
@@ -0,0 +1,13 @@
1
+ /** Only a proven new successful terminal Turn closes a child's retry episode. */
2
+ export function closeSuccessfulChildCapacity(state, children, parentId) {
3
+ const closed = [];
4
+ for (const child of children) {
5
+ const retained = state.child_capacity?.[child.session_id];
6
+ if (!retained || child.parent_session_id !== parentId || child.provenance === "parent_mismatch"
7
+ || child.status !== "completed" || !child.terminal_turn_id || child.terminal_turn_id === retained.turn_id)
8
+ continue;
9
+ delete state.child_capacity[child.session_id];
10
+ closed.push(child.session_id);
11
+ }
12
+ return closed;
13
+ }
@@ -12,6 +12,12 @@ export function retainedChildObserver(input) {
12
12
  const provider_error = safeNativeFailure(child.provider_error);
13
13
  return { session_id: child.session_id, parent_session_id: child.parent_session_id, status: "failed", source: child.source, terminal_turn_id: child.terminal_turn_id,
14
14
  ...(typeof child.terminal_observed_at === "string" && Number.isFinite(Date.parse(child.terminal_observed_at)) ? { terminal_observed_at: child.terminal_observed_at } : {}),
15
+ ...(child.native_source ? { native_source: child.native_source } : {}),
16
+ ...(child.native_turn_items ? { native_turn_items: {
17
+ ...(typeof child.native_turn_items.observed === "boolean" ? { observed: child.native_turn_items.observed } : {}),
18
+ ...(typeof child.native_turn_items.possible_effects === "boolean" ? { possible_effects: child.native_turn_items.possible_effects } : {}),
19
+ ...(typeof child.native_turn_items.pending === "boolean" ? { pending: child.native_turn_items.pending } : {})
20
+ } } : {}),
15
21
  ...(provider_error ? { provider_error } : {}) };
16
22
  };
17
23
  const accept = (children) => {
@@ -26,6 +32,8 @@ export function retainedChildObserver(input) {
26
32
  if (sameScope(observation.scope)) {
27
33
  if (Array.isArray(observation.children))
28
34
  accept(observation.children);
35
+ for (const key of observation.resolved ?? [])
36
+ retained.delete(key);
29
37
  if (Object.hasOwn(observation, "gap"))
30
38
  gap = observation.gap ?? null;
31
39
  }
@@ -35,23 +43,31 @@ export function retainedChildObserver(input) {
35
43
  return;
36
44
  input.assertOwner();
37
45
  const failed = children.filter(child => child.parent_session_id === input.scope.root_session_id && child.status === "failed" && child.terminal_turn_id && !retained.has(`${child.session_id}:${child.terminal_turn_id}`)).map(normalized);
38
- if (!failed.length && !gap)
46
+ // This is an authoritative current native snapshot. Older failed Turns
47
+ // remain in the journal/episode history, not in the current-outcome fence.
48
+ const resolved = [...retained].filter(([, old]) => children.some(child => child.parent_session_id === input.scope.root_session_id
49
+ && child.provenance !== "parent_mismatch" && child.session_id === old.session_id && ["completed", "failed"].includes(child.status)
50
+ && child.terminal_turn_id && child.terminal_turn_id !== old.terminal_turn_id)).map(([key]) => key);
51
+ if (!failed.length && !resolved.length && !gap)
39
52
  return;
40
53
  const fd = fs.openSync(input.file, "a", 0o600);
41
54
  try {
42
- fs.writeSync(fd, `${JSON.stringify({ scope: input.scope, children: failed, gap: null })}\n`);
55
+ fs.writeSync(fd, `${JSON.stringify({ scope: input.scope, children: failed, ...(resolved.length ? { resolved } : {}), gap: null })}\n`);
43
56
  fs.fsyncSync(fd);
44
57
  }
45
58
  finally {
46
59
  fs.closeSync(fd);
47
60
  }
48
61
  accept(failed);
62
+ for (const key of resolved)
63
+ retained.delete(key);
49
64
  gap = null;
50
65
  };
51
66
  return {
52
67
  failed: () => [...retained.values()],
53
68
  gap: () => gap,
54
69
  drain: () => pending ?? Promise.resolve(),
70
+ observeSettledChildren: complete,
55
71
  completePostTurn(receipt) {
56
72
  const evidence = receipt.evidence;
57
73
  const tree = evidence?.subagents;
@@ -0,0 +1,88 @@
1
+ import path from "node:path";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { FrozenGenericExecution } from "./generic-execution.js";
4
+ import { resolveExecutionPolicy, resolveStageTransition, stageExecutionCwd } from "./execution-policy.js";
5
+ import { readFlowRunExecutionBundle } from "./runs.js";
6
+ import { hybridDigest } from "./hybrid-contracts.js";
7
+ /** Adapt generic routing to shared fanout APIs, not to SDLC settings. No alias,
8
+ * operator file, merge policy or project default participates in this selection.
9
+ */
10
+ export function genericControllerRouting(execution) {
11
+ if (!(execution instanceof FrozenGenericExecution) || execution.settings.execution !== "managed") {
12
+ throw new AppError("controller_execution_invalid", "Managed controller requires managed frozen execution", 1, { effect: "no_effect" });
13
+ }
14
+ const snapshot = execution.snapshot();
15
+ const profiles = Object.fromEntries(snapshot.profiles.map(value => [value.id, structuredClone(value.profile)]));
16
+ const delegation = { mode: execution.settings.delegation.mode,
17
+ ...(execution.settings.delegation.mode === "external" ? { max_parallel: execution.settings.delegation.max_parallel } : {}) };
18
+ const policy = { agent_profile_id: execution.settings.root_profile, delegation,
19
+ stage_overrides: Object.fromEntries(Object.entries(execution.settings.stage_profiles).map(([stage, id]) => [stage, { agent_profile_id: id }])) };
20
+ for (const profile of Object.values(profiles))
21
+ Object.freeze(profile);
22
+ Object.freeze(profiles);
23
+ for (const override of Object.values(policy.stage_overrides))
24
+ Object.freeze(override);
25
+ Object.freeze(policy.stage_overrides);
26
+ Object.freeze(delegation);
27
+ Object.freeze(policy);
28
+ return {
29
+ kind: "descriptor", policy, profiles, mergeMode: null,
30
+ transition(stage, fromStage, _currentRun) {
31
+ const selected = execution.stage(stage);
32
+ if (fromStage)
33
+ execution.stage(fromStage);
34
+ return { ...resolveExecutionPolicy({ ddFlowHome: "", policy, profiles, stage }), session_mode: selected.session_mode };
35
+ },
36
+ cwd(stage, _mergeTargetWorkspace, _currentRun) { execution.stage(stage); return path.resolve(snapshot.scope.workspace); }
37
+ };
38
+ }
39
+ /** Restore execution from RUN authority, never from a generic manifest copy.
40
+ * Legacy SDLC manifests retain their original settings and routing behavior.
41
+ */
42
+ export function selectControllerExecution(context, run, manifest) {
43
+ if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind)) {
44
+ const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
45
+ if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind || bundle.descriptor.descriptor.id === "sdlc")
46
+ throw new AppError("controller_execution_invalid", "Generic controller requires its retained non-SDLC bundle", 1);
47
+ const binding = { bundle_sha256: bundle.sha256, descriptor_sha256: bundle.descriptor.sha256, execution_sha256: bundle.execution.sha256 };
48
+ if (manifest && (manifest.schema_id !== "dd-flow/run-controller-manifest@2" || manifest.execution_profile !== undefined
49
+ || !manifest.execution_binding || Object.keys(manifest.execution_binding).length !== 3
50
+ || Object.entries(binding).some(([key, value]) => manifest.execution_binding[key] !== value)
51
+ || manifest.request?.project_root !== run.project_root || manifest.request?.run_id !== run.id)) {
52
+ throw new AppError("controller_execution_invalid", "Controller execution binding differs from retained RUN authority", 1);
53
+ }
54
+ return { ...genericControllerRouting(bundle.execution), bundle, binding, legacy: undefined };
55
+ }
56
+ const index = JSON.parse(run.index_json);
57
+ const legacy = manifest ? manifest.execution_profile : index.execution_profile;
58
+ if (!legacy?.settings?.execution || !legacy.agent_profiles)
59
+ throw new AppError("execution_profile_not_frozen", "Managed launch requires the RUN's frozen execution profiles", 1);
60
+ const policy = legacy.settings.execution, profiles = legacy.agent_profiles;
61
+ const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : undefined;
62
+ if (index.execution_bundle && (!bundle || bundle.descriptor.descriptor.id !== "sdlc"))
63
+ throw new AppError("controller_execution_invalid", "SDLC controller lost its retained bundle authority", 1);
64
+ if (bundle) {
65
+ if (legacy.settings.stage_session_mode !== bundle.execution.settings.stage_session_mode)
66
+ throw new AppError("controller_execution_invalid", "SDLC session policy differs from its retained execution", 1);
67
+ for (const stage of bundle.descriptor.descriptor.stages) {
68
+ const selected = resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy, profiles, stage: stage.id });
69
+ const expected = bundle.execution.work(stage.id, selected.worker.id);
70
+ if (hybridDigest(selected.coordinator) !== hybridDigest(expected.coordinator) || hybridDigest(selected.worker) !== hybridDigest(expected.worker)
71
+ || selected.delegation.mode !== expected.delegation.mode
72
+ || selected.delegation.mode === "external" && selected.delegation.max_parallel !== expected.delegation.max_parallel) {
73
+ throw new AppError("controller_execution_invalid", "SDLC routing differs from its retained execution", 1, { stage: stage.id });
74
+ }
75
+ }
76
+ }
77
+ return {
78
+ kind: "sdlc", policy, profiles, bundle: bundle ?? undefined, binding: undefined, legacy,
79
+ mergeMode: legacy.settings.merge_mode,
80
+ transition(stage, fromStage, currentRun = run) {
81
+ return resolveStageTransition({ ddFlowHome: context.ddFlowHome, policy, profiles, ...(fromStage ? { fromStage } : {}), stage,
82
+ projectRoot: currentRun.project_root, workspaceRoot: currentRun.workspace_root, stageSessionMode: legacy.settings.stage_session_mode, mergeMode: legacy.settings.merge_mode });
83
+ },
84
+ cwd(stage, mergeTargetWorkspace, currentRun = run) {
85
+ return stageExecutionCwd({ stage, projectRoot: currentRun.project_root, workspaceRoot: currentRun.workspace_root, ...(mergeTargetWorkspace ? { mergeTargetWorkspace } : {}) });
86
+ }
87
+ };
88
+ }
@@ -1,13 +1,15 @@
1
1
  import { AppError, errorRecord } from "../shared/errors.js";
2
2
  import { normalizeNativeChildren, safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
3
- import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, subagentCapacityKey, verifiedNativeWorkFailure } from "./vnext-fanout.js";
4
- import { getFlowRunVariables } from "./runs.js";
3
+ import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, getStageFanoutCapacity, verifiedNativeWorkFailure } from "./vnext-fanout.js";
5
4
  import { dispatchVnextPlanReview } from "./vnext-plan-review.js";
6
5
  import { resolveExecutionPolicy } from "./execution-policy.js";
7
6
  import { lifecycleCommandCompletionInstruction, renderDelegationInstructions, stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
8
- import { assertWorkLaunchReady } from "./work-registry.js";
7
+ import { assertWorkLaunchReady, workStartCommand } from "./work-registry.js";
8
+ import { prepareMemoryNativeReviewCopy } from "./memory-native-review-copy.js";
9
+ import { registerMemoryRepairBaselineFromStage, reserveMemoryRepairWriterCopyFromStage, materializeMemoryRepairWriterCopyFromStage, prepareMemoryRepairDispatchFromStage, registerMemorySecondRepairBaselineFromStage, reserveMemorySecondRepairCopyFromStage, materializeMemorySecondRepairCopyFromStage, prepareMemorySecondRepairDispatchFromStage, registerMemoryRepairCurrentViewFromStage, registerMemorySecondRepairCurrentViewFromStage } from "./memory-review-handler.js";
9
10
  import { requireProjectByRoot } from "./projects.js";
10
- import { stageWorkIds } from "./stage-work-graph.js";
11
+ import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
12
+ import { runtimeCommand } from "./runtime-command.js";
11
13
  function semanticCompletionPrompt(stage, completed) {
12
14
  const task = stage === "plan-review"
13
15
  ? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
@@ -80,7 +82,7 @@ export async function nextControllerFanout(context, input) {
80
82
  }
81
83
  const prompt = (action, text) => ({ kind: "prompt", action, prompt: action === "dispatch_work" ? text : `${text}\n\n${lifecycleCommandCompletionInstruction}`, fingerprint: JSON.stringify({ stage: input.stage, parent: fanout.parent_work_id, state: fanout.state, graph: fanout.graph_revision }) });
82
84
  if (fanout.state === "coordinator_required")
83
- return prompt("materialize_graph", `Continue the current ${input.stage} Stage using its authoritative packet. Materialize only its required Work graph with real dependencies. Do not launch children, finish the Stage, or start a successor. Return after registering the graph; the runtime will dispatch its ready Work.`);
85
+ return prompt("materialize_graph", `Continue the current ${input.stage} Stage using its authoritative packet. Materialize only its required Work graph, declaring real dependencies at registration. If the packet's CLI command registers the graph, use its returned Work graph unchanged: do not add or alter dependencies after registration. Accepted predecessor Stages already fence their completed producers; do not add cross-stage edges merely to restate input provenance. Do not launch children, finish the Stage, or start a successor. Return after registering the graph; the runtime will dispatch its ready Work.`);
84
86
  if (fanout.works.failed || fanout.works.cancelled) {
85
87
  const projectId = requireProjectByRoot(context, input.projectRoot).id;
86
88
  const members = stageWorkIds(context, projectId, input.runId, input.stage);
@@ -106,22 +108,62 @@ export async function nextControllerFanout(context, input) {
106
108
  const override = payload ? JSON.parse(payload).execution_override : undefined;
107
109
  return { work, resolved: resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy: input.policy, profiles: input.profiles, stage: input.stage, ...(override ? { work: override } : {}) }) };
108
110
  });
111
+ const prepareRepair = (work) => {
112
+ const row = context.db.get("SELECT payload_json,result_schema FROM works WHERE work_id=?", [work.work_id]);
113
+ const payload = JSON.parse(row?.payload_json ?? "null");
114
+ if (payload?.kind !== "memory-repair" || row?.result_schema !== "dd-flow/memory-repair-work-result@1")
115
+ return null;
116
+ const scope = { projectRoot: input.projectRoot, runId: input.runId, workId: work.work_id };
117
+ const second = payload.memory_repair?.wave === 2;
118
+ if (!second && payload.memory_repair?.wave !== 1)
119
+ throw new AppError("fanout_contract_invalid", "Memory repair wave is invalid", 1);
120
+ const registerBaseline = second ? registerMemorySecondRepairBaselineFromStage : registerMemoryRepairBaselineFromStage;
121
+ try {
122
+ registerBaseline(context, scope);
123
+ }
124
+ catch (error) {
125
+ const missing = error instanceof AppError && (second
126
+ ? error.code === "memory_second_repair_source_invalid" && error.details.reason === "registered_second_current_view_required"
127
+ : error.code === "memory_review_handler_invalid" && error.details.reason === "current_view_registration_required");
128
+ if (!missing)
129
+ throw error;
130
+ (second ? registerMemorySecondRepairCurrentViewFromStage : registerMemoryRepairCurrentViewFromStage)(context, scope);
131
+ registerBaseline(context, scope);
132
+ }
133
+ (second ? reserveMemorySecondRepairCopyFromStage : reserveMemoryRepairWriterCopyFromStage)(context, scope);
134
+ (second ? materializeMemorySecondRepairCopyFromStage : materializeMemoryRepairWriterCopyFromStage)(context, scope);
135
+ return (second ? prepareMemorySecondRepairDispatchFromStage : prepareMemoryRepairDispatchFromStage)(context, scope);
136
+ };
109
137
  const external = choices.filter(choice => choice.resolved.delegation.mode === "external");
110
138
  if (external.length) {
111
139
  const maximum = Math.min(...external.map(choice => choice.resolved.delegation.max_parallel ?? 1));
112
- return { kind: "external", works: external.slice(0, maximum).map(choice => choice.work) };
140
+ const selected = external.slice(0, maximum).map(choice => choice.work);
141
+ for (const work of selected)
142
+ prepareRepair(work);
143
+ return { kind: "external", works: selected };
113
144
  }
114
- const variables = getFlowRunVariables(context, input);
115
- const capacity = variables.variables[subagentCapacityKey];
145
+ const capacity = getStageFanoutCapacity(context, { ...input, nativeOnly: true }).available_slots;
116
146
  if (typeof capacity !== "number" || !Number.isInteger(capacity) || capacity < 1)
117
147
  return { kind: "wait", reason: "qualified_capacity_required" };
118
- const wave = choices.slice(0, capacity).map(choice => choice.work);
148
+ const wave = choices.slice(0, capacity).map(choice => {
149
+ const work = choice.work;
150
+ const row = context.db.get("SELECT payload_json,result_schema FROM works WHERE work_id=?", [work.work_id]);
151
+ const payload = JSON.parse(row?.payload_json ?? "null");
152
+ if (payload?.kind === "memory-review" && row?.result_schema === "dd-flow/memory-review-work-result@1") {
153
+ const copy = prepareMemoryNativeReviewCopy(context, work.work_id, true);
154
+ return { ...work, start_command: `cd '${copy.root.replaceAll("'", "'\\''")}' && ${workStartCommand(context, assertWorkLaunchReady(context, work.work_id))}` };
155
+ }
156
+ const packet = prepareRepair(work);
157
+ const command = workStartCommand(context, assertWorkLaunchReady(context, work.work_id));
158
+ if (!packet)
159
+ return { ...work, start_command: command };
160
+ // Ordinary literal shell grammar, not an invented native spawn CWD field.
161
+ const quotedRoot = "'" + packet.copy_root.replaceAll("'", "'\\''") + "'";
162
+ return { ...work, start_command: `cd ${quotedRoot} && ${command}` };
163
+ });
119
164
  for (const work of wave)
120
165
  if (!work.work_id || !work.start_command)
121
166
  throw new AppError("fanout_contract_invalid", "Ready Work omits its trusted start command", 1);
122
- if (input.stage === "plan-review" || input.stage === "code-review")
123
- for (const work of wave)
124
- assertWorkLaunchReady(context, work.work_id);
125
167
  const totalWorks = fanout.works.created + fanout.works.running + fanout.works.completed + fanout.works.failed + fanout.works.cancelled;
126
168
  return prompt("dispatch_work", renderDelegationInstructions({ harness: input.harness, stage: input.stage, capacity, works: wave, totalWorks, remainingWorks: fanout.works.created - wave.length }));
127
169
  }
@@ -129,6 +171,23 @@ export async function nextControllerFanout(context, input) {
129
171
  return { kind: "wait", reason: "work_receipts_pending" };
130
172
  if (fanout.works.created)
131
173
  throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
174
+ if (input.stage === "review") {
175
+ const projectId = requireProjectByRoot(context, input.projectRoot).id;
176
+ const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [projectId, input.runId]);
177
+ if (run?.flow_kind === "mb-init") {
178
+ const attempt = stageAttempt(context, projectId, input.runId, "review");
179
+ const repair = context.db.get(`SELECT CASE event_type WHEN 'memory.second_repair_registration' THEN 2 ELSE 1 END AS wave
180
+ FROM audit_events WHERE project_id=? AND event_type IN ('memory.repair_registration','memory.second_repair_registration')
181
+ AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.attempt')=?
182
+ AND json_extract(payload_json,'$.scope.parent_work_id')=? ORDER BY wave DESC LIMIT 1`, [projectId, input.runId, attempt, fanout.parent_work_id]);
183
+ if (repair && !context.db.get(`SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.corrected_review_registration'
184
+ AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.attempt')=?
185
+ AND json_extract(payload_json,'$.scope.parent_work_id')=? AND json_extract(payload_json,'$.wave')=?`, [projectId, input.runId, attempt, fanout.parent_work_id, repair.wave])) {
186
+ const args = ["--run", input.runId, "--project-root", input.projectRoot, "--wave", String(repair.wave), "--json"];
187
+ return prompt("semantic_completion", `Correction wave ${repair.wave} Work has settled. Before Stage finish, run ${runtimeCommand(context, ["memory", "corrections", "checks", ...args])}, then ${runtimeCommand(context, ["memory", "review", "selective-register", ...args])}. Read both complete responses, then end this Turn so the controller can dispatch the independent selective reviewers. Do not finish the Stage or treat repair Work completion as independent acceptance.`);
188
+ }
189
+ }
190
+ }
132
191
  return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed));
133
192
  }
134
193
  /** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
@@ -6,7 +6,7 @@ import { isActiveProtocolStatus, isActiveQueueStatus, isQueueHistoryStatus } fro
6
6
  import { AppError } from "../shared/errors.js";
7
7
  import { prepareOutputFile } from "../cli/input-preparation.js";
8
8
  import { ensureDir, resolveProjectRoot, resolveProtocolPlanPath } from "../storage/paths.js";
9
- import { dashboardMarkdownPath, globalDashboardHtmlPath, globalDashboardJsonPath, globalDashboardMarkdownPath, projectDashboardHtmlPath, projectDashboardJsonPath, projectSummaryJsonPath, protocolDashboardHtmlPath, protocolDashboardJsonPath, readProjectConfig } from "./config.js";
9
+ import { dashboardMarkdownPath, globalDashboardHtmlPath, globalDashboardJsonPath, globalDashboardMarkdownPath, projectDashboardHtmlPath, projectDashboardJsonPath, projectDashboardDir, projectSummaryJsonPath, protocolDashboardHtmlPath, protocolDashboardJsonPath, readProjectConfig } from "./config.js";
10
10
  import { queueForProject } from "./merge-queue.js";
11
11
  import { publishProjectSummary } from "./project-summary.js";
12
12
  import { requireProjectByRoot } from "./projects.js";
@@ -215,7 +215,13 @@ export function autoRefreshDashboards(context, input) {
215
215
  // Their derived timestamp must not mutate the captured workspace.
216
216
  if (projectRecoveryGuarded(context, project.id))
217
217
  return { ok: true, skipped: true, reason: "run_recovery_guarded" };
218
- const config = readProjectConfig(context, project.id);
218
+ const configured = readProjectConfig(context, project.id);
219
+ // Automatic observation must not change the source baseline owned by init.
220
+ // Keep projections in Home; do not hide runtime-created paths in Git or
221
+ // weaken source preservation by excluding user-owned .tasks directories.
222
+ const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind='mb-init' AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
223
+ const config = initializing ? { ...configured, dashboard: { ...configured.dashboard,
224
+ markdown_path: path.join(projectDashboardDir(context.ddFlowHome, project.id), "project-dashboard.md") } } : configured;
219
225
  if (!config.dashboard.auto_refresh) {
220
226
  return { ok: true, skipped: true, reason: "dashboard_auto_refresh_disabled" };
221
227
  }
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ import { AppError } from "../shared/errors.js";
3
+ import { readDescriptorStageInput, readDescriptorStageInputForRecovery } from "./descriptor-stage-coordinator.js";
4
+ import { runtimeHarness } from "./execution-policy.js";
5
+ import { stageGraphScope } from "./stage-work-graph.js";
6
+ import { hybridDigest } from "./hybrid-contracts.js";
7
+ /** SQL-only physical coordinator proof, independent of allocation/prompt
8
+ * custody. It creates no Session, hook claim or provider operation.
9
+ */
10
+ export function readDescriptorCoordinatorSession(context, input, refuse = reason => {
11
+ throw new AppError("descriptor_coordinator_session_invalid", "Coordinator has no matching physical Session custody", 1, { reason, effect: "no_effect" });
12
+ }) {
13
+ const { scope, bundle, workId } = input;
14
+ const rows = context.db.all("SELECT ws.id,ws.session_id,ws.hook_event_id,ws.prompt_path,ws.result_path,s.harness,s.provider_session_id FROM work_sessions ws JOIN sessions s ON s.session_id=ws.session_id AND s.project_id=? WHERE ws.work_id=? AND ws.status='running' AND s.run_id=? AND s.current_stage=? AND s.status='active' LIMIT 2", [scope.projectId, workId, scope.runId, scope.stage]);
15
+ if (rows.length !== 1)
16
+ refuse("active_coordinator_session_required");
17
+ const session = rows[0], route = bundle.execution.stage(scope.stage);
18
+ const selected = input.recoveryRead ? readDescriptorStageInputForRecovery(context, scope, bundle, input.recoveryRead) : readDescriptorStageInput(context, scope, bundle), directory = bundle.descriptor.stage(scope.stage).directory;
19
+ if (session.prompt_path !== path.join(selected.runRoot, directory, "coordinator-prompt.md")
20
+ || session.result_path !== path.join(selected.runRoot, directory, "result-input.json"))
21
+ refuse("coordinator_packet_link_changed");
22
+ if (session.harness !== runtimeHarness(route.profile)
23
+ || !context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND session_id=? AND harness=? AND provider_session_id=? AND status='claimed' AND event_name='PreToolUse'", [scope.projectId, session.hook_event_id, session.session_id, session.harness, session.provider_session_id]))
24
+ refuse("coordinator_start_custody_missing");
25
+ const bindings = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='stage.coordinator_binding' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')=? AND json_extract(payload_json,'$.work_session_id')=? LIMIT 33", [scope.projectId, scope.runId, scope.stage, session.id]);
26
+ if (!bindings.length || bindings.length > 32 || bindings.some(row => Buffer.byteLength(row.payload_json) > 32768))
27
+ refuse("physical_binding_missing");
28
+ const binding = JSON.parse(bindings[0].payload_json);
29
+ if (bindings.some(row => hybridDigest(JSON.parse(row.payload_json)) !== hybridDigest(binding))
30
+ || binding.work_id !== workId || binding.session_id !== session.session_id || binding.harness !== session.harness || binding.provider_session_id !== session.provider_session_id
31
+ || binding.execution_sha256 !== bundle.execution.sha256 || binding.profile_id !== route.profile.id || binding.profile_sha256 !== hybridDigest(route.profile)
32
+ || binding.session_mode !== route.session_mode || hybridDigest(binding.scope) !== hybridDigest({ ...stageGraphScope(context, scope.projectId, scope.runId, scope.stage), stage: scope.stage }))
33
+ refuse("physical_binding_changed");
34
+ return hybridDigest({ session, binding });
35
+ }
@@ -0,0 +1,53 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { nextWorkId } from "./ids.js";
5
+ import { appendAudit } from "./audit.js";
6
+ const event = "flow_run.descriptor_root_work";
7
+ function refuse(reason) {
8
+ throw new AppError("descriptor_root_work_invalid", "RUN root Work lacks independent allocation custody", 1, { reason, effect: "no_effect" });
9
+ }
10
+ const binding = (scope, bundle) => ({ projectId: scope.projectId, runId: scope.runId, bundle_sha256: bundle.sha256 });
11
+ /** SQL-only read. Root identity is retained independently, never discovered
12
+ * from an arbitrary root row or reconstructed after receipt loss.
13
+ */
14
+ export function readDescriptorRootWork(context, scope, bundle) {
15
+ return context.db.readSnapshot(() => {
16
+ const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
17
+ const receipts = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? LIMIT 2", [scope.projectId, event, scope.runId]);
18
+ if (receipts.length !== 1 || Buffer.byteLength(receipts[0].payload_json) > 16 * 1024)
19
+ refuse("root_work_receipt_missing");
20
+ const receipt = JSON.parse(receipts[0].payload_json);
21
+ const expected = binding(scope, bundle);
22
+ if (typeof receipt.work_id !== "string" || hybridDigest(receipt) !== hybridDigest({ run_id: scope.runId, work_id: receipt.work_id, binding: expected }))
23
+ refuse("root_work_changed");
24
+ const roots = context.db.all("SELECT work_id,task,payload_json,status,launch_policy,result_schema,depends_on_json FROM works WHERE project_id=? AND run_id=? AND parent_work_id IS NULL LIMIT 2", [scope.projectId, scope.runId]);
25
+ if (roots.length !== 1)
26
+ refuse("ambiguous_root_work");
27
+ const root = roots[0];
28
+ if (root.work_id !== receipt.work_id || root.task !== `Coordinate ${bundle.descriptor.descriptor.id}`
29
+ || root.payload_json !== JSON.stringify({ descriptor_run_coordinator: expected })
30
+ || root.launch_policy !== "reuse_allowed" || root.result_schema !== null || root.depends_on_json !== "[]")
31
+ refuse("root_work_custody_missing");
32
+ if (JSON.parse(run.index_json).root_work_id !== root.work_id)
33
+ refuse("root_work_anchor_missing");
34
+ return { workId: root.work_id, status: root.status };
35
+ });
36
+ }
37
+ /** Called only in the same writer as fresh descriptor RUN allocation. Stage
38
+ * handlers consume this identity; they never allocate a second RUN root.
39
+ */
40
+ export function allocateDescriptorRootWork(context, scope, bundle) {
41
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
42
+ if (context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND parent_work_id IS NULL", [scope.projectId, scope.runId])
43
+ || context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=?", [scope.projectId, event, scope.runId]))
44
+ refuse("root_work_already_allocated");
45
+ const workId = nextWorkId(context, scope.projectId, bundle.descriptor.descriptor.id), now = context.now();
46
+ const expected = binding(scope, bundle);
47
+ context.db.run("INSERT INTO works (work_id,project_id,run_id,parent_work_id,task,launch_policy,depends_on_json,status,payload_json,created_at,updated_at) VALUES (?,?,?,NULL,?,'reuse_allowed','[]','created',?,?,?)", [workId, scope.projectId, scope.runId, `Coordinate ${bundle.descriptor.descriptor.id}`, JSON.stringify({ descriptor_run_coordinator: expected }), now, now]);
48
+ context.db.run("UPDATE runs SET index_json=json_set(index_json,'$.root_work_id',?) WHERE project_id=? AND id=?", [workId, scope.projectId, scope.runId]);
49
+ appendAudit(context, { projectId: scope.projectId, eventType: event, payload: { run_id: scope.runId, work_id: workId, binding: expected } });
50
+ if (readDescriptorRootWork(context, scope, bundle).workId !== workId)
51
+ refuse("root_work_not_retained");
52
+ return workId;
53
+ }
@@ -0,0 +1,105 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { requireProjectByRoot } from "./projects.js";
3
+ import { resolveProjectRoot } from "../storage/paths.js";
4
+ import { appendFlowRunTimelineEvent, assertPreparedRunBundleSqlAuthority, completeDescriptorRunState, gitFacts, publishRunProjections, readFlowRunExecutionBundle, withStageSettlement } from "./runs.js";
5
+ import { readDescriptorRootWork } from "./descriptor-root-work.js";
6
+ import { readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
7
+ import { entityReferenceVariants } from "../shared/entity-references.js";
8
+ import { stageLifecycleMatchKey } from "./hooks.js";
9
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
10
+ import { assertRunMutationAllowed } from "./run-recovery-guard.js";
11
+ import { appendAudit } from "./audit.js";
12
+ import { hybridDigest } from "./hybrid-contracts.js";
13
+ const event = "flow_run.descriptor_run_settlement";
14
+ function refuse(reason) {
15
+ throw new AppError("descriptor_run_settlement_invalid", "Terminal descriptor RUN lacks exact settlement custody", 1, { reason, effect: "no_effect" });
16
+ }
17
+ function selected(context, scope, bundle, matchKeys) {
18
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
19
+ const root = readDescriptorRootWork(context, scope, bundle);
20
+ const run = context.db.get("SELECT status,index_json FROM runs WHERE project_id=? AND id=?", [scope.projectId, scope.runId]);
21
+ const index = JSON.parse(run.index_json);
22
+ const terminal = index.stage_runs.at(-1);
23
+ if (!bundle.descriptor.descriptor.terminal_stages.includes(scope.stage)
24
+ || terminal?.stage !== scope.stage || terminal.status !== "done")
25
+ refuse("terminal_stage_not_accepted");
26
+ const data = readAcceptedStageData(context, scope);
27
+ const stageReceipt = readAcceptedDescriptorStageSettlement(context, scope, bundle, matchKeys);
28
+ if (context.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND work_id<>? AND status IN ('created','running','paused') LIMIT 1", [scope.projectId, scope.runId, root.workId])
29
+ || 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]))
30
+ refuse("work_still_active");
31
+ const prior = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? LIMIT 2", [scope.projectId, event, scope.runId]);
32
+ if (prior.length > 1 || prior.some(row => Buffer.byteLength(row.payload_json) > 32 * 1024))
33
+ refuse("ambiguous_receipt");
34
+ if (root.status !== (prior.length ? "completed" : "running") || run.status !== (prior.length ? "done" : "running"))
35
+ refuse("terminal_state_changed");
36
+ return { rootId: root.workId, result: data.bytes.toString("utf8"), binding: { ...scope, work_id: root.workId, bundle_sha256: bundle.sha256, result_sha256: data.result.sha256, input_sha256: data.input_sha256, stage_receipt_sha256: hybridDigest(stageReceipt) }, prior: prior[0] ? JSON.parse(prior[0].payload_json) : null };
37
+ }
38
+ /** Runtime closure only. A terminal handler must first verify semantic/check/
39
+ * review/Git acceptance, revalidate physical proof, and supply its SQL fence.
40
+ * Transcript usage collection is deliberately outside this atomic writer.
41
+ */
42
+ export class PreparedDescriptorRunSettlement {
43
+ #scope;
44
+ #bundle;
45
+ #owner;
46
+ #git;
47
+ #generation;
48
+ #matchKeys;
49
+ constructor(scope, bundle, owner, git, generation, matchKeys) {
50
+ this.#scope = scope;
51
+ this.#bundle = bundle;
52
+ this.#owner = owner;
53
+ this.#git = git;
54
+ this.#generation = generation;
55
+ this.#matchKeys = matchKeys;
56
+ Object.freeze(this);
57
+ }
58
+ static prepare(context, input) {
59
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), bundle = readFlowRunExecutionBundle(context, input);
60
+ if (!bundle)
61
+ refuse("bundle_required");
62
+ const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
63
+ const matchKeys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, input.stage, input.projectRoot));
64
+ const snapshot = context.db.readSnapshot(() => ({ owner: selected(context, scope, bundle, matchKeys), generation: assertRunMutationAllowed(context, scope.projectId, scope.runId) }));
65
+ const git = snapshot.owner.prior?.git ?? gitFacts(bundle.execution.snapshot().scope.workspace);
66
+ return new PreparedDescriptorRunSettlement(scope, bundle, snapshot.owner, git, snapshot.generation, matchKeys);
67
+ }
68
+ apply(context, assertHandlerCustody) {
69
+ if (typeof assertHandlerCustody !== "function")
70
+ refuse("handler_fence_required");
71
+ const bundle = readFlowRunExecutionBundle(context, { projectRoot: this.#bundle.execution.snapshot().scope.projectRoot, runId: this.#scope.runId });
72
+ if (!bundle || bundle.sha256 !== this.#bundle.sha256)
73
+ refuse("bundle_changed");
74
+ const observed = context.db.readSnapshot(() => selected(context, this.#scope, this.#bundle, this.#matchKeys));
75
+ if (!observed.prior && hybridDigest(gitFacts(this.#bundle.execution.snapshot().scope.workspace)) !== hybridDigest(this.#git))
76
+ refuse("git_observation_changed");
77
+ const receipt = { run_id: this.#scope.runId, work_id: this.#owner.rootId, binding: this.#owner.binding, git: this.#git };
78
+ const changed = withStageSettlement(context, () => {
79
+ assertRunMutationAllowed(context, this.#scope.projectId, this.#scope.runId, this.#generation);
80
+ assertHandlerCustody(context);
81
+ const current = selected(context, this.#scope, this.#bundle, this.#matchKeys);
82
+ if (hybridDigest(current.binding) !== hybridDigest(this.#owner.binding) || current.result !== this.#owner.result)
83
+ refuse("terminal_custody_changed");
84
+ if (current.prior) {
85
+ if (hybridDigest(current.prior) !== hybridDigest(receipt)
86
+ || context.db.get("SELECT result FROM works WHERE work_id=?", [current.rootId])?.result !== current.result)
87
+ refuse("settlement_changed");
88
+ return false;
89
+ }
90
+ if (this.#owner.prior)
91
+ refuse("settlement_receipt_missing");
92
+ const now = context.now();
93
+ if (context.db.run("UPDATE works SET status='completed',result=?,completed_at=?,updated_at=? WHERE work_id=? AND project_id=? AND run_id=? AND status='running'", [current.result, now, now, current.rootId, this.#scope.projectId, this.#scope.runId]).changes !== 1)
94
+ refuse("root_changed");
95
+ appendFlowRunTimelineEvent(context, this.#scope.projectId, this.#scope.runId, { at: now, type: "work_completed", work_id: current.rootId, descriptor_terminal: true });
96
+ completeDescriptorRunState(context, this.#scope, this.#bundle, this.#git);
97
+ appendAudit(context, { projectId: this.#scope.projectId, eventType: event, payload: receipt });
98
+ return true;
99
+ });
100
+ // A committed terminal receipt can outlive an interrupted file publisher.
101
+ // Replay restores projections from current SQL, without settling again.
102
+ publishRunProjections(context, this.#scope.projectId, this.#scope.runId);
103
+ return { changed, run_id: this.#scope.runId, work_id: this.#owner.rootId, status: "done" };
104
+ }
105
+ }