@deksden-com/dd-flow-cli 0.9.0-beta.103 → 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.
- package/CHANGELOG.md +6 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/run-cli.js +32 -11
- package/dist/harness-runtime/bin/dd-agy.mjs +1 -1
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +2 -2
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +8 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +11 -6
- package/dist/protocol/local-files.js +18 -9
- package/dist/runtime/context.js +2 -2
- package/dist/services/cleanup.js +81 -32
- package/dist/services/code-checks.js +18 -19
- package/dist/services/continuation-outcome.js +1 -1
- package/dist/services/controller-fanout.js +3 -3
- package/dist/services/eval-snapshots.js +69 -41
- package/dist/services/execution-policy.js +12 -1
- package/dist/services/hooks.js +150 -42
- package/dist/services/lanes.js +23 -18
- package/dist/services/lifecycle-command.js +11 -2
- package/dist/services/lifecycle-invocations.js +291 -54
- package/dist/services/managed-processes.js +35 -23
- package/dist/services/merge-queue.js +71 -67
- package/dist/services/merge-server.js +3 -11
- package/dist/services/migrations.js +28 -12
- package/dist/services/plan-runtime.js +8 -4
- package/dist/services/projects.js +3 -11
- package/dist/services/protocols.js +98 -68
- package/dist/services/recovery-snapshot-database.js +43 -8
- package/dist/services/run-control-worker.js +15 -32
- package/dist/services/run-control.js +3 -17
- package/dist/services/run-controller-capture.js +58 -27
- package/dist/services/run-controller-process.js +21 -30
- package/dist/services/run-controller-recovery.js +36 -37
- package/dist/services/run-controller.js +81 -86
- package/dist/services/run-fork.js +10 -6
- package/dist/services/run-projection.js +19 -64
- package/dist/services/run-recovery-runtime.js +24 -14
- package/dist/services/run-recovery.js +56 -54
- package/dist/services/runs.js +218 -160
- package/dist/services/runtime-budget.js +19 -45
- package/dist/services/runtime-scope-capture.js +8 -6
- package/dist/services/runtime-scope-control.js +49 -39
- package/dist/services/runtime-scope-resume.js +134 -122
- package/dist/services/runtime-scope-worker.js +48 -49
- package/dist/services/scope-writer-cleanup.js +26 -0
- package/dist/services/stage-context.js +6 -1
- package/dist/services/stage-lifecycle.js +1 -1
- package/dist/services/stage-pause.js +13 -29
- package/dist/services/stage-report-renderer.js +116 -6
- package/dist/services/usage.js +2 -9
- package/dist/services/vnext-code-review.js +10 -7
- package/dist/services/vnext-code.js +9 -17
- package/dist/services/vnext-merge.js +64 -21
- package/dist/services/vnext-plan-review.js +43 -30
- package/dist/services/vnext-plan.js +28 -24
- package/dist/services/vnext-protocolize.js +183 -20
- package/dist/services/vnext-specify.js +27 -19
- package/dist/services/work-registry.js +229 -150
- package/dist/storage/database.js +286 -56
- package/dist/storage/foreign-key-transaction.js +39 -0
- package/dist/storage/immutable-input.js +45 -0
- package/dist/storage/import-guard.js +102 -0
- package/dist/storage/native-cleanup.js +52 -0
- package/dist/storage/projection-lock.js +73 -0
- package/dist/storage/writer-contract.js +3 -3
- package/dist/storage/writer-migration.js +24 -41
- package/package.json +1 -1
- 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
|
-
|
|
40
|
+
publishImmutableInput(questionPath, Buffer.from(question), "hitl_question_conflict");
|
|
40
41
|
const now = context.now();
|
|
41
|
-
context.db.
|
|
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.
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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);
|
package/dist/services/usage.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
256
|
-
|
|
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 {
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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 {
|
|
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
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
381
|
-
|
|
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
|
-
|
|
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) {
|
|
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}` })); }); }
|