@deksden-com/dd-flow-cli 0.9.0-beta.87 → 0.9.0-beta.89
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 +14 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/command-inputs.js +13 -10
- package/dist/cli/help.js +4 -0
- package/dist/cli/hook-ingress.js +2 -2
- package/dist/cli/run-cli.js +48 -15
- package/dist/services/codex-hook-delivery.js +3 -3
- package/dist/services/command-context.js +80 -0
- package/dist/services/continuation-outcome.js +20 -0
- package/dist/services/controller-fanout.js +4 -2
- package/dist/services/hooks.js +21 -15
- package/dist/services/lifecycle-command.js +27 -0
- package/dist/services/lifecycle-invocations.js +119 -38
- package/dist/services/recovery-snapshot-database.js +1 -1
- package/dist/services/repair-intents.js +40 -0
- package/dist/services/run-controller.js +12 -3
- package/dist/services/runtime-command.js +43 -0
- package/dist/services/stage-pause.js +7 -21
- package/dist/services/stage-work-graph.js +35 -0
- package/dist/services/vnext-code-review.js +13 -11
- package/dist/services/vnext-code.js +39 -21
- package/dist/services/vnext-fanout.js +27 -6
- package/dist/services/vnext-merge.js +13 -10
- package/dist/services/vnext-plan-review.js +6 -5
- package/dist/services/work-registry.js +18 -9
- package/dist/storage/database.js +15 -0
- package/package.json +1 -1
|
@@ -7,10 +7,10 @@ import { resolveProjectRoot } from "../storage/paths.js";
|
|
|
7
7
|
import { requireProjectByRoot } from "./projects.js";
|
|
8
8
|
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, freezeFlowRunReviewMode, gitFacts } from "./runs.js";
|
|
9
9
|
import { validateSchema } from "./schema-validation.js";
|
|
10
|
-
import {
|
|
10
|
+
import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
|
|
11
11
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
12
12
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
13
|
-
import { addVnextCodeRepair, prepareVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
13
|
+
import { addVnextCodeRepair, prepareVnextCodeRepair, registerAggregateCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
14
14
|
import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
15
15
|
import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
16
|
import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
|
|
@@ -18,6 +18,7 @@ import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
|
18
18
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
19
19
|
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
20
20
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
21
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
21
22
|
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
22
23
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
23
24
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
@@ -93,8 +94,8 @@ export function startVnextCodeReview(context, input, prepared = prepareVnextCode
|
|
|
93
94
|
fs.writeFileSync(promptPath, prompt);
|
|
94
95
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
95
96
|
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 } : {}) });
|
|
96
|
-
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items });
|
|
97
|
-
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
97
|
+
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items, stage, cycle });
|
|
98
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
98
99
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
99
100
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
|
|
100
101
|
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 } : {}), orchestration, review: { groups, works: registered }, next: { finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
@@ -225,15 +226,14 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
225
226
|
outcome: "repair_required",
|
|
226
227
|
retry_after_workspace_change: true,
|
|
227
228
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
retry_command: finishCommand(context, run.id, projectRoot, decisionFile, failed[0].id)
|
|
229
|
+
continuation: registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks: finalChecks, generation }),
|
|
230
|
+
instruction: "Repair Work is already registered. End this Turn so the controller can launch a fresh child; after it finishes, repeat finish with the already accepted review decision. Do not redo review."
|
|
231
231
|
});
|
|
232
232
|
const stopTarget = executionStopTarget(run);
|
|
233
233
|
const nextAction = stopTarget === "merge_completed" ? "start_merge" : "code_review_completed";
|
|
234
234
|
const now = context.now();
|
|
235
235
|
const startedAt = stageStartedAt(run, now);
|
|
236
|
-
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result: `Independent CODE review completed: ${reviewers.length} reviewer Work(s), ${repairs.length} repair Work(s).`, acceptance: ["all_selected_review_groups_completed", "all_material_findings_classified", ...(repairs.length ? ["all_accepted_repairs_completed"] : []), "current_final_gate_passed"], run_changed_files: changedPaths(run.workspace_root), checks: receipts.map((receipt) => receipt.command), evidence: [...reviewers.flatMap((work) => readJsonString(work.result).findings.flatMap((finding) => finding.evidence_refs)), ...receipts.map((receipt) => runRef(run.id, home, receipt.receipt_path))], next_action: nextAction, code_review: { mode, reviewer_work_ids: reviewers.map((work) => work.work_id), repair_work_ids: repairs.map((work) => work.work_id), decision } }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command:
|
|
236
|
+
const report = { schema_id: "dd-flow/stage-report@1", run_id: run.id, stage, generated_at: now, verdict: "done", semantic: { result: `Independent CODE review completed: ${reviewers.length} reviewer Work(s), ${repairs.length} repair Work(s).`, acceptance: ["all_selected_review_groups_completed", "all_material_findings_classified", ...(repairs.length ? ["all_accepted_repairs_completed"] : []), "current_final_gate_passed"], run_changed_files: changedPaths(run.workspace_root), checks: receipts.map((receipt) => receipt.command), evidence: [...reviewers.flatMap((work) => readJsonString(work.result).findings.flatMap((finding) => finding.evidence_refs)), ...receipts.map((receipt) => runRef(run.id, home, receipt.receipt_path))], next_action: nextAction, code_review: { mode, reviewer_work_ids: reviewers.map((work) => work.work_id), repair_work_ids: repairs.map((work) => work.work_id), decision } }, mechanical: { started_at: startedAt, finished_at: now, wall_clock_ms: Math.max(0, Date.parse(now) - Date.parse(startedAt)), git: gitFacts(run.workspace_root), session_stats_command: runtimeCommand(context, ["stat", "run", "sessions", "ls", "--run", run.id, "--project-root", projectRoot, "--json"]), usage_stats_command: runtimeCommand(context, ["stat", "usage", "--run", run.id, "--project-root", projectRoot, "--json"]) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } };
|
|
237
237
|
// The request must exist before CODE-REVIEW becomes terminal. Otherwise a
|
|
238
238
|
// merge-gate error creates an irrecoverable `done` report with no MRG-*.
|
|
239
239
|
const acceptedPaths = changedPaths(run.workspace_root);
|
|
@@ -259,7 +259,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
259
259
|
}
|
|
260
260
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
261
261
|
});
|
|
262
|
-
return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context,
|
|
262
|
+
return { ok: true, run_id: run.id, stage, outcome, report_path: path.join(root, "stage-report.json"), next_action: nextAction, ...(mergeRequest ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } } : {}) };
|
|
263
263
|
}
|
|
264
264
|
export function readCodeReviewDecision(file, accepted, explicit = false, prepared) {
|
|
265
265
|
if (!accepted)
|
|
@@ -327,7 +327,7 @@ export function assertCodeReviewCoverage(groups, reviewers) {
|
|
|
327
327
|
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
|
|
328
328
|
}
|
|
329
329
|
export function reviewerTask(home, group) { return `Read-only independent CODE review for ${group.key}. Assess every assigned aspect exactly once: ${group.aspect_ids.join(", ")}. Read the accepted PLAN, ${path.join(home, "05-code", "stage-report.json")}, and any files needed under the bounded CODE evidence root ${path.join(home, "05-code")}. Verify that universal and exclusive constraints (only, any, all, never, whole) and stated exceptions were not narrowed. For every changed mutation guarded by membership, ownership, authorization or parent lifecycle state, trace the decision to the write boundary: the predicate must remain in the write statement or the guard and write must share one explicit transaction with the needed lock. A separate earlier read is not proof of current authority or lifecycle state; report a material finding when this invariant is broken. Evidence closes a claim only when it exercises the named failure mechanism; a sequential negative test does not prove a concurrency race, and proof limits cannot waive an accepted obligation. Report only material, evidenced defects: a violated obligation or rule, direct evidence, impact, and minimum required outcome. Do not report taste, cosmetics, or untargeted refactoring. Use local finding ids FIND-001, FIND-002, and so on; dd-flow adds the Work-qualified canonical reference. Return dd-flow/code-review-result@1.`; }
|
|
330
|
-
function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.",
|
|
330
|
+
function orchestratorPrompt(context, input) { const { template, checks } = input; const decision = path.join(input.root, "decision.json"); return ["<stage_identity>", `- RUN: ${input.run.id}`, `- root Work: ${input.rootWork.work_id}`, `- stage: ${stage}`, `- mode: ${input.mode}`, "</stage_identity>", "", "<trusted_runtime_context>", `- project root: ${input.projectRoot}`, `- immutable write workspace: ${input.run.workspace_root}`, `- stage workspace: ${input.root}`, `- bounded CODE evidence root: ${path.join(input.run.run_root, "05-code")}`, "CODE is already semantically verified and all declared checks passed. Do not redo CODE verification; conduct independent quality review.", "</trusted_runtime_context>", "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", "Do not choose a provider delegation tool from this stage prompt. At the Work-graph boundary, return immediately; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. Each reviewer Work must run in one fresh child session. The coordinator must never invoke a Work start_command itself: a child runs its exact start_command after native delegation. Reviewers are read-only and must not create subagents. Do not interrupt a quiet worker.", "Ready reviewer Works: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]), `Write the decision to this exact absolute path, not a relative look-alike: ${decision}`, `Finish only after every reviewer Work settles and that exact file exists: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer results use local FIND-NNN ids. Classify them by the canonical WRK-.../FIND-NNN finding_ref returned by dd-flow. Fix P0/P1. Fix bounded safe P2 by default; defer only a legitimate P2 with a named DEF. P3 is an observation or a reasoned rejection, not automatic repair/DEF. For every disposition fix, check_refs is mandatory: choose the one or more causal checks from the accepted list below that the repair must rerun. Do not guess a CHK id or copy every check. The first successful Finish freezes this decision and creates one repair Work when needed. Do not invoke that Work's start_command from the coordinator Session: end this Turn so the controller can launch a fresh child, then call the returned next.finish_command after it completes. Internal invocation IDs are runtime-owned and must not be copied or reused. Review is not repeated.", "If the aggregate gate fails with repair_required, its repair Work is already registered with all causal checks. End this Turn so the controller launches a fresh child. Do not copy receipt IDs, create another repair, invoke its start_command in the coordinator, or edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
|
|
331
331
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
332
332
|
const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
|
|
333
333
|
const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
|
|
@@ -483,7 +483,9 @@ function requireRun(context, projectId, runId) { const run = context.db.get("SEL
|
|
|
483
483
|
throw new AppError("not_found", "RUN is not registered", 1); return run; }
|
|
484
484
|
function requireHome(run) { if (!run.run_root)
|
|
485
485
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
486
|
-
export function finishCommand(context, runId, projectRoot, decision, retryCheckId) {
|
|
486
|
+
export function finishCommand(context, runId, projectRoot, decision, retryCheckId) {
|
|
487
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "code-review", "--decision-file", decision, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
|
|
488
|
+
}
|
|
487
489
|
function findFiles(root, name) { if (!fs.existsSync(root))
|
|
488
490
|
return []; const out = []; for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
489
491
|
const file = path.join(root, entry.name);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
+
import { ensureAggregateRepair, ensureRepairIntent } from "./repair-intents.js";
|
|
2
3
|
import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
|
|
3
4
|
import fs from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
@@ -8,7 +9,8 @@ import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckD
|
|
|
8
9
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
10
|
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
10
11
|
import { validateSchema } from "./schema-validation.js";
|
|
11
|
-
import {
|
|
12
|
+
import { runtimeCommand, checkRetryArgs } from "./runtime-command.js";
|
|
13
|
+
import { stageCycle, stageGraphScope } from "./stage-work-graph.js";
|
|
12
14
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
13
15
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
14
16
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
@@ -91,8 +93,8 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
91
93
|
if (prepared.resumed) {
|
|
92
94
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
93
95
|
bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
94
|
-
const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
95
|
-
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command:
|
|
96
|
+
const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
97
|
+
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) } };
|
|
96
98
|
}
|
|
97
99
|
fs.mkdirSync(root, { recursive: true });
|
|
98
100
|
const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
|
|
@@ -119,7 +121,7 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
119
121
|
dataSchemaId: "dd-flow/stage-report@1"
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
|
-
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
124
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
123
125
|
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
|
|
124
126
|
return {
|
|
125
127
|
ok: true,
|
|
@@ -133,7 +135,7 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
133
135
|
graph,
|
|
134
136
|
orchestration,
|
|
135
137
|
readiness,
|
|
136
|
-
next: { refresh_graph_command:
|
|
138
|
+
next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) }
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
export async function finishVnextCode(context, input) {
|
|
@@ -206,14 +208,14 @@ export async function finishVnextCode(context, input) {
|
|
|
206
208
|
});
|
|
207
209
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
208
210
|
if (failed.length) {
|
|
211
|
+
const continuation = registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks, generation });
|
|
209
212
|
throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
|
|
210
213
|
failures: failed,
|
|
211
214
|
outcome: "repair_required",
|
|
212
215
|
retry_after_workspace_change: true,
|
|
213
216
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
retry_command: finishCommand(context, run.id, projectRoot, input.verificationFile, failed[0].id)
|
|
217
|
+
continuation,
|
|
218
|
+
instruction: "Repair Work is already registered. End this coordinator Turn; the controller will launch a fresh child. After it completes, update semantic verification and finish the Stage."
|
|
217
219
|
});
|
|
218
220
|
}
|
|
219
221
|
const finalReceipts = receipts;
|
|
@@ -256,8 +258,8 @@ export async function finishVnextCode(context, input) {
|
|
|
256
258
|
wall_clock_ms: timing.wall_clock_ms,
|
|
257
259
|
git: gitFacts(run.workspace_root),
|
|
258
260
|
graph: codeWorkGraph(context, project.id, run.id),
|
|
259
|
-
session_stats_command:
|
|
260
|
-
usage_stats_command:
|
|
261
|
+
session_stats_command: runtimeCommand(context, ["stat", "run", "sessions", "ls", "--run", run.id, "--project-root", projectRoot, "--json"]),
|
|
262
|
+
usage_stats_command: runtimeCommand(context, ["stat", "usage", "--run", run.id, "--project-root", projectRoot, "--json"])
|
|
261
263
|
},
|
|
262
264
|
artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html", summary: "stage-report.md" },
|
|
263
265
|
validation: { permission_scope: "known_targets_only", memory_bank_scope: "changed_files_and_links_only", status: "passed" }
|
|
@@ -307,16 +309,30 @@ export async function finishVnextCode(context, input) {
|
|
|
307
309
|
outcome: "accepted",
|
|
308
310
|
report_path: path.join(root, "stage-report.json"),
|
|
309
311
|
next_action: next,
|
|
310
|
-
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context,
|
|
312
|
+
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "code-review", "--project-root", projectRoot, "--json"])) } } : next === "start_merge" ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", run.id, "--stage", "merge", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } } : {})
|
|
311
313
|
};
|
|
312
314
|
}
|
|
315
|
+
export function registerAggregateCodeRepair(context, input) {
|
|
316
|
+
const project = requireProjectByRoot(context, input.projectRoot);
|
|
317
|
+
const origins = codeWorks(context, project.id, input.runId).filter(work => !packet(work)?.repair && work.status === "completed");
|
|
318
|
+
const failedDeclarations = new Set(input.failures.flatMap(receipt => [receipt.declaration_id, ...receipt.check_refs]));
|
|
319
|
+
const objective = ["Investigate and repair all retained aggregate check failures while preserving accepted requirements and non-goals. If evidence proves an external blocker or a contradiction, report it; do not invent a product change.", ...input.failures.map(receipt => `Receipt: ${receipt.receipt_path}\nstdout: ${receipt.stdout_path}\nstderr: ${receipt.stderr_path}`)].join("\n\n");
|
|
320
|
+
try {
|
|
321
|
+
return ensureAggregateRepair(context, { projectId: project.id, runId: input.runId, stage: input.stage, failures: input.failures, origins: origins.map(work => work.work_id), ...(input.generation !== undefined ? { generation: input.generation } : {}) }, () => String(addVnextCodeRepair(context, { registrationOwned: true, projectRoot: input.projectRoot, runId: input.runId, stage: input.stage, ...(input.stage === "code-review" ? { reviewCycle: stageCycle(context, project.id, input.runId, input.stage) } : {}), checkReceiptId: input.failures[0].id, originWorkIds: origins.map(work => work.work_id), objective, reviewChecks: input.checks.filter(check => failedDeclarations.has(check.id) || failedDeclarations.has(check.canonical_ref ?? "")) }).repair_work_id));
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (!(error instanceof AppError))
|
|
325
|
+
throw error;
|
|
326
|
+
throw new AppError(error.code, error.message, error.exitCode, { ...error.details, stage: input.stage, run_id: input.runId, failures: input.failures, cause: { code: input.stage === "code" ? "code_gate_failed" : "code_review_gate_failed", message: "Aggregate checks failed; repair registration was rejected" } });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
313
329
|
export function prepareVnextCodeRepair(context, input) {
|
|
314
330
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
315
331
|
const project = requireProjectByRoot(context, projectRoot);
|
|
316
332
|
const run = requireRun(context, project.id, input.runId);
|
|
317
333
|
const home = requireHome(run);
|
|
318
334
|
if (input.reviewCycle) {
|
|
319
|
-
const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle);
|
|
335
|
+
const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle && (input.checkReceiptId ? packet(work)?.repair?.check_receipt_id === input.checkReceiptId : Boolean(packet(work)?.repair?.review_findings?.length)));
|
|
320
336
|
if (previous)
|
|
321
337
|
return { kind: "replay", project, run, previous };
|
|
322
338
|
}
|
|
@@ -434,8 +450,11 @@ export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRe
|
|
|
434
450
|
if (prepared.kind === "replay")
|
|
435
451
|
return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous), reused: true };
|
|
436
452
|
const { parentWorkId, items, key, receipt } = prepared;
|
|
437
|
-
const
|
|
438
|
-
const id =
|
|
453
|
+
const create = () => String(addWorkBatch(context, { parentWorkId, prepared: items, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"), ...(input.reviewCycle ? { cycle: input.reviewCycle } : {}) }).work_ids[key]);
|
|
454
|
+
const id = input.registrationOwned ? create() : ensureRepairIntent(context, {
|
|
455
|
+
projectId: project.id, runId: run.id, stage: input.stage ?? (input.reviewCycle ? "code-review" : "code"),
|
|
456
|
+
causes: input.checkReceiptId ? { receipt: input.checkReceiptId, origins: [...input.originWorkIds].sort() } : input.reviewCycle ? { review_cycle: input.reviewCycle, findings: input.reviewFindings } : { verification: input.verification, unresolved: input.semanticUnresolved, origins: [...input.originWorkIds].sort() }
|
|
457
|
+
}, create).work_id;
|
|
439
458
|
const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]);
|
|
440
459
|
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
441
460
|
type: "code_repair_created",
|
|
@@ -448,7 +467,7 @@ export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRe
|
|
|
448
467
|
}
|
|
449
468
|
/** Retain the causal declaration for worker context; run_at still records its original gate. */
|
|
450
469
|
export function selectRepairChecks(input) {
|
|
451
|
-
const candidates = input.failedCheck ? [input.failedCheck] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
470
|
+
const candidates = input.failedCheck ? [input.failedCheck, ...(input.reviewChecks ?? [])] : input.reviewChecks ?? input.semanticChecks ?? [];
|
|
452
471
|
const checks = candidates.map((check) => ({ ...check, purpose: `${input.semanticChecks ? "Prove the CODE verification repair" : "Prove the CODE-REVIEW repair"}: ${check.purpose}` }));
|
|
453
472
|
return uniqueBy(checks, (check) => check.id);
|
|
454
473
|
}
|
|
@@ -499,11 +518,11 @@ function coordinatorPrompt(context, input) {
|
|
|
499
518
|
"</code_graph>",
|
|
500
519
|
"",
|
|
501
520
|
"<execution_commands>",
|
|
502
|
-
"Do not choose a provider delegation tool from this stage prompt. At each work_fanout boundary, stop at the Work-graph boundary; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. It launches only entries listed in graph.ready and uses at most the qualified capacity. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain. The coordinator launches those children only through the controller-supplied native delegation instruction; it must never invoke a Work start_command itself. Each child receives its complete packet from dd-flow and runs its own exact start_command. After a Work finishes, the controller uses the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " +
|
|
503
|
-
"A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return once: a completed command with a non-zero exit and structured `code_gate_failed` output is its terminal result, not a reason to keep waiting.
|
|
504
|
-
`A repairable engine, harness, or environment failure is not a user question. Write the evidence summary to
|
|
521
|
+
"Do not choose a provider delegation tool from this stage prompt. At each work_fanout boundary, stop at the Work-graph boundary; the shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract. It launches only entries listed in graph.ready and uses at most the qualified capacity. Every registered CODE Work runs in a fresh child Session, including a serial dependency chain. The coordinator launches those children only through the controller-supplied native delegation instruction; it must never invoke a Work start_command itself. Each child receives its complete packet from dd-flow and runs its own exact start_command. After a Work finishes, the controller uses the graph returned by work finish to launch newly ready Work. To refresh the parent graph yourself use the exact command: " + runtimeCommand(context, ["work", "ls", "--run", input.run.id, "--ready", "--project-root", input.projectRoot, "--json"]),
|
|
522
|
+
"A quiet child is still running until the harness reports its turn completed, failed, cancelled or explicitly needs attention. An elapsed nominal wait, silence, or no new artifact is not an unresponsive-worker failure. Never interrupt, replace, relaunch, or stage-block a still-running child for that reason, even if an external controller asks. Long work finish and stage finish commands emit check progress on stderr. After you issue the exact CODE stage finish command, wait for that same command to return once: a completed command with a non-zero exit and structured `code_gate_failed` output is its terminal result, not a reason to keep waiting. A repair_required continuation already registered the repair: end this Turn. Do not invoke that Work's start_command from the coordinator Session, inspect its PID, start a second finish command, or infer failure from quiet output. Close a disposable child only after its Work is accepted or explicitly failed/cancelled and the harness reports the turn settled.",
|
|
523
|
+
`A repairable engine, harness, or environment failure is not a user question. Write the evidence summary to ${path.join(requireHome(input.run), "works", input.rootWork.work_id, "block-summary.md")}, then record it with a separate command: ${runtimeCommand(context, ["stage", "block", input.run.id, "--stage", "code", "--work", input.rootWork.work_id, "--kind", "<engine|harness|environment>", "--code", "<stable-code>", "--summary-file", `@run/works/${input.rootWork.work_id}/block-summary.md`, "--retryable", "--project-root", input.projectRoot, "--json"])}. Choose kind and stable-code from evidence; this example is not an executable continuation. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
|
|
505
524
|
`When every CODE and repair Work is completed, write ${path.join(input.root, "code-verification.json")} using the exact contract below. Mark passed only when all accepted requirements and current-gate acceptance criteria are implemented or explicitly evidenced; list every remaining issue in unresolved. Every evidence_refs item must already exist as a relative workspace path or run://${input.run.id}/ path. Do not claim a browser or other check receipt that was not retained. Then finish: ${finishCommand(context, input.run.id, input.projectRoot, path.join(input.root, "code-verification.json"))}`,
|
|
506
|
-
"
|
|
525
|
+
"A code_gate_failed response with continuation.kind=repair_required has already registered one repair Work with all failed receipts. End this Turn so the controller can dispatch it in a fresh child. Do not recreate the repair, copy origin IDs, write a repair task file or execute the child start command yourself. After repair, update semantic verification and finish again.",
|
|
507
526
|
"</execution_commands>",
|
|
508
527
|
"",
|
|
509
528
|
"<verification_contract>",
|
|
@@ -689,8 +708,7 @@ function requireHome(run) {
|
|
|
689
708
|
return run.run_root;
|
|
690
709
|
}
|
|
691
710
|
export function finishCommand(context, runId, projectRoot, verificationFile, retryCheckId) {
|
|
692
|
-
|
|
693
|
-
return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`);
|
|
711
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "code", "--verification-file", verificationFile, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
|
|
694
712
|
}
|
|
695
713
|
function readVerification(context, input) {
|
|
696
714
|
return prepareJsonSchema(input.file, "CODE verification", {
|
|
@@ -9,6 +9,7 @@ import { listWorks, failWork, cancelWork } from "./work-registry.js";
|
|
|
9
9
|
import { storageSessionId } from "./session-identity.js";
|
|
10
10
|
import { reconcileUnfinishedChecks } from "./code-checks.js";
|
|
11
11
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
12
|
+
import { stageWorkIds, stageGraphScope } from "./stage-work-graph.js";
|
|
12
13
|
export const subagentCapacityKey = "runtime.subagent.available_slots";
|
|
13
14
|
/** External concurrency is policy, never evidence of native child qualification. */
|
|
14
15
|
export function getStageFanoutCapacity(context, input) {
|
|
@@ -40,7 +41,7 @@ export function fanoutState(input) {
|
|
|
40
41
|
}
|
|
41
42
|
export function fanoutFile(stageRoot) { return path.join(stageRoot, "fanout.json"); }
|
|
42
43
|
export function writeFanoutDescriptor(stageRoot, descriptor) {
|
|
43
|
-
const value = { schema_id: "dd-flow/work-fanout@1", kind: "work_fanout", ...descriptor };
|
|
44
|
+
const value = { schema_id: descriptor.scope ? "dd-flow/work-fanout@2" : "dd-flow/work-fanout@1", kind: "work_fanout", ...descriptor };
|
|
44
45
|
fs.writeFileSync(fanoutFile(stageRoot), `${JSON.stringify(value, null, 2)}\n`);
|
|
45
46
|
return value;
|
|
46
47
|
}
|
|
@@ -49,7 +50,7 @@ export function readFanoutDescriptor(stageRoot) {
|
|
|
49
50
|
if (!fs.existsSync(file))
|
|
50
51
|
return null;
|
|
51
52
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
52
|
-
if (
|
|
53
|
+
if (!["dd-flow/work-fanout@1", "dd-flow/work-fanout@2"].includes(String(value.schema_id)) || value.kind !== "work_fanout" || typeof value.stage !== "string" || typeof value.parent_work_id !== "string" || !["none", "plan-review"].includes(String(value.dispatch)) || typeof value.capacity_required !== "boolean" || value.schema_id === "dd-flow/work-fanout@2" && (!value.scope || [value.scope.project_id, value.scope.run_id, value.scope.attempt, value.scope.cycle].some(item => typeof item !== "string"))) {
|
|
53
54
|
throw new AppError("runtime_missing", "Stage fan-out descriptor is invalid", 1, { file });
|
|
54
55
|
}
|
|
55
56
|
return value;
|
|
@@ -64,14 +65,30 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
64
65
|
if (!run.run_root)
|
|
65
66
|
throw new AppError("runtime_artifact_missing", "RUN artifact root is unavailable", 1, { run_id: input.runId });
|
|
66
67
|
const stageRoot = path.join(run.run_root, vnextStageDirectory(input.stage));
|
|
67
|
-
|
|
68
|
+
let descriptor = readFanoutDescriptor(stageRoot);
|
|
69
|
+
// Registration is authoritative even if projection publication was interrupted,
|
|
70
|
+
// or an otherwise review-off stage acquired an aggregate repair graph.
|
|
71
|
+
if (!descriptor && context.db.get("SELECT 1 FROM sqlite_master WHERE name = 'repair_intents'")) {
|
|
72
|
+
const members = stageWorkIds(context, project.id, input.runId, input.stage);
|
|
73
|
+
const parents = context.db.all("SELECT w.work_id, w.parent_work_id FROM repair_intents i JOIN works w ON w.work_id = i.work_id WHERE i.project_id = ? AND i.run_id = ? AND i.stage = ?", [project.id, input.runId, input.stage]).filter(work => members.has(work.work_id));
|
|
74
|
+
const roots = [...new Set(parents.map(work => work.parent_work_id))];
|
|
75
|
+
if (roots.length > 1)
|
|
76
|
+
throw new AppError("stage_graph_conflict", "Registered repairs disagree on their stage coordinator", 1);
|
|
77
|
+
if (roots.length === 1)
|
|
78
|
+
descriptor = { schema_id: "dd-flow/work-fanout@2", scope: stageGraphScope(context, project.id, input.runId, input.stage), kind: "work_fanout", stage: input.stage, parent_work_id: roots[0], dispatch: "none", capacity_required: true };
|
|
79
|
+
}
|
|
68
80
|
if (!descriptor)
|
|
69
81
|
return { ok: true, run_id: input.runId, stage: input.stage, orchestration: null };
|
|
70
82
|
if (descriptor.stage !== input.stage)
|
|
71
83
|
throw new AppError("runtime_missing", "Stage fan-out descriptor belongs to another stage", 1, { requested_stage: input.stage, descriptor_stage: descriptor.stage });
|
|
72
84
|
const capacity = getStageFanoutCapacity(context, { projectRoot, runId: input.runId, stage: input.stage });
|
|
73
|
-
const
|
|
74
|
-
const
|
|
85
|
+
const members = stageWorkIds(context, project.id, input.runId, input.stage);
|
|
86
|
+
const currentScope = stageGraphScope(context, project.id, input.runId, input.stage);
|
|
87
|
+
const compatible = descriptor.scope ? Object.entries(currentScope).every(([key, value]) => descriptor.scope[key] === value) : members.size > 0;
|
|
88
|
+
if (!compatible)
|
|
89
|
+
return { ok: true, run_id: input.runId, stage: input.stage, orchestration: { ...descriptor, state: "compatibility_required", compatibility: { code: "stage_graph_compatibility_required", message: "Legacy or stale stage graph has no authoritative current membership; use the pinned engine", expected_scope: currentScope } } };
|
|
90
|
+
const all = listWorks(context, { parentWorkId: descriptor.parent_work_id, includeResults: false, workIds: members });
|
|
91
|
+
const ready = { works: all.works.filter(work => work.ready) };
|
|
75
92
|
const counts = Object.fromEntries(["created", "running", "completed", "failed", "cancelled"].map((status) => [status, all.works.filter((work) => work.status === status).length]));
|
|
76
93
|
const hasWork = all.works.length > 0;
|
|
77
94
|
const capacityKnown = capacity.available_slots !== null;
|
|
@@ -86,6 +103,7 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
86
103
|
// authoritative prompt. An empty graph is therefore not a success.
|
|
87
104
|
state: fanoutState({ dispatch: descriptor.dispatch, hasWork, capacityRequired: descriptor.capacity_required, capacityKnown, created: counts.created ?? 0, running: counts.running ?? 0 }),
|
|
88
105
|
capacity,
|
|
106
|
+
graph_revision: all.works.map(work => ({ work_id: work.work_id, status: work.status, depends_on: work.depends_on })),
|
|
89
107
|
works: { ...counts, ready: ready.works.map((work) => ({ work_id: work.work_id, task: work.task, launch_policy: work.launch_policy, start_command: work.start_command })) }
|
|
90
108
|
}
|
|
91
109
|
};
|
|
@@ -99,7 +117,10 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
99
117
|
const status = getVnextFanoutStatus(context, input);
|
|
100
118
|
if (!status.orchestration)
|
|
101
119
|
return { ok: true, issues: [], reconciled: [] };
|
|
120
|
+
if (status.orchestration.state === "compatibility_required")
|
|
121
|
+
throw new AppError("stage_graph_compatibility_required", "Cannot reconcile a legacy or stale graph", 1);
|
|
102
122
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
123
|
+
const members = stageWorkIds(context, project.id, input.runId, input.stage);
|
|
103
124
|
const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
|
|
104
125
|
const issues = [], reconciled = [];
|
|
105
126
|
for (const child of value.children) {
|
|
@@ -107,7 +128,7 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
107
128
|
continue;
|
|
108
129
|
const sessionId = storageSessionId({ harness_id: value.harness_id, session_id: String(child.session_id) });
|
|
109
130
|
const links = context.db.all(`SELECT ws.id, ws.work_id, ws.status, w.status AS work_status, w.parent_work_id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN sessions s ON s.project_id = w.project_id AND s.session_id = ws.session_id WHERE w.project_id = ? AND w.run_id = ? AND ws.session_id = ? AND s.parent_session_id = ? ORDER BY ws.created_at`, [project.id, input.runId, sessionId, parent]);
|
|
110
|
-
const active = links.filter(link => link.status === "running" && link.work_status === "running" && link.parent_work_id === status.orchestration.parent_work_id);
|
|
131
|
+
const active = links.filter(link => link.status === "running" && link.work_status === "running" && link.parent_work_id === status.orchestration.parent_work_id && members.has(link.work_id));
|
|
111
132
|
if (!active.length) {
|
|
112
133
|
if (!links.length)
|
|
113
134
|
issues.push({ code: "child_launch_association_missing", session_id: child.session_id, outcome: child.status });
|
|
@@ -11,7 +11,8 @@ import { requireProjectByRoot } from "./projects.js";
|
|
|
11
11
|
import { nextMergeRequestId } from "./ids.js";
|
|
12
12
|
import { readProjectConfig } from "./config.js";
|
|
13
13
|
import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareFlowRunStageAttachment, prepareVnextMergeSourceRepairAttempt } from "./runs.js";
|
|
14
|
-
import {
|
|
14
|
+
import { stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
15
|
+
import { runtimeCommand, checkRetryArgs, recoveryEvidenceInput } from "./runtime-command.js";
|
|
15
16
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
16
17
|
import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
17
18
|
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
@@ -102,7 +103,7 @@ export function prepareVnextMergeRequest(context, input) {
|
|
|
102
103
|
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]);
|
|
103
104
|
if (raced)
|
|
104
105
|
return requestView(context, raced);
|
|
105
|
-
const 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 } });
|
|
106
|
+
const child = createChildWork(context, { parentWorkId: root.work_id, stage: "merge", 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 } });
|
|
106
107
|
const requestId = nextMergeRequestId(context);
|
|
107
108
|
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]);
|
|
108
109
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_request_queued", merge_request_id: requestId, work_id: child.work_id, route, source_commit: sourceCommit, target_branch: targetBranch });
|
|
@@ -290,7 +291,7 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
|
|
|
290
291
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
291
292
|
if (failed.length) {
|
|
292
293
|
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]);
|
|
293
|
-
throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: repairCommand(context, request, projectRoot),
|
|
294
|
+
throw new AppError("merge_gate_failed", "Integrated target checks failed. Classify retained evidence: source defects use source repair; restored environment uses retry.", 2, { failures: failed, repair_command: repairCommand(context, request, projectRoot), continuation: { ...recoveryEvidenceInput(run.run_root, failed[0].id), command_after_input: finishCommand(context, run.id, request, projectRoot, failed[0].id) } });
|
|
294
295
|
}
|
|
295
296
|
const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
|
|
296
297
|
const missingRefs = frozenGateRefs(gate).filter((ref) => !passedRefs.has(ref));
|
|
@@ -403,7 +404,7 @@ export async function repairVnextMerge(context, input) {
|
|
|
403
404
|
if (request.project_id === project.id && request.status === "superseded" && errorCode(request) === "merge_source_repair") {
|
|
404
405
|
const saved = JSON.parse(request.last_error_json);
|
|
405
406
|
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
|
|
406
|
-
return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: { repair_work_id: saved.repair_work_id }, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context,
|
|
407
|
+
return { ok: true, run_id: request.run_id, merge_request_id: request.merge_request_id, status: "superseded", source_repair: { repair_work_id: saved.repair_work_id }, next: { kind: "start_stage", stage: "code", command: managedLifecycleCommand(context, runtimeCommand(context, ["stage", "start", request.run_id, "--stage", "code", "--project-root", projectRoot, "--json", "--progress-jsonl"])) } };
|
|
407
408
|
}
|
|
408
409
|
if (request.project_id !== project.id || request.status !== "action_required" || errorCode(request) !== "merge_gate_failed") {
|
|
409
410
|
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) });
|
|
@@ -422,14 +423,14 @@ export async function repairVnextMerge(context, input) {
|
|
|
422
423
|
await failWork(context, request.executor_work_id, `Superseded by source repair after failed integration gate ${failed[0]}.`);
|
|
423
424
|
assertRunMutationAllowed(context, project.id, request.run_id, generation);
|
|
424
425
|
const prepared = prepareVnextMergeSourceRepairAttempt(context, { projectRoot, runId: request.run_id, mergeRequestId: request.merge_request_id });
|
|
425
|
-
const repair = addVnextCodeRepair(context, { projectRoot, runId: request.run_id, checkReceiptId: failed[0], originWorkIds, reviewCycle: `MRR-${request.merge_request_id}`, objective: `Repair source behavior evidenced by failed integration check ${failed[0]}.` });
|
|
426
|
+
const repair = addVnextCodeRepair(context, { projectRoot, runId: request.run_id, stage: "code", checkReceiptId: failed[0], originWorkIds, reviewCycle: `MRR-${request.merge_request_id}`, objective: `Repair source behavior evidenced by failed integration check ${failed[0]}.` });
|
|
426
427
|
const now = context.now();
|
|
427
428
|
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]);
|
|
428
429
|
releaseMergeLane(context, projectRoot, request, `MERGE ${request.merge_request_id} moved to source repair`);
|
|
429
430
|
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 });
|
|
430
|
-
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,
|
|
431
|
+
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"])) } };
|
|
431
432
|
}
|
|
432
|
-
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
|
|
433
|
+
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"); }
|
|
433
434
|
function mergeReport(context, run, request, semantic, receipts) { const now = context.now(); const cleanup = cleanupReceiptPath(run); return { schema_id: "dd-flow/stage-report@2", run_id: run.id, stage, generated_at: now, verdict: "done", summary: semantic.summary, semantic: { result: semantic.summary, acceptance: ["source_commit_frozen", "integration_commit_created", "merge_gate_passed", "delivery_confirmed"], changed_files: [], checks: receipts.map((item) => item.command), evidence: [applyReceiptPath(context, request), ...receipts.map((item) => item.receipt_path), ...(fs.existsSync(cleanup) ? [cleanup] : [])], next_action: "merge_completed", merge: { merge_request_id: request.merge_request_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source_commit: request.source_commit, execution_target_head: request.execution_target_head, accepted_tree: request.accepted_tree, integration_commit: request.integration_commit, route: request.execution_route, delivery: executionSettings(run).merge_delivery, cleanup: executionSettings(run).merge_cleanup, verification_summary: semantic.verification_summary, residual_risks: semantic.residual_risks } }, mechanical: { started_at: request.lock_acquired_at, finished_at: now, git: gitFacts(request.target_workspace), queue: queueStatus(context, request) }, artifacts: { json: "stage-report.json", markdown: "stage-report.md", html: "stage-report.html" }, validation: { status: "passed" } }; }
|
|
434
435
|
function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
|
|
435
436
|
function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
@@ -588,6 +589,8 @@ function findRootWork(context, projectId, runId) { const work = context.db.get("
|
|
|
588
589
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return work; }
|
|
589
590
|
function requireRootWork(context, projectId, runId) { const work = findRootWork(context, projectId, runId); if (work.status !== "running")
|
|
590
591
|
throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
|
|
591
|
-
function applyCommand(context, request, projectRoot) { return managedLifecycleCommand(context,
|
|
592
|
-
function repairCommand(context, request, projectRoot) { return managedLifecycleCommand(context,
|
|
593
|
-
export function finishCommand(context, runId, request, projectRoot, retryCheckId) {
|
|
592
|
+
function applyCommand(context, request, projectRoot) { return managedLifecycleCommand(context, runtimeCommand(context, ["merge", "apply", request.merge_request_id, "--work", request.executor_work_id, "--project-root", projectRoot, "--json", "--progress-jsonl"])); }
|
|
593
|
+
function repairCommand(context, request, projectRoot) { return managedLifecycleCommand(context, runtimeCommand(context, ["merge", "repair", request.merge_request_id, "--project-root", projectRoot, "--json"])); }
|
|
594
|
+
export function finishCommand(context, runId, request, projectRoot, retryCheckId) {
|
|
595
|
+
return managedLifecycleCommand(context, runtimeCommand(context, ["stage", "finish", runId, "--stage", "merge", "--request", request.merge_request_id, "--work", request.executor_work_id, "--project-root", projectRoot, "--json", "--progress-jsonl", ...checkRetryArgs(context, projectRoot, runId, retryCheckId)]));
|
|
596
|
+
}
|
|
@@ -20,6 +20,7 @@ import { assertPortableArtifactRef } from "./portable-refs.js";
|
|
|
20
20
|
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
21
21
|
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
22
22
|
import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
23
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
23
24
|
const stage = "plan-review";
|
|
24
25
|
const stageDir = vnextStageDirectory(stage);
|
|
25
26
|
const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
|
|
@@ -115,7 +116,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
115
116
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
116
117
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
117
118
|
return withStageSettlement(context, () => {
|
|
118
|
-
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems });
|
|
119
|
+
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
|
|
119
120
|
const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
|
|
120
121
|
writeReport(root, report);
|
|
121
122
|
completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html" });
|
|
@@ -144,7 +145,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
144
145
|
markReviewPending(home, groups);
|
|
145
146
|
const workSessionId = String(binding.work_session_id);
|
|
146
147
|
const sessionId = String(binding.session_id);
|
|
147
|
-
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true });
|
|
148
|
+
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
148
149
|
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", read_only_input: { parent_work_id: workId }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
|
|
149
150
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
150
151
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
|
|
@@ -176,7 +177,7 @@ export function dispatchVnextPlanReview(context, input) {
|
|
|
176
177
|
const file = path.join(root, ".dispatch.json");
|
|
177
178
|
writeJson(file, { works: pending.map((group) => ({ key: group.key, task: group.task, depends_on: group.depends_on, launch_policy: "fresh_agent_required", result_schema: "dd-flow/plan-review-result@1", payload: { kind: "plan-review", read_only: true, group: { key: group.key, aspect_ids: group.aspect_ids } } })) });
|
|
178
179
|
try {
|
|
179
|
-
addWorkBatch(context, { parentWorkId: parent.work_id, file });
|
|
180
|
+
addWorkBatch(context, { parentWorkId: parent.work_id, file, stage });
|
|
180
181
|
}
|
|
181
182
|
finally {
|
|
182
183
|
fs.rmSync(file, { force: true });
|
|
@@ -306,7 +307,7 @@ function finishPlanReviewOwned(context, input) {
|
|
|
306
307
|
function preparedExisting(context, input) {
|
|
307
308
|
const promptPath = path.join(input.root, "stage-prompt.md");
|
|
308
309
|
const binding = bindStageCoordinatorWork(context, { workId: input.workId, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(input.root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
309
|
-
const orchestration = input.orchestration ?? writeFanoutDescriptor(input.root, { stage, parent_work_id: input.workId, dispatch: "plan-review", capacity_required: true });
|
|
310
|
+
const orchestration = input.orchestration ?? writeFanoutDescriptor(input.root, { stage, parent_work_id: input.workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, input.run.project_id, input.run.id, stage) });
|
|
310
311
|
return { ok: true, resumed: true, run_id: input.run.id, stage, outcome: "review_required", work_id: input.workId, id: binding.work_session_id, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), orchestration, plan_review: { requested_mode: input.requested, effective_mode: input.effective, policy_source: policySource(input.run), groups: input.groups }, next: { dispatch_command: dispatchCommand(context, input.run.id, input.projectRoot), finish_command: finishCommand(context, input.run.id, input.projectRoot, path.join(input.root, "decision.json")) } };
|
|
311
312
|
}
|
|
312
313
|
function orchestratorPrompt(context, input) {
|
|
@@ -432,7 +433,7 @@ catch {
|
|
|
432
433
|
} }
|
|
433
434
|
function rootWorkId(context, projectId, runId) { const root = context.db.get("SELECT work_id FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!root)
|
|
434
435
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
|
|
435
|
-
function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch }); }
|
|
436
|
+
function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch, stage: "code" }); }
|
|
436
437
|
function readDecision(context, projectRoot, runId, runRoot, file) { return prepareJsonSchema(file, "PLAN-REVIEW decision", { schemaName: "plan-review-decision", projectRoot, runId, runRoot, ddFlowHome: context.ddFlowHome }).value; }
|
|
437
438
|
function preserveDecisionReceipt(root, decision, preparedText) {
|
|
438
439
|
const receipt = path.join(root, "decision.receipt.json");
|