@deksden-com/dd-flow-cli 0.9.0-beta.122 → 0.9.0-beta.124

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 (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +23 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/cli/run-cli.js +3 -0
  5. package/dist/domain/stage-catalog.js +1 -1
  6. package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
  7. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
  8. package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
  9. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +117 -21
  10. package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +58 -13
  12. package/dist/harness-runtime/lib/dd-codex.mjs +16 -26
  13. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
  14. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
  15. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
  16. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
  17. package/dist/harness-runtime/lib/dd-zcode.mjs +24 -14
  18. package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
  19. package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
  20. package/dist/harness-runtime/lib/managed-daemon.mjs +100 -31
  21. package/dist/harness-runtime/lib/model-observations.mjs +16 -9
  22. package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
  23. package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
  24. package/dist/schemas/flow-run.schema.json +1 -0
  25. package/dist/schemas/plan-review-decision.schema.json +3 -3
  26. package/dist/services/code-checks.js +14 -18
  27. package/dist/services/codex-capacity.js +251 -31
  28. package/dist/services/controller-fanout.js +1 -0
  29. package/dist/services/eval-snapshots.js +13 -8
  30. package/dist/services/external-work-launch.js +36 -11
  31. package/dist/services/harness-adapter.js +21 -15
  32. package/dist/services/managed-processes.js +14 -6
  33. package/dist/services/merge-server.js +35 -12
  34. package/dist/services/review-copy.js +21 -0
  35. package/dist/services/review-deferral.js +23 -0
  36. package/dist/services/run-controller-adapter.js +33 -8
  37. package/dist/services/run-controller-recovery.js +26 -2
  38. package/dist/services/run-controller.js +91 -24
  39. package/dist/services/run-observations.js +7 -2
  40. package/dist/services/run-recovery-runtime.js +49 -3
  41. package/dist/services/runs.js +1 -1
  42. package/dist/services/stage-report-renderer.js +25 -2
  43. package/dist/services/verification-matrix.js +57 -8
  44. package/dist/services/vnext-code-review.js +22 -8
  45. package/dist/services/vnext-merge.js +20 -2
  46. package/dist/services/vnext-plan-review.js +87 -19
  47. package/dist/services/work-registry.js +11 -2
  48. package/package.json +1 -1
@@ -8,6 +8,7 @@ import { managedProcessBinding, processIsAlive, resourceHome } from "./managed-p
8
8
  import { getResourceDatabase } from "../storage/database.js";
9
9
  import { requireManagedDaemonBinding } from "./managed-daemon-binding.js";
10
10
  import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
11
+ import { CAPACITY_POLICY, overloadBurst } from "../harness-runtime/lib/codex-capacity-policy.mjs";
11
12
  /** Pin retained Session loading and its first acknowledgement Turn, never a general fence bypass. */
12
13
  export function prepareRunRecoveryDispatch(context, input) {
13
14
  if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(input.operationId) || !path.isAbsolute(input.stateDir))
@@ -59,7 +60,9 @@ export function prepareRunRecoveryDispatch(context, input) {
59
60
  /** Publish only an exact successor to a failed, effect-free recovery ACK Turn. */
60
61
  export function retainRecoveryCapacityIntent(context, input) {
61
62
  const { intent } = input;
62
- if (![1, 2].includes(intent.ordinal) || !intent.turn_id || !intent.not_before)
63
+ if (!Number.isSafeInteger(intent.ordinal) || intent.ordinal < 1 || !intent.turn_id || !Number.isFinite(Date.parse(intent.not_before)) || intent.policy !== CAPACITY_POLICY
64
+ || !intent.refusal || intent.refusal.policy !== CAPACITY_POLICY || intent.refusal.ordinal !== intent.ordinal - 1 || intent.refusal.turn_id !== intent.turn_id
65
+ || !Number.isFinite(Date.parse(intent.refusal.first_observed_at)) || intent.deadline !== null)
63
66
  throw new AppError("recovery_capacity_intent_invalid", "Recovery capacity successor is invalid", 1);
64
67
  context.db.writeTransaction(() => {
65
68
  const guard = recoveryGuard(context, input.projectId, input.runId);
@@ -71,12 +74,26 @@ export function retainRecoveryCapacityIntent(context, input) {
71
74
  if (controller?.generation !== input.generation || ["completed", "cancelled", "superseded"].includes(controller.status) || control?.status !== "resuming")
72
75
  throw new AppError("recovery_capacity_owner_changed", "Recovery controller or control changed before successor publication", 1);
73
76
  const dispatch = JSON.parse(binding.runtime_dispatch_json);
77
+ if (intent.root_operation_id !== dispatch.operation_id || intent.session_id !== binding.provider_session_id || intent.refusal.session_id !== binding.provider_session_id)
78
+ throw new AppError("recovery_capacity_intent_conflict", "Recovery capacity intent belongs to another root or Session", 1);
74
79
  const currentProcess = getResourceDatabase(resourceHome(context)).get("SELECT * FROM managed_processes WHERE id = ?", [dispatch.process_id]);
75
80
  if (!currentProcess || currentProcess.state !== "running" || currentProcess.lease_expires_at <= context.now() || currentProcess.owner_id !== dispatch.owner_id
76
81
  || currentProcess.project_id !== input.projectId || currentProcess.run_id !== input.runId)
77
82
  throw new AppError("recovery_capacity_owner_changed", "Replacement daemon lease changed before successor publication", 1);
78
83
  const prior = dispatch.capacity_intents ?? [];
79
- const predecessor = intent.ordinal === 1 ? dispatch.operation_id : prior[0]?.operation_id;
84
+ for (let index = 0; index < prior.length; index++) {
85
+ const retained = prior[index];
86
+ if (retained.policy !== CAPACITY_POLICY || retained.ordinal !== index + 1 || retained.root_operation_id !== dispatch.operation_id || retained.session_id !== binding.provider_session_id
87
+ || retained.predecessor !== (index === 0 ? dispatch.operation_id : prior[index - 1].operation_id)
88
+ || retained.operation_id !== `${dispatch.operation_id}:capacity:${index + 1}:session.prompt` || retained.refusal?.ordinal !== index
89
+ || retained.refusal.turn_id !== retained.turn_id || retained.refusal.session_id !== binding.provider_session_id)
90
+ throw new AppError("recovery_capacity_intent_conflict", "Retained recovery capacity chain is invalid", 1);
91
+ }
92
+ const refusals = dispatch.capacity_failures ?? [];
93
+ const observed = refusals.find(item => item.ordinal === intent.refusal.ordinal);
94
+ if (!observed || JSON.stringify(observed) !== JSON.stringify(intent.refusal) || overloadBurst(refusals))
95
+ throw new AppError("recovery_capacity_intent_conflict", "Recovery successor lacks an immutable non-burst refusal binding", 1);
96
+ const predecessor = intent.ordinal === 1 ? dispatch.operation_id : prior[intent.ordinal - 2]?.operation_id;
80
97
  if (prior.length !== intent.ordinal - 1 || predecessor !== intent.predecessor || intent.operation_id !== `${dispatch.operation_id}:capacity:${intent.ordinal}:session.prompt`)
81
98
  throw new AppError("recovery_capacity_intent_conflict", "Recovery capacity successor does not extend the pinned ACK", 1);
82
99
  const failed = context.db.get("SELECT status FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [intent.predecessor, input.controllerId, input.generation]);
@@ -87,7 +104,7 @@ export function retainRecoveryCapacityIntent(context, input) {
87
104
  const changed = context.db.run("UPDATE run_recovery_bindings SET runtime_dispatch_json = ? WHERE recovery_id = ? AND daemon_id = ? AND status = 'prepared' AND runtime_dispatch_json = ?", [JSON.stringify(updated), input.recoveryId, input.daemonId, binding.runtime_dispatch_json]);
88
105
  if (changed.changes !== 1)
89
106
  throw new AppError("recovery_capacity_intent_conflict", "Recovery dispatch changed before successor publication", 1);
90
- const marked = context.db.run("UPDATE run_controller_operations SET status = 'failed', receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_intent: next }), context.now(), intent.predecessor, input.controllerId, input.generation]);
107
+ const marked = context.db.run("UPDATE run_controller_operations SET status = 'failed', receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_failure: next.refusal, capacity_intent: next }), context.now(), intent.predecessor, input.controllerId, input.generation]);
91
108
  if (marked.changes !== 1)
92
109
  throw new AppError("recovery_capacity_intent_conflict", "Recovery predecessor changed before successor publication", 1);
93
110
  });
@@ -113,6 +130,35 @@ export function recoveryRuntimeAdmission(context, record, input) {
113
130
  if (guard.status === "resuming" && binding.status === "prepared" && input.operation === "session.resume")
114
131
  return typeof dispatch.resume_operation_id === "string" && input.operationId === dispatch.resume_operation_id && input.nativeSessionId === binding.provider_session_id;
115
132
  const active = dispatch.capacity_intents?.at(-1);
133
+ if (active) {
134
+ const ordinal = dispatch.capacity_intents.length;
135
+ const predecessor = ordinal === 1 ? dispatch.operation_id : dispatch.capacity_intents[ordinal - 2].operation_id;
136
+ if (active.policy !== CAPACITY_POLICY || active.ordinal !== ordinal || active.root_operation_id !== dispatch.operation_id || active.session_id !== binding.provider_session_id
137
+ || active.predecessor !== predecessor || active.operation_id !== `${dispatch.operation_id}:capacity:${ordinal}:session.prompt`
138
+ || active.refusal?.policy !== CAPACITY_POLICY || active.refusal.ordinal !== ordinal - 1 || active.refusal.turn_id !== active.turn_id
139
+ || active.refusal.session_id !== binding.provider_session_id || active.deadline !== null || !Number.isFinite(Date.parse(active.not_before)))
140
+ return false;
141
+ const failure = dispatch.capacity_failures?.find(item => item.ordinal === ordinal - 1);
142
+ if (!failure || JSON.stringify(failure) !== JSON.stringify(active.refusal))
143
+ return false;
144
+ try {
145
+ if (overloadBurst(dispatch.capacity_failures))
146
+ return false;
147
+ }
148
+ catch {
149
+ return false;
150
+ }
151
+ const predecessorRow = context.db.get("SELECT status,receipt_json FROM run_controller_operations WHERE operation_id = ? AND controller_id = ? AND generation = ?", [predecessor, record.owner_id, guard.generation]);
152
+ if (predecessorRow?.status !== "failed")
153
+ return false;
154
+ try {
155
+ if (JSON.stringify(JSON.parse(predecessorRow.receipt_json).capacity_intent) !== JSON.stringify(active))
156
+ return false;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ }
116
162
  if (active && active.not_before > context.now())
117
163
  return false;
118
164
  return guard.status === "resuming" && binding.status === "prepared" && input.operation === "session.prompt"
@@ -85,7 +85,7 @@ export function startFlowRun(context, input, prepared = prepareFlowRunStart(cont
85
85
  run_id: runId,
86
86
  short_id: shortId,
87
87
  flow_kind: flowKind,
88
- ...(["vnext_specify", "vnext_protocolize"].includes(flowKind) ? { verification_matrix_contract: "dd-flow/verification-matrix@1" } : {}),
88
+ ...(["vnext_specify", "vnext_protocolize"].includes(flowKind) ? { verification_matrix_contract: "dd-flow/verification-matrix@1", final_check_coverage_contract: "dd-flow/final-check-coverage@1" } : {}),
89
89
  subject: {
90
90
  id: subjectId,
91
91
  title: subjectId
@@ -39,6 +39,7 @@ export function acceptStagePacket(context, input) {
39
39
  context.db.afterCommit(() => publishStagePacket(context, input.projectId, input.runId, input.stage, input.prepared.file));
40
40
  }
41
41
  export function publishStagePacket(context, projectId, runId, stage, file) {
42
+ file = resolveStagePublicationPath(context, projectId, runId, file);
42
43
  assertPublicationPath(context, projectId, runId, file);
43
44
  fs.mkdirSync(path.dirname(file), { recursive: true });
44
45
  withProjectionLock(`${file}.publish-lock`, () => {
@@ -47,7 +48,7 @@ export function publishStagePacket(context, projectId, runId, stage, file) {
47
48
  const index = run ? JSON.parse(run.index_json) : null;
48
49
  const currentAttempt = index?.stage_runs?.find((item) => item.stage === stage)?.attempt;
49
50
  const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' ORDER BY id DESC", [projectId]);
50
- const packet = rows.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.attempt === currentAttempt && item.file === file);
51
+ const packet = rows.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.attempt === currentAttempt && resolveStagePublicationPath(context, projectId, runId, item.file) === file);
51
52
  if (!packet)
52
53
  return;
53
54
  const bytes = Buffer.from(packet.base64, "base64");
@@ -78,6 +79,7 @@ export function acceptStageReport(context, input) {
78
79
  }
79
80
  /** Replays the latest committed receipt, never an obsolete attempt. */
80
81
  export function publishStageReport(context, projectId, runId, stage, root) {
82
+ root = resolveStagePublicationPath(context, projectId, runId, root);
81
83
  assertPublicationPath(context, projectId, runId, root);
82
84
  withProjectionLock(`${root}.report-publish-lock`, () => {
83
85
  assertPublicationPath(context, projectId, runId, root);
@@ -85,7 +87,7 @@ export function publishStageReport(context, projectId, runId, stage, root) {
85
87
  const index = run ? JSON.parse(run.index_json) : null;
86
88
  const currentAttempt = index?.stage_runs?.find((item) => item.stage === stage)?.attempt;
87
89
  const events = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_report' ORDER BY id DESC", [projectId]);
88
- const receipt = events.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.root === root && item.attempt === currentAttempt);
90
+ const receipt = events.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.attempt === currentAttempt && resolveStagePublicationPath(context, projectId, runId, item.root) === root);
89
91
  if (!receipt)
90
92
  return;
91
93
  fs.mkdirSync(root, { recursive: true });
@@ -104,6 +106,27 @@ export function publishStageReport(context, projectId, runId, stage, root) {
104
106
  }
105
107
  });
106
108
  }
109
+ /** Operational destinations follow validated imports, while audit bytes retain
110
+ * their original locators. SQL-only so readers may also use it in settlement. */
111
+ export function resolveStagePublicationPath(context, projectId, runId, locator) {
112
+ const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
113
+ if (!run?.run_root || !path.isAbsolute(locator))
114
+ throw new AppError("stage_packet_invalid", "Stage publication has no absolute bound RUN locator", 1, { run_id: runId, locator });
115
+ let destination = path.resolve(locator);
116
+ const mappings = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.publication_relocation' ORDER BY id", [projectId]);
117
+ for (const row of mappings) {
118
+ const mapping = JSON.parse(row.payload_json);
119
+ if (mapping.run_id !== runId || !path.isAbsolute(mapping.from) || !path.isAbsolute(mapping.to))
120
+ continue;
121
+ const relative = path.relative(mapping.from, destination);
122
+ if (relative && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
123
+ destination = path.resolve(mapping.to, relative);
124
+ }
125
+ const relative = path.relative(run.run_root, destination);
126
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
127
+ throw new AppError("publication_pending", "Stage publication escaped its bound RUN lineage", 1, { effect: "committed", run_id: runId, locator });
128
+ return destination;
129
+ }
107
130
  /** Recheck the actual destination after preparation: a missing leaf may have
108
131
  * an existing symlink ancestor, and retained packet bytes never authorize it. */
109
132
  function assertPublicationPath(context, projectId, runId, file) {
@@ -4,13 +4,19 @@ import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
6
6
  import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations } from "./code-checks.js";
7
- import { acceptStagePacket, prepareStagePacket, publishStagePacket } from "./stage-report-renderer.js";
7
+ import { acceptStagePacket, prepareStagePacket, publishStagePacket, resolveStagePublicationPath } from "./stage-report-renderer.js";
8
8
  import { assertRunMutationAllowed } from "./run-recovery.js";
9
9
  export const verificationMatrixContract = "dd-flow/verification-matrix@1";
10
+ export const finalCheckCoverageContract = "dd-flow/final-check-coverage@1";
10
11
  /** A view of already validated facts: never a gate or a receipt selector. */
11
12
  export function buildVerificationMatrix(input) {
12
13
  const sourceCatalog = new Map();
14
+ const pathChecksums = new Map();
13
15
  for (const source of input.sources) {
16
+ const location = JSON.stringify([source.root, source.path]);
17
+ if (pathChecksums.has(location) && pathChecksums.get(location) !== source.sha256)
18
+ throw new AppError("verification_matrix_source_changed", "Matrix source aliases have conflicting checksums", 1, { root: source.root, path: source.path });
19
+ pathChecksums.set(location, source.sha256);
14
20
  const identity = JSON.stringify([source.role, source.root, source.path]);
15
21
  const prior = sourceCatalog.get(identity);
16
22
  if (prior && prior.sha256 !== source.sha256)
@@ -105,9 +111,24 @@ export function prepareRunVerificationMatrix(context, input) {
105
111
  const ownership = Object.fromEntries(protocols.map((protocol, i) => [protocol, protocolize.obligation_ownership.filter(item => item.member_keys.includes(protocolize.delivery.members[i].key)).map(item => item.obligation_id)]));
106
112
  const skeleton = input.stage === "plan" && input.role === "input";
107
113
  const plans = [];
108
- const predecessor = input.stage === "plan-review" ? "plan" : input.stage === "code" ? "plan-review" : input.stage === "code-review" ? "code" : input.stage === "merge" ? "code-review" : null;
109
- const priorMatrix = input.role === "input" && predecessor ? latestRunVerificationMatrix(context, input.projectId, input.runId, predecessor) : null;
114
+ const reviewSkipped = index.settings?.code_review?.mode === "off" && Boolean(index.settings.code_review.reason?.trim()) && !index.stage_runs?.some(item => item.stage === "code-review");
115
+ const predecessor = input.stage === "plan-review" ? "plan" : input.stage === "code" ? "plan-review" : input.stage === "code-review" ? "code" : input.stage === "merge" ? reviewSkipped ? "code" : "code-review" : null;
116
+ const priorMatrix = input.role === "input" && predecessor ? latestRunVerificationMatrix(context, input.projectId, input.runId, predecessor, "output") : null;
117
+ if (input.role === "input" && predecessor && !priorMatrix)
118
+ throw new AppError("verification_matrix_packet_missing", "Marked Stage requires its accepted predecessor output matrix", 1, { stage: input.stage, predecessor });
110
119
  if (!skeleton) {
120
+ const batchPath = "03-plan/code-work-batch.json", batchFile = safePath(home, batchPath);
121
+ if (!fs.existsSync(batchFile))
122
+ throw new AppError("verification_matrix_source_missing", "Marked matrix requires its generated CODE batch", 1, { path: batchPath });
123
+ const batchBytes = fs.readFileSync(batchFile), batchSha = digest(batchBytes);
124
+ const acceptedBatch = priorMatrix?.matrix.sources.find(source => source.role === "code_work_batch");
125
+ if (acceptedBatch && acceptedBatch.sha256 !== batchSha)
126
+ throw new AppError("verification_matrix_source_changed", "CODE batch differs from accepted predecessor matrix", 1, { path: batchPath, expected: acceptedBatch.sha256, current: batchSha });
127
+ const batchSnapshot = `${vnextStageDirectory(input.stage)}/verification/sources/${batchSha}/code-work-batch.json`;
128
+ packets.push(prepareStagePacket(safePath(home, batchSnapshot), batchBytes));
129
+ sources.push({ role: "code_work_batch", root: "run", path: batchSnapshot, sha256: batchSha });
130
+ if (input.stage === "merge" && input.role === "output")
131
+ sources.push({ role: "code_work_batch_current", root: "run", path: batchPath, sha256: batchSha });
111
132
  for (const protocol of protocols) {
112
133
  const relative = `.memory-bank/protocol/${protocol}/plan.json`, file = safePath(workspace, relative);
113
134
  // PLAN bytes are retained before MERGE cleanup; later recovery does not
@@ -158,6 +179,10 @@ export function prepareRunVerificationMatrix(context, input) {
158
179
  let finalBindings;
159
180
  if (input.stage === "merge") {
160
181
  const gate = readSource("merge_gate", "run", "07-merge/merge-gate.json");
182
+ if (index.final_check_coverage_contract === finalCheckCoverageContract) {
183
+ for (const role of final ? ["baseline", "source", "target"] : ["baseline", "source"])
184
+ readSource(`check_profile_${role}`, "run", `07-merge/check-profile-${role}.json`);
185
+ }
161
186
  const semanticRefs = new Set(plans.flatMap(plan => plan.acceptance.flatMap(entry => entry.check_refs.map(id => `${plan.protocol_id}/${id}`))));
162
187
  policyChecks = gate.checks.filter(check => !semanticRefs.has(check.canonical_ref ?? check.id));
163
188
  if (final) {
@@ -204,6 +229,20 @@ export function prepareRunVerificationMatrix(context, input) {
204
229
  return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: "unavailable", reason: "receipt_file_missing" };
205
230
  const bytes = fs.readFileSync(receiptFile);
206
231
  sources.push({ role: `receipt:${ref}`, root: "run", path: portable.receipt_path, sha256: digest(bytes) });
232
+ const retainedReceipt = JSON.parse(bytes.toString("utf8"));
233
+ for (const artifact of retainedReceipt.artifacts ?? []) {
234
+ if (path.isAbsolute(artifact.path) || artifact.path.includes("\0") || artifact.path.split(/[\\/]/).includes(".."))
235
+ throw new AppError("verification_matrix_path_invalid", "Native artifact must remain inside its evidence directory", 1, { path: artifact.path });
236
+ const artifactPath = slash(path.join(path.dirname(portable.receipt_path), "artifacts", artifact.path));
237
+ const artifactFile = safePath(home, artifactPath);
238
+ if (fs.existsSync(artifactFile))
239
+ sources.push({ role: `artifact:${ref}:${artifact.path}`, root: "run", path: artifactPath, sha256: digest(fs.readFileSync(artifactFile)) });
240
+ }
241
+ if (receipt.status !== "aborted" && fs.existsSync(receipt.completion_path)) {
242
+ const completionPath = portable.completion_path;
243
+ const completionFile = safePath(home, completionPath);
244
+ sources.push({ role: `completion:${ref}`, root: "run", path: completionPath, sha256: digest(fs.readFileSync(completionFile)) });
245
+ }
207
246
  return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: valid ? "retained" : "unavailable", ...(!valid ? { reason: "receipt_evidence_invalid" } : {}) };
208
247
  });
209
248
  const works = context.db.all("SELECT work_id, payload_json FROM works WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]).flatMap(work => {
@@ -218,6 +257,8 @@ export function prepareRunVerificationMatrix(context, input) {
218
257
  const markdown = prepareStagePacket(safePath(home, `${root}/verification-matrix.md`), Buffer.from(renderVerificationMatrix(matrix)));
219
258
  packets.push(json, markdown);
220
259
  const metadata = { contract: verificationMatrixContract, role: header.role, completeness: header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(home, json.file)), sha256: json.sha256 }, markdown: { path: slash(path.relative(home, markdown.file)), sha256: markdown.sha256 } };
260
+ if (index.final_check_coverage_contract === finalCheckCoverageContract)
261
+ metadata.coverage_contract = finalCheckCoverageContract;
221
262
  const predecessors = (index.stage_runs ?? []).filter(item => item.stage !== input.stage && item.status === "done").map(item => ({ stage: item.stage, ...(item.attempt ? { attempt: item.attempt } : {}), ...(item.data_sha256 ? { data_sha256: item.data_sha256 } : {}) }));
222
263
  return { projectId: input.projectId, runId: input.runId, stage: input.stage, home, attempt, generation, packets, matrix, metadata, predecessors };
223
264
  }
@@ -266,9 +307,9 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
266
307
  continue;
267
308
  if (matrix.schema_id !== verificationMatrixContract || matrix.header.run_id !== runId || matrix.header.stage !== stage || matrix.header.stage_attempt !== attempt)
268
309
  throw new AppError("stage_packet_invalid", "Retained matrix owner differs from its packet", 1);
269
- safePath(run.run_root, path.relative(run.run_root, packet.file), false);
310
+ packet.file = resolveStagePublicationPath(context, projectId, runId, packet.file);
270
311
  const mdFile = packet.file.replace(/\.json$/, ".md");
271
- const paired = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && item.file === mdFile);
312
+ const paired = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && resolveStagePublicationPath(context, projectId, runId, item.file) === mdFile);
272
313
  if (!paired)
273
314
  throw new AppError("stage_packet_invalid", "Retained matrix lacks its exact Markdown packet", 1);
274
315
  const mdBytes = Buffer.from(paired.base64, "base64");
@@ -277,9 +318,11 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
277
318
  const md = { file: mdFile, bytes: mdBytes, sha256: paired.sha256 };
278
319
  const json = { file: packet.file, bytes, sha256: packet.sha256 };
279
320
  const metadata = { contract: verificationMatrixContract, role: matrix.header.role, completeness: matrix.header.completeness, source_fingerprint: matrix.header.source_fingerprint, json: { path: slash(path.relative(run.run_root, packet.file)), sha256: packet.sha256 }, markdown: { path: slash(path.relative(run.run_root, mdFile)), sha256: md.sha256 } };
280
- const sourcePackets = matrix.sources.filter(source => source.role.startsWith("plan:") && source.root === "run").map(source => {
321
+ if (index.final_check_coverage_contract === finalCheckCoverageContract)
322
+ metadata.coverage_contract = finalCheckCoverageContract;
323
+ const sourcePackets = matrix.sources.filter(source => (source.role.startsWith("plan:") || source.role === "code_work_batch") && source.root === "run").map(source => {
281
324
  const file = safePath(run.run_root, source.path, false);
282
- const retained = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && item.file === file && item.sha256 === source.sha256);
325
+ const retained = rows.map(row => JSON.parse(row.payload_json)).find(item => item.run_id === runId && item.stage === stage && item.attempt === attempt && resolveStagePublicationPath(context, projectId, runId, item.file) === file && item.sha256 === source.sha256);
283
326
  if (!retained)
284
327
  throw new AppError("stage_packet_invalid", "Matrix PLAN source lacks retained bytes", 1, { path: source.path });
285
328
  const bytes = Buffer.from(retained.base64, "base64");
@@ -294,7 +337,13 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
294
337
  }
295
338
  function portableReceipt(receipt, home) {
296
339
  const relative = (file) => slash(path.relative(home, safePath(home, path.relative(home, file))));
297
- return { ...receipt, receipt_path: relative(receipt.receipt_path), completion_path: relative(receipt.completion_path), stdout_path: relative(receipt.stdout_path), stderr_path: relative(receipt.stderr_path), artifacts: receipt.artifacts.map(item => ({ ...item, path: relative(item.path) })) };
340
+ return { ...receipt, receipt_path: relative(receipt.receipt_path), completion_path: relative(receipt.completion_path), stdout_path: relative(receipt.stdout_path), stderr_path: relative(receipt.stderr_path), artifacts: receipt.artifacts.map(item => {
341
+ const artifactPath = path.isAbsolute(item.path) ? path.relative(path.join(path.dirname(receipt.receipt_path), "artifacts"), item.path) : item.path;
342
+ if (path.isAbsolute(artifactPath) || artifactPath.includes("\0") || artifactPath.split(/[\\/]/).includes(".."))
343
+ throw new AppError("verification_matrix_path_invalid", "Native artifact must remain inside its evidence directory", 1, { path: item.path });
344
+ safePath(home, path.join(path.dirname(relative(receipt.receipt_path)), "artifacts", artifactPath));
345
+ return { ...item, path: slash(artifactPath) };
346
+ }) };
298
347
  }
299
348
  function safePath(root, relative, inspectFilesystem = true) {
300
349
  if (path.isAbsolute(relative) || relative.split(/[\\/]/).includes(".."))
@@ -25,6 +25,8 @@ import { prepareVnextMergeRequest } from "./vnext-merge.js";
25
25
  import { validateVnextCodeHandoff } from "./vnext-plan.js";
26
26
  import { assertRunMutationAllowed } from "./run-recovery.js";
27
27
  import { prepareJsonFile, prepareOutputFile } from "../cli/input-preparation.js";
28
+ import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
29
+ import { reviewDefEvidence } from "./review-deferral.js";
28
30
  const stage = "code-review";
29
31
  const stageDir = vnextStageDirectory(stage);
30
32
  const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
@@ -56,14 +58,15 @@ export function prepareVnextCodeReviewStart(context, input) {
56
58
  const cycle = reviewCycle(run);
57
59
  const codeReportSha256 = sha256File(path.join(home, "05-code", "stage-report.json"));
58
60
  const groups = mode.mode === "off" ? [] : reviewGroups(home);
59
- const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", review_cycle: cycle, group, read_only: true } })) };
61
+ const reviewerInputs = mode.mode === "off" ? [] : reviewerSourceInputs(run.workspace_root, groups.flatMap(group => group.aspect_ids));
62
+ const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: `${reviewerTask(home, group)}\n${reviewerSourceInstruction(run.workspace_root, group.aspect_ids)}`, launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", review_cycle: cycle, group, read_only: true } })) };
60
63
  const items = mode.mode === "off" ? [] : prepareWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch });
61
64
  const template = mode.mode === "off" ? "" : read(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md"));
62
65
  const checks = mode.mode === "off" ? [] : acceptedCodeChecks(context, project.id, run.id).map(({ id, purpose }) => ({ id, purpose }));
63
66
  for (const name of ["review-context.json", ...(mode.mode === "off" ? ["decision.json"] : ["review-work-batch.json", "fanout.json"])])
64
67
  prepareOutputFile(path.join(root, name));
65
68
  const matrix = mode.mode === "off" ? null : prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", stageAttempt: attachment.attempt });
66
- return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks, matrix };
69
+ return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks, matrix, reviewerInputs };
67
70
  }
68
71
  export function startVnextCodeReview(context, input, prepared = prepareVnextCodeReviewStart(context, input)) {
69
72
  const { projectRoot, project, run, root, rootWork, existingPrompt } = prepared;
@@ -88,7 +91,7 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
88
91
  freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
89
92
  const promptPath = path.join(root, "stage-prompt.md");
90
93
  const reviewContextPath = path.join(root, "review-context.json");
91
- fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id, version: 3 } } : {}), code_report_sha256: codeReportSha256, review_batch_sha256: sha256Value(`${JSON.stringify(batch, null, 2)}\n`), aspect_map_checksums: aspectMapChecksums }, null, 2)}\n`);
94
+ fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", reviewer_inputs: prepared.reviewerInputs, cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id, version: 3 } } : {}), code_report_sha256: codeReportSha256, review_batch_sha256: sha256Value(`${JSON.stringify(batch, null, 2)}\n`), aspect_map_checksums: aspectMapChecksums }, null, 2)}\n`);
92
95
  if (mode.mode === "off") {
93
96
  fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
94
97
  const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n${stageLifecycleInstruction}\n`;
@@ -194,6 +197,7 @@ export async function finishVnextCodeReview(context, input) {
194
197
  validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
195
198
  const cycle = reviewContext.cycle ?? "initial";
196
199
  const reviewers = reviewerWorks(context, project.id, run.id, cycle);
200
+ const defEvidence = decision.findings.filter(item => item.disposition === "defer").map(item => reviewDefEvidence(run.workspace_root, item.def_id));
197
201
  if (mode !== "off") {
198
202
  assertCodeReviewCoverage(reviewGroups(home), reviewers);
199
203
  const incomplete = reviewers.filter((work) => work.status !== "completed");
@@ -202,6 +206,8 @@ export async function finishVnextCodeReview(context, input) {
202
206
  for (const work of reviewers)
203
207
  validateReviewerResult(context, work, run);
204
208
  const canonical = reviewDecisionForRepair(decision, reviewers);
209
+ if (reviewContext.accepted_decision && JSON.stringify(reviewContext.def_evidence ?? []) !== JSON.stringify(defEvidence))
210
+ throw new AppError("deferral_invalid", "Accepted DEF evidence changed during review repair", 2);
205
211
  const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
206
212
  let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
207
213
  const frozenShaFile = path.join(root, ".decision-sha256");
@@ -213,7 +219,7 @@ export async function finishVnextCodeReview(context, input) {
213
219
  // Accept the value once. Repairs consume this decision, not a mutable
214
220
  // operator file. Reuse the existing context artifact and review cycle.
215
221
  const contextFile = path.join(root, "review-context.json"), temporary = `${contextFile}.tmp-${crypto.randomUUID()}`;
216
- fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
222
+ fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision, def_evidence: defEvidence }, null, 2)}\n`);
217
223
  fs.renameSync(temporary, contextFile);
218
224
  const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, decisionReasonsByFinding: Object.fromEntries(canonical.decision.findings.map((item) => [item.finding_ref, item.reason])), objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
219
225
  const retained = latestRunVerificationMatrix(context, project.id, run.id, stage, "input");
@@ -261,6 +267,9 @@ export async function finishVnextCodeReview(context, input) {
261
267
  const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
262
268
  if (matrix)
263
269
  Object.assign(report.semantic, { verification_matrix: matrix.metadata });
270
+ if (JSON.stringify(defEvidence) !== JSON.stringify(decision.findings.filter(item => item.disposition === "defer").map(item => reviewDefEvidence(run.workspace_root, item.def_id))))
271
+ throw new AppError("deferral_invalid", "DEF evidence changed while final checks ran", 2);
272
+ Object.assign(report.semantic, { def_evidence: defEvidence });
264
273
  const preparedReport = prepareStageReport(report);
265
274
  const completionInput = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: report.mechanical.git };
266
275
  withStageSettlement(context, () => {
@@ -352,9 +361,12 @@ export function assertCodeReviewCoverage(groups, reviewers) {
352
361
  throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
353
362
  }
354
363
  export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Verify that universal and exclusive constraints (only, any, all, never, whole) and stated exceptions were not narrowed. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Evidence closes a claim only when it exercises the named failure mechanism; a sequential negative test does not prove a concurrency race, and proof limits cannot waive an accepted obligation. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
355
- function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", "Ready reviewer Works: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]), `Write the decision to this exact absolute path, not a relative look-alike: ${decision}`, `Finish only after every reviewer Work settles and that exact file exists: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Do not invoke that Work's start_command from the coordinator Session: end this Turn so the controller can launch a fresh child, then call the returned next.finish_command after it completes. Internal invocation IDs are runtime-owned and must not be copied or reused. Review is not repeated.", "If the aggregate gate fails with repair_required, its repair Work is already registered with all causal checks. End this Turn so the controller launches a fresh child. Do not copy receipt IDs, create another repair, invoke its start_command in the coordinator, or edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
364
+ function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE passed its declared checks; each check proves only its named contour and proof limits, not complete semantic acceptance. Conduct independent quality review and do not manually rerun every declared check.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", "Ready reviewer Works: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]), `Write the decision to this exact absolute path, not a relative look-alike: ${decision}`, `Finish only after every reviewer Work settles and that exact file exists: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Do not invoke that Work's start_command from the coordinator Session: end this Turn so the controller can launch a fresh child, then call the returned next.finish_command after it completes. Internal invocation IDs are runtime-owned and must not be copied or reused. Review is not repeated.", "If the aggregate gate fails with repair_required, its repair Work is already registered with all causal checks. End this Turn so the controller launches a fresh child. Do not copy receipt IDs, create another repair, invoke its start_command in the coordinator, or edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
356
365
  function canonicalReviewEvidence(decision, reviewers) {
357
- const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
366
+ const known = new Map(canonicalCodeFindings(reviewers).map((item) => [item.finding_ref, item.finding]));
367
+ const refs = decision.findings.map(item => item.finding_ref);
368
+ if (new Set(refs).size !== refs.length || refs.some(ref => !known.has(ref)))
369
+ throw new AppError("review_evidence_invalid", "CODE decisions must use unique known canonical finding references", 2, { finding_refs: refs });
358
370
  const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
359
371
  const duplicates = new Set();
360
372
  for (const item of decision.findings.filter((entry) => entry.disposition === "duplicate")) {
@@ -370,6 +382,9 @@ function canonicalReviewEvidence(decision, reviewers) {
370
382
  }
371
383
  if (!cursor)
372
384
  throw new AppError("review_evidence_invalid", "Duplicate finding chain must end at a decided canonical finding", 2, { finding_ref: item.finding_ref });
385
+ if (["p0", "p1"].includes(known.get(item.finding_ref).priority) && cursor.disposition !== "fix") {
386
+ throw new AppError("review_material_disposition_invalid", "P0/P1 duplicate findings must resolve to a fixed canonical finding", 2, { finding_ref: item.finding_ref, canonical_finding_ref: cursor.finding_ref, disposition: cursor.disposition });
387
+ }
373
388
  duplicates.add(item.finding_ref);
374
389
  }
375
390
  return {
@@ -420,8 +435,7 @@ function validateDecision(context, input) {
420
435
  if (item.disposition === "fix" && !repaired.some((work) => requiredChecks.every((ref) => reviewCheckRefs(work).includes(ref))))
421
436
  throw new AppError("review_repair_check_missing", "A fixed CODE-REVIEW finding needs a repair Work that retains every selected causal check", 2, { finding_ref: findingRef, check_refs: requiredChecks });
422
437
  if (finding.priority === "p2" && item.disposition === "defer") {
423
- if (!item.def_id || !fs.existsSync(path.join(input.workspaceRoot, ".memory-bank", "defs", `${item.def_id}.md`)))
424
- throw new AppError("deferral_invalid", "A deferred P2 requires a named durable DEF", 2, { finding_ref: findingRef, def_id: item.def_id ?? null });
438
+ reviewDefEvidence(input.workspaceRoot, item.def_id);
425
439
  }
426
440
  if (finding.priority === "p3" && item.disposition === "defer")
427
441
  throw new AppError("deferral_invalid", "P3 observations are not deferred as DEF", 2, { finding_ref: findingRef });
@@ -17,7 +17,7 @@ import { runtimeCommand, checkRetryArgs, recoveryEvidenceInput } from "./runtime
17
17
  import { acceptStageReport, prepareStageReport, publishStageReport } from "./stage-report-renderer.js";
18
18
  import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
19
19
  import { addVnextCodeRepair } from "./vnext-code.js";
20
- import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations, mergeCheckProfileHash, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
20
+ import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations, mergeCheckProfileHash, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
21
21
  import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
22
22
  import { ensureLaneWorkspace, prepareLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
23
23
  import { assertRunMutationAllowed } from "./run-recovery.js";
@@ -64,6 +64,8 @@ export function prepareVnextMergeRequest(context, input) {
64
64
  validateMergeAcceptance(run.workspace_root, protocols, semantic);
65
65
  const acceptanceRefs = mergeAcceptanceRefs(run.workspace_root, protocols);
66
66
  const effective = effectiveCheckDeclarations(targetWorkspace, semantic, ["work", "code", "readiness", "merge"]);
67
+ for (const check of effective)
68
+ validateCheckDeclaration(run.workspace_root, check);
67
69
  if (run.workspace_root !== targetWorkspace && effective.length === 0)
68
70
  throw new AppError("merge_gate_missing", "A real source/target integration requires at least one semantic or policy merge check", 2, { run_id: run.id, protocol_ids: protocols });
69
71
  const ignored = ignoredGitPaths(context, project.id);
@@ -73,6 +75,8 @@ export function prepareVnextMergeRequest(context, input) {
73
75
  const gateFile = path.join(freezeRoot, "merge-gate.json");
74
76
  fs.mkdirSync(freezeRoot, { recursive: true });
75
77
  const profileHash = mergeCheckProfileHash(run.workspace_root, targetWorkspace, effective);
78
+ retainMergeProfile(freezeRoot, "baseline", targetWorkspace);
79
+ retainMergeProfile(freezeRoot, "source", run.workspace_root);
76
80
  freezeMergeGate(gateFile, { runId: run.id, protocols, checks: effective, acceptanceRefs, profileHash, now: context.now() });
77
81
  let sourceCommit;
78
82
  let sourcePaths;
@@ -294,6 +298,9 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
294
298
  const gate = prepared.gate, checks = gate.checks;
295
299
  if (input.retryCheckId)
296
300
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
301
+ for (const check of checks)
302
+ validateCheckDeclaration(request.target_workspace, check);
303
+ retainMergeProfile(path.join(requireHome(run), stageDir), "target", request.target_workspace);
297
304
  const acceptancePath = path.join(requireHome(run), stageDir, "merge-gate-acceptance.json");
298
305
  const recovered = readMergeGateAcceptance(context, acceptancePath, request, gate);
299
306
  const resumingGate = ["checks_passed", "integration_committed", "delivered"].includes(request.checkpoint);
@@ -501,7 +508,7 @@ function mergeReport(context, run, request, semantic, receipts) {
501
508
  }
502
509
  function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
503
510
  function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
504
- return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.availability === "available").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
511
+ throw new AppError("merge_plan_missing", "MERGE requires every accepted PLAN", 2, { protocol_id: protocol }); const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
505
512
  function validateMergeAcceptance(workspace, protocols, checks) { const known = new Set(checks.map((check) => check.canonical_ref)); const missing = mergeAcceptanceRefs(workspace, protocols).filter((ref) => !known.has(ref)); if (missing.length)
506
513
  throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a missing check", 2, { check_refs: missing }); }
507
514
  function freezeMergeGate(file, input) { const canonical = JSON.stringify({ checks: input.checks, acceptance_refs: [...input.acceptanceRefs].sort() }); const hash = cryptoHash(canonical); if (fs.existsSync(file)) {
@@ -519,6 +526,17 @@ function readFrozenMergeGate(file, requestId) { try {
519
526
  catch (cause) {
520
527
  throw new AppError("merge_gate_missing", "MERGE gate is missing or invalid", 1, { merge_request_id: requestId, file, cause: String(cause) });
521
528
  } }
529
+ function retainMergeProfile(root, role, workspace) {
530
+ const { profile, hash, file: profileFile } = readCodeCheckProfile(workspace);
531
+ const file = path.join(root, `check-profile-${role}.json`);
532
+ const bytes = `${JSON.stringify({ schema_id: "dd-flow/retained-check-profile@1", profile, profile_sha256: hash, profile_bytes: profile ? fs.readFileSync(profileFile, "utf8") : null }, null, 2)}\n`;
533
+ if (fs.existsSync(file)) {
534
+ if (fs.readFileSync(file, "utf8") !== bytes)
535
+ throw new AppError("check_profile_drift", "Retained MERGE profile changed", 2, { role, file });
536
+ }
537
+ else
538
+ fs.writeFileSync(file, bytes, { flag: "wx" });
539
+ }
522
540
  function frozenGateRefs(gate) { return [...new Set([...gate.checks.map(check => check.canonical_ref ?? check.id), ...gate.acceptanceRefs])].sort(); }
523
541
  function writeMergeGateAcceptance(context, file, request, gate, acceptedTree, receipts) { const persisted = new Map(checkReceipts(context, { projectId: request.project_id, runId: request.run_id }).map(receipt => [receipt.id, receipt])); const value = { schema_id: "dd-flow/merge-gate-acceptance@2", merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, gate_hash: gate.gateHash, profile_hash: gate.profileHash, accepted_tree: acceptedTree, receipts: receipts.map(receipt => ({ id: receipt.id, execution_refs: [...(persisted.get(receipt.id)?.check_refs ?? receipt.check_refs)].sort(), binding_refs: [...receipt.check_refs].sort(), input_hash: receipt.input_hash })) }; const temporary = `${file}.tmp-${process.pid}-${crypto.randomUUID()}`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
524
542
  function readMergeGateAcceptance(context, file, request, gate) { try {