@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149

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 (254) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +13 -3
  5. package/dist/cli/command-metadata.js +51 -16
  6. package/dist/cli/diagnostic-invocation.js +46 -0
  7. package/dist/cli/diagnostic.js +120 -0
  8. package/dist/cli/help.js +67 -1
  9. package/dist/cli/hook-ingress.js +27 -3
  10. package/dist/cli/memory-flow-admission.js +9 -3
  11. package/dist/cli/memory-init-invocation.js +10 -3
  12. package/dist/cli/memory-init.js +77 -68
  13. package/dist/cli/memory-storage-admission.js +3 -2
  14. package/dist/cli/memory-storage-qualification.js +105 -0
  15. package/dist/cli/memory-storage-transition.js +123 -0
  16. package/dist/cli/memory-upgrade.js +189 -0
  17. package/dist/cli/native-hook-ingress.js +14 -4
  18. package/dist/cli/native-hook-preparation.js +55 -0
  19. package/dist/cli/response-file.js +2 -1
  20. package/dist/cli/run-cli.js +79 -40
  21. package/dist/cli/setup.js +2 -1
  22. package/dist/cli.js +18 -0
  23. package/dist/domain/diagnostic-aspects.js +103 -0
  24. package/dist/domain/diagnostic-flows.js +8 -0
  25. package/dist/domain/stage-handler-identities.js +8 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  31. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  32. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  33. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  34. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
  35. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  37. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  38. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  39. package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
  40. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
  41. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  42. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  43. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  44. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  45. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  46. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  47. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  48. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  49. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  50. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  51. package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
  52. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  53. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  54. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  55. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  56. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  57. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  58. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  59. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  60. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
  61. package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
  62. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  63. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  64. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  65. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  66. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  67. package/dist/schemas/execution-bundle.schema.json +7 -1
  68. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  69. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  70. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  71. package/dist/schemas/release-impact.schema.json +3 -3
  72. package/dist/schemas/stage-start-response.schema.json +4 -0
  73. package/dist/services/accepted-intent.js +13 -1
  74. package/dist/services/canon.js +43 -7
  75. package/dist/services/check-environment.js +21 -0
  76. package/dist/services/check-runner-evidence.js +267 -15
  77. package/dist/services/cli-operation-classifier.js +1 -1
  78. package/dist/services/cli-upgrade-allowlist.js +1 -1
  79. package/dist/services/code-checks.js +34 -19
  80. package/dist/services/compatibility-preflight.js +5 -2
  81. package/dist/services/controller-execution.js +10 -4
  82. package/dist/services/controller-fanout.js +34 -10
  83. package/dist/services/controller-memory-task.js +45 -0
  84. package/dist/services/controller-memory-worker.js +49 -0
  85. package/dist/services/dashboard.js +2 -2
  86. package/dist/services/descriptor-run-settlement.js +5 -3
  87. package/dist/services/descriptor-run-start.js +46 -20
  88. package/dist/services/descriptor-stage-coordinator.js +66 -13
  89. package/dist/services/descriptor-stage-settlement.js +2 -1
  90. package/dist/services/descriptor-stage-start.js +40 -4
  91. package/dist/services/diagnostic-evidence.js +73 -0
  92. package/dist/services/diagnostic-report-publication.js +72 -0
  93. package/dist/services/diagnostic-report.js +67 -0
  94. package/dist/services/diagnostic-run-bundle.js +74 -0
  95. package/dist/services/diagnostic-snapshot.js +159 -0
  96. package/dist/services/diagnostic-stage-handlers.js +108 -0
  97. package/dist/services/diagnostic-stage-results.js +132 -0
  98. package/dist/services/diagnostic-start.js +84 -0
  99. package/dist/services/diagnostic-work-batch.js +34 -0
  100. package/dist/services/diagnostic-work-collection.js +97 -0
  101. package/dist/services/diagnostic-work-registration.js +101 -0
  102. package/dist/services/diagnostic-work-results.js +93 -0
  103. package/dist/services/engines.js +64 -14
  104. package/dist/services/eval-snapshots.js +186 -119
  105. package/dist/services/execution-bundles.js +19 -7
  106. package/dist/services/external-work-launch.js +5 -3
  107. package/dist/services/flow-catalog.js +13 -4
  108. package/dist/services/frozen-engine.js +148 -11
  109. package/dist/services/hook-memory-routing.js +188 -0
  110. package/dist/services/hook-recovery-guard.js +14 -0
  111. package/dist/services/hook-session-observation.js +24 -0
  112. package/dist/services/hooks.js +27 -8
  113. package/dist/services/hybrid-contracts.js +4 -13
  114. package/dist/services/hybrid-digest.js +14 -0
  115. package/dist/services/installed-flow-source.js +47 -4
  116. package/dist/services/lifecycle-invocations.js +179 -38
  117. package/dist/services/memory-accepted-composition.js +5 -4
  118. package/dist/services/memory-completed-correction-wave.js +23 -15
  119. package/dist/services/memory-compose-handler.js +163 -21
  120. package/dist/services/memory-composition-acceptance.js +186 -10
  121. package/dist/services/memory-composition-graph.js +42 -11
  122. package/dist/services/memory-composition-registration.js +23 -8
  123. package/dist/services/memory-composition-work-custody.js +14 -9
  124. package/dist/services/memory-composition-work-results.js +15 -3
  125. package/dist/services/memory-corrected-review-batch.js +10 -3
  126. package/dist/services/memory-corrected-review-findings.js +4 -2
  127. package/dist/services/memory-corrected-review-inputs.js +10 -5
  128. package/dist/services/memory-corrected-review-receipt.js +17 -9
  129. package/dist/services/memory-corrected-review-registration.js +3 -2
  130. package/dist/services/memory-correction-check-receipt.js +20 -4
  131. package/dist/services/memory-custody-snapshot.js +36 -0
  132. package/dist/services/memory-finish-evidence.js +44 -22
  133. package/dist/services/memory-finish-handler.js +37 -23
  134. package/dist/services/memory-finish-integration.js +218 -48
  135. package/dist/services/memory-finish-readiness.js +66 -19
  136. package/dist/services/memory-git-integration.js +87 -18
  137. package/dist/services/memory-historical-corrected-review.js +21 -15
  138. package/dist/services/memory-historical-original-review.js +22 -9
  139. package/dist/services/memory-historical-repair-registration.js +14 -8
  140. package/dist/services/memory-historical-second-review.js +30 -11
  141. package/dist/services/memory-init-contexts.js +44 -2
  142. package/dist/services/memory-init-controller-start.js +27 -8
  143. package/dist/services/memory-init-git-policy.js +50 -21
  144. package/dist/services/memory-init-lint-capability.js +24 -6
  145. package/dist/services/memory-init-native-capacity.js +17 -4
  146. package/dist/services/memory-init-preflight.js +21 -17
  147. package/dist/services/memory-init-request-candidate.js +73 -17
  148. package/dist/services/memory-init-request-input.js +23 -7
  149. package/dist/services/memory-init-run-bundle.js +49 -12
  150. package/dist/services/memory-init-storage.js +17 -7
  151. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  152. package/dist/services/memory-layer.js +71 -6
  153. package/dist/services/memory-lint-config.js +111 -2
  154. package/dist/services/memory-native-review-copy.js +5 -67
  155. package/dist/services/memory-normal-flow-entry.js +12 -0
  156. package/dist/services/memory-ordinary-defaults.js +22 -5
  157. package/dist/services/memory-preflight-handler.js +15 -0
  158. package/dist/services/memory-repair-current-view.js +1 -1
  159. package/dist/services/memory-repair-document-delta.js +70 -33
  160. package/dist/services/memory-repair-history.js +12 -7
  161. package/dist/services/memory-repair-physical-binding.js +1 -119
  162. package/dist/services/memory-repair-publication-plan.js +5 -3
  163. package/dist/services/memory-repair-publication-source.js +1 -1
  164. package/dist/services/memory-repair-publication.js +3 -2
  165. package/dist/services/memory-repair-registration.js +2 -1
  166. package/dist/services/memory-repair-work-baseline.js +4 -2
  167. package/dist/services/memory-repair-work-completion.js +32 -12
  168. package/dist/services/memory-repair-work-results.js +21 -2
  169. package/dist/services/memory-repair-workspace.js +11 -2
  170. package/dist/services/memory-repair-writer-copy.js +2 -1
  171. package/dist/services/memory-result-binding.js +78 -12
  172. package/dist/services/memory-retained-review-authority.js +17 -8
  173. package/dist/services/memory-retained-second-baseline.js +1 -1
  174. package/dist/services/memory-retained-second-repairs.js +12 -6
  175. package/dist/services/memory-review-correction-plan.js +47 -12
  176. package/dist/services/memory-review-handler.js +17 -17
  177. package/dist/services/memory-review-inputs.js +55 -9
  178. package/dist/services/memory-review-limits.js +10 -0
  179. package/dist/services/memory-review-receipt.js +2 -2
  180. package/dist/services/memory-review-registration.js +13 -4
  181. package/dist/services/memory-review-work-custody.js +3 -1
  182. package/dist/services/memory-review-work-input.js +26 -7
  183. package/dist/services/memory-review-work-results.js +26 -4
  184. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  185. package/dist/services/memory-second-correction-plan.js +2 -1
  186. package/dist/services/memory-second-repair-launch.js +4 -4
  187. package/dist/services/memory-second-repair-source.js +1 -1
  188. package/dist/services/memory-stage-dispatch.js +38 -1
  189. package/dist/services/memory-stage-results.js +113 -22
  190. package/dist/services/memory-tool-probe.js +25 -0
  191. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  192. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  193. package/dist/services/memory-upgrade-evidence.js +100 -0
  194. package/dist/services/memory-upgrade-inventory.js +442 -0
  195. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  196. package/dist/services/memory-upgrade-publication.js +413 -0
  197. package/dist/services/memory-upgrade-selection.js +430 -0
  198. package/dist/services/memory-upgrade-start.js +60 -0
  199. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  200. package/dist/services/memory-verification-acceptance.js +38 -17
  201. package/dist/services/memory-verification-checks.js +92 -27
  202. package/dist/services/memory-verification-receipt.js +6 -5
  203. package/dist/services/memory-verify-handler.js +15 -13
  204. package/dist/services/memory-work-result-json.js +12 -0
  205. package/dist/services/memory-work-result-preparation.js +5 -4
  206. package/dist/services/merge-server.js +4 -2
  207. package/dist/services/migrations.js +127 -98
  208. package/dist/services/native-daemon-history.js +2 -1
  209. package/dist/services/physical-startup.js +45 -0
  210. package/dist/services/project-registry.js +97 -0
  211. package/dist/services/project-summary.js +1 -1
  212. package/dist/services/projects.js +3 -93
  213. package/dist/services/prompt-delivery.js +71 -0
  214. package/dist/services/provider-capacity.js +112 -46
  215. package/dist/services/recovery-snapshot-database.js +32 -4
  216. package/dist/services/release-cache.js +8 -0
  217. package/dist/services/repair-intents.js +1 -12
  218. package/dist/services/repair-receipt.js +13 -0
  219. package/dist/services/run-control-worker.js +35 -18
  220. package/dist/services/run-control.js +2 -1
  221. package/dist/services/run-controller-adapter.js +2 -1
  222. package/dist/services/run-controller-process.js +4 -7
  223. package/dist/services/run-controller-recovery.js +8 -2
  224. package/dist/services/run-controller.js +215 -94
  225. package/dist/services/run-fork.js +1 -1
  226. package/dist/services/run-recovery-guard.js +30 -0
  227. package/dist/services/run-recovery.js +11 -44
  228. package/dist/services/run-start-custody.js +17 -5
  229. package/dist/services/runs.js +6 -1
  230. package/dist/services/runtime-scope-resume.js +12 -6
  231. package/dist/services/runtime-scope-stop.js +3 -2
  232. package/dist/services/runtime-scope-worker.js +46 -19
  233. package/dist/services/runtime-service.js +2 -1
  234. package/dist/services/sessions.js +3 -24
  235. package/dist/services/setup.js +63 -13
  236. package/dist/services/stage-handlers.js +22 -7
  237. package/dist/services/stage-pause.js +111 -44
  238. package/dist/services/version-status.js +1 -1
  239. package/dist/services/vnext-code.js +2 -2
  240. package/dist/services/vnext-fanout.js +51 -9
  241. package/dist/services/vnext-plan-review.js +3 -5
  242. package/dist/services/work-registry.js +117 -39
  243. package/dist/services/workspace-bootstrap.js +2 -1
  244. package/dist/services/workspace-files.js +1 -1
  245. package/dist/shared/entity-references.js +1 -8
  246. package/dist/storage/data-preservation.js +65 -0
  247. package/dist/storage/database.js +53 -19
  248. package/dist/storage/import-guard.js +84 -27
  249. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  250. package/dist/storage/paired-writer-migration.js +424 -0
  251. package/dist/storage/writer-contract.js +11 -4
  252. package/dist/storage/writer-migration.js +313 -52
  253. package/dist/storage/writer-restore.js +217 -0
  254. package/package.json +13 -6
@@ -9,7 +9,9 @@ import { finishVnextMerge, startVnextMerge } from "./vnext-merge.js";
9
9
  import { ResolvedFlowDescriptor } from "./flow-descriptors.js";
10
10
  import { vnextStages } from "../domain/stage-catalog.js";
11
11
  import { sdlcHandlerIdentities as ids } from "../domain/stage-handler-identities.js";
12
- import { memoryInitStageHandlers } from "./memory-stage-dispatch.js";
12
+ import { memoryInitStageHandlers, memoryUpgradeStageHandlers } from "./memory-stage-dispatch.js";
13
+ import { diagnosticStageHandlers } from "./diagnostic-stage-handlers.js";
14
+ import { isDiagnosticFlow, diagnosticFlowStages } from "../domain/diagnostic-flows.js";
13
15
  /** Engine-owned code, not a plugin registration surface or executable descriptor expression. */
14
16
  export const sdlcStageHandlers = Object.freeze({
15
17
  specify: Object.freeze({ ...ids.specify, entry: launchVnextSpecify, finish: finishVnextSpecifyStage }),
@@ -23,8 +25,8 @@ export const sdlcStageHandlers = Object.freeze({
23
25
  /** A fresh inventory cannot mutate the registered handler functions. */
24
26
  export function bundledHandlerContext(blobs) {
25
27
  return {
26
- entryHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers)].map(handler => handler.entryId)),
27
- finishHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers)].map(handler => handler.finishId)), blobs
28
+ entryHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers), ...diagnosticStageHandlers].map(handler => handler.entryId)),
29
+ finishHandlers: new Set([...Object.values(sdlcStageHandlers), ...Object.values(memoryInitStageHandlers), ...Object.values(memoryUpgradeStageHandlers), ...diagnosticStageHandlers].map(handler => handler.finishId)), blobs
28
30
  };
29
31
  }
30
32
  /** Handler identity is also stage/flow scoped: a known CODE handler is not a memory handler. */
@@ -40,8 +42,9 @@ export function admitBundledFlow(input, sha256, blobs) {
40
42
  throw new AppError("flow_handler_scope_invalid", "SDLC handlers require the registered seven-stage layout and transitions", 2, { phase: "prepare", effect: "no_effect" });
41
43
  }
42
44
  }
43
- if (flow.descriptor.id === "mb-init") {
44
- const stages = Object.keys(memoryInitStageHandlers);
45
+ if (flow.descriptor.id === "mb-init" || flow.descriptor.id === "mb-upgrade") {
46
+ const upgrade = flow.descriptor.id === "mb-upgrade";
47
+ const stages = Object.keys(upgrade ? memoryUpgradeStageHandlers : memoryInitStageHandlers);
45
48
  const edges = stages.slice(0, -1).map((from, index) => ({ from, to: stages[index + 1], on: "accepted" }));
46
49
  // Historical packs have only accepted transitions; diagnostic VERIFY uses
47
50
  // the same REVIEW handler without granting another route or stage.
@@ -59,12 +62,24 @@ export function admitBundledFlow(input, sha256, blobs) {
59
62
  schemaId = JSON.parse(flow.stageBlob(stage.id, "result_schema").toString("utf8")).$id;
60
63
  }
61
64
  catch { /* Invalid schema cannot authorize this stage handler. */ }
62
- if (schemaId !== `dd-flow/mb-init-${stage.id}-result@1`)
65
+ if (schemaId !== (upgrade ? "dd-flow/mb-upgrade-stage-result@1" : `dd-flow/mb-init-${stage.id}-result@1`))
63
66
  throw new AppError("flow_handler_scope_invalid", "Memory init stage requires its own registered result schema", 2, { phase: "prepare", effect: "no_effect", stage: stage.id });
64
67
  }
65
68
  }
69
+ if (isDiagnosticFlow(flow.descriptor.id)) {
70
+ const stages = diagnosticFlowStages[flow.descriptor.id];
71
+ if (flow.descriptor.entry_stage !== "preflight" || flow.descriptor.terminal_stages.length !== 1 || flow.descriptor.terminal_stages[0] !== "report"
72
+ || flow.descriptor.stages.length !== stages.length || flow.descriptor.edges.length !== stages.length - 1
73
+ || flow.descriptor.stages.some((stage, index) => stage.id !== stages[index] || stage.directory !== `${String(index + 1).padStart(2, "0")}-${stage.id}`)
74
+ || stages.slice(0, -1).some((stage, index) => !flow.descriptor.edges.some(edge => edge.from === stage && edge.to === stages[index + 1] && edge.on === "accepted"))
75
+ || flow.descriptor.stages.some(stage => JSON.parse(flow.stageBlob(stage.id, "result_schema").toString("utf8")).$id !== "dd-flow/diagnostic-stage-result@1")) {
76
+ throw new AppError("flow_handler_scope_invalid", "Diagnostic handlers require their registered four-stage layout and result schema", 2, { phase: "prepare", effect: "no_effect" });
77
+ }
78
+ }
66
79
  for (const stage of flow.descriptor.stages) {
67
- const handlers = flow.descriptor.id === "sdlc" ? Object.values(sdlcStageHandlers) : flow.descriptor.id === "mb-init" ? Object.values(memoryInitStageHandlers) : [];
80
+ const handlers = flow.descriptor.id === "sdlc" ? Object.values(sdlcStageHandlers) : flow.descriptor.id === "mb-init" ? Object.values(memoryInitStageHandlers)
81
+ : flow.descriptor.id === "mb-upgrade" ? Object.values(memoryUpgradeStageHandlers)
82
+ : diagnosticStageHandlers.filter(handler => handler.flowId === flow.descriptor.id);
68
83
  const handler = handlers.find(candidate => candidate.stage === stage.id);
69
84
  if (!handler || stage.entry_handler !== handler.entryId || stage.finish_handler !== handler.finishId) {
70
85
  throw new AppError("flow_handler_scope_invalid", "Registered handlers do not belong to this flow stage", 2, { phase: "prepare", effect: "no_effect", flow_id: flow.descriptor.id, stage: stage.id });
@@ -17,8 +17,11 @@ import { assertRunMutationAllowed } from "./run-recovery.js";
17
17
  import { runtimeCommand } from "./runtime-command.js";
18
18
  import { prepareAgentOutputFile } from "../storage/agent-output.js";
19
19
  import { latestRunVerificationMatrix } from "./verification-matrix.js";
20
- import { validateAcceptedIntentReview } from "./accepted-intent.js";
20
+ import { acceptedIntentReviewFingerprint, prepareAcceptedIntentSources, validatePreparedAcceptedIntentReview } from "./accepted-intent.js";
21
21
  export { flowCommand } from "./runtime-command.js";
22
+ export function stageResumeSourcePath(pause, answer) {
23
+ return path.join(path.dirname(pause.question_path), `resume-${crypto.createHash("sha256").update(`${pause.id}\0${answer}`).digest("hex")}.json`);
24
+ }
22
25
  export function prepareStagePause(context, input) {
23
26
  if (typeof input.question !== "string" || !input.question.trim())
24
27
  throw new AppError("validation", "stage pause requires a non-empty user question", 2);
@@ -116,7 +119,31 @@ export function resumeStageAfterUser(context, input) {
116
119
  // reserving SQLite's writer; an unreferenced candidate has no flow authority.
117
120
  publishImmutableInput(answerPath, Buffer.from(input.answer), "hitl_answer_conflict");
118
121
  const answerPacket = prepareStagePacket(answerPath, Buffer.from(input.answer));
119
- context.db.writeTransaction(() => {
122
+ // Capture the exact continuation inputs and already-issued commands before
123
+ // accepting the answer. A publication retry must not issue new authority.
124
+ const retryCommands = lifecycleRetryCommands(context, stage.stage);
125
+ const resumeSourcePath = stageResumeSourcePath(pause, input.answer);
126
+ let review;
127
+ let reviewPreparationError;
128
+ const previousAnswer = context.db.get("SELECT MAX(id) AS id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ?", [project.id, run.id])?.id ?? 0;
129
+ if (["plan-review", "code-review"].includes(stage.stage)) {
130
+ try {
131
+ const home = requireRunHome(run), source = latestRunVerificationMatrix(context, project.id, run.id, stage.stage, "input");
132
+ const reviewContext = path.join(home, stage.stage === "plan-review" ? "04-plan-review/work-context.json" : "06-code-review/review-context.json");
133
+ const retainedContext = fs.existsSync(reviewContext) ? JSON.parse(fs.readFileSync(reviewContext, "utf8")) : null;
134
+ const currentSources = acceptedIntentReviewFingerprint(source?.metadata.accepted_sources) ? prepareAcceptedIntentSources(context, { projectId: project.id, runId: run.id, home, stageDirectory: "intake", marked: true }).sources : [];
135
+ review = { home, source, retainedContext, currentSources };
136
+ }
137
+ catch (error) {
138
+ if (!(error instanceof AppError))
139
+ throw error;
140
+ reviewPreparationError = error;
141
+ }
142
+ }
143
+ return context.db.writeTransaction(() => {
144
+ const currentAnswer = context.db.get("SELECT MAX(id) AS id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ?", [project.id, run.id])?.id ?? 0;
145
+ if (currentAnswer !== previousAnswer)
146
+ throw new AppError("runtime_state_changed", "Accepted inputs changed during resume preparation", 1);
120
147
  const identity = claimStageResumeHookEvent(context, { projectId: project.id, runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, eventKey: hookEventId });
121
148
  if (workSession.session_id !== identity.sessionId) {
122
149
  bindSessionForResume(context, work.work_id, identity, now);
@@ -130,49 +157,56 @@ export function resumeStageAfterUser(context, input) {
130
157
  acceptStagePacket(context, { projectId: project.id, runId: run.id, stage: stage.stage, prepared: answerPacket });
131
158
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_resume_session_bound", stage: stage.stage, work_id: work.work_id, session_id: identity.sessionId });
132
159
  context.db.afterCommit(() => refreshRunWorkProjection(context, project.id, run.id));
133
- });
134
- const prompt = evidence.prompt;
135
- const retryCommands = lifecycleRetryCommands(context, stage.stage);
136
- const intentInstructions = [];
137
- if (["plan-review", "code-review"].includes(stage.stage)) {
138
- try {
139
- const home = requireRunHome(run), source = latestRunVerificationMatrix(context, project.id, run.id, stage.stage, "input");
140
- const reviewContext = path.join(home, stage.stage === "plan-review" ? "04-plan-review/work-context.json" : "06-code-review/review-context.json");
141
- const retainedContext = fs.existsSync(reviewContext) ? JSON.parse(fs.readFileSync(reviewContext, "utf8")) : null;
142
- validateAcceptedIntentReview(context, { projectId: project.id, runId: run.id, home, stage: stage.stage, decisionFrozen: Boolean(retainedContext?.accepted_decision) }, source?.metadata.accepted_sources, retainedContext?.accepted_intent_review_fingerprint);
160
+ const prompt = evidence.prompt;
161
+ const intentInstructions = [];
162
+ if (["plan-review", "code-review"].includes(stage.stage)) {
163
+ try {
164
+ if (reviewPreparationError)
165
+ throw reviewPreparationError;
166
+ if (review) {
167
+ const id = context.db.get("SELECT id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.pause_id') = ? ORDER BY id DESC LIMIT 1", [project.id, run.id, pause.id]).id;
168
+ const identity = `accepted_hitl:${String(id).padStart(12, "0")}:${pause.id}`;
169
+ const sources = [...review.currentSources, { role: `${identity}:0-question`, sha256: crypto.createHash("sha256").update(evidence.question).digest("hex") }, { role: `${identity}:1-answer`, sha256: crypto.createHash("sha256").update(input.answer).digest("hex") }];
170
+ validatePreparedAcceptedIntentReview(context, { projectId: project.id, runId: run.id, home: review.home, stage: stage.stage, decisionFrozen: Boolean(review.retainedContext?.accepted_decision) }, review.source?.metadata.accepted_sources, review.retainedContext?.accepted_intent_review_fingerprint, sources);
171
+ }
172
+ }
173
+ catch (error) {
174
+ if (!(error instanceof AppError))
175
+ throw error;
176
+ intentInstructions.push(`Accepted-input review boundary: ${error.code}: ${error.message}`);
177
+ if (error.code === "accepted_intent_review_ack_required")
178
+ intentInstructions.push(`Apply/classify this own-stage clarification through the existing coordinator correction/repair route. Include this exact field in the decision: ${JSON.stringify({ accepted_intent_acknowledgement: error.details.accepted_intent_acknowledgement })}. Independent reviewer results prove only the earlier retained inputs; do not claim they reviewed the new answer.`);
179
+ else
180
+ intentInstructions.push("Do not accept stale independent review as coverage of this answer; report the actionable upstream revision/evidence blocker.");
181
+ }
143
182
  }
144
- catch (error) {
145
- if (!(error instanceof AppError))
146
- throw error;
147
- intentInstructions.push(`Accepted-input review boundary: ${error.code}: ${error.message}`);
148
- if (error.code === "accepted_intent_review_ack_required")
149
- intentInstructions.push(`Apply/classify this own-stage clarification through the existing coordinator correction/repair route. Include this exact field in the decision: ${JSON.stringify({ accepted_intent_acknowledgement: error.details.accepted_intent_acknowledgement })}. Independent reviewer results prove only the earlier retained inputs; do not claim they reviewed the new answer.`);
150
- else
151
- intentInstructions.push("Do not accept stale independent review as coverage of this answer; report the actionable upstream revision/evidence blocker.");
152
- }
153
- }
154
- const continuation = [
155
- prompt.trimEnd(),
156
- "",
157
- "<hitl_resume>",
158
- `- Pause: ${pause.id}`,
159
- `- Question source: ${pause.question_path}`,
160
- `- Answer source: ${answerPath}`,
161
- "- Continue the same stage, Work and attempt. Do not call stage start or repeat completed preparation.",
162
- ...intentInstructions,
163
- ...(retryCommands.length ? ["- A prior no-effect lifecycle rejection now has these authoritative replacement commands; correct the rejected input and use only these replacements:", ...retryCommands.map(command => ` ${command}`)] : []),
164
- "",
165
- "<user_question>",
166
- evidence.question,
167
- "</user_question>",
168
- "",
169
- "<user_answer>",
170
- input.answer,
171
- "</user_answer>",
172
- "</hitl_resume>",
173
- ""
174
- ].join("\n");
175
- return { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id: pause.id, question_path: pause.question_path, answer_path: answerPath, prompt_path: workSession.prompt_path, worker_prompt_markdown: continuation, next_action: `continue_${stage.stage}` };
183
+ const continuation = [
184
+ prompt.trimEnd(),
185
+ "",
186
+ "<hitl_resume>",
187
+ `- Pause: ${pause.id}`,
188
+ `- Question source: ${pause.question_path}`,
189
+ `- Answer source: ${answerPath}`,
190
+ "- Continue the same stage, Work and attempt. Do not call stage start or repeat completed preparation.",
191
+ ...intentInstructions,
192
+ ...(retryCommands.length ? ["- A prior no-effect lifecycle rejection now has these authoritative replacement commands; correct the rejected input and use only these replacements:", ...retryCommands.map(command => ` ${command}`)] : []),
193
+ "",
194
+ "<user_question>",
195
+ evidence.question,
196
+ "</user_question>",
197
+ "",
198
+ "<user_answer>",
199
+ input.answer,
200
+ "</user_answer>",
201
+ "</hitl_resume>",
202
+ ""
203
+ ].join("\n");
204
+ const result = { ok: true, outcome: "resumed", run_id: run.id, work_id: work.work_id, stage: stage.stage, pause_id: pause.id, question_path: pause.question_path, answer_path: answerPath, resume_source_path: resumeSourcePath, prompt_path: workSession.prompt_path, worker_prompt_markdown: continuation, next_action: `continue_${stage.stage}` };
205
+ const scope = context.env.DD_FLOW_INVOCATION_SCOPE ? JSON.parse(context.env.DD_FLOW_INVOCATION_SCOPE) : null;
206
+ const bytes = Buffer.from(`${JSON.stringify({ invocation_id: context.env.DD_FLOW_CURRENT_INVOCATION ?? null, generation: scope?.generation ?? null, result })}\n`);
207
+ acceptStagePacket(context, { projectId: project.id, runId: run.id, stage: stage.stage, prepared: { file: resumeSourcePath, bytes, sha256: crypto.createHash("sha256").update(bytes).digest("hex") } });
208
+ return result;
209
+ });
176
210
  }
177
211
  export function stagePauseCommand(context, input) {
178
212
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
@@ -180,6 +214,39 @@ export function stagePauseCommand(context, input) {
180
214
  const questionInputPath = prepareAgentOutputFile(path.join(requireRunHome(run), "works", input.workId, "question-input.md"));
181
215
  return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "pause", shortEntityReference(input.runId), "--stage", input.stage, "--work", shortEntityReference(input.workId), "--question-file", questionInputPath, "--project-root", input.projectRoot, "--json"]));
182
216
  }
217
+ /** Recover only the exact resume committed with this invocation; never resume again. */
218
+ export function recoverCommittedStageResume(context, input) {
219
+ const project = requireProjectByRoot(context, input.projectRoot), run = requireRun(context, project.id, input.runId);
220
+ const work = requireWork(context, project.id, run.id, input.workId);
221
+ const index = JSON.parse(run.index_json);
222
+ const current = index.stage_runs.find(stage => stage.stage === input.stage);
223
+ if (!current || current.status !== "running" || index.current_stage && index.current_stage !== input.stage || work.status !== "running")
224
+ return null;
225
+ const resumed = context.db.get("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.stage') = ? AND json_extract(payload_json, '$.work_id') = ? ORDER BY id DESC LIMIT 1", [project.id, run.id, input.stage, work.work_id]);
226
+ if (!resumed)
227
+ return null;
228
+ const acceptedPause = JSON.parse(resumed.payload_json);
229
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.stage') = ? AND json_extract(payload_json, '$.attempt') = ? AND json_extract(payload_json, '$.file') LIKE '%/resume-%.json' ORDER BY id DESC LIMIT 257", [project.id, run.id, input.stage, current.attempt]);
230
+ if (rows.length > 256)
231
+ throw new AppError("stage_resume_receipt_invalid", "Resume receipt count exceeds its bound", 1);
232
+ let result = null;
233
+ for (const row of rows) {
234
+ const packet = JSON.parse(row.payload_json);
235
+ const bytes = Buffer.from(packet.base64, "base64");
236
+ if (bytes.length > 8 * 1024 * 1024 || bytes.toString("base64") !== packet.base64 || crypto.createHash("sha256").update(bytes).digest("hex") !== packet.sha256)
237
+ throw new AppError("stage_resume_receipt_invalid", "Resume receipt custody differs from its committed bytes", 1);
238
+ const retained = JSON.parse(bytes.toString("utf8"));
239
+ if (retained.invocation_id !== input.id)
240
+ continue;
241
+ const value = retained.result;
242
+ if (retained.generation !== input.generation || !value || value.run_id !== run.id || value.work_id !== work.work_id || value.stage !== input.stage
243
+ || value.pause_id !== acceptedPause.pause_id || value.answer_path !== acceptedPause.answer_path || value.resume_source_path !== packet.file || typeof value.worker_prompt_markdown !== "string"
244
+ || result && JSON.stringify(result) !== JSON.stringify(value))
245
+ throw new AppError("stage_resume_receipt_invalid", "Resume receipt does not match the current invocation and accepted pause", 1);
246
+ result = value;
247
+ }
248
+ return result;
249
+ }
183
250
  /** Render separate file-write and lifecycle-call instructions for HITL. */
184
251
  export function stagePauseCommandTemplate(command) {
185
252
  const questionFile = command.match(/--question-file\s+(?:'([^']*)'|"([^"]*)"|(\S+))/)?.slice(1).find(Boolean) ?? "<question-file-from-command>";
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { parse as parseYaml } from "yaml";
5
- const canonicalOnlyFlowNames = ["mb-init.md", "mb-upgrade.md", "mb-distill.md"];
5
+ export const canonicalOnlyFlowNames = Object.freeze(["mb-init.md", "mb-upgrade.md", "mb-distill.md", "mb-audit.md", "review.md"]);
6
6
  export function resolveStatusProjectRoot(input) {
7
7
  if (input.requestedRoot) {
8
8
  const absolute = path.resolve(input.requestedRoot);
@@ -194,7 +194,7 @@ export async function finishVnextCode(context, input) {
194
194
  const retainedMatrix = latestRunVerificationMatrix(context, project.id, run.id, stage);
195
195
  const retainedMatrixInstruction = verificationMatrixInstruction(retainedMatrix?.metadata ?? null, home, "the current semantic verification and repair");
196
196
  if (verification.verdict === "blocked") {
197
- return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again.\n${retainedMatrixInstruction}` };
197
+ return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke next.finish_command. Do not repeat while the blocker remains unresolved.\n${retainedMatrixInstruction}`, next: { finish_command: finishCommand(context, run.id, projectRoot, input.verificationFile) } };
198
198
  }
199
199
  if (verification.verdict === "passed" && verification.unresolved.length > 0) {
200
200
  throw new AppError("verification_contradictory", "A passed CODE verification cannot contain unresolved obligations", 2, { unresolved: verification.unresolved });
@@ -209,7 +209,7 @@ export async function finishVnextCode(context, input) {
209
209
  verification,
210
210
  objective: verification.summary
211
211
  });
212
- return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke this same stage finish command again.\n${retainedMatrixInstruction}` };
212
+ return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke next.finish_command.\n${retainedMatrixInstruction}`, next: { finish_command: finishCommand(context, run.id, projectRoot, input.verificationFile) } };
213
213
  }
214
214
  const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
215
215
  if (input.retryCheckId) {
@@ -15,6 +15,8 @@ import { renderNativeChildFollowupInstruction } from "../harness-runtime/lib/del
15
15
  import { readDescriptorRootWork } from "./descriptor-root-work.js";
16
16
  import { selectMemoryCompositionRegistration } from "./memory-composition-registration.js";
17
17
  import { safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
18
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
19
+ import { readRegisteredDiagnosticGraph } from "./diagnostic-work-registration.js";
18
20
  export const subagentCapacityKey = "runtime.subagent.available_slots";
19
21
  /** External concurrency is policy, never evidence of native child qualification. */
20
22
  export function getStageFanoutCapacity(context, input) {
@@ -105,7 +107,7 @@ export function verifiedNativeWorkFailure(context, input) {
105
107
  }
106
108
  export function fanoutState(input) {
107
109
  if (!input.hasWork)
108
- return input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
110
+ return input.registeredEmpty ? "children_settled" : input.dispatch === "none" ? "coordinator_required" : "dispatch_required";
109
111
  if (input.capacityRequired && !input.capacityKnown)
110
112
  return "capacity_required";
111
113
  if (input.created || input.running)
@@ -133,14 +135,17 @@ export function readFanoutDescriptor(stageRoot) {
133
135
  * productive assignments, including reviewer/correction waves.
134
136
  */
135
137
  export function memoryStageFanoutDescriptor(context, input) {
136
- if (!["compose", "review"].includes(input.stage))
138
+ if (!["compose", "upgrade", "review"].includes(input.stage))
137
139
  return null;
138
140
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
139
141
  const run = context.db.get("SELECT flow_kind,index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
140
- if (run?.flow_kind !== "mb-init")
142
+ if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind))
143
+ return null;
144
+ const authoringStage = run.flow_kind === "mb-init" ? "compose" : "upgrade";
145
+ if (input.stage !== authoringStage && input.stage !== "review")
141
146
  return null;
142
147
  const bundle = readFlowRunExecutionBundle(context, input);
143
- if (!bundle || bundle.descriptor.descriptor.id !== "mb-init")
148
+ if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind)
144
149
  throw new AppError("stage_graph_compatibility_required", "Memory graph requires its admitted RUN bundle", 1);
145
150
  const index = JSON.parse(run.index_json);
146
151
  if (index.current_stage !== input.stage || !index.stage_runs.some(stage => stage.stage === input.stage && stage.status === "running"))
@@ -153,7 +158,7 @@ export function memoryStageFanoutDescriptor(context, input) {
153
158
  if (id === root.workId)
154
159
  continue;
155
160
  const work = context.db.get("SELECT parent_work_id,result_schema FROM works WHERE project_id=? AND run_id=? AND work_id=?", [project.id, input.runId, id]);
156
- const schemas = input.stage === "compose" ? ["dd-flow/memory-composition-work-result@1"] : ["dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"];
161
+ const schemas = input.stage === authoringStage ? ["dd-flow/memory-composition-work-result@1"] : ["dd-flow/memory-review-work-result@1", "dd-flow/memory-repair-work-result@1"];
157
162
  if (!work || work.parent_work_id !== root.workId || !schemas.includes(work.result_schema ?? ""))
158
163
  throw new AppError("stage_graph_conflict", "Memory graph contains an unassigned Work", 1);
159
164
  const custody = readWorkBatchCustody(context, id);
@@ -161,11 +166,11 @@ export function memoryStageFanoutDescriptor(context, input) {
161
166
  || custody.scope.cycle !== scope.cycle || custody.scope.stage !== input.stage || custody.scope.parent_work_id !== root.workId)
162
167
  throw new AppError("stage_graph_conflict", "Memory graph contains foreign allocation custody", 1);
163
168
  }
164
- if (input.stage === "compose") {
169
+ if (input.stage === authoringStage) {
165
170
  const children = context.db.all("SELECT work_id FROM works WHERE project_id=? AND run_id=? AND parent_work_id=?", [project.id, input.runId, root.workId]).filter(work => members.has(work.work_id));
166
171
  const registered = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? LIMIT 1", [project.id, input.runId, scope.attempt, scope.cycle]);
167
172
  if (children.length || registered) {
168
- const registration = selectMemoryCompositionRegistration(context, { ...scope, stage: "compose", parent_work_id: root.workId });
173
+ const registration = selectMemoryCompositionRegistration(context, { ...scope, stage: authoringStage, parent_work_id: root.workId });
169
174
  const ids = new Set(Object.values(registration.work_ids));
170
175
  if (ids.size !== children.length || children.some(work => !ids.has(work.work_id)))
171
176
  throw new AppError("stage_graph_conflict", "Memory composition differs from its registered graph", 1);
@@ -173,6 +178,30 @@ export function memoryStageFanoutDescriptor(context, input) {
173
178
  }
174
179
  return { schema_id: "dd-flow/work-fanout@2", kind: "work_fanout", scope, stage: input.stage, parent_work_id: root.workId, dispatch: "none", capacity_required: true };
175
180
  }
181
+ /** Diagnostic graphs are registered by the Stage handler, never fanout.json. */
182
+ export function diagnosticStageFanoutDescriptor(context, input) {
183
+ if (!["audit", "analyze", "review"].includes(input.stage))
184
+ return null;
185
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
186
+ const run = context.db.get("SELECT flow_kind,index_json FROM runs WHERE project_id=? AND id=?", [project.id, input.runId]);
187
+ if (!run || !isDiagnosticFlow(run.flow_kind))
188
+ return null;
189
+ const index = JSON.parse(run.index_json);
190
+ if (index.current_stage !== input.stage || !index.stage_runs.some(stage => stage.stage === input.stage && stage.status === "running"))
191
+ return null;
192
+ const bundle = readFlowRunExecutionBundle(context, input);
193
+ if (!bundle || bundle.manifest.flow_id !== run.flow_kind)
194
+ throw new AppError("stage_graph_conflict", "Diagnostic graph requires its frozen bundle", 1);
195
+ const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
196
+ const root = readDescriptorRootWork(context, scope, bundle), graph = readRegisteredDiagnosticGraph(context, scope);
197
+ const members = stageWorkIds(context, project.id, input.runId, input.stage);
198
+ if (!graph || graph.parent_work_id !== root.workId || graph.bundle_sha256 !== bundle.sha256 || root.status !== "running"
199
+ || !members.has(root.workId) || members.size !== Object.keys(graph.work_ids).length + 1
200
+ || Object.values(graph.work_ids).some(id => !members.has(id)))
201
+ throw new AppError("stage_graph_conflict", "Diagnostic graph requires its exact registered Work; replay the issued Stage entry if registration was interrupted", 1);
202
+ return { schema_id: "dd-flow/work-fanout@2", kind: "work_fanout", scope: stageGraphScope(context, project.id, input.runId, input.stage),
203
+ stage: input.stage, parent_work_id: root.workId, dispatch: "none", capacity_required: true };
204
+ }
176
205
  /** Read-only projection for an external orchestrator. dd-flow keeps the graph authority. */
177
206
  export function getVnextFanoutStatus(context, input) {
178
207
  const projectRoot = resolveProjectRoot(input.projectRoot);
@@ -185,7 +214,9 @@ export function getVnextFanoutStatus(context, input) {
185
214
  const index = JSON.parse(run.index_json);
186
215
  const bundle = index.execution_bundle ? readFlowRunExecutionBundle(context, { projectRoot, runId: input.runId }) : null;
187
216
  const stageRoot = path.join(run.run_root, bundle ? bundle.descriptor.stage(input.stage).directory : vnextStageDirectory(input.stage));
188
- let descriptor = memoryStageFanoutDescriptor(context, input) ?? readFanoutDescriptor(stageRoot);
217
+ const memoryDescriptor = memoryStageFanoutDescriptor(context, input);
218
+ const diagnosticDescriptor = diagnosticStageFanoutDescriptor(context, input);
219
+ let descriptor = memoryDescriptor ?? diagnosticDescriptor ?? (bundle && isDiagnosticFlow(bundle.manifest.flow_id) ? null : readFanoutDescriptor(stageRoot));
189
220
  // Registration is authoritative even if projection publication was interrupted,
190
221
  // or an otherwise review-off stage acquired an aggregate repair graph.
191
222
  if (!descriptor && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'repair_intents'")) {
@@ -211,6 +242,17 @@ export function getVnextFanoutStatus(context, input) {
211
242
  const ready = { works: all.works.filter(work => work.ready) };
212
243
  const counts = Object.fromEntries(["created", "running", "completed", "failed", "cancelled"].map((status) => [status, all.works.filter((work) => work.status === status).length]));
213
244
  const hasWork = all.works.length > 0;
245
+ // Only SQL-owned memory registration can attest a deliberately empty
246
+ // graph. A mutable fanout.json must never turn missing Work into success.
247
+ let registeredEmpty = false;
248
+ if (diagnosticDescriptor && !hasWork) {
249
+ registeredEmpty = Object.keys(readRegisteredDiagnosticGraph(context, { projectId: project.id, runId: input.runId, stage: input.stage }).work_ids).length === 0;
250
+ }
251
+ if (memoryDescriptor && input.stage === "upgrade" && !hasWork
252
+ && context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.composition_registration' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.scope.stage')='upgrade' AND json_extract(payload_json,'$.scope.attempt')=? AND json_extract(payload_json,'$.scope.cycle')=? AND json_extract(payload_json,'$.scope.parent_work_id')=? LIMIT 1", [project.id, input.runId, currentScope.attempt, currentScope.cycle, descriptor.parent_work_id])) {
253
+ const registered = selectMemoryCompositionRegistration(context, { ...currentScope, stage: "upgrade", parent_work_id: descriptor.parent_work_id });
254
+ registeredEmpty = Object.keys(registered.work_ids).length === 0;
255
+ }
214
256
  const capacityKnown = capacity.available_slots !== null;
215
257
  return {
216
258
  ok: true,
@@ -221,7 +263,7 @@ export function getVnextFanoutStatus(context, input) {
221
263
  // PLAN-REVIEW has a deterministic dispatcher. CODE and CODE-REVIEW do
222
264
  // not: their coordinator first materializes the Work graph from its
223
265
  // authoritative prompt. An empty graph is therefore not a success.
224
- state: fanoutState({ dispatch: descriptor.dispatch, hasWork, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
266
+ state: fanoutState({ dispatch: descriptor.dispatch, hasWork, registeredEmpty, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
225
267
  capacity,
226
268
  graph_revision: all.works.map(work => ({ work_id: work.work_id, status: work.status, depends_on: work.depends_on })),
227
269
  works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: work.start_command })) }
@@ -217,8 +217,6 @@ export function dispatchVnextPlanReview(context, input) {
217
217
  const waves = plannedReviewWaves(groups, capacity.availableSlots);
218
218
  return { ok: true, run_id: run.id, stage, capacity: { available_slots: capacity.availableSlots, source: capacity.source }, routing: { group_count: groups.length, planned_waves: waves.length, waves, preference: "Use one wave when semantic compatibility permits; hard dependencies create later waves." }, review_works: reviews, next_action: "launch_ready_reviews" };
219
219
  }
220
- // Capacity is a RUN fact shared by every fan-out stage.
221
- const reviewCapacityKey = subagentCapacityKey;
222
220
  /** Capacity is observed immediately before this stage dispatches reviewers. */
223
221
  export function recordVnextPlanReviewCapacity(context, input) {
224
222
  if (!Number.isInteger(input.availableSlots) || input.availableSlots < 0 || input.availableSlots > 15) {
@@ -228,15 +226,15 @@ export function recordVnextPlanReviewCapacity(context, input) {
228
226
  const project = requireProjectByRoot(context, projectRoot);
229
227
  const run = requireRun(context, projectRoot, input.runId);
230
228
  const variables = getFlowRunVariables(context, { projectRoot, runId: run.id });
231
- const prior = variables.variables[reviewCapacityKey];
229
+ const prior = variables.variables[subagentCapacityKey];
232
230
  if (typeof prior === "number") {
233
231
  if (prior === input.availableSlots)
234
232
  return { ok: true, run_id: run.id, capacity: { available_slots: prior, source: "harness_qualification", reused: true } };
235
- setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
233
+ setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: subagentCapacityKey, value: input.availableSlots });
236
234
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_refreshed", previous_available_slots: prior, available_slots: input.availableSlots, source: "harness_qualification" });
237
235
  return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, previous_available_slots: prior, source: "harness_qualification", refreshed: true } };
238
236
  }
239
- setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: reviewCapacityKey, value: input.availableSlots });
237
+ setRuntimeRunVariable(context, { projectRoot, runId: run.id, key: subagentCapacityKey, value: input.availableSlots });
240
238
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "subagent_capacity_observed", available_slots: input.availableSlots, source: "harness_qualification" });
241
239
  return { ok: true, run_id: run.id, capacity: { available_slots: input.availableSlots, source: "harness_qualification" } };
242
240
  }