@deksden-com/dd-flow-cli 0.8.0 → 0.9.0-beta.0
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 +16 -0
- package/README.md +30 -3
- package/dist/build-info.json +4 -4
- package/dist/cli/help.js +13 -61
- package/dist/cli/run-cli.js +55 -24
- package/dist/domain/stage-catalog.js +2 -2
- package/dist/schemas/agent-profile.schema.json +17 -0
- package/dist/schemas/code-review-decision.schema.json +5 -3
- package/dist/schemas/merge-result.schema.json +15 -0
- package/dist/schemas/vnext-protocol-plan.schema.json +1 -1
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/code-checks.js +125 -208
- package/dist/services/eval-snapshots.js +10 -1
- package/dist/services/harness-adapter.js +59 -0
- package/dist/services/hooks.js +87 -237
- package/dist/services/ids.js +18 -1
- package/dist/services/lifecycle-command.js +288 -0
- package/dist/services/merge-server.js +124 -0
- package/dist/services/runs.js +7 -2
- package/dist/services/sessions.js +18 -27
- package/dist/services/stage-pause.js +13 -0
- package/dist/services/vnext-code-review.js +70 -16
- package/dist/services/vnext-code.js +45 -18
- package/dist/services/vnext-execution-profile.js +6 -3
- package/dist/services/vnext-merge.js +330 -0
- package/dist/services/vnext-plan.js +13 -5
- package/dist/services/vnext-specify.js +5 -2
- package/dist/services/vnext-workspace-policy.js +8 -2
- package/dist/services/work-registry.js +35 -7
- package/dist/storage/database.js +66 -0
- package/package.json +2 -1
|
@@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { AppError } from "../shared/errors.js";
|
|
6
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
7
7
|
import { requireProjectByRoot } from "./projects.js";
|
|
8
|
-
import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
|
|
8
|
+
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
|
|
9
9
|
import { validateSchema } from "./schema-validation.js";
|
|
10
10
|
import { flowCommand } from "./stage-pause.js";
|
|
11
11
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
@@ -17,6 +17,7 @@ import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
|
17
17
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
18
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
19
19
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
20
|
+
import { ensureVnextMergeRequest } from "./vnext-merge.js";
|
|
20
21
|
const stage = "code-review";
|
|
21
22
|
const stageDir = vnextStageDirectory(stage);
|
|
22
23
|
const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
|
|
@@ -84,11 +85,24 @@ export function addVnextCodeReviewRepair(context, input) {
|
|
|
84
85
|
const origins = codeWorks(context, project.id, run.id).map((work) => work.work_id);
|
|
85
86
|
if (!origins.length)
|
|
86
87
|
throw new AppError("runtime_missing", "CODE-REVIEW repair requires completed CODE Work", 1);
|
|
88
|
+
const checks = new Map(codeWorks(context, project.id, run.id).flatMap((work) => {
|
|
89
|
+
const payload = readJsonString(work.payload_json);
|
|
90
|
+
return Array.isArray(payload.checks) ? payload.checks : [];
|
|
91
|
+
}).map((check) => [check.id, check]));
|
|
92
|
+
const requestedCheckRefs = [...new Set(selected.flatMap(({ finding_ref }) => input.checkRefsByFinding[finding_ref] ?? []))];
|
|
93
|
+
if (!requestedCheckRefs.length)
|
|
94
|
+
throw new AppError("review_repair_check_required", "Each accepted CODE-REVIEW repair needs an explicit causal CHK-* check reference", 2, { finding_ids: input.findingIds });
|
|
95
|
+
const unknown = requestedCheckRefs.filter((ref) => !checks.has(ref));
|
|
96
|
+
if (unknown.length)
|
|
97
|
+
throw new AppError("review_check_reference_unknown", "CODE-REVIEW finding cites a check absent from the accepted CODE handoff", 2, { check_refs: unknown });
|
|
98
|
+
const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref)).filter((check) => check.run_at !== "external");
|
|
87
99
|
return addVnextCodeRepair(context, {
|
|
88
100
|
projectRoot,
|
|
89
101
|
runId: run.id,
|
|
90
102
|
reviewFindingIds: selected.map((finding) => finding.finding_ref),
|
|
91
103
|
reviewEvidenceRefs: selected.flatMap(({ finding }) => finding.evidence_refs),
|
|
104
|
+
reviewChecks,
|
|
105
|
+
reviewCheckRefs: requestedCheckRefs,
|
|
92
106
|
originWorkIds: origins,
|
|
93
107
|
objective: input.objective
|
|
94
108
|
});
|
|
@@ -101,15 +115,17 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
101
115
|
const root = path.join(home, stageDir);
|
|
102
116
|
const mode = effectiveMode(run, changedPaths(run.workspace_root));
|
|
103
117
|
const reportPath = path.join(root, "stage-report.json");
|
|
104
|
-
if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath))
|
|
105
|
-
|
|
118
|
+
if (stageStatus(run, stage) === "done" && fs.existsSync(reportPath)) {
|
|
119
|
+
const report = readJson(reportPath);
|
|
120
|
+
return { ok: true, resumed: true, run_id: run.id, stage, outcome: "accepted", report_path: reportPath, next_action: report.semantic?.next_action ?? "code_review_completed" };
|
|
121
|
+
}
|
|
106
122
|
const rootWork = requireRootWork(context, project.id, run.id);
|
|
107
123
|
const coordinator = context.db.get("SELECT session_id, result_path FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [rootWork.work_id]);
|
|
108
124
|
if (!coordinator || path.resolve(coordinator.result_path ?? "") !== path.resolve(reportPath))
|
|
109
125
|
throw new AppError("code_review_session_binding_conflict", "CODE-REVIEW can finish only after its successful stage start bound the coordinator Session and report path", 1, { run_id: run.id, expected_result_path: reportPath, actual_result_path: coordinator?.result_path ?? null });
|
|
110
126
|
const decisionFile = input.decisionFile ?? path.join(root, "decision.json");
|
|
111
127
|
if (!fs.existsSync(decisionFile) && mode === "off")
|
|
112
|
-
fs.writeFileSync(decisionFile, `${JSON.stringify({ schema_id: "dd-flow/code-review-decision@
|
|
128
|
+
fs.writeFileSync(decisionFile, `${JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] }, null, 2)}\n`);
|
|
113
129
|
validateSchema({ schemaName: "code-review-decision", file: decisionFile, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id });
|
|
114
130
|
const decision = readJson(decisionFile);
|
|
115
131
|
const reviewers = reviewerWorks(context, project.id, run.id);
|
|
@@ -120,6 +136,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
120
136
|
for (const work of reviewers)
|
|
121
137
|
validateReviewerResult(context, work, run);
|
|
122
138
|
const canonical = reviewDecisionForRepair(decision, reviewers);
|
|
139
|
+
const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
|
|
123
140
|
let repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
124
141
|
const decisionSha = sha256File(decisionFile);
|
|
125
142
|
const frozenShaFile = path.join(root, ".decision-sha256");
|
|
@@ -127,8 +144,8 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
127
144
|
throw new AppError("review_decision_changed", "The accepted CODE-REVIEW decision cannot change during repair", 2, { decision_file: decisionFile });
|
|
128
145
|
const fixIds = canonical.fix_ids;
|
|
129
146
|
if (fixIds.length && repairs.length === 0) {
|
|
147
|
+
const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
|
|
130
148
|
fs.writeFileSync(frozenShaFile, `${decisionSha}\n`);
|
|
131
|
-
const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
|
|
132
149
|
return { ok: true, run_id: run.id, stage, outcome: "repair_required", decision_sha256: decisionSha, repair, instruction: "Run the returned repair Work in one fresh child session. When it completes, invoke the same stage finish command again with the unchanged decision file.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
|
|
133
150
|
}
|
|
134
151
|
repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
@@ -144,20 +161,33 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
144
161
|
if (unchangedFailures.length)
|
|
145
162
|
throw new AppError("code_review_gate_repair_required", "CODE-REVIEW final gate already failed for the unchanged workspace; repair the evidenced failure before retrying", 2, { outcome: "repair_required", retry_after_workspace_change: true, workspace_fingerprint: unchangedFailures[0].workspace_fingerprint, failures: unchangedFailures });
|
|
146
163
|
const receipts = repairs.length ? await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, artifactDir: stageDir, scope: "aggregate", checks: finalChecks, ...(input.progress ? { progress: input.progress } : {}) }) : [];
|
|
147
|
-
const failed = receipts.filter((receipt) => receipt.status
|
|
164
|
+
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
148
165
|
if (failed.length)
|
|
149
|
-
throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
|
|
166
|
+
throw new AppError("code_review_gate_failed", "CODE-REVIEW repair changed the project but the aggregate gate failed", 2, {
|
|
167
|
+
failures: failed,
|
|
168
|
+
outcome: "repair_required",
|
|
169
|
+
retry_after_workspace_change: true,
|
|
170
|
+
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
171
|
+
repair_command: `${flowCommand(context)} work repair add --run ${run.id} --from-check ${failed[0].id} --origin-work <WORK-ID> --task-stdin --project-root ${JSON.stringify(projectRoot)} --json`
|
|
172
|
+
});
|
|
173
|
+
const stopTarget = executionStopTarget(run);
|
|
174
|
+
const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
|
|
150
175
|
const now = context.now();
|
|
151
176
|
const startedAt = stageStartedAt(run, now);
|
|
152
|
-
writeProtocolFlowStatus(run.workspace_root, home, run.id, "CODE-REVIEW complete; this RUN reached its configured terminal boundary.");
|
|
153
|
-
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result:
|
|
177
|
+
writeProtocolFlowStatus(run.workspace_root, home, run.id, nextAction === "start_merge" ? "CODE-REVIEW complete; MERGE is queued." : "CODE-REVIEW complete; this RUN reached its configured terminal boundary.");
|
|
178
|
+
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result: `Independent CODE review completed: ${reviewers.length} reviewer Work(s), ${repairs.length} repair Work(s).`, acceptance: ["all_selected_review_groups_completed", "all_material_findings_classified", ...(repairs.length ? ["all_accepted_repairs_completed", "post_repair_final_gate_passed"] : [])], run_changed_files: changedPaths(run.workspace_root), checks: receipts.map((receipt) => receipt.command), evidence: [...reviewers.flatMap((work) => readJsonString(work.result).findings.flatMap((finding) => finding.evidence_refs)), ...receipts.map((receipt) => runRef(run.id, home, receipt.receipt_path))], next_action: nextAction, code_review: { mode, reviewer_work_ids: reviewers.map((work) => work.work_id), repair_work_ids: repairs.map((work) => work.work_id), decision } }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command: `${flowCommand(context)} stat run sessions ls --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json`, usage_stats_command: `${flowCommand(context)} stat usage --run ${run.id} --project-root ${JSON.stringify(projectRoot)} --json` }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } };
|
|
154
179
|
writeReport(root, report);
|
|
155
180
|
completeFlowRunStage(context, { 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" });
|
|
156
|
-
await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
157
181
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
|
|
158
|
-
|
|
182
|
+
if (nextAction === "start_merge")
|
|
183
|
+
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
|
|
184
|
+
else {
|
|
185
|
+
await finishWork(context, rootWork.work_id, JSON.stringify(report, null, 2));
|
|
186
|
+
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_review_completed", nextAction: undefined });
|
|
187
|
+
}
|
|
159
188
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
160
|
-
|
|
189
|
+
const mergeRequest = nextAction === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id }) : null;
|
|
190
|
+
return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } } : {}) };
|
|
161
191
|
}
|
|
162
192
|
function reviewGroups(run, home, mode) {
|
|
163
193
|
const aspectMapFiles = findFiles(path.join(home, "03-plan"), "aspect-map.json");
|
|
@@ -173,7 +203,7 @@ function reviewGroups(run, home, mode) {
|
|
|
173
203
|
return Array.from({ length: Math.ceil(all.length / chunk) }, (_, index) => ({ key: `group-${index + 1}`, aspect_ids: all.slice(index * chunk, (index + 1) * chunk) }));
|
|
174
204
|
}
|
|
175
205
|
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")}. 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. Finding ids must use the unique prefix FIND-${group.key}-. Return dd-flow/code-review-result@1.`; }
|
|
176
|
-
function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); 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_home_path, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Each reviewer Work must run in one fresh child session. The coordinator must never claim a reviewer Work itself. Start each ready Work with its exact start_command from the work graph; it receives the task and result schema. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Finish only after every reviewer Work settles and you have written ${decision}: ${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. The first Finish freezes this decision and creates one repair Work when needed. Run it, then call the same Finish command again. Review is not repeated.", "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@
|
|
206
|
+
function orchestratorPrompt(context, input) { const template = read(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md")); const decision = path.join(input.root, "decision.json"); const checks = acceptedCodeChecks(context, input.run.project_id, input.run.id).map(({ id, purpose }) => ({ id, purpose })); 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_home_path, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Each reviewer Work must run in one fresh child session. The coordinator must never claim a reviewer Work itself. Start each ready Work with its exact start_command from the work graph; it receives the task and result schema. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", `Ready reviewer Works: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`, `Finish only after every reviewer Work settles and you have written ${decision}: ${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. Run it, then call the same Finish command again. Review is not repeated.", "If the post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then stop so the runner can dispatch the new repair. Do not 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"); }
|
|
177
207
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
178
208
|
const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
|
|
179
209
|
const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
|
|
@@ -210,6 +240,8 @@ export function reviewDecisionForRepair(decision, reviewers) {
|
|
|
210
240
|
if (["p0", "p1"].includes(finding.priority) && item.disposition !== "fix") {
|
|
211
241
|
throw new AppError("review_material_disposition_invalid", "P0/P1 CODE-REVIEW findings must be fixed, not rejected or deferred", 2, { finding_ref: findingRef, disposition: item.disposition });
|
|
212
242
|
}
|
|
243
|
+
if (item.disposition === "fix" && !item.check_refs?.length)
|
|
244
|
+
throw new AppError("review_repair_check_required", "Each accepted CODE-REVIEW repair needs an explicit causal CHK-* check reference", 2, { finding_ref: findingRef });
|
|
213
245
|
}
|
|
214
246
|
return { ...canonical, fix_ids: canonical.decision.findings.filter((item) => item.disposition === "fix").map((item) => item.finding_ref) };
|
|
215
247
|
}
|
|
@@ -217,15 +249,24 @@ function validateDecision(context, input) {
|
|
|
217
249
|
const findings = canonicalCodeFindings(input.reviewers);
|
|
218
250
|
const known = new Map(findings.map((item) => [item.finding_ref, item.finding]));
|
|
219
251
|
const decisions = new Map(input.decision.findings.map((item) => [item.finding_ref, item]));
|
|
220
|
-
const
|
|
252
|
+
const repairedBy = new Map();
|
|
253
|
+
for (const work of input.repairs) {
|
|
254
|
+
const result = readJsonString(work.result);
|
|
255
|
+
for (const ref of Array.isArray(result.resolved_finding_refs) ? result.resolved_finding_refs.filter((value) => typeof value === "string") : [])
|
|
256
|
+
repairedBy.set(ref, [...(repairedBy.get(ref) ?? []), work]);
|
|
257
|
+
}
|
|
221
258
|
for (const { finding_ref: findingRef, finding } of findings) {
|
|
222
259
|
const item = decisions.get(findingRef);
|
|
223
260
|
if (!item)
|
|
224
261
|
throw new AppError("review_evidence_invalid", "Every material CODE review finding needs one decision", 2, { finding_ref: findingRef });
|
|
225
|
-
|
|
262
|
+
const repaired = repairedBy.get(findingRef) ?? [];
|
|
263
|
+
if (["p0", "p1"].includes(finding.priority) && !(item.disposition === "fix" && repaired.length))
|
|
226
264
|
throw new AppError("review_unresolved_material", "P0/P1 findings must be explicitly resolved by a completed clean repair Work", 2, { finding_ref: findingRef });
|
|
227
|
-
if (item.disposition === "fix" && !repaired.
|
|
265
|
+
if (item.disposition === "fix" && !repaired.length)
|
|
228
266
|
throw new AppError("review_repair_missing", "A fixed CODE-REVIEW finding needs a completed clean repair Work that explicitly resolves it", 2, { finding_ref: findingRef });
|
|
267
|
+
const requiredChecks = item.check_refs ?? [];
|
|
268
|
+
if (item.disposition === "fix" && !repaired.some((work) => requiredChecks.every((ref) => reviewCheckRefs(work).includes(ref))))
|
|
269
|
+
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 });
|
|
229
270
|
if (finding.priority === "p2" && item.disposition === "defer") {
|
|
230
271
|
if (!item.def_id || !fs.existsSync(path.join(input.workspaceRoot, ".memory-bank", "defs", `${item.def_id}.md`)))
|
|
231
272
|
throw new AppError("deferral_invalid", "A deferred P2 requires a named durable DEF", 2, { finding_ref: findingRef, def_id: item.def_id ?? null });
|
|
@@ -241,6 +282,16 @@ function validateReviewerResult(context, work, run) { if (!work.result)
|
|
|
241
282
|
throw new AppError("review_evidence_invalid", "Reviewer Work has no result", 2, { work_id: work.work_id }); const file = path.join(requireHome(run), stageDir, `${work.work_id}.result.json`); fs.writeFileSync(file, work.result); validateSchema({ schemaName: "code-review-result", file, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id }); }
|
|
242
283
|
function reviewerWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const p = readJsonString(work.payload_json); return Boolean(p && p.kind === "code-review"); }); }
|
|
243
284
|
function codeWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).filter((work) => readJsonString(work.payload_json).schema_id === "dd-flow/code-work-packet@5" && !reviewFindingIds(work).length); }
|
|
285
|
+
function acceptedCodeChecks(context, projectId, runId) { return codeWorks(context, projectId, runId).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }); }
|
|
286
|
+
function validateRepairChecks(context, input) { const known = new Set(acceptedCodeChecks(context, input.projectId, input.runId).map((check) => check.id)); const selected = {}; for (const item of input.decision.findings.filter((item) => item.disposition === "fix")) {
|
|
287
|
+
const refs = [...new Set(item.check_refs ?? [])];
|
|
288
|
+
if (!refs.length)
|
|
289
|
+
throw new AppError("review_repair_check_required", "Each accepted CODE-REVIEW repair needs an explicit causal CHK-* check reference", 2, { finding_ref: item.finding_ref });
|
|
290
|
+
const unknown = refs.filter((ref) => !known.has(ref));
|
|
291
|
+
if (unknown.length)
|
|
292
|
+
throw new AppError("review_check_reference_unknown", "CODE-REVIEW repair selects a check absent from the accepted CODE handoff", 2, { finding_ref: item.finding_ref, check_refs: unknown });
|
|
293
|
+
selected[item.finding_ref] = refs;
|
|
294
|
+
} return selected; }
|
|
244
295
|
function reviewRepairWorks(context, projectId, runId, root) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => isCodeReviewStageRepair(work, root)); }
|
|
245
296
|
export function isCodeReviewStageRepair(work, root) {
|
|
246
297
|
if (reviewFindingIds(work).length)
|
|
@@ -256,8 +307,11 @@ export function isCodeReviewStageRepair(work, root) {
|
|
|
256
307
|
}
|
|
257
308
|
function reviewFindingIds(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
258
309
|
return []; const ids = repair.review_finding_ids; return Array.isArray(ids) ? ids.filter((value) => typeof value === "string") : []; }
|
|
310
|
+
function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
311
|
+
return []; const refs = repair.review_check_refs; return Array.isArray(refs) ? refs.filter((value) => typeof value === "string") : []; }
|
|
259
312
|
function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
|
|
260
313
|
function effectiveMode(run, paths) { const index = JSON.parse(run.index_json); const requested = index.settings?.code_review?.mode ?? "auto"; return requested === "auto" ? (paths.length ? "standard" : "off") : requested; }
|
|
314
|
+
function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
|
|
261
315
|
function changedPaths(workspaceRoot) { try {
|
|
262
316
|
return execFileSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: workspaceRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split(/\r?\n/).filter(Boolean).map((line) => line.slice(3).replace(/^.* -> /, ""));
|
|
263
317
|
}
|
|
@@ -15,6 +15,7 @@ import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
|
15
15
|
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
|
|
16
16
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
17
17
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
18
|
+
import { ensureVnextMergeRequest } from "./vnext-merge.js";
|
|
18
19
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
19
20
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
20
21
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
@@ -161,7 +162,7 @@ export async function finishVnextCode(context, input) {
|
|
|
161
162
|
release();
|
|
162
163
|
}
|
|
163
164
|
}
|
|
164
|
-
const failed = receipts.filter((receipt) => receipt.status
|
|
165
|
+
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
165
166
|
if (failed.length) {
|
|
166
167
|
throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
|
|
167
168
|
failures: failed,
|
|
@@ -177,7 +178,7 @@ export async function finishVnextCode(context, input) {
|
|
|
177
178
|
const timing = stageTiming(home, stage, now);
|
|
178
179
|
const changedPaths = [...new Set(works.flatMap((work) => resultPaths(work.result)))];
|
|
179
180
|
const next = nextAction(run, changedPaths);
|
|
180
|
-
writeProtocolFlowStatus(run.workspace_root, home, run.id, next === "start_code_review" ? "CODE complete; CODE-REVIEW is next." : "CODE complete; this RUN reached its configured terminal boundary.");
|
|
181
|
+
writeProtocolFlowStatus(run.workspace_root, home, run.id, next === "start_code_review" ? "CODE complete; CODE-REVIEW is next." : next === "start_merge" ? "CODE complete; MERGE is queued." : "CODE complete; this RUN reached its configured terminal boundary.");
|
|
181
182
|
const report = {
|
|
182
183
|
schema_id: "dd-flow/stage-report@1",
|
|
183
184
|
run_id: run.id,
|
|
@@ -234,8 +235,9 @@ export async function finishVnextCode(context, input) {
|
|
|
234
235
|
completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "code_completed", nextAction: undefined });
|
|
235
236
|
}
|
|
236
237
|
else {
|
|
237
|
-
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction:
|
|
238
|
+
advanceFlowRun(context, { projectRoot, runId: run.id, status: "running", verdict: "code_completed", nextAction: next });
|
|
238
239
|
}
|
|
240
|
+
const mergeRequest = next === "start_merge" ? ensureVnextMergeRequest(context, { projectRoot, runId: run.id }) : null;
|
|
239
241
|
return {
|
|
240
242
|
ok: true,
|
|
241
243
|
run_id: run.id,
|
|
@@ -243,7 +245,7 @@ export async function finishVnextCode(context, input) {
|
|
|
243
245
|
outcome: "accepted",
|
|
244
246
|
report_path: path.join(root, "stage-report.json"),
|
|
245
247
|
next_action: next,
|
|
246
|
-
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: `${flowCommand(context)} stage start ${run.id} --stage code-review --project-root ${JSON.stringify(projectRoot)} --json` } } : {})
|
|
248
|
+
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: `${flowCommand(context)} stage start ${run.id} --stage code-review --project-root ${JSON.stringify(projectRoot)} --json` } } : next === "start_merge" ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } } : {})
|
|
247
249
|
};
|
|
248
250
|
}
|
|
249
251
|
export function addVnextCodeRepair(context, input) {
|
|
@@ -289,7 +291,7 @@ export function addVnextCodeRepair(context, input) {
|
|
|
289
291
|
repair: {
|
|
290
292
|
origin_work_ids: origins.map((work) => work.work_id),
|
|
291
293
|
...(receipt ? { check_receipt_id: receipt.id, failure_receipt_path: receipt.receipt_path } : {}),
|
|
292
|
-
...(input.reviewFindingIds?.length ? { review_finding_ids: unique(input.reviewFindingIds), review_evidence_refs: unique(input.reviewEvidenceRefs ?? []) } : {})
|
|
294
|
+
...(input.reviewFindingIds?.length ? { review_finding_ids: unique(input.reviewFindingIds), review_evidence_refs: unique(input.reviewEvidenceRefs ?? []), review_check_refs: unique(input.reviewCheckRefs ?? (input.reviewChecks ?? []).map((check) => check.id)) } : {})
|
|
293
295
|
},
|
|
294
296
|
task: input.objective,
|
|
295
297
|
semantic_spine: {
|
|
@@ -301,7 +303,9 @@ export function addVnextCodeRepair(context, input) {
|
|
|
301
303
|
},
|
|
302
304
|
requirements: uniqueBy(invariantPackets.flatMap((value) => value.requirements), (value) => value.id),
|
|
303
305
|
acceptance: uniqueBy(invariantPackets.flatMap((value) => value.acceptance), (value) => JSON.stringify(value)),
|
|
304
|
-
|
|
306
|
+
// A CODE-REVIEW repair changes delivered code/evidence, never the
|
|
307
|
+
// already accepted PLAN or its ownership declaration.
|
|
308
|
+
document_updates: [],
|
|
305
309
|
required_read: unique([...(receipt ? [receipt.receipt_path] : input.reviewEvidenceRefs ?? []), ...packets.flatMap((value) => value.required_read)]),
|
|
306
310
|
discovery_boundary: unique(packets.flatMap((value) => value.discovery_boundary)),
|
|
307
311
|
// These are collision-avoidance hints. Receipt paths enrich the coordinator
|
|
@@ -310,11 +314,11 @@ export function addVnextCodeRepair(context, input) {
|
|
|
310
314
|
// Receipts record the resolved shell command. A repair must retain the
|
|
311
315
|
// accepted declaration (including its immutable @check alias) and only
|
|
312
316
|
// change when it runs, so work finish can validate it again.
|
|
313
|
-
checks:
|
|
317
|
+
checks: selectRepairChecks({ ...(failedCheck ? { failedCheck } : {}), ...(input.reviewChecks ? { reviewChecks: input.reviewChecks } : {}) }),
|
|
314
318
|
provides_checks: [],
|
|
315
319
|
stop_conditions: unique([
|
|
316
320
|
...invariantPackets.flatMap((value) => value.stop_conditions),
|
|
317
|
-
...(receipt ? ["Stop and report the blocker if the proposed repair contradicts an accepted requirement or non-goal."] : [])
|
|
321
|
+
...(receipt ? ["Stop and report the blocker if the proposed repair contradicts an accepted requirement or non-goal."] : ["Do not modify accepted PLAN artifacts, code-work-batch.json, or review decisions. Stop and report if the finding cannot be repaired within accepted scope."])
|
|
318
322
|
]),
|
|
319
323
|
depends_on: origins.map((work) => work.work_id),
|
|
320
324
|
result_schema: "dd-flow/code-work-result@2"
|
|
@@ -340,6 +344,13 @@ export function addVnextCodeRepair(context, input) {
|
|
|
340
344
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
341
345
|
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
342
346
|
}
|
|
347
|
+
/** The repair Work runs only the check that motivated it; the stage retains the aggregate gate. */
|
|
348
|
+
export function selectRepairChecks(input) {
|
|
349
|
+
const checks = input.failedCheck
|
|
350
|
+
? [{ ...input.failedCheck, purpose: "Re-run the failed accepted check.", run_at: "work" }]
|
|
351
|
+
: (input.reviewChecks ?? []).map((check) => ({ ...check, purpose: `Prove the CODE-REVIEW repair: ${check.purpose}`, run_at: "work" }));
|
|
352
|
+
return uniqueBy(checks, (check) => check.id);
|
|
353
|
+
}
|
|
343
354
|
function receiptRepairPaths(workspaceRoot, receipt) {
|
|
344
355
|
const root = path.resolve(workspaceRoot);
|
|
345
356
|
const text = [receipt.stdout_path, receipt.stderr_path]
|
|
@@ -394,7 +405,7 @@ function coordinatorPrompt(context, input) {
|
|
|
394
405
|
"A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return: do not inspect its PID, start a second finish command, or infer failure from quiet output. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
|
|
395
406
|
`A repairable engine, harness, or environment failure is not a user question. Record it without finishing CODE: ${flowCommand(context)} stage block ${input.run.id} --stage code --work ${input.rootWork.work_id} --kind <engine|harness|environment> --code <stable-code> --summary-stdin --retryable --project-root ${JSON.stringify(input.projectRoot)} --json. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
|
|
396
407
|
`When every CODE and repair Work is completed, write ${path.join(input.root, "code-verification.json")} using the exact contract below. Mark passed only when all accepted requirements and current-gate acceptance criteria are implemented or explicitly evidenced; list every remaining issue in unresolved. Every evidence_refs item must already exist as a relative workspace path or run://${input.run.id}/ path. Do not claim a browser or other check receipt that was not retained. Then finish: ${finishCommand(context, input.run.id, input.projectRoot, path.join(input.root, "code-verification.json"))}`,
|
|
397
|
-
"If the aggregate gate fails,
|
|
408
|
+
"If the aggregate gate fails, do not stop after `code_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use its returned repair command with the relevant completed origin Work IDs and a concise repair objective, then stop so the runner can dispatch the newly declared repair. Do not edit invisibly in the root orchestrator.",
|
|
398
409
|
"</execution_commands>",
|
|
399
410
|
"",
|
|
400
411
|
"<verification_contract>",
|
|
@@ -416,8 +427,9 @@ export function profileCommands(workspaceRoot) {
|
|
|
416
427
|
function latestReceiptsByCommand(receipts) {
|
|
417
428
|
const latest = new Map();
|
|
418
429
|
for (const receipt of receipts)
|
|
419
|
-
|
|
420
|
-
|
|
430
|
+
for (const ref of receipt.check_refs)
|
|
431
|
+
latest.set(ref, receipt);
|
|
432
|
+
return [...new Set(latest.values())];
|
|
421
433
|
}
|
|
422
434
|
/**
|
|
423
435
|
* A CODE finish may be retried after a client loses the long command's output.
|
|
@@ -429,8 +441,10 @@ export function reusableAggregateGateReceipts(context, input) {
|
|
|
429
441
|
const current = workspaceFingerprint(input.workspaceRoot);
|
|
430
442
|
const latest = new Map();
|
|
431
443
|
for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId })) {
|
|
432
|
-
if (receipt.scope === "aggregate"
|
|
433
|
-
|
|
444
|
+
if (receipt.scope === "aggregate")
|
|
445
|
+
for (const ref of receipt.check_refs)
|
|
446
|
+
if (expected.has(ref))
|
|
447
|
+
latest.set(ref, receipt);
|
|
434
448
|
}
|
|
435
449
|
if (latest.size !== expected.size)
|
|
436
450
|
return null;
|
|
@@ -480,10 +494,10 @@ function processAlive(pid) {
|
|
|
480
494
|
return false;
|
|
481
495
|
}
|
|
482
496
|
}
|
|
483
|
-
function verificationProjection(works, receipts) {
|
|
497
|
+
export function verificationProjection(works, receipts) {
|
|
484
498
|
const packets = works.map((work) => packet(work)).filter((value) => value !== null);
|
|
485
499
|
const declarations = uniqueBy(packets.flatMap((value) => value.checks), (value) => value.id);
|
|
486
|
-
const receiptByDeclaration = new Map(receipts.
|
|
500
|
+
const receiptByDeclaration = new Map(receipts.flatMap((receipt) => receipt.check_refs.map((ref) => [ref, receipt])));
|
|
487
501
|
const evidence = new Map();
|
|
488
502
|
for (const work of works) {
|
|
489
503
|
const result = readJsonResult(work.result);
|
|
@@ -492,9 +506,19 @@ function verificationProjection(works, receipts) {
|
|
|
492
506
|
evidence.set(item.criterion_id, unique([...(evidence.get(item.criterion_id) ?? []), ...(item.refs ?? [])]));
|
|
493
507
|
}
|
|
494
508
|
const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
|
|
509
|
+
const hasEvidence = (criterionId) => (evidence.get(criterionId)?.length ?? 0) > 0;
|
|
510
|
+
const isPassed = (checkId, criterionId) => receiptByDeclaration.get(checkId)?.status === "passed" || (declarations.find((check) => check.id === checkId)?.run_at === "external" && hasEvidence(criterionId));
|
|
495
511
|
return {
|
|
496
|
-
checks: declarations.map((check) => {
|
|
497
|
-
|
|
512
|
+
checks: declarations.map((check) => {
|
|
513
|
+
const receipt = receiptByDeclaration.get(check.id);
|
|
514
|
+
const externallyEvidenced = check.run_at === "external" && acceptance.some((item) => item.check_refs.includes(check.id) && hasEvidence(item.criterion_id));
|
|
515
|
+
return { id: check.id, run_at: check.run_at, status: receipt?.status ?? (externallyEvidenced ? "evidence_recorded" : ["work", "code", "readiness"].includes(check.run_at) ? "failed" : "not_due"), receipt_ref: receipt?.receipt_path ?? null };
|
|
516
|
+
}),
|
|
517
|
+
acceptance: acceptance.map((item) => {
|
|
518
|
+
const external = item.check_refs.some((id) => declarations.find((check) => check.id === id)?.run_at === "external");
|
|
519
|
+
const confirmed = item.check_refs.every((id) => isPassed(id, item.criterion_id)) && hasEvidence(item.criterion_id);
|
|
520
|
+
return { criterion_id: item.criterion_id, gate: item.gate, status: confirmed ? (external ? "confirmed_with_external_evidence" : "confirmed") : ["work", "code", "readiness"].includes(item.gate) ? "unresolved" : "not_due", check_refs: item.check_refs, evidence_refs: evidence.get(item.criterion_id) ?? [] };
|
|
521
|
+
})
|
|
498
522
|
};
|
|
499
523
|
}
|
|
500
524
|
function readJsonResult(value) { try {
|
|
@@ -640,7 +664,10 @@ function nextAction(run, changedPaths) {
|
|
|
640
664
|
const index = JSON.parse(run.index_json);
|
|
641
665
|
const requested = index.settings?.code_review?.mode ?? "auto";
|
|
642
666
|
const enabled = requested === "off" ? false : requested === "auto" ? changedPaths.length > 0 : true;
|
|
643
|
-
|
|
667
|
+
const stopTarget = index.execution_profile?.settings?.stop_target;
|
|
668
|
+
if (enabled && (stopTarget === "code_review_completed" || stopTarget === "merge_completed"))
|
|
669
|
+
return "start_code_review";
|
|
670
|
+
return stopTarget === "merge_completed" ? "start_merge" : "code_completed";
|
|
644
671
|
}
|
|
645
672
|
function stageStatus(run, name) {
|
|
646
673
|
try {
|
|
@@ -13,14 +13,17 @@ export function loadVnextExecutionProfile(projectRoot) {
|
|
|
13
13
|
throw new AppError("execution_profile_missing", "vNext flow requires .memory-bank/dd-flow/project-execution.json", 1, { file, cause: String(error) });
|
|
14
14
|
}
|
|
15
15
|
const profile = value;
|
|
16
|
-
if (profile.schema_id !== "dd-flow/project-execution@
|
|
16
|
+
if (profile.schema_id !== "dd-flow/project-execution@2"
|
|
17
17
|
|| (profile.stage_session_mode !== undefined && profile.stage_session_mode !== "same_session" && profile.stage_session_mode !== "new_session")
|
|
18
18
|
|| !["auto", "off", "standard", "deep"].includes(String(profile.plan_review_mode))
|
|
19
19
|
|| (profile.code_review_mode !== undefined && !["auto", "off", "standard", "deep"].includes(String(profile.code_review_mode)))
|
|
20
|
-
|| !["
|
|
20
|
+
|| !["same_session", "server"].includes(String(profile.merge_mode))
|
|
21
|
+
|| profile.merge_delivery?.strategy !== "local"
|
|
22
|
+
|| !["retain", "delete_after_success"].includes(String(profile.merge_cleanup?.source))
|
|
23
|
+
|| !["code_completed", "code_review_completed", "merge_completed"].includes(String(profile.stop_target))
|
|
21
24
|
|| !profile.code_bootstrap || typeof profile.code_bootstrap.command !== "string" || !profile.code_bootstrap.command.trim()
|
|
22
25
|
|| typeof profile.code_bootstrap.policy_ref !== "string" || !profile.code_bootstrap.policy_ref.trim()) {
|
|
23
|
-
throw new AppError("execution_profile_invalid", "Project execution profile does not match dd-flow/project-execution@
|
|
26
|
+
throw new AppError("execution_profile_invalid", "Project execution profile does not match dd-flow/project-execution@2", 1, { file });
|
|
24
27
|
}
|
|
25
28
|
return { ...profile, stage_session_mode: profile.stage_session_mode ?? "same_session", code_review_mode: profile.code_review_mode ?? "off" };
|
|
26
29
|
}
|