@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
@@ -10,7 +10,7 @@ export function isUpgradeAllowlisted(args) {
10
10
  return true;
11
11
  if (family === "engine" && command === "resolve")
12
12
  return true;
13
- if (family === "migration" && ["impact", "plan", "report", "verify", "apply"].includes(command ?? ""))
13
+ if (family === "migration" && ["impact", "readiness", "plan", "report", "verify", "apply"].includes(command ?? ""))
14
14
  return true;
15
15
  if (family === "memory" && command === "permissions" && parsed.positional[2] === "preflight")
16
16
  return true;
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- import { candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
2
+ import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
3
3
  export { assertSourceRepairEligible, 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";
@@ -16,6 +16,7 @@ import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, manag
16
16
  import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
17
17
  import { getResourceDatabase } from "../storage/database.js";
18
18
  import { readBoundedReleaseFile } from "./release-source.js";
19
+ import { ownedCheckEnvironment, validateCheckEnvironment } from "./check-environment.js";
19
20
  const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
20
21
  // Receipt reservation precedes child spawn so an interrupted CLI leaves an
21
22
  // auditable attempt. A second caller must not mistake that small handoff window
@@ -61,10 +62,7 @@ export function terminalCheckSettled(context, record) {
61
62
  }
62
63
  }
63
64
  export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
64
- if (checkEnvironment !== undefined && (!checkEnvironment || typeof checkEnvironment !== "object" || Array.isArray(checkEnvironment)
65
- || Object.keys(checkEnvironment).length !== 1 || typeof checkEnvironment.CODEX_HOME !== "string"
66
- || checkEnvironment.CODEX_HOME.length > 4096 || checkEnvironment.CODEX_HOME.includes("\0") || !path.isAbsolute(checkEnvironment.CODEX_HOME)))
67
- throw new AppError("invalid_check_environment", "Owned check environment must name one absolute Codex home", 2, { effect: "no_effect" });
65
+ const selected = validateCheckEnvironment(checkEnvironment);
68
66
  const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
69
67
  // The managed CLI's Node must win over a provider app's PATH: a Rosetta
70
68
  // provider can otherwise run x64 checks against arm64-installed modules.
@@ -72,8 +70,7 @@ export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
72
70
  delete environment.NODE_PATH;
73
71
  for (const key of ["DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH", "DD_FLOW_CHECK_RUNNER_PROGRESS_PATH", "DD_FLOW_CHECK_INVOCATION_ID", "DD_FLOW_CHECK_ID", "DD_FLOW_CHECK_INPUT_HASH", "DD_FLOW_CHECK_VERIFICATION_EPOCH", "DD_FLOW_CHECK_COMMAND"])
74
72
  delete environment[key];
75
- if (checkEnvironment)
76
- environment.CODEX_HOME = checkEnvironment.CODEX_HOME;
73
+ Object.assign(environment, selected);
77
74
  return environment;
78
75
  }
79
76
  export function workspaceOperationId(workspaceRoot) { return `workspace:${crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex")}`; }
@@ -302,6 +299,9 @@ export class PreparedCheckEvidence {
302
299
  Object.freeze(this);
303
300
  }
304
301
  static prepare(context, input) {
302
+ const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
303
+ if (checkEnvironment)
304
+ input = { ...input, checkEnvironment };
305
305
  validateCheckEvidenceContext(input.evidenceContext);
306
306
  const fail = () => { throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, { effect: "no_effect" }); };
307
307
  if (!input.checks.length || (input.scope === "work") !== Boolean(input.workId) || input.mode !== undefined && input.mode !== "diagnostic")
@@ -343,7 +343,13 @@ export class PreparedCheckEvidence {
343
343
  const references = deduplicate(checks, commands).map(group => {
344
344
  const inputHash = checkInputHash(input.workspaceRoot, group, profileHash, workspaceInputFingerprint(input.workspaceRoot, state.files, group.inputs, [input.runHome]), input.evidenceContext, input.checkEnvironment);
345
345
  const receipt = receipts.filter(item => item.input_hash === inputHash && item.profile_hash === profileHash && item.scope === input.scope && item.work_id === (input.workId ?? null)).at(-1);
346
- if (!receipt || !checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
346
+ if (!receipt)
347
+ throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, {
348
+ effect: "no_effect", reason: "input_identity_mismatch", expected_input_hash: inputHash,
349
+ candidates: receipts.filter(item => item.scope === input.scope && item.work_id === (input.workId ?? null))
350
+ .slice(-16).map(item => ({ id: item.id, input_hash: item.input_hash, profile_hash: item.profile_hash }))
351
+ });
352
+ if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
347
353
  fail();
348
354
  passed = passed && receipt.status === "passed";
349
355
  for (const artifact of receipt.artifacts)
@@ -482,6 +488,9 @@ function checkEvidenceSqlSnapshot(context, input) {
482
488
  }));
483
489
  }
484
490
  export async function runCodeChecks(context, input) {
491
+ const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
492
+ if (checkEnvironment)
493
+ input = { ...input, checkEnvironment };
485
494
  validateCheckEvidenceContext(input.evidenceContext);
486
495
  const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
487
496
  let gate;
@@ -652,8 +661,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
652
661
  if (result.failure)
653
662
  physicalFailures.push(result.failure);
654
663
  const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
664
+ const candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
655
665
  if (group.runnerEvidence)
656
- retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch, fingerprint_scope: "check_inputs", input_manifest: reserved.inputManifest ?? null, execution_contract_hash: executionContractHash }, input.workspaceRoot, physicalFailures, candidateFiles);
666
+ retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch, fingerprint_scope: "check_inputs", input_manifest: reserved.inputManifest ?? null, execution_contract_hash: executionContractHash }, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
657
667
  const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(runnerEvidence), ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
658
668
  artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
659
669
  const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
@@ -661,6 +671,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
661
671
  receipt.required_artifacts = requiredArtifacts;
662
672
  receipt.candidate_root = input.workspaceRoot;
663
673
  receipt.candidate_files = candidateFiles;
674
+ receipt.candidate_absences = candidateAbsences;
664
675
  receipt.execution_failures = physicalFailures;
665
676
  context.db.run("UPDATE check_receipts SET execution_failures_json = ? WHERE project_id = ? AND id = ? AND status = 'running'", [JSON.stringify(physicalFailures), input.projectId, id]);
666
677
  const completion = readCompletion(completionPath);
@@ -748,6 +759,7 @@ export function reconcileUnfinishedChecks(context, input) {
748
759
  if (scoped && !completionSnapshot)
749
760
  physicalFailures.push({ code: "completion_snapshot_unknown", message: "Detached check has no bound scoped completion snapshot; current inputs cannot prove inputs at command completion" });
750
761
  let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
762
+ let candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
751
763
  if (receipt?.runner_evidence_required) {
752
764
  const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
753
765
  if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
@@ -757,13 +769,14 @@ export function reconcileUnfinishedChecks(context, input) {
757
769
  if (proven.candidate_root) {
758
770
  physicalFailures.push(...(proven.execution_failures ?? []));
759
771
  candidateFiles = proven.candidate_files ?? candidateFiles;
772
+ candidateAbsences = proven.candidate_absences ?? [];
760
773
  }
761
774
  else {
762
775
  physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
763
776
  fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
764
777
  }
765
778
  }
766
- retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles);
779
+ retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
767
780
  receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), ...runnerProofArtifacts(runnerEvidence), "engine-outcome.json"])];
768
781
  artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
769
782
  }
@@ -776,6 +789,7 @@ export function reconcileUnfinishedChecks(context, input) {
776
789
  const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null } : {}), completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: afterFingerprint, mutation_paths: mutationPaths, artifacts: artifacts.items, finished_at: finishedAt };
777
790
  repaired.candidate_root = input.workspaceRoot;
778
791
  repaired.candidate_files = candidateFiles;
792
+ repaired.candidate_absences = candidateAbsences;
779
793
  repaired.execution_failures = physicalFailures;
780
794
  writeReceipt(pendingReceipt.receipt_path, repaired);
781
795
  if (status === "failed")
@@ -974,12 +988,12 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
974
988
  if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
975
989
  try {
976
990
  const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
977
- if (value.schema_id === "dd-flow/check-execution-outcome@1" && value.id === binding.id && value.command === binding.command && value.input_hash === binding.input_hash && value.verification_epoch === binding.verification_epoch && (value.fingerprint_scope ?? null) === (binding.fingerprint_scope ?? null) && (value.execution_contract_hash ?? null) === (binding.execution_contract_hash ?? null) && JSON.stringify(value.input_manifest ?? null) === JSON.stringify(binding.input_manifest ?? null) && typeof value.candidate_root === "string" && path.isAbsolute(value.candidate_root) && (value.candidate_files === undefined || Array.isArray(value.candidate_files) && value.candidate_files.length <= 64 && value.candidate_files.every((file) => typeof file === "string" && path.isAbsolute(file) && Buffer.byteLength(file) <= 4096)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
991
+ if (value.schema_id === "dd-flow/check-execution-outcome@1" && value.id === binding.id && value.command === binding.command && value.input_hash === binding.input_hash && value.verification_epoch === binding.verification_epoch && (value.fingerprint_scope ?? null) === (binding.fingerprint_scope ?? null) && (value.execution_contract_hash ?? null) === (binding.execution_contract_hash ?? null) && JSON.stringify(value.input_manifest ?? null) === JSON.stringify(binding.input_manifest ?? null) && typeof value.candidate_root === "string" && path.isAbsolute(value.candidate_root) && (value.candidate_files === undefined || Array.isArray(value.candidate_files) && value.candidate_files.length <= 64 && value.candidate_files.every((file) => typeof file === "string" && path.isAbsolute(file) && Buffer.byteLength(file) <= 4096)) && (value.candidate_absences === undefined || candidateAbsencesValid(value.candidate_absences)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
978
992
  outcome = value;
979
993
  }
980
994
  catch { /* Missing physical proof cannot grant attribution. */ }
981
995
  }
982
- return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}) };
996
+ return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}), ...(outcome.candidate_absences ? { candidate_absences: outcome.candidate_absences } : {}) };
983
997
  }
984
998
  function executionFailures(input) {
985
999
  const failures = [];
@@ -997,18 +1011,18 @@ function executionFailures(input) {
997
1011
  failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
998
1012
  return failures;
999
1013
  }
1000
- function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles) {
1014
+ function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles, candidateAbsences = []) {
1001
1015
  const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
1002
1016
  if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
1003
1017
  throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
1004
- fs.writeFileSync(temporary, JSON.stringify({ schema_id: "dd-flow/check-execution-outcome@1", id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch, fingerprint_scope: binding.fingerprint_scope ?? null, input_manifest: binding.input_manifest ?? null, execution_contract_hash: binding.execution_contract_hash ?? null, candidate_root: candidateRoot, candidate_files: candidateFiles, execution_failures: failures }), { flag: "wx", mode: 0o600 });
1018
+ fs.writeFileSync(temporary, JSON.stringify({ schema_id: "dd-flow/check-execution-outcome@1", id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch, fingerprint_scope: binding.fingerprint_scope ?? null, input_manifest: binding.input_manifest ?? null, execution_contract_hash: binding.execution_contract_hash ?? null, candidate_root: candidateRoot, candidate_files: candidateFiles, candidate_absences: candidateAbsences, execution_failures: failures }), { flag: "wx", mode: 0o600 });
1005
1019
  fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
1006
1020
  }
1007
1021
  export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
1008
1022
  /** One NUL-aware source of truth for paths differing from HEAD. Rename/copy
1009
1023
  * records carry a second path; retain both because the old side is a deletion. */
1010
1024
  export function workspaceChangedPaths(root) {
1011
- const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
1025
+ const status = spawnSync("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], { cwd: root, env: { ...process.env, LC_ALL: "C" }, encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
1012
1026
  // A workspace can be intentionally gitless. Keep the same conservative
1013
1027
  // file inventory used for fingerprints instead of making result validation
1014
1028
  // fail before the work contract can report its actual error.
@@ -1041,7 +1055,7 @@ export function ignoredWorkspacePaths(root, paths) {
1041
1055
  const normalized = [...new Set(paths.map((item) => normalizeWorkspacePath(root, item)))];
1042
1056
  if (!normalized.length)
1043
1057
  return [];
1044
- const status = spawnSync("git", ["check-ignore", "-z", "--stdin"], { cwd: root, encoding: "utf8", input: `${normalized.join("\0")}\0`, maxBuffer: 64 * 1024 * 1024 });
1058
+ const status = spawnSync("git", ["check-ignore", "-z", "--stdin"], { cwd: root, env: { ...process.env, LC_ALL: "C" }, encoding: "utf8", input: `${normalized.join("\0")}\0`, maxBuffer: 64 * 1024 * 1024 });
1045
1059
  // The canonical owned hook has one meaning across Git status, check cache
1046
1060
  // and MERGE, without changing a sibling worktree's shared Git excludes.
1047
1061
  const gitless = isGitlessWorkspace(root, status);
@@ -1150,12 +1164,13 @@ function environmentFingerprint(workspaceRoot, inputs, checkEnvironment) {
1150
1164
  // and tool selection; omit only known process/control bookkeeping.
1151
1165
  const bookkeeping = new Set(["PWD", "OLDPWD", "SHLVL", "_", "DD_FLOW_DAEMON_ID",
1152
1166
  "DD_FLOW_CURRENT_INVOCATION", "DD_FLOW_INVOCATION_READONLY", "DD_FLOW_INVOCATION_SCOPE", "DD_FLOW_CODEX_STATE_DIR",
1167
+ "DD_FLOW_HOOK_DEADLINE_AT", // Per-call transport budget, not a project check input.
1153
1168
  "DD_FLOW_RUNTIME_OWNER", "DD_FLOW_CONTROLLER_TOKEN", "DD_FLOW_CONTROL_TOKEN", "DD_FLOW_SCOPE_CONTROL_TOKEN",
1154
1169
  "DD_FLOW_BIN", "DD_FLOW_ENGINE_HOME", "DD_FLOW_RUNTIME_BUDGET", "DD_FLOW_WORK_ID", "DD_FLOW_SESSION_ID", "CODEX_THREAD_ID", "DD_EVAL_OPERATION_ID",
1155
- "CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION"]);
1170
+ "CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION", "CODEX_TOOL_CALL_ID"]);
1156
1171
  return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment)
1157
1172
  .filter(([name, value]) => value !== undefined && !bookkeeping.has(name)
1158
- && (inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
1173
+ && (name === "PLAYWRIGHT_BROWSERS_PATH" || inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
1159
1174
  .sort(([left], [right]) => left.localeCompare(right)))).digest("hex");
1160
1175
  }
1161
1176
  function acquireWorkspaceGate(context, input) {
@@ -1330,7 +1345,7 @@ function checkReceiptEvidenceValidForMode(receipt, mode) {
1330
1345
  // Receipt annotations are not execution authority. Compare the SQL-owned
1331
1346
  // failures with the independently hash/binding-checked engine sidecar.
1332
1347
  const hasEngineOutcome = receipt.required_artifacts.includes("engine-outcome.json") || receipt.artifacts.some(artifact => artifact.path === "engine-outcome.json" || artifact.path === path.join(path.dirname(receipt.receipt_path), "artifacts", "engine-outcome.json"));
1333
- if (hasEngineOutcome && !isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []))
1348
+ if (hasEngineOutcome && (!isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []) || !isDeepStrictEqual(retainedProof.candidate_absences ?? [], receipt.candidate_absences ?? [])))
1334
1349
  return false;
1335
1350
  const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
1336
1351
  if (stored.runner_evidence_required && mode !== "diagnostic" && !runnerEvidencePass(readRunnerEvidence(root, stored.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch })))
@@ -13,7 +13,7 @@ export async function preflightCliCompatibility(context, args, env = process.env
13
13
  if (resolvedProjectRoot === null && parsed.positional[0] === "runtime" && parsed.positional[1] === "process"
14
14
  && !parsed.options.has("project-root") && !parsed.options.has("root"))
15
15
  return { ok: true, classification, compatibility: null };
16
- const { resolveOperationProjectRoot, selectEngine } = await import("./engines.js");
16
+ const { resolveOperationProjectRoot, selectEngine, selectBoundExecutingEngine } = await import("./engines.js");
17
17
  const projectRoot = resolvedProjectRoot ?? resolveOperationProjectRoot(context, args);
18
18
  if (!projectRoot) {
19
19
  return { ok: true, classification, compatibility: null };
@@ -21,7 +21,10 @@ export async function preflightCliCompatibility(context, args, env = process.env
21
21
  // Compatibility checks the executing build/manifest identity. Routing and
22
22
  // binding validate snapshot bytes before dispatch; maintenance must not
23
23
  // rehash the dependency closure or spawn health probes on every lease ACK.
24
- const selection = selectEngine(context, { projectRoot, env }, { allowCurrentInProcess: true, verifyCurrentContent: false }, classification);
24
+ // Read-only probes may intentionally inspect a copied RUN before its new
25
+ // executor is bound. Writer authority is neither required nor granted here.
26
+ const selection = (classification.mode === "read_only_diagnostics" ? null : selectBoundExecutingEngine(context, args, projectRoot, env))
27
+ ?? selectEngine(context, { projectRoot, env }, { allowCurrentInProcess: true, verifyCurrentContent: false }, classification);
25
28
  const compatibility = compatibilityReport(selection, classification);
26
29
  if (classification.mode === "read_only_diagnostics" || classification.mode === "mb_upgrade") {
27
30
  return { ok: true, classification, compatibility };
@@ -4,10 +4,11 @@ import { FrozenGenericExecution } from "./generic-execution.js";
4
4
  import { resolveExecutionPolicy, resolveStageTransition, stageExecutionCwd } from "./execution-policy.js";
5
5
  import { readFlowRunExecutionBundle } from "./runs.js";
6
6
  import { hybridDigest } from "./hybrid-contracts.js";
7
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
7
8
  /** Adapt generic routing to shared fanout APIs, not to SDLC settings. No alias,
8
9
  * operator file, merge policy or project default participates in this selection.
9
10
  */
10
- export function genericControllerRouting(execution) {
11
+ export function genericControllerRouting(execution, workspaceRoot) {
11
12
  if (!(execution instanceof FrozenGenericExecution) || execution.settings.execution !== "managed") {
12
13
  throw new AppError("controller_execution_invalid", "Managed controller requires managed frozen execution", 1, { effect: "no_effect" });
13
14
  }
@@ -33,14 +34,17 @@ export function genericControllerRouting(execution) {
33
34
  execution.stage(fromStage);
34
35
  return { ...resolveExecutionPolicy({ ddFlowHome: "", policy, profiles, stage }), session_mode: selected.session_mode };
35
36
  },
36
- cwd(stage, _mergeTargetWorkspace, _currentRun) { execution.stage(stage); return path.resolve(snapshot.scope.workspace); }
37
+ cwd(stage, _mergeTargetWorkspace, currentRun) {
38
+ execution.stage(stage);
39
+ return path.resolve(currentRun?.workspace_root ?? workspaceRoot ?? snapshot.scope.workspace);
40
+ }
37
41
  };
38
42
  }
39
43
  /** Restore execution from RUN authority, never from a generic manifest copy.
40
44
  * Legacy SDLC manifests retain their original settings and routing behavior.
41
45
  */
42
46
  export function selectControllerExecution(context, run, manifest) {
43
- if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind)) {
47
+ if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind)) {
44
48
  const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
45
49
  if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind || bundle.descriptor.descriptor.id === "sdlc")
46
50
  throw new AppError("controller_execution_invalid", "Generic controller requires its retained non-SDLC bundle", 1);
@@ -51,7 +55,9 @@ export function selectControllerExecution(context, run, manifest) {
51
55
  || manifest.request?.project_root !== run.project_root || manifest.request?.run_id !== run.id)) {
52
56
  throw new AppError("controller_execution_invalid", "Controller execution binding differs from retained RUN authority", 1);
53
57
  }
54
- return { ...genericControllerRouting(bundle.execution), bundle, binding, legacy: undefined };
58
+ // Bundle admission has already checked the current RUN workspace against
59
+ // its trusted relocation receipts; frozen scope remains historical input.
60
+ return { ...genericControllerRouting(bundle.execution, run.workspace_root), bundle, binding, legacy: undefined };
55
61
  }
56
62
  const index = JSON.parse(run.index_json);
57
63
  const legacy = manifest ? manifest.execution_profile : index.execution_profile;
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  import { AppError, errorRecord } from "../shared/errors.js";
2
3
  import { normalizeNativeChildren, safeNativeFailure } from "../harness-runtime/lib/native-children.mjs";
3
4
  import { getVnextFanoutStatus, observeVnextNativeChildren, reconcileVnextFanout, getStageFanoutCapacity, verifiedNativeWorkFailure } from "./vnext-fanout.js";
@@ -10,7 +11,9 @@ import { registerMemoryRepairBaselineFromStage, reserveMemoryRepairWriterCopyFro
10
11
  import { requireProjectByRoot } from "./projects.js";
11
12
  import { stageAttempt, stageWorkIds } from "./stage-work-graph.js";
12
13
  import { runtimeCommand } from "./runtime-command.js";
13
- function semanticCompletionPrompt(stage, completed) {
14
+ import { promptDeliveryInstruction } from "./prompt-delivery.js";
15
+ import { currentStageFinishCommand } from "./lifecycle-invocations.js";
16
+ function semanticCompletionPrompt(stage, completed, command) {
14
17
  const task = stage === "plan-review"
15
18
  ? "Classify the reviewer findings, apply every accepted correction in the current PLAN-REVIEW Work, and write the authoritative review decision."
16
19
  : stage === "code-review"
@@ -19,7 +22,7 @@ function semanticCompletionPrompt(stage, completed) {
19
22
  return [
20
23
  `All declared ${stage} child Work has settled (${completed} completed).`,
21
24
  task,
22
- "Then invoke the exact Stage finish command from the original authoritative packet.",
25
+ `Then invoke this exact currently issued Stage finish command:\n\n${command}\n\nRefresh the authoritative verification evidence first; an earlier rejected or successful nonterminal command only reads its retained outcome.`,
23
26
  "If that command returns either outcome = repair_required with a repair Work ID, or error.details.continuation.kind = repair_required, the repair Work is already registered. End this Turn so the controller can dispatch it. Otherwise follow the command's actual result.",
24
27
  "Do not create speculative repair Work or start a successor Stage."
25
28
  ].join(" ");
@@ -29,17 +32,20 @@ export function controllerStageEntryPrompt(stage, command) {
29
32
  return [
30
33
  `Start the assigned ${stage} Stage now. Your first tool call must be this exact standalone command:`,
31
34
  command,
32
- "Read the complete authoritative packet from the command's response_file result, including worker_prompt_markdown. If a read is truncated, continue until the entire packet has been read.",
35
+ promptDeliveryInstruction,
33
36
  "If the returned packet has orchestration.kind = work_fanout, this Turn is only Stage entry: acknowledge the packet and end your Turn immediately. The packet describes the whole Stage, but its graph/dispatch/finish instructions belong to later controller continuations. Do not list, start, launch or finish Work, create children, change files, run checks, or finish the Stage in this entry Turn. The controller will inspect the graph and send the next assignment with exact issued commands (or dispatch external workers itself).",
34
37
  "Otherwise perform this Stage's semantic work and stop at its boundary or a declared HITL boundary. Do not start a successor Stage.",
35
38
  stageLifecycleInstruction
36
39
  ].join("\n\n");
37
40
  }
38
- export function controllerStageResumePrompt(pauseId, command) {
41
+ export function controllerStageResumePrompt(packet, resumedPromptPath) {
39
42
  return [
40
- `The user supplied the raw answer for pause ${pauseId}. Your first tool call must be this exact standalone command:`,
41
- command,
42
- "Do not alter the answer. Follow the returned packet, then stop at this Stage boundary.",
43
+ `The accepted canonical answer for pause ${packet.pause_id} (${packet.stage}, ${packet.work_id}) is retained at this absolute read-only path: ${JSON.stringify(packet.answer_file)} (sha256 ${packet.answer_sha256}). Do not replace it with a placeholder or ask the user to supply it again. A null pause.answer_path before resume does not mean the answer is missing.`,
44
+ resumedPromptPath
45
+ ? `This exact answer was already committed. Do not invoke stage resume again. Read the recovered authoritative continuation at ${JSON.stringify(resumedPromptPath)} and perform its current Stage instructions.`
46
+ : `Only while this exact pause still awaits its answer, your first tool call must be this exact standalone command. It applies the already accepted bound answer file:\n\n${packet.command}`,
47
+ ...(packet.resume_source_path ? [`If this pause has already resumed, do not invoke stage resume again. Read the complete recovered result.worker_prompt_markdown in ${JSON.stringify(packet.resume_source_path)} instead, and follow that authoritative continuation. The controller restores this committed packet before dispatch. A different pause requires its own accepted answer.`] : []),
48
+ `Do not alter the answer. ${promptDeliveryInstruction} Follow the returned packet, then stop at this Stage boundary.`,
43
49
  stageLifecycleInstruction
44
50
  ].join("\n\n");
45
51
  }
@@ -173,8 +179,8 @@ export async function nextControllerFanout(context, input) {
173
179
  throw new AppError("fanout_graph_stalled", "Created Work has no ready or running dependency path", 1);
174
180
  if (input.stage === "review") {
175
181
  const projectId = requireProjectByRoot(context, input.projectRoot).id;
176
- const run = context.db.get("SELECT flow_kind FROM runs WHERE project_id=? AND id=?", [projectId, input.runId]);
177
- if (run?.flow_kind === "mb-init") {
182
+ const run = context.db.get("SELECT flow_kind,run_root,run_dir FROM runs WHERE project_id=? AND id=?", [projectId, input.runId]);
183
+ if (run?.flow_kind === "mb-init" || run?.flow_kind === "mb-upgrade") {
178
184
  const attempt = stageAttempt(context, projectId, input.runId, "review");
179
185
  const repair = context.db.get(`SELECT CASE event_type WHEN 'memory.second_repair_registration' THEN 2 ELSE 1 END AS wave
180
186
  FROM audit_events WHERE project_id=? AND event_type IN ('memory.repair_registration','memory.second_repair_registration')
@@ -186,9 +192,27 @@ export async function nextControllerFanout(context, input) {
186
192
  const args = ["--run", input.runId, "--project-root", input.projectRoot, "--wave", String(repair.wave), "--json"];
187
193
  return prompt("semantic_completion", `Correction wave ${repair.wave} Work has settled. Before Stage finish, run ${runtimeCommand(context, ["memory", "corrections", "checks", ...args])}, then ${runtimeCommand(context, ["memory", "review", "selective-register", ...args])}. Read both complete responses, then end this Turn so the controller can dispatch the independent selective reviewers. Do not finish the Stage or treat repair Work completion as independent acceptance.`);
188
194
  }
195
+ const wave = repair?.wave ?? 0;
196
+ const runHome = run.run_root ?? run.run_dir;
197
+ if (!runHome || !path.isAbsolute(runHome))
198
+ throw new AppError("runtime_missing", "Memory review collection requires its retained absolute RUN home", 1);
199
+ const collectionArgs = ["--run", input.runId, "--project-root", input.projectRoot, ...(wave ? ["--wave", String(wave)] : []),
200
+ "--json", "--progress-jsonl", "--response-file", path.join(runHome, `memory-review-collection-wave-${wave}.json`)];
201
+ const collect = runtimeCommand(context, ["memory", "review", wave ? "selective-collect" : "collect", ...collectionArgs]);
202
+ return prompt("semantic_completion", [
203
+ `All declared memory review Work has settled. First run ${collect}. Read the complete canonical collection from its response_file; do not decide acceptance from a truncated preview or reviewer counts.`,
204
+ "Only if the collection outcome is accepted (projection.outcome = accepted), integrate its accepted results into the original Stage proposal and invoke the exact Stage finish command from the authoritative packet.",
205
+ wave < 2
206
+ ? `If the outcome is needs_repair, use the returned canonical findings and correction_plan_binding to author substantive repair tasks with exact owned write_paths. Register that plan with the packet's issued memory corrections register command for wave ${wave + 1}, then end this Turn so the controller can dispatch it. Do not invoke Stage finish before corrections, checks and independent selective review complete.`
207
+ : "If the outcome is needs_repair or blocked, the two correction waves are exhausted. Use the issued pause command for the concrete unresolved blocker; do not mark it accepted.",
208
+ "For blocked collection, use the issued pause command. Do not calculate finding hashes, merge or drop findings, edit project files in the coordinator, create speculative Work or start a successor Stage."
209
+ ].join("\n\n"));
189
210
  }
190
211
  }
191
- return prompt("semantic_completion", semanticCompletionPrompt(input.stage, fanout.works.completed));
212
+ const finish = currentStageFinishCommand(context, input.stage);
213
+ 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));
192
216
  }
193
217
  /** Shared pinned runtime normalization; preserve structured errors at the TS boundary. */
194
218
  export function controllerNativeChildren(receipt, rootSessionId) {
@@ -0,0 +1,45 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { AppError } from "../shared/errors.js";
3
+ /** Keep synchronous custody/copy work off the controller's lease event loop.
4
+ * The thread remains inside its registered process; OS-level stop owns both.
5
+ * Do not resolve until exit, including after cancellation or a typed failure. */
6
+ export function runControllerMemoryTask(context, task, signal) {
7
+ const aborted = () => new AppError("controller_memory_task_aborted", "Memory operation was cancelled; reconcile its retained effects", 1, { effect: "unknown" });
8
+ if (signal?.aborted)
9
+ return Promise.reject(aborted());
10
+ return new Promise((resolve, reject) => {
11
+ // Both source tests and installed runtimes use the package's compiled worker.
12
+ const worker = new Worker(new URL("../../dist/services/controller-memory-worker.js", import.meta.url), {
13
+ workerData: { task, env: { ...context.env, DD_FLOW_HOME: context.ddFlowHome } }
14
+ });
15
+ let reply, failure;
16
+ const abort = () => worker.postMessage({ abort: true });
17
+ signal?.addEventListener("abort", abort, { once: true });
18
+ if (signal?.aborted)
19
+ abort();
20
+ worker.on("message", (value) => {
21
+ const invalid = value && typeof value === "object" && ("error" in value
22
+ ? !value.error || typeof value.error.code !== "string" || typeof value.error.message !== "string" || !Number.isInteger(value.error.exitCode)
23
+ || !value.error.details || typeof value.error.details !== "object" || Array.isArray(value.error.details) || "result" in value
24
+ : !("result" in value) || !value.result || typeof value.result !== "object" || Array.isArray(value.result));
25
+ if (reply || !value || typeof value !== "object" || invalid) {
26
+ failure = new AppError("controller_memory_task_invalid", "Memory worker returned an invalid terminal reply", 1, { effect: "unknown" });
27
+ }
28
+ else
29
+ reply = value;
30
+ });
31
+ worker.once("error", error => { failure = error; });
32
+ worker.once("exit", code => {
33
+ signal?.removeEventListener("abort", abort);
34
+ if (failure)
35
+ return reject(failure);
36
+ if (reply && "error" in reply)
37
+ return reject(new AppError(reply.error.code, reply.error.message, reply.error.exitCode, reply.error.details));
38
+ if (signal?.aborted)
39
+ return reject(aborted());
40
+ if (code !== 0 || !reply || !("result" in reply))
41
+ return reject(new AppError("controller_memory_task_failed", "Memory worker exited without its terminal result", 1, { exit_code: code, effect: "unknown" }));
42
+ resolve(reply.result);
43
+ });
44
+ });
45
+ }
@@ -0,0 +1,49 @@
1
+ import { parentPort, workerData } from "node:worker_threads";
2
+ import { createContext } from "../runtime/context.js";
3
+ import { AppError, errorRecord } from "../shared/errors.js";
4
+ import { nextControllerFanout } from "./controller-fanout.js";
5
+ import { launchExternalWork } from "./external-work-launch.js";
6
+ import { requireProjectByRoot } from "./projects.js";
7
+ import { assertControllerLeaseAllowed } from "./run-controller-process.js";
8
+ import { assertManagedProcessLease } from "./managed-processes.js";
9
+ import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
10
+ if (!parentPort)
11
+ throw new AppError("controller_memory_task_invalid", "Memory worker requires its controller thread", 1);
12
+ const port = parentPort, cancellation = new AbortController();
13
+ port.on("message", message => { if (message?.abort === true)
14
+ cancellation.abort(); });
15
+ const { task, env } = workerData;
16
+ let context;
17
+ let reply;
18
+ try {
19
+ context = createContext(env, "prepared_existing");
20
+ 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" });
24
+ if (task.operation === "external") {
25
+ const work = context.db.get("SELECT project_id,run_id FROM works WHERE work_id=?", [task.input.workId]);
26
+ if (!work || work.project_id !== project.id || work.run_id !== task.input.runId)
27
+ throw new AppError("controller_memory_task_invalid", "External Work belongs to another RUN", 1, { effect: "no_effect" });
28
+ }
29
+ const owner = env.DD_FLOW_CONTROLLER_TOKEN ? context.db.get("SELECT * FROM run_controllers WHERE project_id=? AND run_id=? AND owner_token=?", [project.id, task.input.runId, env.DD_FLOW_CONTROLLER_TOKEN]) : undefined;
30
+ if (!owner)
31
+ throw new AppError("controller_owner_mismatch", "Memory task lacks its managed controller owner", 1, { effect: "no_effect" });
32
+ assertControllerLeaseAllowed(context, owner);
33
+ assertManagedProcessLease(context, { id: owner.process_id, leaseToken: owner.process_lease_token, pid: process.pid });
34
+ cancellation.signal.throwIfAborted();
35
+ if (task.operation === "fanout")
36
+ reply = { result: await nextControllerFanout(context, task.input) };
37
+ else if (task.operation === "external")
38
+ reply = { result: await launchExternalWork(context, { ...task.input, signal: cancellation.signal }) };
39
+ else
40
+ throw new AppError("controller_memory_task_invalid", "Unknown memory worker operation", 1, { effect: "no_effect" });
41
+ }
42
+ catch (error) {
43
+ reply = { error: errorRecord(error) };
44
+ }
45
+ finally {
46
+ context?.db.close?.();
47
+ }
48
+ port.postMessage(reply);
49
+ port.close();
@@ -216,10 +216,10 @@ export function autoRefreshDashboards(context, input) {
216
216
  if (projectRecoveryGuarded(context, project.id))
217
217
  return { ok: true, skipped: true, reason: "run_recovery_guarded" };
218
218
  const configured = readProjectConfig(context, project.id);
219
- // Automatic observation must not change the source baseline owned by init.
219
+ // Automatic observation must not change a memory or diagnostic source baseline.
220
220
  // Keep projections in Home; do not hide runtime-created paths in Git or
221
221
  // weaken source preservation by excluding user-owned .tasks directories.
222
- const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind='mb-init' AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
222
+ const initializing = context.db.get("SELECT 1 FROM runs WHERE project_id=? AND flow_kind IN ('mb-init','mb-upgrade','mb-audit','project-review') AND (id=? OR status IN ('created','running','paused','waiting_for_user','blocked')) LIMIT 1", [project.id, input.runId ?? null]);
223
223
  const config = initializing ? { ...configured, dashboard: { ...configured.dashboard,
224
224
  markdown_path: path.join(projectDashboardDir(context.ddFlowHome, project.id), "project-dashboard.md") } } : configured;
225
225
  if (!config.dashboard.auto_refresh) {
@@ -62,17 +62,19 @@ export class PreparedDescriptorRunSettlement {
62
62
  const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
63
63
  const matchKeys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, input.stage, input.projectRoot));
64
64
  const snapshot = context.db.readSnapshot(() => ({ owner: selected(context, scope, bundle, matchKeys), generation: assertRunMutationAllowed(context, scope.projectId, scope.runId) }));
65
- const git = snapshot.owner.prior?.git ?? gitFacts(bundle.execution.snapshot().scope.workspace);
65
+ const operational = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
66
+ const git = snapshot.owner.prior?.git ?? gitFacts(operational.workspace_root ?? operational.project_root);
66
67
  return new PreparedDescriptorRunSettlement(scope, bundle, snapshot.owner, git, snapshot.generation, matchKeys);
67
68
  }
68
69
  apply(context, assertHandlerCustody) {
69
70
  if (typeof assertHandlerCustody !== "function")
70
71
  refuse("handler_fence_required");
71
- const bundle = readFlowRunExecutionBundle(context, { projectRoot: this.#bundle.execution.snapshot().scope.projectRoot, runId: this.#scope.runId });
72
+ const operational = assertPreparedRunBundleSqlAuthority(context, this.#scope, this.#bundle);
73
+ const bundle = readFlowRunExecutionBundle(context, { projectRoot: operational.project_root, runId: this.#scope.runId });
72
74
  if (!bundle || bundle.sha256 !== this.#bundle.sha256)
73
75
  refuse("bundle_changed");
74
76
  const observed = context.db.readSnapshot(() => selected(context, this.#scope, this.#bundle, this.#matchKeys));
75
- if (!observed.prior && hybridDigest(gitFacts(this.#bundle.execution.snapshot().scope.workspace)) !== hybridDigest(this.#git))
77
+ if (!observed.prior && hybridDigest(gitFacts(operational.workspace_root ?? operational.project_root)) !== hybridDigest(this.#git))
76
78
  refuse("git_observation_changed");
77
79
  const receipt = { run_id: this.#scope.runId, work_id: this.#owner.rootId, binding: this.#owner.binding, git: this.#git };
78
80
  const changed = withStageSettlement(context, () => {