@deksden-com/dd-flow-cli 0.9.0-beta.104 → 0.9.0-beta.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli/run-cli.js +23 -8
  4. package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
  5. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
  6. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
  7. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -0
  8. package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -1
  9. package/dist/protocol/local-files.js +18 -9
  10. package/dist/runtime/context.js +2 -2
  11. package/dist/services/cleanup.js +81 -32
  12. package/dist/services/code-checks.js +18 -19
  13. package/dist/services/continuation-outcome.js +1 -1
  14. package/dist/services/controller-fanout.js +3 -3
  15. package/dist/services/eval-snapshots.js +69 -41
  16. package/dist/services/execution-policy.js +12 -1
  17. package/dist/services/hooks.js +150 -42
  18. package/dist/services/lanes.js +23 -18
  19. package/dist/services/lifecycle-command.js +11 -2
  20. package/dist/services/lifecycle-invocations.js +291 -54
  21. package/dist/services/managed-processes.js +35 -23
  22. package/dist/services/merge-queue.js +71 -67
  23. package/dist/services/merge-server.js +3 -11
  24. package/dist/services/migrations.js +28 -12
  25. package/dist/services/plan-runtime.js +8 -4
  26. package/dist/services/projects.js +3 -11
  27. package/dist/services/protocols.js +98 -68
  28. package/dist/services/recovery-snapshot-database.js +43 -8
  29. package/dist/services/run-control-worker.js +15 -32
  30. package/dist/services/run-control.js +3 -17
  31. package/dist/services/run-controller-capture.js +58 -27
  32. package/dist/services/run-controller-process.js +21 -30
  33. package/dist/services/run-controller-recovery.js +36 -37
  34. package/dist/services/run-controller.js +81 -86
  35. package/dist/services/run-fork.js +10 -6
  36. package/dist/services/run-projection.js +19 -64
  37. package/dist/services/run-recovery-runtime.js +24 -14
  38. package/dist/services/run-recovery.js +56 -54
  39. package/dist/services/runs.js +218 -160
  40. package/dist/services/runtime-budget.js +19 -45
  41. package/dist/services/runtime-scope-capture.js +8 -6
  42. package/dist/services/runtime-scope-control.js +49 -39
  43. package/dist/services/runtime-scope-resume.js +134 -122
  44. package/dist/services/runtime-scope-worker.js +48 -49
  45. package/dist/services/scope-writer-cleanup.js +26 -0
  46. package/dist/services/stage-context.js +6 -1
  47. package/dist/services/stage-lifecycle.js +1 -1
  48. package/dist/services/stage-pause.js +13 -29
  49. package/dist/services/stage-report-renderer.js +116 -6
  50. package/dist/services/usage.js +2 -9
  51. package/dist/services/vnext-code-review.js +10 -7
  52. package/dist/services/vnext-code.js +9 -17
  53. package/dist/services/vnext-merge.js +64 -21
  54. package/dist/services/vnext-plan-review.js +43 -30
  55. package/dist/services/vnext-plan.js +28 -24
  56. package/dist/services/vnext-protocolize.js +183 -20
  57. package/dist/services/vnext-specify.js +27 -19
  58. package/dist/services/work-registry.js +229 -150
  59. package/dist/storage/database.js +286 -56
  60. package/dist/storage/foreign-key-transaction.js +39 -0
  61. package/dist/storage/immutable-input.js +45 -0
  62. package/dist/storage/import-guard.js +102 -0
  63. package/dist/storage/native-cleanup.js +52 -0
  64. package/dist/storage/projection-lock.js +73 -0
  65. package/dist/storage/writer-contract.js +3 -3
  66. package/dist/storage/writer-migration.js +24 -41
  67. package/package.json +1 -1
  68. package/tools/repair-paused-run-status.mjs +48 -23
@@ -6,6 +6,7 @@ import { lifecycleRetryCommands, managedLifecycleCommand } from "./lifecycle-inv
6
6
  import { AppError } from "../shared/errors.js";
7
7
  import { entityReferenceVariants, shortEntityReference } from "../shared/entity-references.js";
8
8
  import { resolveProjectRoot } from "../storage/paths.js";
9
+ import { publishImmutableInput } from "../storage/immutable-input.js";
9
10
  import { resolveWorkReference } from "../storage/work-references.js";
10
11
  import { findRecentMatchingHookEvent, claimStageResumeHookEvent, stageResumeMatchKey } from "./hooks.js";
11
12
  import { requireProjectByRoot } from "./projects.js";
@@ -36,21 +37,15 @@ export function pauseStageForUser(context, input) {
36
37
  const pauseRoot = path.join(requireRunHome(run), "intake", "hitl", `${pauseId}-${safeStage(input.stage)}`);
37
38
  const questionPath = path.join(pauseRoot, "question.md");
38
39
  fs.mkdirSync(pauseRoot, { recursive: true });
39
- fs.writeFileSync(questionPath, question);
40
+ publishImmutableInput(questionPath, Buffer.from(question), "hitl_question_conflict");
40
41
  const now = context.now();
41
- context.db.beginWriteTransaction();
42
- try {
42
+ context.db.writeTransaction(() => {
43
43
  const paused = context.db.run("UPDATE works SET status = 'paused', updated_at = ? WHERE work_id = ? AND status = 'running'", [now, work.work_id]);
44
44
  if (paused.changes !== 1)
45
45
  throw new AppError("invalid_work_state", "Work changed before pause could be accepted", 1, { work_id: work.work_id });
46
46
  pauseFlowRunStage(context, { projectRoot, runId: run.id, stage: stage.stage, workId: work.work_id, pauseId, questionPath });
47
- context.db.exec("COMMIT");
48
- }
49
- catch (error) {
50
- context.db.exec("ROLLBACK");
51
- throw error;
52
- }
53
- refreshRunWorkProjection(context, project.id, run.id);
47
+ context.db.afterCommit(() => refreshRunWorkProjection(context, project.id, run.id));
48
+ });
54
49
  const answerInputPath = path.join(pauseRoot, "answer-input.md");
55
50
  const resume = stageResumeCommand(context, { runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, answerInputPath });
56
51
  const resumeTemplate = `Write the complete user answer exactly to ${quote([answerInputPath])}, then run:\n${resume}`;
@@ -107,17 +102,11 @@ export function resumeStageAfterUser(context, input) {
107
102
  }).eventKey;
108
103
  const now = context.now();
109
104
  const answerPath = path.join(path.dirname(pause.question_path), `answer-${crypto.createHash("sha256").update(`${pause.id}\0${input.answer}`).digest("hex")}.md`);
110
- context.db.beginWriteTransaction();
111
- let identity;
112
- try {
113
- identity = claimStageResumeHookEvent(context, { projectId: project.id, runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, eventKey: hookEventId });
114
- try {
115
- fs.writeFileSync(answerPath, input.answer, { flag: "wx", mode: 0o600 });
116
- }
117
- catch (error) {
118
- if (error.code !== "EEXIST" || fs.readFileSync(answerPath, "utf8") !== input.answer)
119
- throw error;
120
- }
105
+ // The answer is an immutable, content-addressed input. Prepare it before
106
+ // reserving SQLite's writer; an unreferenced candidate has no flow authority.
107
+ publishImmutableInput(answerPath, Buffer.from(input.answer), "hitl_answer_conflict");
108
+ context.db.writeTransaction(() => {
109
+ const identity = claimStageResumeHookEvent(context, { projectId: project.id, runId: run.id, stage: stage.stage, workId: work.work_id, projectRoot, eventKey: hookEventId });
121
110
  if (workSession.session_id !== identity.sessionId) {
122
111
  bindSessionForResume(context, work.work_id, identity, now);
123
112
  context.db.run("UPDATE work_sessions SET status = 'completed', updated_at = ?, completed_at = ? WHERE id = ?", [now, now, workSession.id]);
@@ -127,14 +116,9 @@ export function resumeStageAfterUser(context, input) {
127
116
  if (resumed.changes !== 1)
128
117
  throw new AppError("invalid_work_state", "Work changed before resume could be accepted", 1, { work_id: work.work_id });
129
118
  resumeFlowRunStage(context, { projectRoot, runId: run.id, stage: stage.stage, workId: work.work_id, answerPath });
130
- context.db.exec("COMMIT");
131
- }
132
- catch (error) {
133
- context.db.exec("ROLLBACK");
134
- throw error;
135
- }
136
- refreshRunWorkProjection(context, project.id, run.id);
137
- appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_resume_session_bound", stage: stage.stage, work_id: work.work_id, session_id: identity.sessionId });
119
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "stage_resume_session_bound", stage: stage.stage, work_id: work.work_id, session_id: identity.sessionId });
120
+ context.db.afterCommit(() => refreshRunWorkProjection(context, project.id, run.id));
121
+ });
138
122
  const prompt = evidence.prompt;
139
123
  const retryCommands = lifecycleRetryCommands(context, stage.stage);
140
124
  const continuation = [
@@ -1,14 +1,129 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { withProjectionLock } from "../storage/projection-lock.js";
3
6
  import { writeJsonAtomic } from "../storage/paths.js";
4
7
  /** Deterministic JSON -> Markdown/HTML projection shared by every stage. */
5
8
  export function writeStageReport(root, report) {
6
9
  const json = path.join(root, "stage-report.json");
7
10
  const markdown = path.join(root, "stage-report.md");
8
11
  const html = path.join(root, "stage-report.html");
12
+ const normalized = normalizeStageReport(report);
13
+ writeJsonAtomic(json, normalized);
14
+ const text = renderStageReportMarkdown(normalized);
15
+ fs.writeFileSync(markdown, text);
16
+ fs.writeFileSync(html, renderStageReportHtml(normalized, text));
17
+ return { json, markdown, html };
18
+ }
19
+ export function prepareStageReport(report) {
20
+ const normalized = normalizeStageReport(report);
21
+ const json = `${JSON.stringify(normalized, null, 2)}\n`;
22
+ const markdown = renderStageReportMarkdown(normalized);
23
+ const html = renderStageReportHtml(normalized, markdown);
24
+ return { normalized, bytes: { json, markdown, html }, sha256: crypto.createHash("sha256").update(json).digest("hex") };
25
+ }
26
+ export function prepareStagePacket(file, bytes) {
27
+ if (fs.existsSync(file) && !bytes.equals(fs.readFileSync(file)))
28
+ throw new AppError("stage_packet_conflict", "An existing Stage packet differs from prepared bytes", 2, { file });
29
+ return { file, bytes, sha256: crypto.createHash("sha256").update(bytes).digest("hex") };
30
+ }
31
+ export function acceptStagePacket(context, input) {
32
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
33
+ const index = run ? JSON.parse(run.index_json) : null;
34
+ const attempt = index?.stage_runs?.find((item) => item.stage === input.stage)?.attempt;
35
+ if (!attempt)
36
+ throw new AppError("stage_packet_owner_missing", "Stage packet has no owning attempt", 1, { run_id: input.runId, stage: input.stage });
37
+ const payload = { run_id: input.runId, stage: input.stage, attempt, file: input.prepared.file, sha256: input.prepared.sha256, base64: input.prepared.bytes.toString("base64") };
38
+ context.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.stage_packet', 0, ?, ?)", [input.projectId, JSON.stringify(payload), context.now()]);
39
+ context.db.afterCommit(() => publishStagePacket(context, input.projectId, input.runId, input.stage, input.prepared.file));
40
+ }
41
+ export function publishStagePacket(context, projectId, runId, stage, file) {
42
+ fs.mkdirSync(path.dirname(file), { recursive: true });
43
+ withProjectionLock(`${file}.publish-lock`, () => {
44
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
45
+ const index = run ? JSON.parse(run.index_json) : null;
46
+ const currentAttempt = index?.stage_runs?.find((item) => item.stage === stage)?.attempt;
47
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' ORDER BY id DESC", [projectId]);
48
+ const packet = rows.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.attempt === currentAttempt && item.file === file);
49
+ if (!packet)
50
+ return;
51
+ const bytes = Buffer.from(packet.base64, "base64");
52
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== packet.sha256)
53
+ throw new AppError("stage_packet_invalid", "Stored Stage packet hash does not match its bytes", 1, { file });
54
+ fs.mkdirSync(path.dirname(file), { recursive: true });
55
+ try {
56
+ fs.writeFileSync(file, bytes, { flag: "wx" });
57
+ }
58
+ catch (error) {
59
+ if (error.code !== "EEXIST")
60
+ throw error;
61
+ if (!bytes.equals(fs.readFileSync(file)))
62
+ throw new AppError("stage_packet_conflict", "Accepted Stage packet differs from the published file", 1, { file, run_id: runId, stage, attempt: currentAttempt });
63
+ }
64
+ });
65
+ }
66
+ /** SQL receipt and exact bytes are committed with the owning Stage transition. */
67
+ export function acceptStageReport(context, input) {
68
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
69
+ const index = run ? JSON.parse(run.index_json) : null;
70
+ const attempt = index?.stage_runs?.find((item) => item.stage === input.stage)?.attempt;
71
+ if (!attempt)
72
+ throw new AppError("stage_report_owner_missing", "Stage report has no owning attempt", 1, { run_id: input.runId, stage: input.stage });
73
+ const payload = { run_id: input.runId, stage: input.stage, attempt, root: input.root, sha256: input.prepared.sha256, bytes: input.prepared.bytes };
74
+ context.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.stage_report', 0, ?, ?)", [input.projectId, JSON.stringify(payload), context.now()]);
75
+ context.db.afterCommit(() => publishStageReport(context, input.projectId, input.runId, input.stage, input.root));
76
+ }
77
+ /** Replays the latest committed receipt, never an obsolete attempt. */
78
+ export function publishStageReport(context, projectId, runId, stage, root) {
79
+ withProjectionLock(`${root}.report-publish-lock`, () => {
80
+ const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
81
+ const index = run ? JSON.parse(run.index_json) : null;
82
+ const currentAttempt = index?.stage_runs?.find((item) => item.stage === stage)?.attempt;
83
+ const events = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_report' ORDER BY id DESC", [projectId]);
84
+ const receipt = events.map((row) => JSON.parse(row.payload_json)).find((item) => item.run_id === runId && item.stage === stage && item.root === root && item.attempt === currentAttempt);
85
+ if (!receipt)
86
+ return;
87
+ fs.mkdirSync(root, { recursive: true });
88
+ for (const [name, bytes] of Object.entries(receipt.bytes)) {
89
+ const file = path.join(root, `stage-report.${name === "json" ? "json" : name === "markdown" ? "md" : "html"}`);
90
+ try {
91
+ fs.writeFileSync(file, bytes, { flag: "wx" });
92
+ }
93
+ catch (error) {
94
+ if (error.code !== "EEXIST")
95
+ throw error;
96
+ if (fs.readFileSync(file, "utf8") !== bytes)
97
+ throw new AppError("stage_report_conflict", "Accepted Stage report differs from the published file", 1, { run_id: runId, stage, attempt: currentAttempt, file });
98
+ }
99
+ }
100
+ });
101
+ }
102
+ /** Admission repair for predecessor reports; the stage being retried is archived separately. */
103
+ export function reconcileRunStageReports(context, projectId, runId, exceptStage) {
104
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_report' ORDER BY id DESC", [projectId]);
105
+ const seen = new Set();
106
+ for (const row of rows) {
107
+ const receipt = JSON.parse(row.payload_json);
108
+ if (receipt.run_id !== runId || typeof receipt.stage !== "string" || typeof receipt.root !== "string" || receipt.stage === exceptStage || seen.has(receipt.stage))
109
+ continue;
110
+ seen.add(receipt.stage);
111
+ publishStageReport(context, projectId, runId, receipt.stage, receipt.root);
112
+ }
113
+ const packets = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_packet' ORDER BY id DESC", [projectId]);
114
+ const packetFiles = new Set();
115
+ for (const row of packets) {
116
+ const packet = JSON.parse(row.payload_json);
117
+ if (packet.run_id !== runId || typeof packet.stage !== "string" || typeof packet.file !== "string" || packet.stage === exceptStage || packetFiles.has(packet.file))
118
+ continue;
119
+ packetFiles.add(packet.file);
120
+ publishStagePacket(context, projectId, runId, packet.stage, packet.file);
121
+ }
122
+ }
123
+ function normalizeStageReport(report) {
9
124
  const semantic = object(report.semantic);
10
125
  const sourceArtifacts = object(report.artifacts);
11
- const normalized = {
126
+ return {
12
127
  ...report,
13
128
  schema_id: "dd-flow/stage-report@2",
14
129
  summary: string(report.summary) || string(semantic.result) || "Stage completed.",
@@ -27,11 +142,6 @@ export function writeStageReport(root, report) {
27
142
  html: string(sourceArtifacts.html) || "stage-report.html"
28
143
  }
29
144
  };
30
- writeJsonAtomic(json, normalized);
31
- const text = renderStageReportMarkdown(normalized);
32
- fs.writeFileSync(markdown, text);
33
- fs.writeFileSync(html, renderStageReportHtml(normalized, text));
34
- return { json, markdown, html };
35
145
  }
36
146
  export function renderStageReportMarkdown(report) {
37
147
  const semantic = object(report.semantic);
@@ -167,8 +167,7 @@ export function recalculateRunUsage(context, input) {
167
167
  const usageFinal = final && unsettledSessions.length === 0;
168
168
  const persistRunProjection = !input.sessionId && !input.stage;
169
169
  if (persistRunProjection) {
170
- context.db.exec("SAVEPOINT run_usage");
171
- try {
170
+ context.db.writeTransaction(() => {
172
171
  context.db.run("DELETE FROM usage WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId]);
173
172
  for (const row of rows) {
174
173
  context.db.run(`INSERT INTO usage (id, project_id, run_id, session_id, checkpoint, stage, stage_attempt, observed_at, total_tokens, input_tokens, cached_input_tokens, cache_read_input_tokens, cache_write_input_tokens, uncached_input_tokens, output_tokens, reasoning_output_tokens, source_kind, source_locator, source_sha256, source_size_bytes, source_mtime_ms, token_event_at, turn_id, turn_attribution, parser_version, extraction_status, diagnostic_code, created_at)
@@ -187,13 +186,7 @@ export function recalculateRunUsage(context, input) {
187
186
  AND status = 'idle'
188
187
  AND NOT EXISTS (SELECT 1 FROM work_sessions WHERE session_id = ? AND status = 'running')`, [observedAt, session.completed_at, input.projectId, input.runId, session.session_id, session.session_id]);
189
188
  }
190
- context.db.exec("RELEASE run_usage");
191
- }
192
- catch (error) {
193
- context.db.exec("ROLLBACK TO run_usage");
194
- context.db.exec("RELEASE run_usage");
195
- throw error;
196
- }
189
+ });
197
190
  }
198
191
  const observations = persistRunProjection ? runObservations(context, input.projectId, input.runId) : null;
199
192
  const legacyTools = toolCallsForLinks(context, links, accountingAt);
@@ -1,11 +1,12 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
2
3
  import crypto from "node:crypto";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { AppError } from "../shared/errors.js";
6
7
  import { resolveProjectRoot } from "../storage/paths.js";
7
8
  import { requireProjectByRoot } from "./projects.js";
8
- import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
9
+ import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, prepareFlowRunStageCompletion, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
9
10
  import { validateSchema } from "./schema-validation.js";
10
11
  import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
11
12
  import { assertStageStartHookEvent } from "./hooks.js";
@@ -15,7 +16,7 @@ import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, wo
15
16
  import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
16
17
  import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
17
18
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
18
- import { writeStageReport } from "./stage-report-renderer.js";
19
+ import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
19
20
  import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
20
21
  import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
21
22
  import { stageGraphScope } from "./stage-work-graph.js";
@@ -85,7 +86,7 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
85
86
  fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id, version: 3 } } : {}), code_report_sha256: codeReportSha256, review_batch_sha256: sha256Value(`${JSON.stringify(batch, null, 2)}\n`), aspect_map_checksums: aspectMapChecksums }, null, 2)}\n`);
86
87
  if (mode.mode === "off") {
87
88
  fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
88
- const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n`;
89
+ const prompt = `<stage_identity>\n- RUN: ${run.id}\n- stage: code-review\n- mode: off\n</stage_identity>\n\nCODE-REVIEW is disabled by the frozen RUN configuration. Finish with: ${finishCommand(context, run.id, projectRoot, path.join(root, "decision.json"))}\n${stageLifecycleInstruction}\n`;
89
90
  fs.writeFileSync(promptPath, prompt);
90
91
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
91
92
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
@@ -94,7 +95,7 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
94
95
  }
95
96
  const batchFile = path.join(root, "review-work-batch.json");
96
97
  fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
97
- const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks });
98
+ const prompt = `${orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks })}\n${stageLifecycleInstruction}\n`;
98
99
  fs.writeFileSync(promptPath, prompt);
99
100
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
100
101
  const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
@@ -244,6 +245,8 @@ export async function finishVnextCodeReview(context, input) {
244
245
  let mergeRequest = null;
245
246
  assertRunMutationAllowed(context, project.id, run.id, generation);
246
247
  const prepareMerge = nextAction === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths }) : null;
248
+ const preparedReport = prepareStageReport(report);
249
+ 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 };
247
250
  withStageSettlement(context, () => {
248
251
  assertRunMutationAllowed(context, project.id, run.id, generation);
249
252
  const currentReviewers = reviewerWorks(context, project.id, run.id, cycle);
@@ -252,8 +255,9 @@ export async function finishVnextCodeReview(context, input) {
252
255
  throw new AppError("reviewer_jobs_incomplete", "Review or repair results changed while the final checks were running", 2);
253
256
  }
254
257
  mergeRequest = prepareMerge?.() ?? null;
255
- writeReport(root, report);
256
- 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" });
258
+ const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
259
+ acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
260
+ completeFlowRunStage(context, completionInput, preparedCompletion);
257
261
  appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_completed", work_id: rootWork.work_id, outcome });
258
262
  if (nextAction === "start_merge")
259
263
  advanceFlowRun(context, { settlementStage: "code-review", projectRoot, runId: run.id, status: "running", verdict: "code_review_completed", nextAction });
@@ -507,4 +511,3 @@ function readJsonString(value) { try {
507
511
  catch {
508
512
  return {};
509
513
  } }
510
- function writeReport(root, report) { writeStageReport(root, report); }
@@ -1,4 +1,5 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
2
3
  import { ensureAggregateRepair, ensureRepairIntent } from "./repair-intents.js";
3
4
  import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
4
5
  import fs from "node:fs";
@@ -7,7 +8,7 @@ import { AppError } from "../shared/errors.js";
7
8
  import { resolveProjectRoot } from "../storage/paths.js";
8
9
  import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
9
10
  import { requireProjectByRoot } from "./projects.js";
10
- import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
11
+ import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, prepareFlowRunStageCompletion, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
11
12
  import { validateSchema } from "./schema-validation.js";
12
13
  import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
13
14
  import { stageCycle, stageGraphScope } from "./stage-work-graph.js";
@@ -16,7 +17,7 @@ import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
16
17
  import { readVnextSpecifyResult } from "./vnext-specify.js";
17
18
  import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
18
19
  import { vnextStageDirectory } from "../domain/stage-catalog.js";
19
- import { writeStageReport } from "./stage-report-renderer.js";
20
+ import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.js";
20
21
  import { prepareVnextMergeRequest } from "./vnext-merge.js";
21
22
  import { assertStageStartHookEvent } from "./hooks.js";
22
23
  import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
@@ -100,7 +101,7 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
100
101
  const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
101
102
  // Graph rendering may issue managed child commands; it belongs to execution.
102
103
  const graph = codeWorkGraph(context, project.id, run.id);
103
- const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph });
104
+ const prompt = `${coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph })}\n${stageLifecycleInstruction}\n`;
104
105
  fs.writeFileSync(promptPath, prompt);
105
106
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
106
107
  const binding = bindStageCoordinatorWork(context, {
@@ -270,6 +271,8 @@ export async function finishVnextCode(context, input) {
270
271
  let mergeRequest = null;
271
272
  assertRunMutationAllowed(context, project.id, run.id, generation);
272
273
  const prepareMerge = next === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
274
+ const preparedReport = prepareStageReport(report);
275
+ 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 };
273
276
  withStageSettlement(context, () => {
274
277
  assertRunMutationAllowed(context, project.id, run.id, generation);
275
278
  const currentWorks = codeWorks(context, project.id, run.id);
@@ -277,17 +280,9 @@ export async function finishVnextCode(context, input) {
277
280
  throw new AppError("code_graph_incomplete", "CODE results changed while the final checks were running", 2);
278
281
  }
279
282
  mergeRequest = prepareMerge?.() ?? null;
280
- writeReport(root, report);
281
- completeFlowRunStage(context, {
282
- projectRoot,
283
- runId: run.id,
284
- stage,
285
- status: "done",
286
- data: "stage-report.json",
287
- dataSchemaId: "dd-flow/stage-report@1",
288
- report: "stage-report.md",
289
- stageReport: "stage-report.html"
290
- });
283
+ const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
284
+ acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
285
+ completeFlowRunStage(context, completionInput, preparedCompletion);
291
286
  const rootWork = requireRootWork(context, project.id, run.id);
292
287
  appendFlowRunTimelineEvent(context, project.id, run.id, {
293
288
  type: "code_accepted",
@@ -807,6 +802,3 @@ export function resumeAcceptedCodeStage(context, run, projectRoot, stage, report
807
802
  completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: next });
808
803
  });
809
804
  }
810
- function writeReport(root, report) {
811
- writeStageReport(root, report);
812
- }
@@ -1,4 +1,5 @@
1
1
  import { managedLifecycleCommand } from "./lifecycle-invocations.js";
2
+ import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
2
3
  import crypto from "node:crypto";
3
4
  import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
4
5
  import fs from "node:fs";
@@ -10,10 +11,10 @@ import { resolveProjectRoot } from "../storage/paths.js";
10
11
  import { requireProjectByRoot } from "./projects.js";
11
12
  import { nextMergeRequestId } from "./ids.js";
12
13
  import { readProjectConfig } from "./config.js";
13
- import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareFlowRunStageAttachment, prepareVnextMergeSourceRepairAttempt } from "./runs.js";
14
+ import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareFlowRunStageAttachment, prepareFlowRunStageCompletion, prepareVnextMergeSourceRepairAttempt, withStageSettlement } from "./runs.js";
14
15
  import { stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
15
16
  import { runtimeCommand, checkRetryArgs, recoveryEvidenceInput } from "./runtime-command.js";
16
- import { writeStageReport } from "./stage-report-renderer.js";
17
+ import { acceptStageReport, prepareStageReport, publishStageReport } from "./stage-report-renderer.js";
17
18
  import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
18
19
  import { addVnextCodeRepair } from "./vnext-code.js";
19
20
  import { checkReceiptEvidenceValid, checkReceipts, codeCheckProfileHash, effectiveCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
@@ -45,6 +46,9 @@ export function prepareVnextMergeRequest(context, input) {
45
46
  const route = settings.merge_mode ?? "same_session";
46
47
  const home = requireHome(run);
47
48
  const protocols = protocolIds(home);
49
+ if (route === "same_session" && path.resolve(run.workspace_root) !== path.resolve(projectRoot)) {
50
+ throw new AppError("merge_session_workspace_conflict", "The existing coordinator Session is bound to the feature workspace, but MERGE executes in the integration checkout", 2, { effect: "no_effect", recoverable: false, source_workspace: run.workspace_root, target_workspace: projectRoot });
51
+ }
48
52
  const sourceBranch = gitValue(run.workspace_root, ["branch", "--show-current"]);
49
53
  if (!sourceBranch)
50
54
  throw new AppError("merge_source_invalid", "MERGE requires a named source branch", 1, { workspace_root: run.workspace_root });
@@ -158,7 +162,7 @@ export async function startVnextMerge(context, input, prepared = prepareVnextMer
158
162
  const binding = pendingWork?.status === "created"
159
163
  ? startStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) })
160
164
  : bindStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
161
- const prompt = existingPrompt ?? mergePrompt(context, { run, request, resultPath, projectRoot }, checks, settings);
165
+ const prompt = existingPrompt ?? `${mergePrompt(context, { run, request, resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n`;
162
166
  if (existingPrompt === null)
163
167
  fs.writeFileSync(promptPath, prompt);
164
168
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
@@ -177,7 +181,7 @@ export async function startVnextMerge(context, input, prepared = prepareVnextMer
177
181
  if (claimed.changes !== 1 && requireRequest(context, current.merge_request_id).status !== "active")
178
182
  throw new AppError("merge_claim_conflict", "MERGE request could not acquire the integration lane", 1, { merge_request_id: current.merge_request_id });
179
183
  const started = startStageCoordinatorWork(context, { workId: current.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
180
- const prompt = mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot }, checks, settings);
184
+ const prompt = `${mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n`;
181
185
  fs.writeFileSync(promptPath, prompt);
182
186
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
183
187
  context.db.run("UPDATE work_sessions SET prompt_path = ?, result_path = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [promptPath, resultPath, context.now(), current.executor_work_id]);
@@ -330,10 +334,11 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
330
334
  assertRunMutationAllowed(context, project.id, run.id, generation);
331
335
  const now = context.now();
332
336
  context.db.run("UPDATE merge_requests SET status = 'completed', checkpoint = 'finalized', result_json = ?, completed_at = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify(semantic), now, now, request.merge_request_id]);
333
- const report = mergeReport(context, run, requireRequest(context, request.merge_request_id), semantic, receipts);
334
337
  const root = path.join(requireHome(run), stageDir);
335
- writeStageReport(root, report);
336
- completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@2", report: "stage-report.md", stageReport: "stage-report.html" });
338
+ const retainedReport = completedMergeReport(context, run, root);
339
+ const report = retainedReport ?? mergeReport(context, run, requireRequest(context, request.merge_request_id), semantic, receipts);
340
+ if (!retainedReport)
341
+ commitMergeStageReport(context, run, projectRoot, root, report);
337
342
  const rootWork = requireRootWork(context, project.id, run.id);
338
343
  finishFanInWork(context, rootWork.work_id, JSON.stringify(report));
339
344
  completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "merge_completed", nextAction: undefined });
@@ -352,7 +357,11 @@ function ensureMergeStageAttached(context, run, projectRoot) {
352
357
  function recoverCompletedMergeRun(context, run, projectRoot, request) {
353
358
  const root = path.join(requireHome(run), stageDir);
354
359
  const reportPath = path.join(root, "stage-report.json");
355
- if (!fs.existsSync(reportPath)) {
360
+ const retainedReport = completedMergeReport(context, run, root);
361
+ let report;
362
+ if (retainedReport)
363
+ report = retainedReport;
364
+ else if (!fs.existsSync(reportPath)) {
356
365
  const semantic = request.result_json ? JSON.parse(request.result_json) : null;
357
366
  const gate = readFrozenMergeGate(path.join(root, "merge-gate.json"), request.merge_request_id);
358
367
  const accepted = readMergeGateAcceptance(context, path.join(root, "merge-gate-acceptance.json"), request, gate);
@@ -360,10 +369,13 @@ function recoverCompletedMergeRun(context, run, projectRoot, request) {
360
369
  throw new AppError("merge_report_missing", "Completed MERGE has insufficient authoritative evidence to rebuild its report", 1, { merge_request_id: request.merge_request_id, report_path: reportPath });
361
370
  verifyLocalDelivery(request);
362
371
  performConfiguredCleanup(run, request);
363
- writeStageReport(root, mergeReport(context, run, request, semantic, accepted.receipts));
372
+ report = mergeReport(context, run, request, semantic, accepted.receipts);
364
373
  }
374
+ else
375
+ report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
365
376
  ensureMergeStageAttached(context, run, projectRoot);
366
- completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@2", report: "stage-report.md", stageReport: "stage-report.html" });
377
+ if (!retainedReport)
378
+ commitMergeStageReport(context, run, projectRoot, root, report);
367
379
  const rootWork = findRootWork(context, run.project_id, run.id);
368
380
  const rootStatus = rootWork.status;
369
381
  if (rootStatus === "running")
@@ -375,23 +387,46 @@ function recoverCompletedMergeRun(context, run, projectRoot, request) {
375
387
  completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "merge_completed", nextAction: undefined });
376
388
  refreshRunWorkProjection(context, run.project_id, run.id);
377
389
  }
390
+ function completedMergeReport(context, run, root) {
391
+ const current = requireRun(context, run.project_id, run.id);
392
+ const index = JSON.parse(current.index_json);
393
+ const entry = index.stage_runs?.find(item => item.stage === stage);
394
+ if (entry?.status !== "done")
395
+ return null;
396
+ const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_report' ORDER BY id DESC", [run.project_id]);
397
+ const receipt = rows.map(row => JSON.parse(row.payload_json))
398
+ .find(item => item.run_id === run.id && item.stage === stage && item.attempt === entry.attempt && item.root === root);
399
+ if (receipt) {
400
+ if (crypto.createHash("sha256").update(receipt.bytes.json).digest("hex") !== receipt.sha256)
401
+ throw new AppError("stage_report_invalid", "Accepted MERGE report receipt has invalid bytes", 1, { run_id: run.id });
402
+ publishStageReport(context, run.project_id, run.id, stage, root);
403
+ return JSON.parse(receipt.bytes.json);
404
+ }
405
+ const file = path.join(root, "stage-report.json");
406
+ if (!fs.existsSync(file))
407
+ throw new AppError("merge_report_missing", "Completed MERGE has no accepted report receipt or published report", 1, { run_id: run.id, report_path: file });
408
+ return JSON.parse(fs.readFileSync(file, "utf8"));
409
+ }
410
+ function commitMergeStageReport(context, run, projectRoot, root, report) {
411
+ const preparedReport = prepareStageReport(report);
412
+ const completion = { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@2", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256 };
413
+ const preparedCompletion = prepareFlowRunStageCompletion(context, completion);
414
+ withStageSettlement(context, () => {
415
+ acceptStageReport(context, { projectId: run.project_id, runId: run.id, stage, root, prepared: preparedReport });
416
+ completeFlowRunStage(context, completion, preparedCompletion);
417
+ });
418
+ }
378
419
  export function routeVnextMergeRequest(context, input) {
379
420
  const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
380
- context.db.beginWriteTransaction();
381
- let request;
382
- try {
383
- request = requireRequest(context, input.requestId);
421
+ const request = context.db.writeTransaction(() => {
422
+ const request = requireRequest(context, input.requestId);
384
423
  if (request.project_id !== project.id || request.status !== "queued")
385
424
  throw new AppError("invalid_merge_state", "Only a queued request without an active dispatch may change route", 2, { status: request.status });
386
425
  assertRunMutationAllowed(context, request.project_id, request.run_id);
387
426
  context.db.run("UPDATE merge_requests SET execution_route = ?, last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = 'queued'", [input.mode, JSON.stringify({ route_reason: input.reason }), context.now(), request.merge_request_id]);
388
427
  context.db.run("UPDATE works SET launch_policy = ?, updated_at = ? WHERE work_id = ? AND status = 'created'", [input.mode === "server" ? "fresh_agent_required" : "reuse_allowed", context.now(), request.executor_work_id]);
389
- context.db.exec("COMMIT");
390
- }
391
- catch (error) {
392
- context.db.exec("ROLLBACK");
393
- throw error;
394
- }
428
+ return request;
429
+ });
395
430
  refreshRunWorkProjection(context, request.project_id, request.run_id);
396
431
  return requestView(context, requireRequest(context, request.merge_request_id));
397
432
  }
@@ -431,7 +466,15 @@ export async function repairVnextMerge(context, input) {
431
466
  return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: repair, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } };
432
467
  }
433
468
  function mergePrompt(context, input, checks, settings) { const pause = stagePauseCommand(context, { runId: input.run.id, stage: "merge", workId: input.request.executor_work_id, projectRoot: input.projectRoot }); const repair = repairCommand(context, input.request, input.projectRoot); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(settings.merge_delivery)}`, `- cleanup: ${JSON.stringify(settings.merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...checks.map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual unmerged paths in the integration workspace. Do not repeat merge apply and do not edit product code, tests, documentation or configuration merely to make a gate pass.", `3. Write the compact semantic result to ${input.resultPath}:`, "```json", JSON.stringify({ schema_id: "dd-flow/merge-result@1", outcome: "completed", summary: "What was integrated.", conflict_resolution: "How material conflicts were resolved, or empty when none.", verification_summary: "Why the integrated result is ready for deterministic checks.", residual_risks: [] }, null, 2), "```", `4. Finish with this exact standalone command and wait for all progress: ${finishCommand(context, input.run.id, input.request, input.projectRoot)}`, `If a gate fails, read its receipt and logs and determine the cause. For a product defect use ${repair}; it restores the target baseline and opens CODE → independent CODE-REVIEW → replacement MRG. For an environment failure restore the declared environment in this MERGE and write the returned recovery JSON file and use continuation.command_after_input. The CLI reruns the real gate and retains the old receipt. Never edit product code in the integration target to make a gate pass.`, "If a material conflict has no reasonable answer in accepted evidence, pause this same Work by writing the question file and then running the separate standalone command below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
434
- function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
469
+ function mergeReport(context, run, request, semantic, receipts) {
470
+ const now = context.now();
471
+ const startedAt = request.lock_acquired_at ?? request.created_at;
472
+ const cleanup = cleanupReceiptPath(run);
473
+ return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary,
474
+ semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } },
475
+ mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(request.target_workspace), queue: queueStatus(context, request) },
476
+ artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } };
477
+ }
435
478
  function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
436
479
  function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
437
480
  return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.availability === "available").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }