@deksden-com/dd-flow-cli 0.9.0-beta.11 → 0.9.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +2 -1
- package/dist/cli/run-cli.js +4 -2
- package/dist/schemas/code-work-batch.schema.json +1 -1
- package/dist/schemas/code-work-result.schema.json +3 -3
- package/dist/services/code-checks.js +9 -17
- package/dist/services/managed-processes.js +13 -3
- package/dist/services/merge-server.js +1 -1
- package/dist/services/runs.js +61 -1
- package/dist/services/vnext-code-review.js +23 -11
- package/dist/services/vnext-code.js +26 -11
- package/dist/services/vnext-merge.js +68 -10
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/work-registry.js +25 -7
- package/dist/storage/database.js +8 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @deksden-com/dd-flow-cli
|
|
2
2
|
|
|
3
|
+
## 0.9.0-beta.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Make long-running CODE checks suspend-safe and prevent an expired lease from
|
|
8
|
+
stealing a live process. Publish `dd-flow/code-work-result@3`, which unifies
|
|
9
|
+
repair resolutions with evidence references assigned by the Work packet.
|
|
10
|
+
|
|
11
|
+
## 0.9.0-beta.12
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Return a failed integration gate to an explicit source CODE and independent
|
|
16
|
+
CODE-REVIEW repair cycle, instead of allowing product repairs in the
|
|
17
|
+
integration workspace.
|
|
18
|
+
|
|
3
19
|
## 0.9.0-beta.11
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/dist/build-info.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cli_package": "@deksden-com/dd-flow-cli",
|
|
3
|
-
"cli_version": "0.9.0-beta.
|
|
4
|
-
"cli_commit": "
|
|
5
|
-
"built_at": "2026-09-
|
|
3
|
+
"cli_version": "0.9.0-beta.13",
|
|
4
|
+
"cli_commit": "020ada108a59291f4462f7d52d3ea64bccd77b56",
|
|
5
|
+
"built_at": "2026-09-05T08:17:23.882Z",
|
|
6
6
|
"built_with_canon": {
|
|
7
|
-
"version":
|
|
8
|
-
"commit":
|
|
9
|
-
"flow_contract":
|
|
10
|
-
"repo_root":
|
|
11
|
-
"memorybank_root":
|
|
12
|
-
"flow_root":
|
|
13
|
-
"layout":
|
|
7
|
+
"version": null,
|
|
8
|
+
"commit": null,
|
|
9
|
+
"flow_contract": null,
|
|
10
|
+
"repo_root": null,
|
|
11
|
+
"memorybank_root": null,
|
|
12
|
+
"flow_root": null,
|
|
13
|
+
"layout": null
|
|
14
14
|
}
|
|
15
15
|
}
|
package/dist/cli/help.js
CHANGED
|
@@ -369,11 +369,12 @@ Usage:
|
|
|
369
369
|
dd-flow merge request status <MRG-ID> --project-root <root> --json
|
|
370
370
|
dd-flow merge request route <MRG-ID> --mode same_session|server --reason <text> --project-root <root> --json
|
|
371
371
|
dd-flow merge apply <MRG-ID> --work <WRK-ID> --project-root <root> --json --progress-jsonl
|
|
372
|
+
dd-flow merge repair <MRG-ID> --project-root <root> --json
|
|
372
373
|
dd-flow merge serve --agent-profile <id> [--once] [--poll-seconds <n>] [--max-parallel-projects <n>] --progress-jsonl
|
|
373
374
|
dd-flow merge server status --json
|
|
374
375
|
dd-flow merge server stop <MSV-ID> --json
|
|
375
376
|
|
|
376
|
-
CODE or CODE-REVIEW creates one MRG request and child MERGE Work. stage start waits in the shared project FIFO, reports progress, locks the execution target baseline and returns the exact apply/finish commands. merge apply is the only supported initial Git mutation. merge serve is a deterministic dispatcher: it launches a fresh Session through the configured harness adapter but does not perform agent work itself. It preserves FIFO inside each project and may serve different projects concurrently up to the explicit bound.`
|
|
377
|
+
CODE or CODE-REVIEW creates one MRG request and child MERGE Work. stage start waits in the shared project FIFO, reports progress, locks the execution target baseline and returns the exact apply/finish commands. merge apply is the only supported initial Git mutation. If its integration gate fails, merge repair restores the target baseline and creates a source CODE → independent CODE-REVIEW → replacement-MRG cycle; it never patches product code in the integration workspace. merge serve is a deterministic dispatcher: it launches a fresh Session through the configured harness adapter but does not perform agent work itself. It preserves FIFO inside each project and may serve different projects concurrently up to the explicit bound.`
|
|
377
378
|
],
|
|
378
379
|
[
|
|
379
380
|
"cleanup",
|
package/dist/cli/run-cli.js
CHANGED
|
@@ -39,7 +39,7 @@ import { finishVnextPlan, isVnextPlanRun, startVnextPlan } from "../services/vne
|
|
|
39
39
|
import { dispatchVnextPlanReview, finishVnextPlanReview, isVnextPlanReviewRun, recordVnextPlanReviewCapacity, startVnextPlanReview } from "../services/vnext-plan-review.js";
|
|
40
40
|
import { addVnextCodeRepair, finishVnextCode, startVnextCode } from "../services/vnext-code.js";
|
|
41
41
|
import { addVnextCodeReviewRepair, finishVnextCodeReview, isVnextCodeReviewRun, startVnextCodeReview } from "../services/vnext-code-review.js";
|
|
42
|
-
import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
|
|
42
|
+
import { applyVnextMerge, finishVnextMerge, getVnextMergeRequest, isVnextMergeRun, repairVnextMerge, routeVnextMergeRequest, startVnextMerge } from "../services/vnext-merge.js";
|
|
43
43
|
import { mergeServerStatus, serveMergeRequests, stopMergeServer } from "../services/merge-server.js";
|
|
44
44
|
import { getVnextFanoutStatus } from "../services/vnext-fanout.js";
|
|
45
45
|
import { pauseStageForUser, resumeStageAfterUser } from "../services/stage-pause.js";
|
|
@@ -1247,6 +1247,8 @@ async function dispatchMerge(context, command, parsed, progress) {
|
|
|
1247
1247
|
}
|
|
1248
1248
|
if (command === "apply")
|
|
1249
1249
|
return applyVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id"), workId: requiredOption(parsed, "work"), ...(progress ? { progress } : {}) });
|
|
1250
|
+
if (command === "repair")
|
|
1251
|
+
return await repairVnextMerge(context, { projectRoot: requiredOption(parsed, "project-root"), requestId: requiredPosition(parsed, 0, "merge-request-id") });
|
|
1250
1252
|
if (command === "request") {
|
|
1251
1253
|
const action = requiredPosition(parsed, 0, "request action");
|
|
1252
1254
|
const requestId = requiredPosition(parsed, 1, "merge-request-id");
|
|
@@ -2324,7 +2326,7 @@ function projectRootForMutation(context, args, result, resolvedScope) {
|
|
|
2324
2326
|
if (family === "merge-queue") {
|
|
2325
2327
|
return projectRootForMergeQueueMutation(context, command, parsed);
|
|
2326
2328
|
}
|
|
2327
|
-
if (family === "merge" && ["one-shot", "apply", "request"].includes(command ?? "")) {
|
|
2329
|
+
if (family === "merge" && ["one-shot", "apply", "repair", "request"].includes(command ?? "")) {
|
|
2328
2330
|
return requiredOption(parsed, "project-root");
|
|
2329
2331
|
}
|
|
2330
2332
|
if (family === "merge-worker" && ["start", "stop"].includes(command ?? "")) {
|
|
@@ -20,6 +20,6 @@
|
|
|
20
20
|
"documentUpdate": {"type": "object", "additionalProperties": false, "required": ["path", "action", "owner", "reason", "baseline_sha256"], "properties": {"path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update"]}, "owner": {"type": "string", "minLength": 1}, "reason": {"type": "string", "minLength": 1}, "baseline_sha256": {"type": ["string", "null"], "pattern": "^[a-f0-9]{64}$"}}},
|
|
21
21
|
"reviewFinding": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "priority", "problem", "impact", "required_outcome", "evidence_refs", "obligation_refs", "decision_reason", "check_refs"], "properties": {"finding_ref": {"type": "string", "minLength": 1}, "priority": {"enum": ["p0", "p1", "p2", "p3"]}, "problem": {"type": "string", "minLength": 1}, "impact": {"type": "string", "minLength": 1}, "required_outcome": {"type": "string", "minLength": 1}, "evidence_refs": {"$ref": "#/$defs/strings"}, "obligation_refs": {"$ref": "#/$defs/strings"}, "decision_reason": {"type": "string", "minLength": 1}, "check_refs": {"$ref": "#/$defs/strings"}}},
|
|
22
22
|
"repair": {"type": "object", "additionalProperties": false, "required": ["origin_work_ids"], "anyOf": [{"required": ["check_receipt_id", "failure_receipt_path"]}, {"required": ["review_findings"]}, {"required": ["semantic_unresolved", "verification_path"]}], "properties": {"origin_work_ids": {"$ref": "#/$defs/strings"}, "check_receipt_id": {"type": "string", "minLength": 1}, "failure_receipt_path": {"type": "string", "minLength": 1}, "review_findings": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/reviewFinding"}}, "semantic_unresolved": {"$ref": "#/$defs/strings"}, "verification_path": {"type": "string", "minLength": 1}}},
|
|
23
|
-
"work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@
|
|
23
|
+
"work": {"type": "object", "additionalProperties": false, "required": ["schema_id", "key", "launch_policy", "source", "task", "semantic_spine", "requirements", "acceptance", "document_updates", "required_read", "discovery_boundary", "planned_write_areas", "checks", "provides_checks", "stop_conditions", "depends_on", "result_schema"], "properties": {"schema_id": {"const": "dd-flow/code-work-packet@5"}, "key": {"type": "string", "minLength": 1}, "launch_policy": {"const": "fresh_agent_required"}, "source": {"$ref": "#/$defs/source"}, "repair": {"$ref": "#/$defs/repair"}, "task": {"type": "string", "minLength": 1}, "semantic_spine": {"$ref": "#/$defs/spine"}, "requirements": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/obligation"}}, "acceptance": {"type": "array", "items": {"$ref": "#/$defs/acceptance"}}, "document_updates": {"type": "array", "items": {"$ref": "#/$defs/documentUpdate"}}, "required_read": {"$ref": "#/$defs/strings", "description": "Mandatory starting sources, not a read allowlist."}, "discovery_boundary": {"$ref": "#/$defs/strings", "description": "Likely discovery areas; additional project-local reads are allowed."}, "planned_write_areas": {"type": "array", "items": {"type": "string", "minLength": 1}, "description": "Soft coordination hints only. A worker may change any necessary project file inside the RUN workspace."}, "checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "provides_checks": {"type": "array", "items": {"$ref": "#/$defs/check"}}, "stop_conditions": {"$ref": "#/$defs/strings", "description": "Hard semantic or external stop conditions; path prediction drift is not a stop condition."}, "depends_on": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}, "result_schema": {"const": "dd-flow/code-work-result@3"}}}
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "dd-flow/code-work-result@
|
|
3
|
+
"$id": "dd-flow/code-work-result@3",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"additionalProperties": false,
|
|
6
6
|
"required": ["schema_id", "summary", "changed_paths", "evidence", "deviations", "blockers"],
|
|
7
7
|
"properties": {
|
|
8
|
-
"schema_id": {"const": "dd-flow/code-work-result@
|
|
8
|
+
"schema_id": {"const": "dd-flow/code-work-result@3"},
|
|
9
9
|
"summary": {"type": "string", "minLength": 1},
|
|
10
10
|
"changed_paths": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
|
|
11
|
-
"evidence": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["
|
|
11
|
+
"evidence": {"type": "array", "items": {"type": "object", "additionalProperties": false, "required": ["obligation_ref", "refs"], "properties": {"obligation_ref": {"type": "string", "minLength": 1}, "refs": {"type": "array", "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}},
|
|
12
12
|
"deviations": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
|
13
13
|
"blockers": {"type": "array", "items": {"type": "string", "minLength": 1}},
|
|
14
14
|
"resolutions": {"type": "array", "uniqueItems": true, "items": {"type": "object", "additionalProperties": false, "required": ["finding_ref", "summary", "evidence_refs"], "properties": {"finding_ref": {"type": "string", "minLength": 1}, "summary": {"type": "string", "minLength": 1}, "evidence_refs": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string", "minLength": 1}}}}}
|
|
@@ -155,7 +155,10 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
155
155
|
const process = processes.filter((item) => item.check_id === pendingReceipt.id).at(-1);
|
|
156
156
|
const receipt = readReceipt(pendingReceipt.receipt_path);
|
|
157
157
|
const completion = receipt ? readCompletion(receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json")) : null;
|
|
158
|
-
|
|
158
|
+
// A stale lease after host sleep is not permission to duplicate a living
|
|
159
|
+
// check. Its durable completion marker or explicit reconciliation decides
|
|
160
|
+
// the outcome before another invocation may start.
|
|
161
|
+
if (process && ["starting", "running", "stopping"].includes(process.state) && (process.state === "starting" || processIsAlive(process))) {
|
|
159
162
|
throw new AppError("check_in_progress", "A previous check invocation is still running", 1, { check_receipt_id: pendingReceipt.id, process_id: process.id, pid: process.pid });
|
|
160
163
|
}
|
|
161
164
|
if (!process && !completion && Date.now() - Date.parse(pendingReceipt.started_at) < receiptStartingGraceMs) {
|
|
@@ -212,7 +215,6 @@ function readCompletion(file) { try {
|
|
|
212
215
|
catch {
|
|
213
216
|
return null;
|
|
214
217
|
} }
|
|
215
|
-
function processLeaseIsCurrent(process) { return Date.parse(process.lease_expires_at ?? "") >= Date.now(); }
|
|
216
218
|
export function unchangedFinalGateFailures(context, input) { const wanted = new Set(input.declarations.flatMap((check) => [check.id, checkRef(check)])); const current = workspaceFingerprint(input.workspaceRoot); const latest = new Map(); for (const receipt of checkReceipts(context, { projectId: input.projectId, runId: input.runId }).filter((item) => item.scope === "aggregate" && item.check_refs.some((ref) => wanted.has(ref))))
|
|
217
219
|
for (const ref of receipt.check_refs)
|
|
218
220
|
latest.set(ref, receipt); return [...new Set(latest.values())].filter((receipt) => receipt.status === "failed" && receipt.before_fingerprint === current); }
|
|
@@ -277,24 +279,14 @@ catch (error) {
|
|
|
277
279
|
terminateProcessGroup(child.pid, "SIGTERM");
|
|
278
280
|
resolve({ exitCode: null, error: error instanceof Error ? error.message : String(error), aborted: true });
|
|
279
281
|
return;
|
|
280
|
-
}
|
|
281
|
-
clearTimeout(timeout); timeout = setTimeout(() => { timedOut = true; terminateProcessGroup(child.pid, "SIGTERM"); escalation = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 5_000); }, inactivityMs); }; const progress = setInterval(() => { const elapsed = Math.floor((Date.now() - started) / 1000); if (!renewLease()) {
|
|
282
|
+
} let leaseLost = false; let error = null; let escalation; const progress = setInterval(() => { const elapsed = Math.floor((Date.now() - started) / 1000); if (!renewLease()) {
|
|
282
283
|
error = "managed process lease lost";
|
|
283
|
-
|
|
284
|
+
leaseLost = true;
|
|
284
285
|
terminateProcessGroup(child.pid, "SIGTERM");
|
|
286
|
+
escalation = setTimeout(() => terminateProcessGroup(child.pid, "SIGKILL"), 5_000);
|
|
285
287
|
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const size = fs.fstatSync(file).size, previous = lastOutputSize.get(file) ?? 0;
|
|
289
|
-
if (size > previous) {
|
|
290
|
-
lastOutputSize.set(file, size);
|
|
291
|
-
armInactivityTimeout();
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
catch { /* terminal stream */ }
|
|
295
|
-
} heartbeat(elapsed); }, 15_000); let timedOut = false; let error = null; let escalation; armInactivityTimeout(); child.once("error", (value) => { error = value.message; }); child.once("close", (exitCode, signal) => { clearInterval(progress); if (timeout)
|
|
296
|
-
clearTimeout(timeout); if (escalation)
|
|
297
|
-
clearTimeout(escalation); resolve({ exitCode, error: error ?? (timedOut ? "timed out after 900 seconds without process output" : signal ? `terminated by ${signal}` : null), aborted: timedOut || Boolean(signal) || Boolean(error) }); }); }); }
|
|
288
|
+
} heartbeat(elapsed); }, 15_000); child.once("error", (value) => { error = value.message; }); child.once("close", (exitCode, signal) => { clearInterval(progress); if (escalation)
|
|
289
|
+
clearTimeout(escalation); resolve({ exitCode, error: error ?? (signal ? `terminated by ${signal}` : null), aborted: leaseLost || Boolean(signal) || Boolean(error) }); }); }); }
|
|
298
290
|
function checkShellScript() {
|
|
299
291
|
return [
|
|
300
292
|
'completion="${DD_FLOW_CHECK_COMPLETION_FILE:-}"',
|
|
@@ -54,6 +54,8 @@ export function processIsAlive(record) {
|
|
|
54
54
|
return !record.pid_started_at || record.pid_started_at === processStartedAt(record.pid);
|
|
55
55
|
}
|
|
56
56
|
function processTreeIsAlive(record) {
|
|
57
|
+
if (processIsAlive(record))
|
|
58
|
+
return true;
|
|
57
59
|
const group = parseMetadata(record.metadata_json).process_group_id;
|
|
58
60
|
if (process.platform !== "win32" && typeof group === "number") {
|
|
59
61
|
try {
|
|
@@ -64,9 +66,13 @@ function processTreeIsAlive(record) {
|
|
|
64
66
|
return false;
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
|
-
return
|
|
69
|
+
return false;
|
|
68
70
|
}
|
|
69
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Expiry proves that observation stopped, not that the owner died. A live PID
|
|
73
|
+
* remains owned until its result is reconciled; only a dead process can be
|
|
74
|
+
* atomically claimed for cleanup.
|
|
75
|
+
*/
|
|
70
76
|
export function claimExpiredManagedProcesses(context, ownerId) {
|
|
71
77
|
const db = registry(context);
|
|
72
78
|
const now = context.now();
|
|
@@ -75,6 +81,8 @@ export function claimExpiredManagedProcesses(context, ownerId) {
|
|
|
75
81
|
const candidates = db.all("SELECT * FROM managed_processes WHERE state IN ('starting','running','stopping','orphaned') AND lease_expires_at < ? ORDER BY lease_expires_at, id", [now]);
|
|
76
82
|
const claimed = [];
|
|
77
83
|
for (const candidate of candidates) {
|
|
84
|
+
if (processTreeIsAlive(candidate))
|
|
85
|
+
continue;
|
|
78
86
|
const token = crypto.randomUUID();
|
|
79
87
|
const result = db.run("UPDATE managed_processes SET owner_id = ?, lease_token = ?, state = 'orphaned', lease_expires_at = ?, updated_at = ? WHERE id = ? AND lease_token = ? AND state IN ('starting','running','stopping','orphaned') AND lease_expires_at < ?", [ownerId, token, leaseExpiry(now), now, candidate.id, candidate.lease_token, now]);
|
|
80
88
|
if (result.changes === 1)
|
|
@@ -150,7 +158,9 @@ function processStartedAt(pid) { const result = spawnSync("ps", ["-o", "lstart="
|
|
|
150
158
|
function releasePortClaims(db, claims) { for (const claim of claims)
|
|
151
159
|
db.run("DELETE FROM managed_resources WHERE resource_kind = 'port' AND resource_key = ? AND lease_token = ?", [claim.key, claim.token]); }
|
|
152
160
|
function terminateOwnedProcess(record, signal) {
|
|
153
|
-
|
|
161
|
+
// Never signal a group merely because its former leader PID was once ours.
|
|
162
|
+
// PID reuse or a departed leader makes group ownership unprovable.
|
|
163
|
+
if (!record.pid || !record.pid_started_at || !processIsAlive(record))
|
|
154
164
|
return;
|
|
155
165
|
const group = parseMetadata(record.metadata_json).process_group_id;
|
|
156
166
|
try {
|
|
@@ -102,7 +102,7 @@ async function dispatch(context, input) {
|
|
|
102
102
|
clearInterval(leaseHeartbeat);
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
function nextServerRequests(context, limit) { const selected = new Set(); return context.db.all("SELECT merge_request_id, project_id, run_id, executor_work_id, target_workspace, execution_route, status, created_at FROM merge_requests WHERE status = 'queued' AND execution_route = 'server' ORDER BY created_at, merge_request_id").filter((request) => { if (selected.has(request.project_id) || context.db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?)) LIMIT 1", [request.project_id, request.created_at, request.created_at, request.merge_request_id]))
|
|
105
|
+
function nextServerRequests(context, limit) { const selected = new Set(); return context.db.all("SELECT merge_request_id, project_id, run_id, executor_work_id, target_workspace, execution_route, status, created_at FROM merge_requests WHERE status = 'queued' AND execution_route = 'server' ORDER BY created_at, merge_request_id").filter((request) => { if (selected.has(request.project_id) || context.db.get("SELECT 1 FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?)) LIMIT 1", [request.project_id, request.created_at, request.created_at, request.merge_request_id]))
|
|
106
106
|
return false; selected.add(request.project_id); return true; }).slice(0, limit); }
|
|
107
107
|
function reconcileExpiredDispatches(context) { const expired = context.db.all("SELECT merge_request_id, executor_work_id FROM merge_requests WHERE status = 'dispatching' AND dispatch_lease_expires_at < ?", [context.now()]); for (const request of expired) {
|
|
108
108
|
const work = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
|
package/dist/services/runs.js
CHANGED
|
@@ -15,7 +15,7 @@ import { refreshRunSessionProjection } from "./run-projection.js";
|
|
|
15
15
|
import { resolveCanonRoot } from "./canon.js";
|
|
16
16
|
import { bindCurrentEngineToRun } from "./engines.js";
|
|
17
17
|
import { executionProfilePath, loadVnextExecutionProfile } from "./vnext-execution-profile.js";
|
|
18
|
-
import { isLegalVnextTransition, vnextStages } from "../domain/stage-catalog.js";
|
|
18
|
+
import { isLegalVnextTransition, vnextStageDirectory, vnextStages } from "../domain/stage-catalog.js";
|
|
19
19
|
import { publicSessionIdentity } from "./session-identity.js";
|
|
20
20
|
const runSchemaId = "dd-flow/flow-run@3";
|
|
21
21
|
const runtimeSchemaId = "dd-flow/flow-run@3";
|
|
@@ -754,6 +754,66 @@ export function advanceFlowRun(context, input) {
|
|
|
754
754
|
const updatedRun = requireRunById(context, project.id, run.id);
|
|
755
755
|
return { ok: true, run: flowRunSummary(updatedRun), index, flow_guidance: guidanceForRun(context, updatedRun, index) };
|
|
756
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* The only legal backward edge in vNext: an integration gate exposed a
|
|
759
|
+
* product defect, so the source must be repaired and reviewed again. Normal
|
|
760
|
+
* stage start remains fail-closed for completed stages.
|
|
761
|
+
*/
|
|
762
|
+
export function prepareVnextMergeSourceRepairAttempt(context, input) {
|
|
763
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
764
|
+
const run = resolveRun(context, project.id, input.runId);
|
|
765
|
+
const index = authoritativeIndex(run);
|
|
766
|
+
const code = index.stage_runs.find((item) => item.stage === "code");
|
|
767
|
+
const review = index.stage_runs.find((item) => item.stage === "code-review");
|
|
768
|
+
const merge = index.stage_runs.find((item) => item.stage === "merge");
|
|
769
|
+
if (!code || code.status !== "done" || !review || review.status !== "done" || !merge || merge.status !== "running") {
|
|
770
|
+
throw new AppError("invalid_merge_repair_state", "Source repair requires accepted CODE/CODE-REVIEW and a running MERGE attempt", 2, {
|
|
771
|
+
run_id: run.id,
|
|
772
|
+
code: code?.status ?? null,
|
|
773
|
+
code_review: review?.status ?? null,
|
|
774
|
+
merge: merge?.status ?? null
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
const root = runArtifactRoot(run);
|
|
778
|
+
for (const stage of ["code", "code-review", "merge"])
|
|
779
|
+
archiveExistingStageAttempt(root, vnextStageDirectory(stage));
|
|
780
|
+
const now = context.now();
|
|
781
|
+
const restarted = {
|
|
782
|
+
...code,
|
|
783
|
+
status: "running",
|
|
784
|
+
started_at: now,
|
|
785
|
+
updated_at: now,
|
|
786
|
+
attempt: nextAttempt(code)
|
|
787
|
+
};
|
|
788
|
+
delete restarted.completed_at;
|
|
789
|
+
delete restarted.duration_ms;
|
|
790
|
+
delete restarted.stage_report;
|
|
791
|
+
delete restarted.data;
|
|
792
|
+
delete restarted.report;
|
|
793
|
+
delete restarted.artifact_aliases;
|
|
794
|
+
const cycle = `MRR-${input.mergeRequestId}`;
|
|
795
|
+
index.stage_runs = index.stage_runs.filter((item) => item.stage !== "code" && item.stage !== "code-review" && item.stage !== "merge");
|
|
796
|
+
index.stage_runs.push(restarted);
|
|
797
|
+
index.variables = { ...(index.variables ?? {}), "merge.source_repair": { cycle, merge_request_id: input.mergeRequestId, prepared_at: now } };
|
|
798
|
+
index.status = "running";
|
|
799
|
+
index.verdict = "merge_source_repair";
|
|
800
|
+
index.next_action = "start_code_repair";
|
|
801
|
+
index.current_stage = "code";
|
|
802
|
+
index.updated_at = now;
|
|
803
|
+
index.attempts = index.stage_runs.map((item) => ({
|
|
804
|
+
stage: item.stage,
|
|
805
|
+
attempt_number: Number((item.attempt ?? "try-001").replace("try-", "")) || 1,
|
|
806
|
+
root: item.dir,
|
|
807
|
+
archive: null,
|
|
808
|
+
status: item.status,
|
|
809
|
+
started_at: item.started_at ?? now,
|
|
810
|
+
finished_at: item.completed_at ?? null
|
|
811
|
+
}));
|
|
812
|
+
persistRunState(context, project, run, index);
|
|
813
|
+
appendAudit(context, { projectId: project.id, eventType: "flow_run.merge_source_repair_prepared", payload: { run_id: run.id, merge_request_id: input.mergeRequestId, cycle } });
|
|
814
|
+
appendRunTimeline(root, { at: now, type: "merge_source_repair_prepared", run_id: run.id, merge_request_id: input.mergeRequestId, cycle });
|
|
815
|
+
return { run_id: run.id, cycle };
|
|
816
|
+
}
|
|
757
817
|
function closeOpenStagesForOverride(index, status, now) {
|
|
758
818
|
const stageStatus = status === "cancelled" ? "skipped" : "failed";
|
|
759
819
|
for (const stage of index.stage_runs) {
|
|
@@ -48,10 +48,11 @@ export function startVnextCodeReview(context, input) {
|
|
|
48
48
|
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prior, id: binding.work_session_id, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), ...(orchestration ? { orchestration } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
49
49
|
}
|
|
50
50
|
const mode = effectiveMode(home, run);
|
|
51
|
+
const cycle = reviewCycle(run);
|
|
51
52
|
freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
|
|
52
53
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
53
54
|
const reviewContextPath = path.join(root, "review-context.json");
|
|
54
|
-
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, workspace_fingerprint: workspaceFingerprint(run.workspace_root), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
|
|
55
|
+
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, workspace_fingerprint: workspaceFingerprint(run.workspace_root), code_report_sha256: sha256File(path.join(home, "05-code", "stage-report.json")) }, null, 2)}\n`);
|
|
55
56
|
if (mode.mode === "off") {
|
|
56
57
|
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`;
|
|
57
58
|
fs.writeFileSync(promptPath, prompt);
|
|
@@ -61,7 +62,7 @@ export function startVnextCodeReview(context, input) {
|
|
|
61
62
|
return { ok: true, run_id: run.id, stage, mode: mode.mode, mode_source: mode.source, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
62
63
|
}
|
|
63
64
|
const groups = reviewGroups(home);
|
|
64
|
-
const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", group, read_only: true } })) };
|
|
65
|
+
const batch = { works: groups.map((group, index) => ({ key: `code-review-${index + 1}`, task: reviewerTask(home, group), launch_policy: "fresh_agent_required", result_schema: "dd-flow/code-review-result@1", payload: { kind: "code-review", review_cycle: cycle, group, read_only: true } })) };
|
|
65
66
|
const batchFile = path.join(root, "review-work-batch.json");
|
|
66
67
|
fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
|
|
67
68
|
const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups });
|
|
@@ -79,7 +80,7 @@ export function addVnextCodeReviewRepair(context, input) {
|
|
|
79
80
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
80
81
|
const project = requireProjectByRoot(context, projectRoot);
|
|
81
82
|
const run = requireRun(context, project.id, input.runId);
|
|
82
|
-
const reviewers = reviewerWorks(context, project.id, run.id);
|
|
83
|
+
const reviewers = reviewerWorks(context, project.id, run.id, reviewCycle(run));
|
|
83
84
|
const findings = canonicalCodeFindings(reviewers);
|
|
84
85
|
const selected = findings.filter((finding) => input.findingIds.includes(finding.finding_ref));
|
|
85
86
|
if (selected.length !== new Set(input.findingIds).size)
|
|
@@ -108,7 +109,8 @@ export function addVnextCodeReviewRepair(context, input) {
|
|
|
108
109
|
reviewFindings: selected.map(({ finding_ref, finding }) => ({ finding_ref, priority: finding.priority, problem: finding.problem, impact: finding.impact, required_outcome: finding.required_outcome, evidence_refs: finding.evidence_refs, obligation_refs: finding.obligation_refs, decision_reason: input.decisionReasonsByFinding?.[finding_ref] ?? input.objective, check_refs: input.checkRefsByFinding[finding_ref] ?? [] })),
|
|
109
110
|
reviewChecks,
|
|
110
111
|
originWorkIds: origins,
|
|
111
|
-
objective: input.objective
|
|
112
|
+
objective: input.objective,
|
|
113
|
+
reviewCycle: reviewCycle(run)
|
|
112
114
|
});
|
|
113
115
|
}
|
|
114
116
|
export async function finishVnextCodeReview(context, input) {
|
|
@@ -140,7 +142,8 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
140
142
|
fs.writeFileSync(decisionFile, `${JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] }, null, 2)}\n`);
|
|
141
143
|
validateSchema({ schemaName: "code-review-decision", file: decisionFile, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
142
144
|
const decision = readJson(decisionFile);
|
|
143
|
-
const
|
|
145
|
+
const cycle = reviewContext.cycle ?? "initial";
|
|
146
|
+
const reviewers = reviewerWorks(context, project.id, run.id, cycle);
|
|
144
147
|
if (mode !== "off") {
|
|
145
148
|
const incomplete = reviewers.filter((work) => work.status !== "completed");
|
|
146
149
|
if (incomplete.length)
|
|
@@ -149,7 +152,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
149
152
|
validateReviewerResult(context, work, run);
|
|
150
153
|
const canonical = reviewDecisionForRepair(decision, reviewers);
|
|
151
154
|
const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
|
|
152
|
-
let repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
155
|
+
let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
153
156
|
const decisionSha = sha256File(decisionFile);
|
|
154
157
|
const frozenShaFile = path.join(root, ".decision-sha256");
|
|
155
158
|
if (fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== decisionSha)
|
|
@@ -160,14 +163,14 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
160
163
|
fs.writeFileSync(frozenShaFile, `${decisionSha}\n`);
|
|
161
164
|
return { ok: true, run_id: run.id, stage, outcome: "repair_required", decision_sha256: decisionSha, repair, instruction: "Run the returned repair Work in one fresh child session. When it completes, invoke the same stage finish command again with the unchanged decision file.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
|
|
162
165
|
}
|
|
163
|
-
repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
166
|
+
repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
164
167
|
const incompleteRepairs = repairs.filter((work) => work.status !== "completed");
|
|
165
168
|
if (incompleteRepairs.length)
|
|
166
169
|
throw new AppError("review_repair_incomplete", "CODE-REVIEW cannot finish while a repair Work is unsettled", 2, { works: incompleteRepairs.map((work) => ({ work_id: work.work_id, status: work.status })) });
|
|
167
170
|
validateDecision(context, { decision: canonical.decision, reviewers: canonical.reviewers, repairs, workspaceRoot: run.workspace_root });
|
|
168
171
|
}
|
|
169
172
|
const outcome = decision.findings.some((item) => item.disposition === "defer") ? "accepted_with_DEF" : "accepted";
|
|
170
|
-
const repairs = reviewRepairWorks(context, project.id, run.id, root);
|
|
173
|
+
const repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
171
174
|
const finalChecks = finalCodeCheckDeclarations(run.workspace_root, codeWorks(context, project.id, run.id).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }));
|
|
172
175
|
const unchangedFailures = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: run.workspace_root, declarations: finalChecks });
|
|
173
176
|
if (unchangedFailures.length)
|
|
@@ -320,7 +323,7 @@ function validateReviewerResult(_context, work, _run) {
|
|
|
320
323
|
if (result.aspects.some((aspect) => aspect.verdict === "blocked"))
|
|
321
324
|
throw new AppError("review_blocked", "A blocked review aspect must be resolved before CODE-REVIEW can finish", 2, { work_id: work.work_id });
|
|
322
325
|
}
|
|
323
|
-
function reviewerWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const
|
|
326
|
+
function reviewerWorks(context, projectId, runId, cycle) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const payload = readJsonString(work.payload_json); return Boolean(payload.kind === "code-review" && (typeof payload.review_cycle === "string" ? payload.review_cycle : "initial") === cycle); }); }
|
|
324
327
|
function codeWorks(context, projectId, runId) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).filter((work) => readJsonString(work.payload_json).schema_id === "dd-flow/code-work-packet@5" && !reviewFindingIds(work).length); }
|
|
325
328
|
function acceptedCodeChecks(context, projectId, runId) { return codeWorks(context, projectId, runId).flatMap((work) => { const payload = readJsonString(work.payload_json); return Array.isArray(payload.checks) ? payload.checks : []; }); }
|
|
326
329
|
function validateRepairChecks(context, input) { const known = new Set(acceptedCodeChecks(context, input.projectId, input.runId).map((check) => check.id)); const selected = {}; for (const item of input.decision.findings.filter((item) => item.disposition === "fix")) {
|
|
@@ -332,7 +335,7 @@ function validateRepairChecks(context, input) { const known = new Set(acceptedCo
|
|
|
332
335
|
throw new AppError("review_check_reference_unknown", "CODE-REVIEW repair selects a check absent from the accepted CODE handoff", 2, { finding_ref: item.finding_ref, check_refs: unknown });
|
|
333
336
|
selected[item.finding_ref] = refs;
|
|
334
337
|
} return selected; }
|
|
335
|
-
function reviewRepairWorks(context, projectId, runId, root) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => isCodeReviewStageRepair(work, root)); }
|
|
338
|
+
function reviewRepairWorks(context, projectId, runId, root, cycle) { return context.db.all("SELECT * FROM works WHERE project_id = ? AND run_id = ? ORDER BY created_at, work_id", [projectId, runId]).filter((work) => { const payload = readJsonString(work.payload_json); return isCodeReviewStageRepair(work, root) && (typeof payload.review_cycle === "string" ? payload.review_cycle : "initial") === cycle; }); }
|
|
336
339
|
export function isCodeReviewStageRepair(work, root) {
|
|
337
340
|
if (reviewFindingIds(work).length)
|
|
338
341
|
return true;
|
|
@@ -350,7 +353,8 @@ function reviewFindingIds(work) { const repair = readJsonString(work.payload_jso
|
|
|
350
353
|
function reviewCheckRefs(work) { const repair = readJsonString(work.payload_json).repair; if (!repair || typeof repair !== "object" || Array.isArray(repair))
|
|
351
354
|
return []; const findings = repair.review_findings; return Array.isArray(findings) ? [...new Set(findings.flatMap((value) => typeof value === "object" && value !== null && Array.isArray(value.check_refs) ? value.check_refs.filter((ref) => typeof ref === "string") : []))] : []; }
|
|
352
355
|
function canonicalCodeFindings(works) { return works.flatMap((work) => readJsonString(work.result).findings.map((finding) => ({ finding_ref: `${work.work_id}/${finding.finding_id}`, finding }))); }
|
|
353
|
-
function effectiveMode(home, run) { const index = JSON.parse(run.index_json);
|
|
356
|
+
function effectiveMode(home, run) { const index = JSON.parse(run.index_json); if (index.variables?.["merge.source_repair"])
|
|
357
|
+
return { mode: "standard", source: "project_policy", reason: "A failed integration gate requires independent review of its source repair." }; const configured = index.settings?.code_review; const requested = configured?.mode ?? "auto"; if (requested !== "auto")
|
|
354
358
|
return { mode: requested, source: configured?.source === "user_instruction" ? "user_instruction" : "project_policy", reason: configured?.reason ?? "Frozen RUN configuration." }; const assessments = findFiles(path.join(home, "03-plan"), "plan.json").map((file) => readJson(file)); const deep = assessments.some((plan) => plan.assessment?.failure_impact?.level === "high" || plan.assessment?.solution_uncertainty?.level === "high"); return { mode: deep ? "deep" : "standard", source: "plan_assessment", reason: deep ? "Accepted PLAN assessment records high impact or uncertainty." : "Accepted PLAN assessment requires the standard independent review." }; }
|
|
355
359
|
function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
|
|
356
360
|
function changedPaths(workspaceRoot) { try {
|
|
@@ -366,6 +370,14 @@ function stageStatus(run, name) { try {
|
|
|
366
370
|
catch {
|
|
367
371
|
return null;
|
|
368
372
|
} }
|
|
373
|
+
function reviewCycle(run) { try {
|
|
374
|
+
const value = JSON.parse(run.index_json);
|
|
375
|
+
const repair = value.variables?.["merge.source_repair"];
|
|
376
|
+
return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.cycle === "string" ? String(repair.cycle) : "initial";
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return "initial";
|
|
380
|
+
} }
|
|
369
381
|
function stageStartedAt(run, fallback) { try {
|
|
370
382
|
return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === stage)?.started_at ?? fallback;
|
|
371
383
|
}
|
|
@@ -31,6 +31,7 @@ export async function startVnextCode(context, input) {
|
|
|
31
31
|
const existingRoot = path.join(home, stageDir);
|
|
32
32
|
const existingPrompt = path.join(existingRoot, "stage-prompt.md");
|
|
33
33
|
const existingStage = stageStatus(run, stage);
|
|
34
|
+
const sourceRepairAttempt = isMergeSourceRepairAttempt(run);
|
|
34
35
|
if (existingStage && fs.existsSync(existingPrompt)) {
|
|
35
36
|
const externalContext = applyExternalStageContext({ stageRoot: existingRoot, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
36
37
|
const prompt = fs.readFileSync(existingPrompt, "utf8");
|
|
@@ -73,14 +74,16 @@ export async function startVnextCode(context, input) {
|
|
|
73
74
|
resultPath: path.join(root, "stage-report.json"),
|
|
74
75
|
...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
|
|
75
76
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
77
|
+
if (!sourceRepairAttempt) {
|
|
78
|
+
attachFlowRunStage(context, {
|
|
79
|
+
projectRoot,
|
|
80
|
+
runId: input.runId,
|
|
81
|
+
stage,
|
|
82
|
+
dir: stageDir,
|
|
83
|
+
status: "running",
|
|
84
|
+
dataSchemaId: "dd-flow/stage-report@1"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
84
87
|
const graph = codeWorkGraph(context, project.id, input.runId);
|
|
85
88
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
86
89
|
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
|
|
@@ -357,7 +360,8 @@ export function addVnextCodeRepair(context, input) {
|
|
|
357
360
|
...(receipt ? ["Stop and report the blocker if the proposed repair contradicts an accepted requirement or non-goal."] : ["Do not modify accepted PLAN artifacts, code-work-batch.json, or review decisions. Stop and report if the finding cannot be repaired within accepted scope."])
|
|
358
361
|
]),
|
|
359
362
|
depends_on: origins.map((work) => work.work_id),
|
|
360
|
-
result_schema: "dd-flow/code-work-result@
|
|
363
|
+
result_schema: "dd-flow/code-work-result@3",
|
|
364
|
+
...(input.reviewCycle ? { review_cycle: input.reviewCycle } : {})
|
|
361
365
|
};
|
|
362
366
|
const file = path.join(home, stageDir, `.repair-${crypto.randomUUID()}.json`);
|
|
363
367
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -545,8 +549,8 @@ export function verificationProjection(works, receipts, options = {}) {
|
|
|
545
549
|
for (const work of works) {
|
|
546
550
|
const result = readJsonResult(work.result);
|
|
547
551
|
for (const item of result.evidence ?? [])
|
|
548
|
-
if (item.
|
|
549
|
-
rawEvidence.set(item.
|
|
552
|
+
if (item.obligation_ref)
|
|
553
|
+
rawEvidence.set(item.obligation_ref, unique([...(rawEvidence.get(item.obligation_ref) ?? []), ...(item.refs ?? [])]));
|
|
550
554
|
}
|
|
551
555
|
const acceptance = uniqueBy(packets.flatMap((value) => value.acceptance), (value) => value.criterion_id);
|
|
552
556
|
const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
|
|
@@ -726,6 +730,8 @@ export function writeProtocolFlowStatus(workspaceRoot, runHome, runId, status) {
|
|
|
726
730
|
}
|
|
727
731
|
function nextAction(run, changedPaths) {
|
|
728
732
|
const index = JSON.parse(run.index_json);
|
|
733
|
+
if (index.variables?.["merge.source_repair"])
|
|
734
|
+
return "start_code_review";
|
|
729
735
|
const requested = index.settings?.code_review?.mode ?? "auto";
|
|
730
736
|
const enabled = requested === "off" ? false : requested === "auto" ? changedPaths.length > 0 : true;
|
|
731
737
|
const stopTarget = index.execution_profile?.settings?.stop_target;
|
|
@@ -741,6 +747,15 @@ function stageStatus(run, name) {
|
|
|
741
747
|
return null;
|
|
742
748
|
}
|
|
743
749
|
}
|
|
750
|
+
function isMergeSourceRepairAttempt(run) {
|
|
751
|
+
try {
|
|
752
|
+
const value = JSON.parse(run.index_json);
|
|
753
|
+
return Boolean(value.variables?.["merge.source_repair"]);
|
|
754
|
+
}
|
|
755
|
+
catch {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
744
759
|
function changedPathsFromReport(file) {
|
|
745
760
|
try {
|
|
746
761
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -8,11 +8,12 @@ import { resolveProjectRoot } from "../storage/paths.js";
|
|
|
8
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
9
|
import { nextMergeRequestId } from "./ids.js";
|
|
10
10
|
import { readProjectConfig } from "./config.js";
|
|
11
|
-
import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
|
|
11
|
+
import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareVnextMergeSourceRepairAttempt } from "./runs.js";
|
|
12
12
|
import { flowCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
13
13
|
import { validateSchema } from "./schema-validation.js";
|
|
14
14
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
15
|
-
import { bindStageCoordinatorWork, createChildWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
15
|
+
import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
16
|
+
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
16
17
|
import { checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
|
|
17
18
|
import { applyExternalStageContext } from "./stage-context.js";
|
|
18
19
|
import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
|
|
@@ -27,7 +28,7 @@ export function ensureVnextMergeRequest(context, input) {
|
|
|
27
28
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
28
29
|
const project = requireProjectByRoot(context, projectRoot);
|
|
29
30
|
const run = requireRun(context, project.id, input.runId);
|
|
30
|
-
const existing = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at LIMIT 1", [project.id, run.id]);
|
|
31
|
+
const existing = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') ORDER BY created_at DESC LIMIT 1", [project.id, run.id]);
|
|
31
32
|
if (existing)
|
|
32
33
|
return requestView(context, existing);
|
|
33
34
|
const settings = executionSettings(run);
|
|
@@ -88,14 +89,14 @@ export function ensureVnextMergeRequest(context, input) {
|
|
|
88
89
|
const now = context.now();
|
|
89
90
|
context.db.exec("BEGIN IMMEDIATE");
|
|
90
91
|
try {
|
|
91
|
-
const raced = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ?", [project.id, run.id]);
|
|
92
|
+
const raced = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') ORDER BY created_at DESC LIMIT 1", [project.id, run.id]);
|
|
92
93
|
if (raced) {
|
|
93
94
|
context.db.exec("COMMIT");
|
|
94
95
|
return requestView(context, raced);
|
|
95
96
|
}
|
|
96
97
|
child = createChildWork(context, { parentWorkId: root.work_id, slug: "merge", task: `Integrate frozen source ${sourceCommit} into ${targetBranch}, resolve material conflicts, and preserve accepted behavior.`, launchPolicy: route === "server" ? "fresh_agent_required" : "reuse_allowed", resultSchema: "dd-flow/merge-result@1", payload: { kind: "merge", checks: semantic, protocol_ids: protocols } });
|
|
97
98
|
requestId = nextMergeRequestId(context);
|
|
98
|
-
context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route, readCodeCheckProfile(targetWorkspace).hash, now, now]);
|
|
99
|
+
context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, replacement_of_merge_request_id, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route, readCodeCheckProfile(targetWorkspace).hash, replacementOf(run), now, now]);
|
|
99
100
|
context.db.exec("COMMIT");
|
|
100
101
|
}
|
|
101
102
|
catch (error) {
|
|
@@ -231,7 +232,7 @@ export async function finishVnextMerge(context, input) {
|
|
|
231
232
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
232
233
|
if (failed.length) {
|
|
233
234
|
context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_gate_failed", failures: failed }), context.now(), request.merge_request_id]);
|
|
234
|
-
throw new AppError("
|
|
235
|
+
throw new AppError("merge_source_repair_required", "Integrated target checks failed. Do not edit the integration workspace: run the returned source-repair command.", 2, { failures: failed, repair_command: `${flowCommand(context)} merge repair ${request.merge_request_id} --project-root ${JSON.stringify(projectRoot)} --json` });
|
|
235
236
|
}
|
|
236
237
|
const requiredRefs = mergeAcceptanceRefs(run.workspace_root, JSON.parse(request.protocol_ids_json));
|
|
237
238
|
const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
|
|
@@ -303,7 +304,33 @@ catch (error) {
|
|
|
303
304
|
context.db.exec("ROLLBACK");
|
|
304
305
|
throw error;
|
|
305
306
|
} refreshRunWorkProjection(context, request.project_id, request.run_id); return requestView(context, requireRequest(context, request.merge_request_id)); }
|
|
306
|
-
|
|
307
|
+
/** Move a failed integration gate back to the source CODE/CODE-REVIEW loop. */
|
|
308
|
+
export async function repairVnextMerge(context, input) {
|
|
309
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
310
|
+
const project = requireProjectByRoot(context, projectRoot);
|
|
311
|
+
const request = requireRequest(context, input.requestId);
|
|
312
|
+
if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
|
|
313
|
+
throw new AppError("invalid_merge_repair_state", "MERGE source repair requires an action-required failed integration gate", 2, { merge_request_id: input.requestId, status: request.status, code: errorCode(request) });
|
|
314
|
+
}
|
|
315
|
+
abortIntegration(request);
|
|
316
|
+
const originWorkIds = sourceCodeWorkIds(context, request.project_id, request.run_id);
|
|
317
|
+
if (!originWorkIds.length)
|
|
318
|
+
throw new AppError("runtime_missing", "MERGE source repair requires accepted CODE Work evidence", 1, { run_id: request.run_id });
|
|
319
|
+
const failed = failedReceiptIds(request);
|
|
320
|
+
if (!failed.length)
|
|
321
|
+
throw new AppError("runtime_missing", "MERGE failure has no durable failed receipt", 1, { merge_request_id: request.merge_request_id });
|
|
322
|
+
// Settle the child while its canonical result path still exists; the next
|
|
323
|
+
// step archives the entire MERGE attempt as immutable evidence.
|
|
324
|
+
await failWork(context, request.executor_work_id, `Superseded by source repair after failed integration gate ${failed[0]}.`);
|
|
325
|
+
const prepared = prepareVnextMergeSourceRepairAttempt(context, { projectRoot, runId: request.run_id, mergeRequestId: request.merge_request_id });
|
|
326
|
+
const repair = addVnextCodeRepair(context, { projectRoot, runId: request.run_id, checkReceiptId: failed[0], originWorkIds, objective: `Repair source behavior evidenced by failed integration check ${failed[0]}.` });
|
|
327
|
+
const now = context.now();
|
|
328
|
+
context.db.run("UPDATE merge_requests SET status = 'superseded', checkpoint = 'source_repair_required', last_error_json = ?, completed_at = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_source_repair", replacement_pending: true, failed_receipt_ids: failed, repair_work_id: repair.repair_work_id }), now, now, request.merge_request_id]);
|
|
329
|
+
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
|
|
330
|
+
appendFlowRunTimelineEvent(context, project.id, request.run_id, { type: "merge_source_repair_created", merge_request_id: request.merge_request_id, repair_work_id: repair.repair_work_id, cycle: prepared.cycle, failed_receipt_ids: failed });
|
|
331
|
+
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: `${flowCommand(context)} stage start ${request.run_id} --stage code --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl` } };
|
|
332
|
+
}
|
|
333
|
+
function mergePrompt(context, input) { const pause = `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`; const repair = `${flowCommand(context)} merge repair ${input.request.merge_request_id} --project-root ${JSON.stringify(input.projectRoot)} --json`; 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(executionSettings(input.run).merge_delivery)}`, `- cleanup: ${JSON.stringify(executionSettings(input.run).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>", ...effectiveMergeChecks(input.run, input.request).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, inspect only the returned receipt/logs, then run this exact source-repair command: ${repair}. It restores the integration target to baseline and creates a CODE → independent CODE-REVIEW → replacement MRG cycle. Do not repair the integration target and do not retry this MRG.`, "If a material conflict has no reasonable answer in accepted evidence, pause this same Work with the exact heredoc below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
|
|
307
334
|
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" } }; }
|
|
308
335
|
function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
|
|
309
336
|
function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
@@ -365,7 +392,7 @@ function releaseMergeLane(context, projectRoot, request, reason) {
|
|
|
365
392
|
}
|
|
366
393
|
catch { /* An expired lease cannot invalidate an already completed MERGE. */ }
|
|
367
394
|
}
|
|
368
|
-
function queueAhead(context, request) { return context.db.get("SELECT COUNT(*) AS count FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?))", [request.project_id, request.created_at, request.created_at, request.merge_request_id])?.count ?? 0; }
|
|
395
|
+
function queueAhead(context, request) { return context.db.get("SELECT COUNT(*) AS count FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled','superseded') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?))", [request.project_id, request.created_at, request.created_at, request.merge_request_id])?.count ?? 0; }
|
|
369
396
|
function queueStatus(context, request) { return { position: queueAhead(context, request) + 1, requests_ahead: queueAhead(context, request), status: request.status, route: request.execution_route }; }
|
|
370
397
|
function requireOwnedActiveRequest(context, input) { const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)); const request = requireRequest(context, input.requestId); if (request.project_id !== project.id || request.executor_work_id !== input.workId || !["active", "action_required"].includes(request.status))
|
|
371
398
|
throw new AppError("invalid_merge_state", "MRG/Work/project is not the active integration owner", 2, { merge_request_id: input.requestId, work_id: input.workId, status: request.status }); return request; }
|
|
@@ -392,11 +419,42 @@ catch (error) {
|
|
|
392
419
|
function gitValue(cwd, args, allowFailure = false) { const result = spawnSync("git", args, { cwd, encoding: "utf8" }); if (result.status !== 0 && !allowFailure)
|
|
393
420
|
throw new AppError("git_operation_failed", `git ${args[0]} failed`, 1, { cwd, args, stderr: result.stderr }); return result.status === 0 ? result.stdout.trim() : ""; }
|
|
394
421
|
function applyReceiptPath(context, request) { const run = requireRun(context, request.project_id, request.run_id); return path.join(requireHome(run), stageDir, "works", request.executor_work_id, "apply-receipt.json"); }
|
|
395
|
-
function requestForRun(context, projectId, runId) { const request = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at LIMIT 1", [projectId, runId]); if (!request)
|
|
422
|
+
function requestForRun(context, projectId, runId) { const request = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at DESC LIMIT 1", [projectId, runId]); if (!request)
|
|
396
423
|
throw new AppError("merge_request_missing", "MERGE request was not materialized by the prior terminal stage", 1, { run_id: runId }); return request; }
|
|
397
424
|
function requireRequest(context, id) { const request = context.db.get("SELECT * FROM merge_requests WHERE merge_request_id = ?", [id]); if (!request)
|
|
398
425
|
throw new AppError("not_found", "MERGE request is not registered", 1, { merge_request_id: id }); return request; }
|
|
399
|
-
function requestView(context, request) { return { ok: true, merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source: { workspace: request.source_workspace, branch: request.source_branch, commit: request.source_commit }, target: { workspace: request.target_workspace, branch: request.target_branch, enqueue_head: request.enqueue_target_head, execution_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit }, route: request.execution_route, status: request.status, checkpoint: request.checkpoint, queue: queueStatus(context, request), created_at: request.created_at, completed_at: request.completed_at }; }
|
|
426
|
+
function requestView(context, request) { return { ok: true, merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source: { workspace: request.source_workspace, branch: request.source_branch, commit: request.source_commit }, target: { workspace: request.target_workspace, branch: request.target_branch, enqueue_head: request.enqueue_target_head, execution_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit }, route: request.execution_route, status: request.status, checkpoint: request.checkpoint, ...(request.replacement_of_merge_request_id ? { replacement_of_merge_request_id: request.replacement_of_merge_request_id } : {}), queue: queueStatus(context, request), created_at: request.created_at, completed_at: request.completed_at }; }
|
|
427
|
+
function replacementOf(run) { try {
|
|
428
|
+
const variables = JSON.parse(run.index_json).variables;
|
|
429
|
+
const repair = variables?.["merge.source_repair"];
|
|
430
|
+
return repair && typeof repair === "object" && !Array.isArray(repair) && typeof repair.merge_request_id === "string" ? String(repair.merge_request_id) : null;
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
return null;
|
|
434
|
+
} }
|
|
435
|
+
function errorCode(request) { try {
|
|
436
|
+
const value = JSON.parse(request.last_error_json ?? "{}");
|
|
437
|
+
return typeof value?.code === "string" ? value.code : null;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return null;
|
|
441
|
+
} }
|
|
442
|
+
function failedReceiptIds(request) { try {
|
|
443
|
+
const value = JSON.parse(request.last_error_json ?? "{}");
|
|
444
|
+
return (value.failures ?? []).flatMap((failure) => typeof failure === "object" && failure !== null && typeof failure.id === "string" ? [String(failure.id)] : []);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return [];
|
|
448
|
+
} }
|
|
449
|
+
function sourceCodeWorkIds(context, projectId, runId) { return context.db.all("SELECT work_id, payload_json FROM works WHERE project_id = ? AND run_id = ? AND status = 'completed' ORDER BY created_at, work_id", [projectId, runId]).flatMap((work) => { try {
|
|
450
|
+
return JSON.parse(work.payload_json ?? "").schema_id === "dd-flow/code-work-packet@5" ? [work.work_id] : [];
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return [];
|
|
454
|
+
} }); }
|
|
455
|
+
function abortIntegration(request) { if (gitValue(request.target_workspace, ["rev-parse", "-q", "--verify", "MERGE_HEAD"], true))
|
|
456
|
+
git(request.target_workspace, ["merge", "--abort"]); const head = gitValue(request.target_workspace, ["rev-parse", "HEAD"]); const branch = gitValue(request.target_workspace, ["branch", "--show-current"]); const dirty = meaningfulStatus(request.target_workspace, []); if (head !== request.execution_target_head || branch !== request.target_branch || dirty.length)
|
|
457
|
+
throw new AppError("merge_recovery_required", "Integration workspace could not be restored to its locked baseline", 1, { merge_request_id: request.merge_request_id, expected_head: request.execution_target_head, head, expected_branch: request.target_branch, branch, dirty }); }
|
|
400
458
|
function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_root, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
|
|
401
459
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
402
460
|
function requireHome(run) { if (!run.run_root)
|
|
@@ -431,7 +431,7 @@ function projectCodeWorkBatch(input) {
|
|
|
431
431
|
provides_checks: value.checks.filter((check) => check.availability === "planned" && check.provided_by === item.id),
|
|
432
432
|
stop_conditions: item.execution_context.stop_conditions,
|
|
433
433
|
depends_on: item.depends_on.map((dependency) => `${protocolId}:${dependency}`),
|
|
434
|
-
result_schema: "dd-flow/code-work-result@
|
|
434
|
+
result_schema: "dd-flow/code-work-result@3"
|
|
435
435
|
});
|
|
436
436
|
}));
|
|
437
437
|
const byProtocol = new Map(input.plans.map((plan) => [plan.protocolId, plan.value.items]));
|
|
@@ -428,7 +428,7 @@ function validateWorkResult(work, result, projectRoot, workspaceRoot, runHome, r
|
|
|
428
428
|
finally {
|
|
429
429
|
fs.rmSync(candidate, { force: true });
|
|
430
430
|
}
|
|
431
|
-
if (work.result_schema === "dd-flow/code-work-result@
|
|
431
|
+
if (work.result_schema === "dd-flow/code-work-result@3")
|
|
432
432
|
validateCodeWorkResult(work, parsed, workspaceRoot, runHome, runId);
|
|
433
433
|
if (work.result_schema === "dd-flow/code-review-result@1")
|
|
434
434
|
validateCodeReviewResult(work, parsed, { workspaceRoot, runHome, runId });
|
|
@@ -442,10 +442,15 @@ function validateCodeWorkResult(work, value, projectRoot, runHome, runId) {
|
|
|
442
442
|
const packet = codePacket(work);
|
|
443
443
|
if (!packet)
|
|
444
444
|
return;
|
|
445
|
-
const
|
|
445
|
+
const acceptedObligations = new Set(packet.acceptance.map((item) => item.criterion_id).filter((item) => typeof item === "string"));
|
|
446
|
+
for (const check of packet.checks)
|
|
447
|
+
acceptedObligations.add(check.id);
|
|
448
|
+
for (const finding of packet.repair?.review_findings ?? [])
|
|
449
|
+
for (const ref of finding.obligation_refs ?? [])
|
|
450
|
+
acceptedObligations.add(ref);
|
|
446
451
|
for (const item of result.evidence ?? []) {
|
|
447
|
-
if (!item.
|
|
448
|
-
throw new AppError("
|
|
452
|
+
if (!item.obligation_ref || !acceptedObligations.has(item.obligation_ref))
|
|
453
|
+
throw new AppError("evidence_obligation_unknown", "CODE Work evidence must reference an obligation assigned to this Work", 2, { work_id: work.work_id, obligation_ref: item.obligation_ref ?? null, accepted_obligation_refs: [...acceptedObligations] });
|
|
449
454
|
for (const ref of item.refs ?? [])
|
|
450
455
|
assertPortableArtifactRef(ref, { workspaceRoot: projectRoot, runHome, runId });
|
|
451
456
|
}
|
|
@@ -479,16 +484,29 @@ export function validateReviewResolutions(workId, assigned, resolutions) {
|
|
|
479
484
|
function renderWorkerPrompt(context, work, run, dependencies) {
|
|
480
485
|
const command = flowCommand(context);
|
|
481
486
|
const packet = codePacket(work);
|
|
487
|
+
const mergeWork = parsePayload(work)?.kind === "merge";
|
|
488
|
+
const writeBoundary = mergeWork
|
|
489
|
+
? ["MERGE does not own product-code repair. Use only the stage packet's merge apply command; resolve only paths that Git reports as unmerged. Do not edit integration code, tests, documentation or configuration to make a gate pass.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result."]
|
|
490
|
+
: [`HARD RULE: project source reads and writes must remain under ${run.workspace_root}.`, "Do not write through project_root, outside workspace_root, into another RUN, or into Git/worktree control data. Do not create, switch, merge or delete branches/worktrees.", "RUN artifacts are read-only evidence: refer to them with run:// URIs and let dd-flow persist your submitted result. Accepted requirements, non-goals and stop_conditions are semantic hard boundaries. planned_write_areas is not."];
|
|
491
|
+
const completionRepair = mergeWork
|
|
492
|
+
? ["A failed integration check is evidence for a source-repair cycle, not permission to repair the integration workspace. Follow the exact merge repair command returned by dd-flow."]
|
|
493
|
+
: ["Read the failed receipt and its stdout/stderr. Fix project-owned source, migration, test, formatting, or configuration errors in this same Work, then call Finish again. Do not invent a cause that does not appear in the retained output."];
|
|
482
494
|
const codeContext = packet ? ["<semantic_spine>", JSON.stringify(packet.semantic_spine, null, 2), "</semantic_spine>", "", ...(packet.repair ? ["<repair_context>", JSON.stringify(packet.repair, null, 2), "Read the failed receipt and its linked stdout/stderr before editing. Preserve the accepted origin context and fix only the evidenced failure.", "</repair_context>", ""] : []), "<accepted_requirements>", JSON.stringify(packet.requirements, null, 2), "</accepted_requirements>", "", "<write_boundary_invariant>", "For a mutation guarded by membership, ownership, authorization or parent lifecycle state, preserve that predicate in the write statement or make guard and write one explicit transaction with the needed lock. A prior read may diagnose an error but never proves a later write remains allowed. Apply this to create, update, delete and parent-state mutations.", "</write_boundary_invariant>", "", "<acceptance_context>", "The criteria below are end-to-end context. Complete this Work's semantic contribution and declared checks; another ordered Work may own a different acceptance surface.", JSON.stringify(packet.acceptance, null, 2), "</acceptance_context>", "", "<required_read>", "These are mandatory starting sources, not a read allowlist. Read any additional project files needed to implement the Work correctly.", ...packet.required_read.map((item) => `- ${resolveRunReferences(item, work.run_id, requireRunHome(run))}`), "</required_read>", "", "<discovery_boundary>", "These are likely discovery areas, not a hard boundary. Expand project-local investigation when required and report material additions.", ...packet.discovery_boundary.map((item) => `- ${item}`), "</discovery_boundary>", "", "<planned_write_areas>", "SOFT COORDINATION HINT ONLY. These paths help the coordinator avoid concurrent collisions. They do not grant or deny write permission and do not limit the files needed for this Work. You may create or change any project file under workspace_root that is necessary and in semantic scope; report every actual changed path.", ...(packet.planned_write_areas.length ? packet.planned_write_areas.map((item) => `- ${item}`) : ["- none predicted; derive the necessary files from the task"]), "</planned_write_areas>", "", ...(packet.provides_checks.length ? ["<provided_checks>", ...packet.provides_checks.map((item) => `- ${item.id}: materialize ${item.command}${item.definition ? ` as ${item.definition}` : ""}; it is not usable until this Work finishes.`), "Update the declared project command or alias before Work finish. The CLI verifies the materialization and then executes the check.", "</provided_checks>", ""] : []), "<verification>", ...packet.checks.map((item) => `- ${item.id} at ${item.run_at}: ${item.command} — ${item.purpose}`), "The CLI executes work-scoped checks and retains their receipts. Report semantic evidence only; do not rerun declared checks manually.", "</verification>", "", "<stop_conditions>", ...packet.stop_conditions.map((item) => `- ${item}`), "</stop_conditions>", ""] : [];
|
|
483
495
|
if (packet)
|
|
484
496
|
codeContext.push("<document_updates>", JSON.stringify(packet.document_updates, null, 2), "Materialize every listed update. dd-flow verifies the resulting file against its PLAN-time baseline.", "</document_updates>", "", "<completion_contract>", "Successful completion requires empty deviations and blockers and every assigned document update in changed_paths. A necessary path outside planned_write_areas is normal coordination drift, not a blocker; include it in changed_paths and continue.", "</completion_contract>", "");
|
|
485
|
-
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<hard_write_boundary>",
|
|
497
|
+
return ["<work>", `- work_id: ${work.work_id}`, `- run_id: ${work.run_id}`, `- project_root: ${run.project_root}`, `- workspace_root: ${run.workspace_root}`, `- run_home: ${requireRunHome(run)}`, "</work>", "", "<hard_write_boundary>", ...writeBoundary, "</hard_write_boundary>", "", ...codeContext, "<dependency_results>", JSON.stringify(dependencies.filter(Boolean), null, 2), "</dependency_results>", "", "<task>", resolveRunReferences(work.task, work.run_id, requireRunHome(run)), "</task>", "", ...(work.result_schema ? ["<result_contract>", `Return JSON matching \`${work.result_schema}\`.`, ...resultSchemaGuidance(work, run.id), "Do not create result.json yourself. Send the JSON to dd-flow on stdin; it atomically validates and stores the canonical receipt.", "</result_contract>", ""] : []), "<completion>", "The CLI runs every declared required check before accepting this Work. A failed receipt means only that the check failed; it is not proof of an engine, harness, dependency, or environment blocker.", ...completionRepair, "Use Fail only for a concrete external blocker after deterministic bootstrap or a contradiction with an accepted requirement/non-goal. Never fail merely because a necessary project path was absent from planned_write_areas.", "Finish may run for several minutes. Preserve the shell tool's process/session handle and poll that same invocation until it exits; progress arrives as JSONL on stderr. Never reissue Finish merely because final stdout has not arrived.", `Finish as one standalone command, piping your JSON object to stdin: ${command} work finish ${work.work_id} --result-stdin --project-root ${JSON.stringify(run.project_root)} --json --progress-jsonl`, `Fail only for an evidenced external or semantic-contract blocker: ${command} work fail ${work.work_id} --reason "receipt path + exact external or semantic blocker" --project-root ${JSON.stringify(run.project_root)} --json`, "</completion>", ""].join("\n");
|
|
486
498
|
}
|
|
487
499
|
function resultSchemaGuidance(work, runId) {
|
|
488
500
|
const schema = work.result_schema;
|
|
489
501
|
const refs = `Evidence refs for project source are relative to workspace_root; RUN evidence uses run://${runId}/path/to/artifact.`;
|
|
490
|
-
if (schema === "dd-flow/code-work-result@
|
|
491
|
-
|
|
502
|
+
if (schema === "dd-flow/code-work-result@3") {
|
|
503
|
+
const packet = codePacket(work);
|
|
504
|
+
const obligationRef = packet?.repair?.check_receipt_id ? packet.checks[0]?.id ?? "CHK-assigned-check" : "AC-001";
|
|
505
|
+
const obligationNote = packet?.repair?.check_receipt_id
|
|
506
|
+
? `This is a gate-repair Work: cite its assigned check id (${obligationRef}) in evidence. Do not substitute an unrelated acceptance criterion.`
|
|
507
|
+
: "Cite an acceptance criterion or check explicitly assigned to this Work in evidence.";
|
|
508
|
+
return [refs, obligationNote, "For a review repair, include exactly one resolution per assigned finding; explain what changed and cite evidence that demonstrates the required outcome:", "```json", JSON.stringify({ schema_id: schema, summary: "What was implemented.", changed_paths: ["project-relative/path"], evidence: [{ obligation_ref: obligationRef, refs: ["project-relative/evidence"] }], deviations: [], blockers: [], resolutions: [{ finding_ref: "WRK-000/FIND-001", summary: "How the required outcome was achieved.", evidence_refs: ["project-relative/evidence"] }] }, null, 2), "```"];
|
|
509
|
+
}
|
|
492
510
|
if (schema === "dd-flow/code-review-result@1") {
|
|
493
511
|
return [refs, "Assess every assigned aspect exactly once. Number findings locally inside this reviewer Work as FIND-001, FIND-002, ...; dd-flow adds the Work id to form the canonical reference.", "Use this complete minimal shape. Report only material, direct-evidence findings; taste and cosmetics are not findings:", "```json", JSON.stringify({ schema_id: schema, verdict: "pass | findings | blocked", summary: "Concise evidence-backed conclusion.", aspects: [{ aspect_id: "assigned_aspect_id", verdict: "pass | findings | blocked", summary: "Conclusion.", evidence_refs: ["path/to/file", `run://${runId}/05-code/checks/receipt.json`] }], findings: [{ finding_id: "FIND-001", aspect_id: "assigned_aspect_id", priority: "p0 | p1 | p2 | p3", problem: "Violated obligation or rule.", impact: "Concrete risk or failure.", required_outcome: "Smallest observable result that closes the defect.", evidence_refs: ["path/to/file"], obligation_refs: ["R-001 | AC-001 | policy ref"] }] }, null, 2), "```"];
|
|
494
512
|
}
|
package/dist/storage/database.js
CHANGED
|
@@ -574,6 +574,7 @@ function migrate(db, dbPath) {
|
|
|
574
574
|
lock_acquired_at TEXT,
|
|
575
575
|
checkpoint TEXT NOT NULL DEFAULT 'queued',
|
|
576
576
|
profile_hash TEXT,
|
|
577
|
+
replacement_of_merge_request_id TEXT,
|
|
577
578
|
adapter_receipt_json TEXT,
|
|
578
579
|
result_json TEXT,
|
|
579
580
|
last_error_json TEXT,
|
|
@@ -585,8 +586,8 @@ function migrate(db, dbPath) {
|
|
|
585
586
|
);
|
|
586
587
|
CREATE INDEX IF NOT EXISTS idx_merge_requests_fifo
|
|
587
588
|
ON merge_requests(project_id, status, created_at, merge_request_id);
|
|
588
|
-
CREATE
|
|
589
|
-
ON merge_requests(project_id, run_id);
|
|
589
|
+
CREATE INDEX IF NOT EXISTS idx_merge_requests_run
|
|
590
|
+
ON merge_requests(project_id, run_id, created_at);
|
|
590
591
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_merge_requests_active_project
|
|
591
592
|
ON merge_requests(project_id)
|
|
592
593
|
WHERE status IN ('active', 'waiting_user', 'action_required', 'recovery_required');
|
|
@@ -811,6 +812,11 @@ function migrate(db, dbPath) {
|
|
|
811
812
|
ensureColumn(db, "runs", "run_root", "ALTER TABLE runs ADD COLUMN run_root TEXT");
|
|
812
813
|
db.prepare("UPDATE runs SET run_root = COALESCE(run_root, run_home_path, run_dir) WHERE run_root IS NULL OR run_root = ''").run();
|
|
813
814
|
ensureColumn(db, "merge_requests", "accepted_tree", "ALTER TABLE merge_requests ADD COLUMN accepted_tree TEXT");
|
|
815
|
+
ensureColumn(db, "merge_requests", "replacement_of_merge_request_id", "ALTER TABLE merge_requests ADD COLUMN replacement_of_merge_request_id TEXT");
|
|
816
|
+
// A failed integration gate may create a new immutable source candidate in
|
|
817
|
+
// the same RUN. The historical unique index made that repair path impossible.
|
|
818
|
+
db.exec("DROP INDEX IF EXISTS idx_merge_requests_run");
|
|
819
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_merge_requests_run ON merge_requests(project_id, run_id, created_at)");
|
|
814
820
|
ensureColumn(db, "runs", "layout_version", "ALTER TABLE runs ADD COLUMN layout_version TEXT");
|
|
815
821
|
ensureColumn(db, "runs", "artifact_root_kind", "ALTER TABLE runs ADD COLUMN artifact_root_kind TEXT");
|
|
816
822
|
ensureColumn(db, "works", "payload_json", "ALTER TABLE works ADD COLUMN payload_json TEXT");
|