@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156

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 (227) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +274 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +250 -33
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -7
  190. package/dist/services/retained-native-hook.js +274 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +25 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +257 -44
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +8 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +255 -69
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
@@ -273,6 +273,51 @@ export function readRunnerEvidenceDetailed(root, contract, binding) {
273
273
  });
274
274
  }
275
275
  export function readRunnerEvidence(root, contract, binding) { return readRunnerEvidenceDetailed(root, contract, binding).evidence; }
276
+ /** Semantic attribution is diagnostic information, never permission to investigate a failed check. */
277
+ export const checkDiagnosisPolicy = "settled-failure@1";
278
+ export function checkDiagnosisDisposition(receipt) {
279
+ if (receipt.status === "running"
280
+ || receipt.execution_failures?.some(failure => ["cleanup_unconfirmed", "engine_outcome_unbound"].includes(failure.code)))
281
+ return "settlement_required";
282
+ if (!["passed", "failed", "aborted"].includes(receipt.status))
283
+ return "invalid_binding";
284
+ // Caller supplies the SQL-owned receipt, never a proposed runner report.
285
+ // Missing, malformed or foreign optional reports are diagnostic failures;
286
+ // they cannot change receipt ownership or grant a successful check.
287
+ if (receipt.status === "passed")
288
+ return "passed";
289
+ // New launchers record physical settlement even for spawn errors/signals.
290
+ // Historical aborted receipts with no such proof must first reconcile.
291
+ if (!receipt.execution_failures?.some(failure => failure.code === "check_execution_settled") && !Number.isSafeInteger(receipt.exit_code))
292
+ return "settlement_required";
293
+ return "diagnosis_required";
294
+ }
295
+ export function assertCheckDiagnosisEligible(receipts, _workspaceRoot) {
296
+ const dispositions = receipts.map(receipt => ({ check_receipt_id: receipt.id, disposition: checkDiagnosisDisposition(receipt) }));
297
+ if (!receipts.length || dispositions.some(item => item.disposition !== "diagnosis_required"))
298
+ throw new AppError("check_gate_blocked", "Check diagnosis requires owned, physically settled failed executions", 2, {
299
+ failures: receipts, dispositions, outcome: "blocked", recoverable: false,
300
+ instruction: "Reconcile any running or unknown check process before changing its workspace. Do not treat a foreign receipt or an agent report as proof of completion."
301
+ });
302
+ }
303
+ /** Human-readable retained facts; tool output remains data, never instructions. */
304
+ export function checkDiagnosisSummary(receipt, workspaceRoot) {
305
+ const available = (file) => {
306
+ try {
307
+ fs.accessSync(file, fs.constants.R_OK);
308
+ return fs.statSync(file).isFile() ? file : `${file} (not a regular file)`;
309
+ }
310
+ catch {
311
+ return `${file} (unavailable; investigate the missing diagnostic, do not invent its content)`;
312
+ }
313
+ };
314
+ return [`Check: ${receipt.id}`, `Command: ${receipt.command}`, `cwd: ${workspaceRoot}`,
315
+ `Outcome: ${receipt.status}; exit: ${receipt.exit_code ?? "unavailable"}; epoch: ${receipt.verification_epoch}`,
316
+ `Receipt: ${available(receipt.receipt_path)}`, `stdout: ${available(receipt.stdout_path)}`, `stderr: ${available(receipt.stderr_path)}`,
317
+ `Execution facts: ${JSON.stringify(receipt.execution_failures ?? [])}`,
318
+ `Report: ${receipt.runner_evidence_required?.path ? path.join(path.dirname(receipt.receipt_path), "artifacts", receipt.runner_evidence_required.path) : "not declared"}`].join("\n");
319
+ }
320
+ /** Historical attribution reader only. New repair routing uses checkDiagnosisDisposition. */
276
321
  export function checkRepairDisposition(receipt) {
277
322
  if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
278
323
  return "tooling_blocker";
@@ -297,11 +342,3 @@ export function checkRepairDisposition(receipt) {
297
342
  return "unclassified";
298
343
  return "source_repair";
299
344
  }
300
- export function assertSourceRepairEligible(receipts, workspaceRoot) {
301
- const dispositions = receipts.map(receipt => {
302
- const drift = workspaceRoot !== undefined && !currentCandidateLibraryInputsValid(receipt, workspaceRoot);
303
- return { check_receipt_id: receipt.id, disposition: drift ? "tooling_blocker" : checkRepairDisposition(receipt), ...(drift ? { reason: "candidate_library_inputs_changed" } : {}) };
304
- });
305
- if (!receipts.length || dispositions.some(item => item.disposition !== "source_repair"))
306
- throw new AppError("check_gate_blocked", "Checks require tooling diagnosis or settlement before product repair; retained nonzero is not a candidate-source verdict", 2, { failures: receipts, dispositions, outcome: "blocked", recoverable: false, instruction: "Inspect retained runner evidence. Do not change product code to repair tooling, unknown observation or unclassified failure. Retry only after establishing and correcting the cause." });
307
- }
@@ -89,6 +89,10 @@ function isReadOnlyDiagnostic(family, command, action) {
89
89
  return ["status", "ls", "show"].includes(command ?? "");
90
90
  if (family === "stage")
91
91
  return command === "fanout" && action === "status";
92
+ if (family === "interactive")
93
+ return command === "inputs" || command === "git-inputs";
94
+ if (family === "delivery")
95
+ return command === "inputs";
92
96
  if (family === "lane") {
93
97
  return (command === "status" ||
94
98
  command === "waiters" ||
@@ -108,7 +112,8 @@ function isReadOnlyDiagnostic(family, command, action) {
108
112
  if (family === "dashboard")
109
113
  return command === "data" || command === "open";
110
114
  if (family === "memory")
111
- return command === "permissions" && action === "preflight";
115
+ return command === "permissions" && action === "preflight" || command === "distill" && action === "inputs"
116
+ || command === "fix" && ["context", "scope"].includes(action ?? "");
112
117
  if (family === "codex" && command === "hooks")
113
118
  return action === "print" || action === "status";
114
119
  if (family === "codex" && command === "home")
@@ -1,6 +1,6 @@
1
1
  import crypto from "node:crypto";
2
- import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, candidateLibraryInputs, candidateLibraryInputsValid, currentCandidateLibraryInputsValid, checkRepairDisposition, readRunnerEvidence, readRunnerEvidenceDetailed, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
3
- export { assertSourceRepairEligible, checkRepairDisposition } from "./check-runner-evidence.js";
2
+ import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, candidateLibraryInputs, candidateLibraryInputsValid, currentCandidateLibraryInputsValid, checkDiagnosisDisposition, readRunnerEvidence, readRunnerEvidenceDetailed, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
3
+ export { assertCheckDiagnosisEligible, checkDiagnosisDisposition, checkRepairDisposition } from "./check-runner-evidence.js";
4
4
  import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
6
  import { ObservationClock, validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
@@ -352,14 +352,16 @@ export class PreparedCheckEvidence {
352
352
  candidates: receipts.filter(item => item.scope === input.scope && item.work_id === (input.workId ?? null))
353
353
  .slice(-16).map(item => ({ id: item.id, input_hash: item.input_hash, profile_hash: item.profile_hash }))
354
354
  });
355
- if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode) || !currentCandidateLibraryInputsValid(receipt, input.workspaceRoot))
355
+ if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode) || input.mode !== "diagnostic" && !currentCandidateLibraryInputsValid(receipt, input.workspaceRoot))
356
356
  fail();
357
357
  passed = passed && receipt.status === "passed";
358
- for (const artifact of receipt.artifacts)
358
+ const availableArtifacts = input.mode === "diagnostic" ? availableDiagnosticArtifacts(receipt) : receipt.artifacts;
359
+ for (const artifact of availableArtifacts)
359
360
  artifacts.push({ root: "run", path: path.relative(input.runHome, path.join(path.dirname(receipt.receipt_path), "artifacts", artifact.path)).split(path.sep).join("/"), sha256: artifact.sha256 });
360
361
  const bytes = fs.readFileSync(receipt.receipt_path);
361
- completions.push({ root: "run", path: path.relative(input.runHome, receipt.completion_path).split(path.sep).join("/"), sha256: crypto.createHash("sha256").update(fs.readFileSync(receipt.completion_path)).digest("hex") });
362
- if (receipt.input_manifest)
362
+ if (input.mode !== "diagnostic" || fs.existsSync(receipt.completion_path))
363
+ completions.push({ root: "run", path: path.relative(input.runHome, receipt.completion_path).split(path.sep).join("/"), sha256: crypto.createHash("sha256").update(readBoundedReleaseFile(receipt.completion_path, 1024 * 1024)).digest("hex") });
364
+ if (receipt.input_manifest && (input.mode !== "diagnostic" || fs.existsSync(path.join(path.dirname(receipt.receipt_path), receipt.input_manifest.path))))
363
365
  completions.push({ root: "run", path: path.relative(input.runHome, path.join(path.dirname(receipt.receipt_path), receipt.input_manifest.path)).split(path.sep).join("/"), sha256: receipt.input_manifest.sha256 });
364
366
  return { id: receipt.id, path: receipt.receipt_path, sha256: crypto.createHash("sha256").update(bytes).digest("hex") };
365
367
  });
@@ -562,7 +564,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
562
564
  const snapshot = workspaceState(input.workspaceRoot, [input.runHome]);
563
565
  const group = groups.find(candidate => candidate.declarations.some(check => check.run_at === predecessor.gate)
564
566
  && (checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, workspaceInputFingerprint(input.workspaceRoot, snapshot.files, candidate.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment) === predecessor.input_hash
565
- || sourceRepairContractMatches(predecessor, candidate, profileHash)
567
+ || diagnosisRetryContractMatches(predecessor, candidate, profileHash, checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, "", input.evidenceContext, input.checkEnvironment, false), checkInputHash(input.workspaceRoot, { ...candidate, gate: predecessor.gate }, profileHash, "", input.evidenceContext, input.checkEnvironment))
566
568
  || input.retryReason === "check_inputs_changed" && staleRetryContractMatches(predecessor, candidate, profileHash, input)));
567
569
  if (!group || retries.has(group))
568
570
  throw new AppError("retry_check_invalid", "Retry must match exactly one current execution contract", 2, { check_receipt_id: receiptId });
@@ -579,7 +581,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
579
581
  const inputFingerprint = inputFilesFingerprint(inputFiles);
580
582
  const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, inputFingerprint, input.evidenceContext, input.checkEnvironment);
581
583
  const epoch = crypto.createHash("sha256").update(`${group.gate}\0${inputFingerprint}`).digest("hex");
582
- const executionContractHash = checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment);
584
+ // Retry preserves the declared check/wave, not the failed conditions being
585
+ // corrected. Observed environment still participates in inputHash/PASS reuse.
586
+ const executionContractHash = checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment, false);
583
587
  const reservationInput = { ...input, retryReceiptIds: retries.has(group) ? [retries.get(group)] : [], generation, group, before, inputFiles, inputFingerprint, executionContractHash, profileHash, inputHash, epoch, startedAt };
584
588
  delete reservationInput.retryReceiptId;
585
589
  const decision = reserveOrReuseReceipt(context, reservationInput);
@@ -592,6 +596,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
592
596
  const { id, evidenceDir, stdoutPath, stderrPath, receiptPath, completionPath } = reserved;
593
597
  let resources = {};
594
598
  let result = { exitCode: null, error: "check did not start", aborted: true };
599
+ let dispatched = false, physicallySettled = false;
595
600
  try {
596
601
  fs.mkdirSync(evidenceDir, { recursive: true });
597
602
  retainInputManifest(context, reserved);
@@ -624,6 +629,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
624
629
  const runnerEnvironment = group.runnerEvidence ? { DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH: path.join(evidenceDir, group.runnerEvidence.path), DD_FLOW_CHECK_RUNNER_PROGRESS_PATH: path.join(evidenceDir, "runner-progress.jsonl"), DD_FLOW_CHECK_INVOCATION_ID: id, DD_FLOW_CHECK_ID: id, DD_FLOW_CHECK_INPUT_HASH: inputHash, DD_FLOW_CHECK_VERIFICATION_EPOCH: epoch } : {};
625
630
  if (group.runnerEvidence)
626
631
  fs.mkdirSync(path.dirname(path.join(evidenceDir, group.runnerEvidence.path)), { recursive: true });
632
+ dispatched = true;
627
633
  result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, input.checkEnvironment), ...browserCheckCapabilityEnvironment(context, input.runId, input.runHome), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...runnerEnvironment, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
628
634
  confirmedExpiry = confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }).lease_expires_at;
629
635
  assertRunMutationAllowed(context, input.projectId, input.runId, generation);
@@ -638,6 +644,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
638
644
  const current = managedProcessStatus(context).find(item => item.id === managed.id);
639
645
  if (processTreeIsAlive(current))
640
646
  throw new AppError("check_in_progress", "The check shell exited but its process tree is still live; retain this receipt and its resources until settlement", 2, { process_id: managed.id, check_receipt_id: id });
647
+ physicallySettled = !result.cleanupUnconfirmed;
641
648
  allocation.release();
642
649
  if (!result.cleanupUnconfirmed)
643
650
  finishManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, state: result.aborted || result.exitCode !== 0 || Boolean(result.error) ? "failed" : "stopped", reason: result.error });
@@ -664,6 +671,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
664
671
  const physicalFailures = executionFailures({ fingerprintScope: "check_inputs", exitCode: result.exitCode, launcherError: result.error ?? undefined, cleanupUnconfirmed: result.cleanupUnconfirmed, missing: artifacts.missing, mutations: mutationPaths, invalidRunner: Boolean(group.runnerEvidence && !runnerEvidence) });
665
672
  if (result.failure)
666
673
  physicalFailures.push(result.failure);
674
+ // This marker is SQL-owned execution metadata, not a semantic runner verdict.
675
+ if (result.aborted && (!dispatched || physicallySettled))
676
+ physicalFailures.push({ code: "check_execution_settled", message: "No owned check process remains; investigate the retained launch or execution failure" });
667
677
  const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
668
678
  const candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
669
679
  const libraryInputs = candidateLibraryInputs(runnerEvidence, input.workspaceRoot);
@@ -826,14 +836,9 @@ function reserveOrReuseReceipt(context, input) {
826
836
  const successor = retryIds.size === 1 ? receipts.find((receipt) => receipt.retry_of_receipt_id === [...retryIds][0] && receipt.input_hash === input.inputHash && receipt.profile_hash === input.profileHash) : null;
827
837
  const reusableSuccessor = successor?.status === "passed" && checkReceiptEvidenceValid(successor);
828
838
  const retryPredecessor = receipts.find(receipt => retryIds.has(receipt.id));
829
- const sourceCorrection = Boolean(retryPredecessor && (sourceRepairContractMatches(retryPredecessor, input.group, input.profileHash)
839
+ const sourceCorrection = Boolean(retryPredecessor && (diagnosisRetryContractMatches(retryPredecessor, input.group, input.profileHash, input.executionContractHash, checkInputHash(input.workspaceRoot, input.group, input.profileHash, "", input.evidenceContext, input.checkEnvironment))
830
840
  || input.retryReason === "check_inputs_changed" && staleRetryContractMatches(retryPredecessor, input.group, input.profileHash, input)));
831
841
  const latestContract = receipts.filter(receipt => receipt.command === input.group.command && receipt.profile_hash === input.profileHash && receipt.gate === input.group.gate && receipt.scope === input.scope && receipt.work_id === (input.workId ?? null)).at(-1);
832
- // Hash files before SQLite's writer reservation. Captured identities remain
833
- // valid historical evidence, but cannot authorize reuse in a changed install.
834
- const priorLibrariesCurrent = !prior || currentCandidateLibraryInputsValid(prior, input.workspaceRoot);
835
- const latestLibrariesCurrent = !latestContract || currentCandidateLibraryInputsValid(latestContract, input.workspaceRoot);
836
- const libraryDrift = (receipt) => { throw new AppError("check_evidence_stale", "Captured library inputs differ from the current candidate installation; request an explicit restored-condition retry", 2, { reason: "candidate_library_inputs_changed", check_receipt_id: receipt.id, effect: "no_effect" }); };
837
842
  return context.db.writeTransaction(() => {
838
843
  assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
839
844
  if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
@@ -869,16 +874,10 @@ function reserveOrReuseReceipt(context, input) {
869
874
  // An unchanged failed contract needs a concrete restored-condition retry,
870
875
  // not repeated coordinator finish until it happens to become green.
871
876
  if (!retryId && latestContract?.fingerprint_scope === "check_inputs" && latestContract.status === "failed"
872
- && latestContract.execution_failures?.some(failure => failure.code === "check_inputs_changed") && latestContract.execution_contract_hash === input.executionContractHash) {
873
- if (!latestLibrariesCurrent)
874
- libraryDrift(latestContract);
877
+ && latestContract.execution_failures?.some(failure => failure.code === "check_inputs_changed") && latestContract.execution_contract_hash === input.executionContractHash)
875
878
  return { receipt: latestContract };
876
- }
877
- if (prior && ["failed", "aborted"].includes(prior.status) && !retryId) {
878
- if (!priorLibrariesCurrent)
879
- libraryDrift(prior);
879
+ if (prior && ["failed", "aborted"].includes(prior.status) && !retryId)
880
880
  return { receipt: prior };
881
- }
882
881
  if (prior?.status === "passed" && !retryIds.has(prior.id) && input.group.reusable && reusablePrior)
883
882
  return { receipt: prior };
884
883
  let ordinal = (context.db.get("SELECT COALESCE(MAX(reservation_sequence), 0) + 1 AS sequence FROM check_receipts WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId])?.sequence ?? 1);
@@ -895,9 +894,12 @@ function reserveOrReuseReceipt(context, input) {
895
894
  return { reserved };
896
895
  }, "checks.reserveOrReuseReceipt");
897
896
  }
898
- function sourceRepairContractMatches(receipt, group, profileHash) {
899
- return checkRepairDisposition(receipt) === "source_repair" && group.command === receipt.command && profileHash === receipt.profile_hash && isDeepStrictEqual(group.inputs, receipt.inputs)
900
- && isDeepStrictEqual(derivedReceiptArtifacts(group, receipt).sort(), [...receipt.required_artifacts].sort()) && isDeepStrictEqual([...group.ports].sort(), Object.keys(receipt.resources.ports).sort());
897
+ function diagnosisRetryContractMatches(receipt, group, profileHash, executionContractHash, legacyExecutionContractHash) {
898
+ return checkDiagnosisDisposition(receipt) === "diagnosis_required" && group.command === receipt.command && profileHash === receipt.profile_hash && isDeepStrictEqual(group.inputs, receipt.inputs)
899
+ && [executionContractHash, legacyExecutionContractHash].includes(receipt.execution_contract_hash ?? "")
900
+ && group.requiredArtifacts.every(artifact => receipt.required_artifacts.includes(artifact))
901
+ && isDeepStrictEqual(group.runnerEvidence ?? null, receipt.runner_evidence_required ?? null)
902
+ && isDeepStrictEqual([...group.ports].sort(), Object.keys(receipt.resources.ports).sort());
901
903
  }
902
904
  function staleCheckRetryEligible(receipt) {
903
905
  if (receipt.fingerprint_scope !== "check_inputs" || receipt.status !== "failed" || receipt.exit_code !== 0 || receipt.retry_reason === "check_inputs_changed"
@@ -908,7 +910,8 @@ function staleCheckRetryEligible(receipt) {
908
910
  }
909
911
  function staleRetryContractMatches(receipt, group, profileHash, input) {
910
912
  return group.reusable && staleCheckRetryEligible(receipt) && isDeepStrictEqual(group.runnerEvidence ?? null, receipt.runner_evidence_required ?? null)
911
- && receipt.execution_contract_hash === checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment);
913
+ && [checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment, false),
914
+ checkInputHash(input.workspaceRoot, group, profileHash, "", input.evidenceContext, input.checkEnvironment)].includes(receipt.execution_contract_hash ?? "");
912
915
  }
913
916
  function receiptFor(input) { return { id: input.id, local_id: input.localId, declaration_id: input.group.declarations[0].id, check_refs: input.group.declarations.map(checkRef), gate: input.group.gate, scope: input.scope, work_id: input.workId ?? null, command: input.group.command, input_hash: input.inputHash, verification_epoch: input.epoch, retry_of_receipt_id: input.retryOf ?? null, retry_reason: input.retryReason ?? null, status: input.status, ...(input.status === "aborted" ? { abort_reason: input.result.error } : {}), exit_code: input.status === "running" ? null : input.result.exitCode, stdout_path: input.stdoutPath, stderr_path: input.stderrPath, receipt_path: input.receiptPath, completion_path: input.completionPath, required_artifacts: input.group.requiredArtifacts, inputs: input.group.inputs, fingerprint_scope: "check_inputs", input_manifest: input.inputManifest ?? null, execution_contract_hash: input.executionContractHash, workspace_fingerprint: input.before.fingerprint, before_fingerprint: input.inputFingerprint, after_fingerprint: input.status === "running" ? input.inputFingerprint : input.after.fingerprint, profile_hash: input.profileHash, mutation_paths: input.mutationPaths, artifacts: input.artifacts, resources: { ports: input.resources }, started_at: input.startedAt, finished_at: input.status === "running" ? input.startedAt : new Date().toISOString() }; }
914
917
  function retainInputManifest(context, reserved) {
@@ -966,8 +969,6 @@ export function validateFailedCheckRetry(context, input) {
966
969
  const declared = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)]));
967
970
  if (!receipt.check_refs.some((ref) => declared.has(ref)))
968
971
  throw new AppError("retry_check_invalid", "--retry-check does not belong to this stage gate", 2, { check_receipt_id: input.receiptId, check_refs: receipt.check_refs });
969
- if (receipt.fingerprint_scope === "check_inputs" && receipt.execution_failures?.some(failure => failure.code === "check_inputs_changed") && input.reason !== "check_inputs_changed")
970
- throw new AppError("retry_check_invalid", "Scoped drift uses its bounded stale transition, not an environment/source retry", 2, { check_receipt_id: receipt.id });
971
972
  if (input.reason === "check_inputs_changed" && (!staleCheckRetryEligible(receipt) || receipt.retry_reason === "check_inputs_changed"))
972
973
  throw new AppError("retry_check_invalid", "Scoped input retry requires a bound, safely completed stale predecessor and one unconsumed episode", 2, { check_receipt_id: receipt.id });
973
974
  return receipt;
@@ -1326,12 +1327,28 @@ function explicitInputFiles(root, inputs, ignoredRoots, sourceFiles) {
1326
1327
  visit(path.resolve(root, input), input, new Set());
1327
1328
  return [...result.entries()];
1328
1329
  }
1329
- function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint, evidenceContext, checkEnvironment) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: environmentFingerprint(workspaceRoot, group.environmentInputs, checkEnvironment), input_fingerprint: inputFingerprint, ...(group.environmentInputs === undefined ? {} : { environment_inputs: group.environmentInputs }), ...(evidenceContext === undefined ? {} : { evidence_context: evidenceContext }) })).digest("hex"); }
1330
+ function checkInputHash(workspaceRoot, group, profileHash, inputFingerprint, evidenceContext, checkEnvironment, observedEnvironment = true) { return crypto.createHash("sha256").update(JSON.stringify({ workspace_root: path.resolve(workspaceRoot), gate: group.reusable ? null : group.gate, command: group.command, required_artifacts: group.requiredArtifacts, ports: group.ports, inputs: group.inputs, ...(group.runnerEvidence ? { runner_evidence: group.runnerEvidence } : {}), profile_hash: profileHash, environment: observedEnvironment ? environmentFingerprint(workspaceRoot, group.environmentInputs, checkEnvironment) : null, input_fingerprint: inputFingerprint, ...(group.environmentInputs === undefined ? {} : { environment_inputs: group.environmentInputs }), ...(evidenceContext === undefined ? {} : { evidence_context: evidenceContext }) })).digest("hex"); }
1330
1331
  function derivedReceiptArtifacts(group, receipt) { return [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(receipt.runner_evidence), ...(receipt.required_artifacts.includes("engine-outcome.json") ? ["engine-outcome.json"] : [])])]; }
1331
1332
  function bindReceipt(receipt, group) { return { ...receipt, gate: group.gate, declaration_id: group.declarations[0].id, check_refs: group.declarations.map(checkRef), required_artifacts: derivedReceiptArtifacts(group, receipt), inputs: group.inputs }; }
1332
1333
  export function checkReceiptEvidenceValid(receipt) { return checkReceiptEvidenceValidForMode(receipt); }
1334
+ function availableDiagnosticArtifacts(receipt) {
1335
+ const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
1336
+ return receipt.artifacts.flatMap(artifact => {
1337
+ const relative = path.isAbsolute(artifact.path) ? path.relative(root, artifact.path) : artifact.path;
1338
+ const observed = collectCheckArtifacts(root, [relative]);
1339
+ return observed.items.filter(item => item.sha256 === artifact.sha256);
1340
+ });
1341
+ }
1333
1342
  function checkReceiptEvidenceValidForMode(receipt, mode) {
1334
1343
  try {
1344
+ if (mode === "diagnostic" && receipt.status !== "passed") {
1345
+ if (checkDiagnosisDisposition(receipt) !== "diagnosis_required")
1346
+ return false;
1347
+ // SQL selects this execution. Only its identity must survive in the
1348
+ // diagnostic packet; missing reports/logs cannot erase a failed command.
1349
+ const stored = JSON.parse(readBoundedReleaseFile(receipt.receipt_path, 1024 * 1024).toString("utf8"));
1350
+ return ["id", "scope", "work_id", "command", "input_hash", "verification_epoch", "status", "exit_code", "started_at", "finished_at"].every(field => (stored[field] ?? null) === (receipt[field] ?? null));
1351
+ }
1335
1352
  const staleDiagnostic = mode === "diagnostic" && receipt.fingerprint_scope === "check_inputs" && receipt.status === "failed" && Number.isInteger(receipt.exit_code)
1336
1353
  && receipt.before_fingerprint !== receipt.after_fingerprint && receipt.mutation_paths.length > 0 && receipt.execution_failures?.some(failure => failure.code === "check_inputs_changed");
1337
1354
  if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
@@ -19,7 +19,10 @@ export async function verifyCodexHookDelivery(env, payload) {
19
19
  throw new AppError("native_hook_timeout", "Native hook deadline expired before delivery verification", 1, { phase: "verification", effect: "no_effect", recoverable: false });
20
20
  const proof = await callDaemon(env.DD_FLOW_CODEX_STATE_DIR, "hook.verify", {
21
21
  daemonId: env.DD_FLOW_DAEMON_ID, rootSessionId: payload.session_id,
22
- sessionId: payload.agent_id ?? payload.session_id, toolCallId: payload.tool_use_id, turnId: payload.turn_id
22
+ sessionId: payload.agent_id ?? payload.session_id, toolCallId: payload.tool_use_id, turnId: payload.turn_id,
23
+ // The daemon verifies the native delivery before retaining it; the source
24
+ // command cannot itself grant admission to the CLI.
25
+ nativePayload: payload, nativeCommand: command
23
26
  }, Math.min(5000, remaining));
24
27
  if (typeof proof.projectRoot !== "string"
25
28
  || typeof proof.ddFlowHome !== "string" || !env.DD_FLOW_HOME || path.resolve(env.DD_FLOW_HOME) !== path.resolve(proof.ddFlowHome)
@@ -27,6 +30,12 @@ export async function verifyCodexHookDelivery(env, payload) {
27
30
  || typeof proof.toolCallId !== "string" || !(proof.parentSessionId === null || typeof proof.parentSessionId === "string")) {
28
31
  throw new AppError("native_hook_unproven", "Native hook proof differs from the requested project/runtime", 1, { effect: "no_effect" });
29
32
  }
33
+ const binding = proof.nativeHookBinding;
34
+ const retained = binding && typeof binding === "object" && !Array.isArray(binding) ? binding : null;
30
35
  return { daemonId: proof.daemonId, rootSessionId: proof.rootSessionId, sessionId: proof.sessionId,
31
- parentSessionId: proof.parentSessionId, toolCallId: proof.toolCallId, projectRoot: proof.projectRoot, command: command };
36
+ parentSessionId: proof.parentSessionId, toolCallId: proof.toolCallId, projectRoot: proof.projectRoot, command: command,
37
+ ...(retained?.schema_id === "dd-flow/hook-request@1" && retained.source_harness === "codex-desktop"
38
+ && retained.daemon_id === proof.daemonId && retained.root_provider_session_id === proof.rootSessionId
39
+ && retained.provider_session_id === proof.sessionId && retained.tool_call_id === proof.toolCallId
40
+ && retained.native_command === command && typeof retained.request_id === "string" ? { nativeHookBinding: retained } : {}) };
32
41
  }
@@ -0,0 +1,95 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { renderNativeChildFollowupInstruction, renderWaitInstructions } from "../harness-runtime/lib/delegation-instructions.mjs";
3
+ import { nativeItemsSettled } from "../harness-runtime/lib/provider-overload-policy.mjs";
4
+ import { parseLifecycleCommand } from "./lifecycle-command.js";
5
+ import { storageSessionId } from "./session-identity.js";
6
+ import { stageWorkIds } from "./stage-work-graph.js";
7
+ import { appendEvent, digest } from "./run-controller-state.js";
8
+ const blocked = (workId, reason, diagnostic = {}) => new AppError("child_work_repair_blocked", "Completed child has no safe automatic Work correction", 1, { work_id: workId, reason, reconciliation_required: true, effect: "no_effect", ...diagnostic });
9
+ /** One durable same-child correction per Work, not a provider retry or a new worker. */
10
+ export function completedChildRepair(context, row, input) {
11
+ const members = stageWorkIds(context, row.project_id, row.run_id, input.stage);
12
+ for (const child of input.children) {
13
+ if (child.status !== "completed")
14
+ continue;
15
+ const links = context.db.all(`SELECT w.work_id,w.status FROM work_sessions ws
16
+ JOIN works w ON w.work_id=ws.work_id JOIN sessions s ON s.session_id=ws.session_id AND s.project_id=w.project_id
17
+ WHERE w.project_id=? AND w.run_id=? AND ws.session_id=? AND s.parent_session_id=?`, [row.project_id, row.run_id, storageSessionId({ harness_id: input.harness, session_id: child.session_id }), storageSessionId({ harness_id: input.harness, session_id: input.parentId })])
18
+ .filter(link => members.has(link.work_id));
19
+ if (!links.some(link => link.status === "running"))
20
+ continue;
21
+ if (links.length !== 1 || child.parent_session_id !== input.parentId || child.provenance === "parent_mismatch"
22
+ || !child.terminal_turn_id || !nativeItemsSettled(child.native_turn_items))
23
+ throw blocked(links[0].work_id, "native_identity_unconfirmed");
24
+ const workId = links[0].work_id;
25
+ if (context.db.get("SELECT 1 FROM check_receipts WHERE project_id=? AND run_id=? AND work_id=? AND status='running' LIMIT 1", [row.project_id, row.run_id, workId]))
26
+ continue;
27
+ if (context.db.get("SELECT 1 FROM works WHERE parent_work_id=? AND status IN ('created','running','paused') LIMIT 1", [workId]))
28
+ throw blocked(workId, "descendants_unsettled");
29
+ const previous = context.db.get(`SELECT e.controller_id,e.data_json FROM run_controller_events e
30
+ JOIN run_controllers c ON c.controller_id=e.controller_id WHERE c.project_id=? AND c.run_id=?
31
+ AND e.event_type='work_repair_reserved' AND json_extract(e.data_json,'$.work_id')=? ORDER BY e.sequence LIMIT 1`, [row.project_id, row.run_id, workId]);
32
+ if (previous) {
33
+ const retained = JSON.parse(previous.data_json);
34
+ const operation = context.db.get("SELECT status,input_json FROM run_controller_operations WHERE operation_id=?", [retained.operation_id]);
35
+ const diagnostic = { operation_id: retained.operation_id, refusal_invocation_id: retained.refusal_invocation_id };
36
+ if (previous.controller_id !== row.controller_id || retained.generation !== row.generation || retained.terminal_turn_id !== child.terminal_turn_id
37
+ || retained.child_session_id !== child.session_id)
38
+ throw blocked(workId, "automatic_episode_exhausted", diagnostic);
39
+ if (operation?.status === "completed" || operation?.status === "failed")
40
+ throw blocked(workId, "followup_admission_unconfirmed", diagnostic);
41
+ // Same operation ID lets the adapter observe an uncertain dispatch. It
42
+ // may dispatch only if its own durable phase proves it never crossed out.
43
+ return retained;
44
+ }
45
+ const refusals = context.db.all("SELECT id,command,scope_json,identity_json,outcome_json FROM lifecycle_invocations WHERE status='settled' AND json_extract(outcome_json,'$.error.details.work_id')=? ORDER BY rowid DESC", [workId]);
46
+ const refusal = refusals.find(attempt => {
47
+ try {
48
+ const scope = JSON.parse(attempt.scope_json), identity = JSON.parse(attempt.identity_json ?? "null");
49
+ const parsed = parseLifecycleCommand(attempt.command);
50
+ return attempt.scope_json === context.env.DD_FLOW_INVOCATION_SCOPE && parsed.kind === "standalone" && parsed.invocation.operation === "work_finish" && scope.runId === row.run_id
51
+ && scope.generation === row.generation && scope.rootSessionId === input.parentId
52
+ && Array.isArray(identity) && identity[2] === child.session_id && identity[3] === input.parentId;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ });
58
+ if (!refusal)
59
+ throw blocked(workId, "repair_refusal_missing");
60
+ const outcome = JSON.parse(refusal.outcome_json), error = outcome.error, details = error?.details;
61
+ const diagnostic = { refusal_invocation_id: refusal.id, primary_error: { code: error?.code, message: error?.message } };
62
+ if (details?.effect !== "no_effect" || details.recoverable !== true || typeof details.retry_command !== "string"
63
+ || typeof outcome.successor_invocation_id !== "string")
64
+ throw blocked(workId, "repair_successor_unproven", diagnostic);
65
+ const successor = context.db.get("SELECT scope_json,status FROM lifecycle_invocations WHERE id=?", [outcome.successor_invocation_id]);
66
+ if (successor?.status !== "issued" || successor.scope_json !== refusal.scope_json)
67
+ throw blocked(workId, "repair_successor_not_issued", diagnostic);
68
+ const instruction = renderNativeChildFollowupInstruction(input.harness, child, { purpose: "work_repair", context: JSON.stringify({ work_id: workId, code: error.code, message: error.message, retry_command: details.retry_command }) });
69
+ if (!instruction)
70
+ throw blocked(workId, "same_child_repair_unsupported", diagnostic);
71
+ return { work_id: workId, generation: row.generation, child_session_id: child.session_id, terminal_turn_id: child.terminal_turn_id, refusal_invocation_id: refusal.id,
72
+ successor_invocation_id: outcome.successor_invocation_id, operation_id: `${row.controller_id}:work-repair:${digest([row.run_id, workId])}`,
73
+ prompt: `Continue only the existing child for this unfinished Work. This is one bounded correction, not a new fanout wave.\n${instruction}\n\nA follow-up submission acknowledgement or the child's previous completed Turn is not completion of this correction. Wait for this same child's follow-up; never resend the message to poll it.\n${renderWaitInstructions({ harness: input.harness, stage: input.stage })}` };
74
+ }
75
+ return null;
76
+ }
77
+ export function reserveChildRepair(context, row, repair, assertOwner) {
78
+ context.db.writeTransaction(() => {
79
+ assertOwner();
80
+ if (repair.generation !== row.generation)
81
+ throw blocked(repair.work_id, "generation_changed_before_followup");
82
+ const work = context.db.get("SELECT status FROM works WHERE project_id=? AND run_id=? AND work_id=?", [row.project_id, row.run_id, repair.work_id]);
83
+ if (work?.status !== "running")
84
+ throw blocked(repair.work_id, "work_changed_before_followup");
85
+ const successor = context.db.get("SELECT status FROM lifecycle_invocations WHERE id=?", [repair.successor_invocation_id]);
86
+ if (successor?.status !== "issued")
87
+ throw blocked(repair.work_id, "successor_changed_before_followup");
88
+ const reserved = context.db.get(`SELECT e.controller_id,e.data_json FROM run_controller_events e JOIN run_controllers c ON c.controller_id=e.controller_id
89
+ WHERE c.project_id=? AND c.run_id=? AND e.event_type='work_repair_reserved' AND json_extract(e.data_json,'$.work_id')=?`, [row.project_id, row.run_id, repair.work_id]);
90
+ if (reserved && (reserved.controller_id !== row.controller_id || reserved.data_json !== JSON.stringify(repair)))
91
+ throw blocked(repair.work_id, "episode_reserved_by_another_dispatch");
92
+ if (!reserved)
93
+ appendEvent(context, row.controller_id, "work_repair_reserved", repair);
94
+ });
95
+ }
@@ -5,6 +5,7 @@ import { resolveExecutionPolicy, resolveStageTransition, stageExecutionCwd } fro
5
5
  import { readFlowRunExecutionBundle } from "./runs.js";
6
6
  import { hybridDigest } from "./hybrid-contracts.js";
7
7
  import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
8
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
8
9
  /** Adapt generic routing to shared fanout APIs, not to SDLC settings. No alias,
9
10
  * operator file, merge policy or project default participates in this selection.
10
11
  */
@@ -44,7 +45,9 @@ export function genericControllerRouting(execution, workspaceRoot) {
44
45
  * Legacy SDLC manifests retain their original settings and routing behavior.
45
46
  */
46
47
  export function selectControllerExecution(context, run, manifest) {
47
- if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind)) {
48
+ const boundDistill = run.flow_kind === "mb-distill" && JSON.parse(run.index_json).execution_bundle?.flow_id === "mb-distill";
49
+ const boundDelivery = isDeliveryFlow(run.flow_kind) && JSON.parse(run.index_json).execution_bundle?.flow_id === run.flow_kind;
50
+ if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade", "mb-fix", "review-fix", "interactive"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind) || boundDistill || boundDelivery) {
48
51
  const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
49
52
  if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind || bundle.descriptor.descriptor.id === "sdlc")
50
53
  throw new AppError("controller_execution_invalid", "Generic controller requires its retained non-SDLC bundle", 1);
@@ -12,7 +12,8 @@ import { requireProjectByRoot } from "./projects.js";
12
12
  import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
13
13
  import { runtimeCommand } from "./runtime-command.js";
14
14
  import { promptDeliveryInstruction } from "./prompt-delivery.js";
15
- import { currentStageFinishCommand } from "./lifecycle-invocations.js";
15
+ import { currentStageFinishAuthority } from "./lifecycle-invocations.js";
16
+ import { acceptedChildWork } from "./controller-result-authority.js";
16
17
  function semanticCompletionPrompt(stage, completed, command) {
17
18
  const task = stage === "plan-review"
18
19
  ? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
@@ -71,11 +72,17 @@ export async function nextControllerFanout(context, input) {
71
72
  throw new AppError("stage_graph_compatibility_required", "The retained graph cannot be dispatched by this controller; use the RUN's pinned engine", 1, { stage: input.stage, run_id: input.runId, graph: fanout });
72
73
  if (children.length) {
73
74
  const reconciled = await reconcileVnextFanout(context, { ...input, observations });
74
- const issues = reconciled.issues.filter(issue => !["child_check_in_progress", "child_capacity_pending"].includes(issue.code));
75
+ // A known settled child's Work blocker does not consume an independent
76
+ // ready branch. It remains running, so this cannot manufacture Stage success.
77
+ const issues = reconciled.issues.filter(issue => !["child_check_in_progress", "child_capacity_pending"].includes(issue.code)
78
+ && !(issue.code === "execution_ended_without_work_result" && issue.work_id && input.repairBlockedWorks?.includes(issue.work_id)));
75
79
  if (issues.length)
76
80
  throw new AppError("fanout_reconciliation_required", "Native child outcome requires reconciliation, not replay", 1, { issues });
77
- if (children.some(child => child.status === "running" || child.status === "unknown"))
78
- return { kind: "wait", reason: "native_children_unsettled" };
81
+ const unsettled = children.filter(child => child.status === "running" || child.status === "unknown");
82
+ if (unsettled.length) {
83
+ const project = requireProjectByRoot(context, input.projectRoot);
84
+ return { kind: "wait", reason: unsettled.every(child => acceptedChildWork(context, { project_id: project.id, run_id: input.runId }, input.harness, input.sessionId, child.session_id)) ? "accepted_work_native_cleanup" : "native_children_unsettled" };
85
+ }
79
86
  fanout = getVnextFanoutStatus(context, input).orchestration;
80
87
  }
81
88
  if (input.dispatchFence && !fanout.works.failed)
@@ -155,7 +162,8 @@ export async function nextControllerFanout(context, input) {
155
162
  const work = choice.work;
156
163
  const row = context.db.get("SELECT payload_json,result_schema FROM works WHERE work_id=?", [work.work_id]);
157
164
  const payload = JSON.parse(row?.payload_json ?? "null");
158
- if (payload?.kind === "memory-review" && row?.result_schema === "dd-flow/memory-review-work-result@1") {
165
+ if (payload?.kind === "memory-review" && row?.result_schema === "dd-flow/memory-review-work-result@1"
166
+ || payload?.kind === "interactive-review" && row?.result_schema === "dd-flow/interactive-work-result@1") {
159
167
  const copy = prepareMemoryNativeReviewCopy(context, work.work_id, true);
160
168
  return { ...work, start_command: `cd '${copy.root.replaceAll("'", "'\\''")}' && ${workStartCommand(context, assertWorkLaunchReady(context, work.work_id))}` };
161
169
  }
@@ -171,7 +179,7 @@ export async function nextControllerFanout(context, input) {
171
179
  if (!work.work_id || !work.start_command)
172
180
  throw new AppError("fanout_contract_invalid", "Ready Work omits its trusted start command", 1);
173
181
  const totalWorks = fanout.works.created + fanout.works.running + fanout.works.completed + fanout.works.failed + fanout.works.cancelled;
174
- return prompt("dispatch_work", renderDelegationInstructions({ harness: input.harness, stage: input.stage, capacity, works: wave, totalWorks, remainingWorks: fanout.works.created - wave.length }));
182
+ return { ...prompt("dispatch_work", renderDelegationInstructions({ harness: input.harness, stage: input.stage, capacity, works: wave, totalWorks, remainingWorks: fanout.works.created - wave.length })), work_ids: wave.map(work => work.work_id) };
175
183
  }
176
184
  if (fanout.works.running)
177
185
  return { kind: "wait", reason: "work_receipts_pending" };
@@ -209,10 +217,12 @@ export async function nextControllerFanout(context, input) {
209
217
  ].join("\n\n"));
210
218
  }
211
219
  }
212
- const finish = currentStageFinishCommand(context, input.stage);
220
+ const finish = currentStageFinishAuthority(context, input.stage);
213
221
  if (!finish)
214
- throw new AppError("controller_finish_authority_missing", "Settled child Work has no current issued Stage finish; reconcile lifecycle authority", 1, { stage: input.stage });
215
- return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed, finish));
222
+ throw new AppError("controller_finish_authority_missing", "Settled child Work has no current Stage finish authority", 1, { stage: input.stage, reconciliation_required: true });
223
+ if (finish.status !== "issued" || !finish.command)
224
+ throw new AppError("controller_finish_authority_pending", "Observe the original Stage finish before another productive dispatch", 1, { stage: input.stage, invocation_id: finish.id, status: finish.status, reconciliation_required: true });
225
+ return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed, finish.command));
216
226
  }
217
227
  /** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
218
228
  export function controllerNativeChildren(receipt, rootSessionId) {
@@ -6,7 +6,8 @@ import { launchExternalWork } from "./external-work-launch.js";
6
6
  import { requireProjectByRoot } from "./projects.js";
7
7
  import { assertControllerLeaseAllowed } from "./run-controller-process.js";
8
8
  import { assertManagedProcessLease } from "./managed-processes.js";
9
- import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
9
+ import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
10
+ import { isDeliveryFlow } from "../domain/delivery-flows.js";
10
11
  if (!parentPort)
11
12
  throw new AppError("controller_memory_task_invalid", "Memory worker requires its controller thread", 1);
12
13
  const port = parentPort, cancellation = new AbortController();
@@ -18,9 +19,10 @@ let reply;
18
19
  try {
19
20
  context = createContext(env, "prepared_existing");
20
21
  const project = requireProjectByRoot(context, task.input.projectRoot);
21
- const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [project.id, task.input.runId]);
22
- if (!run || !["mb-init", "mb-upgrade"].includes(run.flow_kind) && !isDiagnosticFlow(run.flow_kind))
23
- throw new AppError("controller_memory_task_invalid", "Worker requires the selected memory or diagnostic RUN", 1, { effect: "no_effect" });
22
+ const run = context.db.get("SELECT flow_kind,index_json FROM runs WHERE project_id=? AND id=?", [project.id, task.input.runId]);
23
+ const delivery = run && isDeliveryFlow(run.flow_kind) && JSON.parse(run.index_json).execution_bundle?.flow_id === run.flow_kind;
24
+ if (!run || !["mb-init", "mb-upgrade", "mb-fix", "interactive"].includes(run.flow_kind) && !isSourceSnapshotFlow(run.flow_kind) && !delivery)
25
+ throw new AppError("controller_memory_task_invalid", "Worker requires the selected memory, interactive, source-snapshot or bound delivery RUN", 1, { effect: "no_effect" });
24
26
  if (task.operation === "external") {
25
27
  const work = context.db.get("SELECT project_id,run_id FROM works WHERE work_id=?", [task.input.workId]);
26
28
  if (!work || work.project_id !== project.id || work.run_id !== task.input.runId)