@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.
- package/CHANGELOG.md +10 -0
- package/README.md +23 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +1 -1
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +66 -16
- package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +39 -10
- package/dist/harness-runtime/lib/dd-codex.mjs +11 -25
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
- package/dist/harness-runtime/lib/dd-zcode.mjs +9 -10
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +100 -32
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/services/code-checks.js +14 -18
- package/dist/services/codex-capacity.js +149 -48
- package/dist/services/controller-fanout.js +1 -0
- package/dist/services/external-work-launch.js +10 -3
- package/dist/services/harness-adapter.js +21 -15
- package/dist/services/managed-processes.js +14 -6
- package/dist/services/merge-server.js +10 -4
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-controller-adapter.js +8 -5
- package/dist/services/run-controller-recovery.js +17 -0
- package/dist/services/run-controller.js +42 -14
- package/dist/services/run-recovery-runtime.js +49 -3
- package/dist/services/runs.js +1 -1
- package/dist/services/verification-matrix.js +30 -1
- package/dist/services/vnext-code-review.js +16 -4
- package/dist/services/vnext-merge.js +20 -2
- package/dist/services/vnext-plan-review.js +81 -18
- package/dist/services/work-registry.js +9 -2
- 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/
|
|
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 = ?
|
|
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
|
-
|
|
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
|
-
&&
|
|
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")
|
|
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 >
|
|
813
|
-
throw new AppError("
|
|
814
|
-
const
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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"
|
package/dist/services/runs.js
CHANGED
|
@@ -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 =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|