@deksden-com/dd-flow-cli 0.9.0-beta.123 → 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 (41) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +23 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/cli/run-cli.js +1 -1
  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 +66 -16
  10. package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
  11. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +39 -10
  12. package/dist/harness-runtime/lib/dd-codex.mjs +11 -25
  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 +9 -10
  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 -32
  21. package/dist/schemas/flow-run.schema.json +1 -0
  22. package/dist/schemas/plan-review-decision.schema.json +3 -3
  23. package/dist/services/code-checks.js +14 -18
  24. package/dist/services/codex-capacity.js +149 -48
  25. package/dist/services/controller-fanout.js +1 -0
  26. package/dist/services/external-work-launch.js +10 -3
  27. package/dist/services/harness-adapter.js +21 -15
  28. package/dist/services/managed-processes.js +14 -6
  29. package/dist/services/merge-server.js +10 -4
  30. package/dist/services/review-deferral.js +23 -0
  31. package/dist/services/run-controller-adapter.js +8 -5
  32. package/dist/services/run-controller-recovery.js +17 -0
  33. package/dist/services/run-controller.js +42 -14
  34. package/dist/services/run-recovery-runtime.js +49 -3
  35. package/dist/services/runs.js +1 -1
  36. package/dist/services/verification-matrix.js +30 -1
  37. package/dist/services/vnext-code-review.js +16 -4
  38. package/dist/services/vnext-merge.js +20 -2
  39. package/dist/services/vnext-plan-review.js +81 -18
  40. package/dist/services/work-registry.js +9 -2
  41. package/package.json +1 -1
@@ -35,7 +35,7 @@ import { resumeControllerSession, failControllerRecovery } from "./run-controlle
35
35
  import { captureControllerBoundary } from "./run-controller-capture.js";
36
36
  import { requestRunControl } from "./run-control-worker.js";
37
37
  import { storageSessionId } from "./session-identity.js";
38
- import { terminalCodexOverload } from "../harness-runtime/lib/dd-codex.mjs";
38
+ import { CAPACITY_POLICY, nativeItemsSettled, overloadBurst, refusalObservation, terminalCodexOverload } from "../harness-runtime/lib/codex-capacity-policy.mjs";
39
39
  export { resumeRunController } from "./run-controller-recovery.js";
40
40
  export function prepareRunControllerLaunch(context, input) {
41
41
  if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(input.requestId) || !/^[a-f0-9]{64}$/.test(input.contextSha256))
@@ -561,8 +561,15 @@ async function executeController(context, row, manifest, state, assertOwnership,
561
561
  const inspect = (args, id) => runHarnessAdapter({ executable: manifest.adapters["codex-desktop"].executable,
562
562
  args, env: controllerEnvironment(context, manifest, session), runtimeOwner: session.owner, operationId: id, timeoutMs: 30_000, signal: cancellation.signal });
563
563
  const resumed = await continueCodexCapacity({ error, sessionId: session.id, operationId: firstOperationId, signal: cancellation.signal, allowSettledDescendants: true,
564
- retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ? AND operation_id IN (?, ?)", [row.controller_id, row.generation, firstOperationId, `${firstOperationId}:capacity:1:session.prompt`])
565
- .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
564
+ retainedIntents: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
565
+ .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_intent?.root_operation_id === firstOperationId ? [retained.capacity_intent] : []; }).sort((a, b) => a.ordinal - b.ordinal),
566
+ retainedFailures: () => context.db.all("SELECT receipt_json FROM run_controller_operations WHERE controller_id = ? AND generation = ?", [row.controller_id, row.generation])
567
+ .flatMap(item => { const retained = JSON.parse(item.receipt_json || "null"); return retained?.capacity_root === firstOperationId && retained.capacity_failure ? [retained.capacity_failure] : []; }),
568
+ retainFailure: (id, refusal) => {
569
+ const changed = context.db.run("UPDATE run_controller_operations SET receipt_json = ?, updated_at = ? WHERE operation_id = ? AND controller_id = ? AND generation = ? AND status = 'outcome_unknown'", [JSON.stringify({ capacity_root: firstOperationId, capacity_failure: refusal }), context.now(), id, row.controller_id, row.generation]);
570
+ if (changed.changes !== 1)
571
+ throw new AppError("capacity_continuation_blocked", "Failed Turn owner changed before its refusal was retained", 1);
572
+ },
566
573
  inspectOperation: id => inspect(["daemon", "operation", "--state-dir", session.state_dir, "--operation-id", id, "--json"], `${id}:capacity-observe`),
567
574
  inspectSession: turnId => inspect(["session", "inspect", "--session-id", session.id, ...(turnId ? ["--turn-id", turnId] : []), ...sessionArguments(context, manifest, session)], `${firstOperationId}:capacity-inspect`),
568
575
  assertCurrent: () => {
@@ -601,7 +608,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
601
608
  return next.kind === "wait" ? "reconciliation_blocked" : "continue";
602
609
  },
603
610
  retainIntent: intent => context.db.writeTransaction(() => {
604
- const changed = 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: intent }), context.now(), intent.predecessor, row.controller_id, row.generation]);
611
+ const changed = 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_root: firstOperationId, capacity_failure: intent.refusal, capacity_intent: intent }), context.now(), intent.predecessor, row.controller_id, row.generation]);
605
612
  if (changed.changes !== 1)
606
613
  throw new AppError("capacity_continuation_blocked", "Failed native operation changed before continuation", 1);
607
614
  appendEvent(context, row.controller_id, "capacity_continuation", intent);
@@ -768,6 +775,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
768
775
  }
769
776
  state.native_baseline = controllerNativeChildren(state.last_receipt ?? {}, session.id).map(child => child.session_id);
770
777
  state.stage = next.stage;
778
+ state.child_capacity = {};
771
779
  saveState(context, row, state);
772
780
  setStatus("running");
773
781
  if (next.stage === "code" || next.stage === "code-review") {
@@ -794,29 +802,49 @@ async function executeController(context, row, manifest, state, assertOwnership,
794
802
  saveState(context, row, state);
795
803
  children = controllerNativeChildren(inspected, session.id).filter(child => !baseline.has(child.session_id));
796
804
  const failed = children.filter(child => child.status === "failed");
797
- const child = failed.length === 1 && children.every(item => item === failed[0] || item.status === "completed") ? failed[0] : undefined;
805
+ // Continue one existing child at a time; another failed child does not
806
+ // create a second fanout wave or share this child's refusal counter.
807
+ const child = children.every(item => item.status === "failed" || item.status === "completed") ? failed.find(item => item.parent_session_id === session.id && item.terminal_turn_id && nativeItemsSettled(item.native_turn_items) && terminalCodexOverload({ code: "turn_interrupted", details: { provider_session_id: item.session_id, turn_id: item.terminal_turn_id, native_turn_id: item.terminal_turn_id, terminal_status: "failed", provider_error: item.provider_error } }, item.session_id)) : undefined;
798
808
  const turnId = child?.terminal_turn_id;
799
809
  const native = child && turnId ? { code: "turn_interrupted", details: { provider_session_id: child.session_id, turn_id: turnId, native_turn_id: turnId,
800
- terminal_status: "failed", provider_error: child.provider_error } } : null;
810
+ terminal_status: "failed", provider_error: child.provider_error, observed_at: child.terminal_observed_at } } : null;
801
811
  const prior = child && state.child_capacity?.[child.session_id];
802
812
  const links = child ? context.db.all("SELECT w.status FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ?", [row.project_id, row.run_id, storageSessionId({ harness_id: "codex-desktop", session_id: child.session_id })]) : [];
803
813
  if (child && turnId && native && terminalCodexOverload(native, child.session_id, turnId)
804
- && child.native_turn_items?.observed === true && (child.native_turn_items.possible_effects === false || child.native_turn_items.pending === false)
814
+ && nativeItemsSettled(child.native_turn_items)
805
815
  && inspected.settled === true && child.parent_session_id === session.id && links.length <= 1
806
- && (!links.length || links[0].status === "running") && ((prior?.attempts ?? 0) < 2 || prior?.turn_id === turnId && prior.dispatched === false)) {
816
+ && (!links.length || links[0].status === "running")) {
807
817
  const prepared = prior?.turn_id === turnId && prior.dispatched === false && prior.operation_id && prior.not_before ? prior : null;
808
818
  if (prior?.turn_id === turnId && !prepared)
809
819
  throw new AppError("capacity_child_followup_unconfirmed", "Parent continuation did not start a new native child Turn", 1, { child_session_id: child.session_id, turn_id: turnId });
810
820
  const attempt = prepared ? prepared.attempts : (prior?.attempts ?? 0) + 1;
821
+ if (!Number.isSafeInteger(attempt) || prior && prior.policy !== CAPACITY_POLICY)
822
+ throw new AppError("capacity_continuation_conflict", "Native child continuation policy or ordinal is incompatible", 1);
823
+ const refusals = [...(prior?.refusals ?? [])];
824
+ if (!refusals.some(item => item.turn_id === turnId)) {
825
+ const refusal = refusalObservation(native, attempt - 1, child.terminal_observed_at ?? context.now());
826
+ refusals.push(refusal);
827
+ state.child_capacity ??= {};
828
+ state.child_capacity[child.session_id] = { ...prior, turn_id: prior?.turn_id ?? turnId, attempts: prior?.attempts ?? 0, policy: CAPACITY_POLICY, refusals };
829
+ saveState(context, row, state);
830
+ }
831
+ const burst = overloadBurst(refusals);
832
+ if (burst)
833
+ throw new AppError("provider_overload_burst", "Native child had two continuation refusals within two minutes", 1, { ...burst, child_session_id: child.session_id, primary_error: native });
811
834
  const wait = prepared ? Math.max(0, Date.parse(prepared.not_before) - Date.now()) : capacityDelay(native, attempt);
812
- if (!Number.isFinite(wait) || wait > 30_000)
813
- throw new AppError("capacity_child_retry_delay_exceeded", "Native child retry delay exceeds the bounded continuation window", 1, { child_session_id: child.session_id, delay_ms: wait });
814
- const operationId = prepared?.operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}:followup:${attempt}`;
835
+ if (!Number.isFinite(wait) || Date.now() + wait > 8.64e15)
836
+ throw new AppError("capacity_retry_metadata_invalid", "Native child retry time is invalid", 1, { child_session_id: child.session_id, delay_ms: wait });
837
+ const rootOperationId = prior?.root_operation_id ?? `${row.controller_id}:child:${digest([child.session_id, turnId]).slice(0, 16)}`;
838
+ const operationId = prepared?.operation_id ?? `${rootOperationId}:followup:${attempt}`;
815
839
  state.child_capacity ??= {};
816
- state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false };
840
+ state.child_capacity[child.session_id] = { turn_id: turnId, attempts: attempt, operation_id: operationId, root_operation_id: rootOperationId, policy: CAPACITY_POLICY, refusals, not_before: prepared?.not_before ?? new Date(Date.now() + wait).toISOString(), dispatched: false };
817
841
  saveState(context, row, state);
818
842
  appendEvent(context, row.controller_id, "capacity_child_followup", { child_session_id: child.session_id, turn_id: turnId, attempt, operation_id: operationId });
819
- await delay(wait, undefined, { signal: cancellation.signal });
843
+ const until = performance.now() + wait;
844
+ while (performance.now() < until) {
845
+ await delay(Math.min(250, Math.max(0, until - performance.now())), undefined, { signal: cancellation.signal });
846
+ assertOwner();
847
+ }
820
848
  assertOwner();
821
849
  const current = getFlowRunStatus(executionContext, { projectRoot: manifest.request.project_root, runId: row.run_id });
822
850
  if (current.continuation.stage !== state.stage || ["terminal", "paused", "blocked", "wait"].includes(current.continuation.kind))
@@ -830,7 +858,7 @@ async function executeController(context, row, manifest, state, assertOwnership,
830
858
  continue;
831
859
  }
832
860
  if (fresh.settled !== true || !still || still.parent_session_id !== session.id || still.status !== "failed" || still.terminal_turn_id !== turnId
833
- || still.native_turn_items?.observed !== true || still.native_turn_items.possible_effects !== false && still.native_turn_items.pending !== false || currentLinks.length > 1
861
+ || !nativeItemsSettled(still.native_turn_items) || currentLinks.length > 1
834
862
  || currentLinks.some(link => link.status !== "running"))
835
863
  throw new AppError("capacity_child_followup_blocked", "Native child or Work changed during capacity wait", 1, { child_session_id: child.session_id, turn_id: turnId });
836
864
  assertOwner();
@@ -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
@@ -7,6 +7,7 @@ import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations }
7
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();
@@ -178,6 +179,10 @@ export function prepareRunVerificationMatrix(context, input) {
178
179
  let finalBindings;
179
180
  if (input.stage === "merge") {
180
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
+ }
181
186
  const semanticRefs = new Set(plans.flatMap(plan => plan.acceptance.flatMap(entry => entry.check_refs.map(id => `${plan.protocol_id}/${id}`))));
182
187
  policyChecks = gate.checks.filter(check => !semanticRefs.has(check.canonical_ref ?? check.id));
183
188
  if (final) {
@@ -224,6 +229,20 @@ export function prepareRunVerificationMatrix(context, input) {
224
229
  return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: "unavailable", reason: "receipt_file_missing" };
225
230
  const bytes = fs.readFileSync(receiptFile);
226
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
+ }
227
246
  return { id: check.id, canonical_ref: ref, declaration: check, result: portable, disposition: valid ? "retained" : "unavailable", ...(!valid ? { reason: "receipt_evidence_invalid" } : {}) };
228
247
  });
229
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 => {
@@ -238,6 +257,8 @@ export function prepareRunVerificationMatrix(context, input) {
238
257
  const markdown = prepareStagePacket(safePath(home, `${root}/verification-matrix.md`), Buffer.from(renderVerificationMatrix(matrix)));
239
258
  packets.push(json, markdown);
240
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;
241
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 } : {}) }));
242
263
  return { projectId: input.projectId, runId: input.runId, stage: input.stage, home, attempt, generation, packets, matrix, metadata, predecessors };
243
264
  }
@@ -297,6 +318,8 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
297
318
  const md = { file: mdFile, bytes: mdBytes, sha256: paired.sha256 };
298
319
  const json = { file: packet.file, bytes, sha256: packet.sha256 };
299
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 } };
321
+ if (index.final_check_coverage_contract === finalCheckCoverageContract)
322
+ metadata.coverage_contract = finalCheckCoverageContract;
300
323
  const sourcePackets = matrix.sources.filter(source => (source.role.startsWith("plan:") || source.role === "code_work_batch") && source.root === "run").map(source => {
301
324
  const file = safePath(run.run_root, source.path, false);
302
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);
@@ -314,7 +337,13 @@ export function latestRunVerificationMatrix(context, projectId, runId, stage, ro
314
337
  }
315
338
  function portableReceipt(receipt, home) {
316
339
  const relative = (file) => slash(path.relative(home, safePath(home, path.relative(home, file))));
317
- 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
+ }) };
318
347
  }
319
348
  function safePath(root, relative, inspectFilesystem = true) {
320
349
  if (path.isAbsolute(relative) || relative.split(/[\\/]/).includes(".."))
@@ -26,6 +26,7 @@ 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
28
  import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
29
+ import { reviewDefEvidence } from "./review-deferral.js";
29
30
  const stage = "code-review";
30
31
  const stageDir = vnextStageDirectory(stage);
31
32
  const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
@@ -196,6 +197,7 @@ export async function finishVnextCodeReview(context, input) {
196
197
  validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
197
198
  const cycle = reviewContext.cycle ?? "initial";
198
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));
199
201
  if (mode !== "off") {
200
202
  assertCodeReviewCoverage(reviewGroups(home), reviewers);
201
203
  const incomplete = reviewers.filter((work) => work.status !== "completed");
@@ -204,6 +206,8 @@ export async function finishVnextCodeReview(context, input) {
204
206
  for (const work of reviewers)
205
207
  validateReviewerResult(context, work, run);
206
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);
207
211
  const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
208
212
  let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
209
213
  const frozenShaFile = path.join(root, ".decision-sha256");
@@ -215,7 +219,7 @@ export async function finishVnextCodeReview(context, input) {
215
219
  // Accept the value once. Repairs consume this decision, not a mutable
216
220
  // operator file. Reuse the existing context artifact and review cycle.
217
221
  const contextFile = path.join(root, "review-context.json"), temporary = `${contextFile}.tmp-${crypto.randomUUID()}`;
218
- 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`);
219
223
  fs.renameSync(temporary, contextFile);
220
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(", ")}` });
221
225
  const retained = latestRunVerificationMatrix(context, project.id, run.id, stage, "input");
@@ -263,6 +267,9 @@ export async function finishVnextCodeReview(context, input) {
263
267
  const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
264
268
  if (matrix)
265
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 });
266
273
  const preparedReport = prepareStageReport(report);
267
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 };
268
275
  withStageSettlement(context, () => {
@@ -356,7 +363,10 @@ export function assertCodeReviewCoverage(groups, reviewers) {
356
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.`; }
357
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"); }
358
365
  function canonicalReviewEvidence(decision, reviewers) {
359
- 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 });
360
370
  const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
361
371
  const duplicates = new Set();
362
372
  for (const item of decision.findings.filter((entry) => entry.disposition === "duplicate")) {
@@ -372,6 +382,9 @@ function canonicalReviewEvidence(decision, reviewers) {
372
382
  }
373
383
  if (!cursor)
374
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
+ }
375
388
  duplicates.add(item.finding_ref);
376
389
  }
377
390
  return {
@@ -422,8 +435,7 @@ function validateDecision(context, input) {
422
435
  if (item.disposition === "fix" && !repaired.some((work) => requiredChecks.every((ref) => reviewCheckRefs(work).includes(ref))))
423
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 });
424
437
  if (finding.priority === "p2" && item.disposition === "defer") {
425
- if (!item.def_id || !fs.existsSync(path.join(input.workspaceRoot, ".memory-bank", "defs", `${item.def_id}.md`)))
426
- 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);
427
439
  }
428
440
  if (finding.priority === "p3" && item.disposition === "defer")
429
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 {