@deksden-com/dd-flow-cli 0.9.0-beta.74 → 0.9.0-beta.79
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 +8 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +1 -1
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1015 -423
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
- package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
- package/dist/harness-runtime/lib/dd-droid.mjs +3 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +10 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +130 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +49 -8
- package/dist/services/cli-operation-classifier.js +12 -24
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +7 -11
- package/dist/services/dashboard.js +94 -31
- package/dist/services/engines.js +75 -61
- package/dist/services/eval-snapshots.js +138 -38
- package/dist/services/execution-policy.js +5 -12
- package/dist/services/hooks.js +226 -203
- package/dist/services/lanes.js +60 -52
- package/dist/services/lifecycle-command.js +13 -1
- package/dist/services/lifecycle-invocations.js +285 -94
- package/dist/services/managed-processes.js +158 -18
- package/dist/services/merge-queue.js +173 -98
- package/dist/services/migrations.js +13 -8
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +11 -2
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +54 -19
- package/dist/services/recovery-observation-budget.js +1 -1
- package/dist/services/run-controller-adapter.js +31 -20
- package/dist/services/run-controller-state.js +5 -0
- package/dist/services/run-controller.js +102 -56
- package/dist/services/run-fork.js +36 -22
- package/dist/services/run-recovery.js +32 -17
- package/dist/services/runs.js +110 -38
- package/dist/services/runtime-budget.js +120 -48
- package/dist/services/runtime-scope-capture.js +2 -2
- package/dist/services/runtime-scope-control.js +2 -2
- package/dist/services/runtime-scope-resume.js +34 -12
- package/dist/services/runtime-scope-worker.js +49 -16
- package/dist/services/runtime-service.js +8 -7
- package/dist/services/schema-validation.js +9 -7
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +42 -16
- package/dist/services/stage-lifecycle.js +82 -100
- package/dist/services/stage-pause.js +54 -24
- package/dist/services/vnext-code-review.js +82 -32
- package/dist/services/vnext-code.js +78 -45
- package/dist/services/vnext-fanout.js +13 -12
- package/dist/services/vnext-merge.js +68 -35
- package/dist/services/vnext-plan-review.js +85 -42
- package/dist/services/vnext-plan.js +91 -70
- package/dist/services/vnext-protocolize.js +116 -50
- package/dist/services/vnext-specify.js +70 -57
- package/dist/services/work-registry.js +207 -94
- package/dist/services/worktrees.js +85 -35
- package/dist/storage/database.js +104 -10
- package/dist/storage/paths.js +17 -4
- package/package.json +1 -1
|
@@ -10,17 +10,18 @@ import { validateSchema } from "./schema-validation.js";
|
|
|
10
10
|
import { flowCommand } from "./stage-pause.js";
|
|
11
11
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
12
12
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
13
|
-
import { addVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
13
|
+
import { addVnextCodeRepair, prepareVnextCodeRepair, resumeAcceptedCodeStage } from "./vnext-code.js";
|
|
14
14
|
import { finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
15
|
-
import { addWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
|
-
import { withStageSettlement } from "./runs.js";
|
|
15
|
+
import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, finishStageCoordinatorWork, refreshRunWorkProjection } from "./work-registry.js";
|
|
16
|
+
import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
|
|
17
17
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
18
18
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
19
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
19
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
20
20
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
21
21
|
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
22
22
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
23
23
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
24
|
+
import { prepareJsonFile, prepareOutputFile } from "../cli/input-preparation.js";
|
|
24
25
|
const stage = "code-review";
|
|
25
26
|
const stageDir = vnextStageDirectory(stage);
|
|
26
27
|
const baselineAspects = ["goal_traceability", "coding_standards_design_review", "verification_evidence_review"];
|
|
@@ -28,34 +29,57 @@ export function isVnextCodeReviewRun(context, input) {
|
|
|
28
29
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
29
30
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
30
31
|
}
|
|
31
|
-
export function
|
|
32
|
+
export function prepareVnextCodeReviewStart(context, input) {
|
|
32
33
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
33
34
|
const project = requireProjectByRoot(context, projectRoot);
|
|
34
35
|
const run = requireRun(context, project.id, input.runId);
|
|
35
36
|
const home = requireHome(run);
|
|
36
|
-
|
|
37
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
37
38
|
requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
38
39
|
if (!fs.existsSync(path.join(home, "05-code", "stage-report.json")))
|
|
39
|
-
throw new AppError("
|
|
40
|
+
throw new AppError("runtime_artifact_missing", "CODE-REVIEW requires an accepted CODE report", 1);
|
|
40
41
|
const root = path.join(home, stageDir);
|
|
41
|
-
fs.mkdirSync(root, { recursive: true });
|
|
42
42
|
const rootWork = requireRootWork(context, project.id, run.id);
|
|
43
43
|
const prior = stageStatus(run, stage);
|
|
44
44
|
const existingPrompt = path.join(root, "stage-prompt.md");
|
|
45
|
-
if (
|
|
45
|
+
if (input.externalContext)
|
|
46
|
+
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
47
|
+
prepareOutputFile(existingPrompt);
|
|
48
|
+
const common = { projectRoot, project, run, home, root, rootWork, existingPrompt };
|
|
49
|
+
if (prior && fs.existsSync(existingPrompt))
|
|
50
|
+
return { ...common, resumed: true, prior, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(root) };
|
|
51
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
52
|
+
const mode = effectiveMode(home, run);
|
|
53
|
+
const cycle = reviewCycle(run);
|
|
54
|
+
const codeReportSha256 = sha256File(path.join(home, "05-code", "stage-report.json"));
|
|
55
|
+
const groups = mode.mode === "off" ? [] : reviewGroups(home);
|
|
56
|
+
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 } })) };
|
|
57
|
+
const items = mode.mode === "off" ? [] : prepareWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch });
|
|
58
|
+
const template = mode.mode === "off" ? "" : read(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code-review.md"));
|
|
59
|
+
const checks = mode.mode === "off" ? [] : acceptedCodeChecks(context, project.id, run.id).map(({ id, purpose }) => ({ id, purpose }));
|
|
60
|
+
for (const name of ["review-context.json", ...(mode.mode === "off" ? ["decision.json"] : ["review-work-batch.json", "fanout.json"])])
|
|
61
|
+
prepareOutputFile(path.join(root, name));
|
|
62
|
+
return { ...common, resumed: false, mode, cycle, codeReportSha256, groups, batch, items, template, checks };
|
|
63
|
+
}
|
|
64
|
+
export function startVnextCodeReview(context, input, prepared = prepareVnextCodeReviewStart(context, input)) {
|
|
65
|
+
const { projectRoot, project, run, root, rootWork, existingPrompt } = prepared;
|
|
66
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
67
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
68
|
+
fs.mkdirSync(root, { recursive: true });
|
|
69
|
+
if (prepared.resumed) {
|
|
46
70
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath: existingPrompt, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
47
71
|
const binding = bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
48
72
|
const prompt = fs.readFileSync(existingPrompt, "utf8");
|
|
49
|
-
const orchestration =
|
|
50
|
-
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")) } };
|
|
73
|
+
const orchestration = prepared.orchestration;
|
|
74
|
+
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.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")) } };
|
|
51
75
|
}
|
|
52
|
-
const mode
|
|
53
|
-
const cycle = reviewCycle(run);
|
|
76
|
+
const { mode, cycle, groups, batch, items, template, checks, codeReportSha256 } = prepared;
|
|
54
77
|
freezeFlowRunReviewMode(context, { projectRoot, runId: run.id, review: "code", mode: mode.mode, source: mode.source, reason: mode.reason });
|
|
55
78
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
56
79
|
const reviewContextPath = path.join(root, "review-context.json");
|
|
57
|
-
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id } } : {}), code_report_sha256:
|
|
80
|
+
fs.writeFileSync(reviewContextPath, `${JSON.stringify({ schema_id: "dd-flow/code-review-context@1", cycle, mode: mode.mode, mode_source: mode.source, mode_reason: mode.reason, ...(mode.mode !== "off" ? { read_only_input: { parent_work_id: rootWork.work_id } } : {}), code_report_sha256: codeReportSha256 }, null, 2)}\n`);
|
|
58
81
|
if (mode.mode === "off") {
|
|
82
|
+
fs.writeFileSync(path.join(root, "decision.json"), `${JSON.stringify(disabledReviewDecision(), null, 2)}\n`);
|
|
59
83
|
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`;
|
|
60
84
|
fs.writeFileSync(promptPath, prompt);
|
|
61
85
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
@@ -63,22 +87,23 @@ export function startVnextCodeReview(context, input) {
|
|
|
63
87
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
64
88
|
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")) } };
|
|
65
89
|
}
|
|
66
|
-
const groups = reviewGroups(home);
|
|
67
|
-
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 } })) };
|
|
68
90
|
const batchFile = path.join(root, "review-work-batch.json");
|
|
69
91
|
fs.writeFileSync(batchFile, `${JSON.stringify(batch, null, 2)}\n`);
|
|
70
|
-
const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups });
|
|
92
|
+
const prompt = orchestratorPrompt(context, { projectRoot, run, root, rootWork, mode: mode.mode, groups, template, checks });
|
|
71
93
|
fs.writeFileSync(promptPath, prompt);
|
|
72
94
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
73
95
|
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 } : {}) });
|
|
74
|
-
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id,
|
|
96
|
+
const registered = addWorkBatch(context, { parentWorkId: rootWork.work_id, value: batch, prepared: items });
|
|
75
97
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
76
98
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
77
99
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "code_review_started", work_id: rootWork.work_id, mode: mode.mode, mode_source: mode.source, groups, registered });
|
|
78
100
|
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")) } };
|
|
79
101
|
}
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
export function prepareVnextCodeReviewRepair(context, input) {
|
|
103
|
+
if (!Array.isArray(input.findingIds) || !input.findingIds.length || input.findingIds.some(id => typeof id !== "string" || !id.trim()))
|
|
104
|
+
throw new AppError("validation", "Repair requires nonempty finding IDs", 2);
|
|
105
|
+
if (!input.checkRefsByFinding || typeof input.checkRefsByFinding !== "object" || Array.isArray(input.checkRefsByFinding) || Object.entries(input.checkRefsByFinding).some(([id, refs]) => !input.findingIds.includes(id) || !Array.isArray(refs) || refs.some(ref => typeof ref !== "string" || !ref.trim())))
|
|
106
|
+
throw new AppError("validation", "Repair check references must map selected findings to check IDs", 2);
|
|
82
107
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
83
108
|
const project = requireProjectByRoot(context, projectRoot);
|
|
84
109
|
const run = requireRun(context, project.id, input.runId);
|
|
@@ -103,7 +128,7 @@ export function addVnextCodeReviewRepair(context, input) {
|
|
|
103
128
|
if (unknown.length)
|
|
104
129
|
throw new AppError("review_check_reference_unknown", "CODE-REVIEW finding cites a check absent from the accepted CODE handoff", 2, { check_refs: unknown });
|
|
105
130
|
const reviewChecks = requestedCheckRefs.map((ref) => checks.get(ref));
|
|
106
|
-
|
|
131
|
+
const repairInput = {
|
|
107
132
|
projectRoot,
|
|
108
133
|
runId: run.id,
|
|
109
134
|
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] ?? [] })),
|
|
@@ -111,7 +136,11 @@ export function addVnextCodeReviewRepair(context, input) {
|
|
|
111
136
|
originWorkIds: origins,
|
|
112
137
|
objective: input.objective,
|
|
113
138
|
reviewCycle: reviewCycle(run)
|
|
114
|
-
}
|
|
139
|
+
};
|
|
140
|
+
return { input: repairInput, value: prepareVnextCodeRepair(context, repairInput) };
|
|
141
|
+
}
|
|
142
|
+
export function addVnextCodeReviewRepair(context, input, prepared = prepareVnextCodeReviewRepair(context, input)) {
|
|
143
|
+
return addVnextCodeRepair(context, prepared.input, prepared.value);
|
|
115
144
|
}
|
|
116
145
|
export async function finishVnextCodeReview(context, input) {
|
|
117
146
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -140,10 +169,14 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
140
169
|
const coordinator = context.db.get("SELECT session_id, result_path FROM work_sessions WHERE work_id = ? AND status = 'running' ORDER BY created_at DESC LIMIT 1", [rootWork.work_id]);
|
|
141
170
|
if (!coordinator || path.resolve(coordinator.result_path ?? "") !== path.resolve(reportPath))
|
|
142
171
|
throw new AppError("code_review_session_binding_conflict", "CODE-REVIEW can finish only after its successful stage start bound the coordinator Session and report path", 1, { run_id: run.id, expected_result_path: reportPath, actual_result_path: coordinator?.result_path ?? null });
|
|
143
|
-
const decisionFile = input.decisionFile ?? path.join(root, "decision.json");
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
172
|
+
const decisionFile = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
|
|
173
|
+
const disabledDecision = disabledReviewDecision();
|
|
174
|
+
// The implicit review-off decision is the one deliberate no-file path. An
|
|
175
|
+
// explicitly supplied --decision-file is still caller input and must be
|
|
176
|
+
// readable before any lifecycle work can proceed.
|
|
177
|
+
const decision = mode === "off" && !input.decision && !input.decisionFile && !fs.existsSync(decisionFile)
|
|
178
|
+
? disabledDecision
|
|
179
|
+
: readCodeReviewDecision(decisionFile, reviewContext.accepted_decision, Boolean(input.decisionFile), input.decision);
|
|
147
180
|
validateSchema({ schemaName: "code-review-decision", file: decisionFile, data: decision, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
148
181
|
const cycle = reviewContext.cycle ?? "initial";
|
|
149
182
|
const reviewers = reviewerWorks(context, project.id, run.id, cycle);
|
|
@@ -158,7 +191,8 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
158
191
|
const checkRefsByFinding = validateRepairChecks(context, { projectId: project.id, runId: run.id, decision: canonical.decision });
|
|
159
192
|
let repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
160
193
|
const frozenShaFile = path.join(root, ".decision-sha256");
|
|
161
|
-
|
|
194
|
+
const decisionHash = sha256Value(input.decisionText ?? JSON.stringify(decision));
|
|
195
|
+
if (!reviewContext.accepted_decision && fs.existsSync(frozenShaFile) && fs.readFileSync(frozenShaFile, "utf8").trim() !== decisionHash)
|
|
162
196
|
throw new AppError("review_decision_changed", "The accepted CODE-REVIEW decision cannot change during repair", 2, { decision_file: decisionFile });
|
|
163
197
|
const fixIds = canonical.fix_ids;
|
|
164
198
|
if (fixIds.length && repairs.length === 0) {
|
|
@@ -168,7 +202,7 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
168
202
|
fs.writeFileSync(temporary, `${JSON.stringify({ ...reviewContext, accepted_decision: decision }, null, 2)}\n`);
|
|
169
203
|
fs.renameSync(temporary, contextFile);
|
|
170
204
|
const repair = addVnextCodeReviewRepair(context, { projectRoot, runId: run.id, findingIds: fixIds, checkRefsByFinding, decisionReasonsByFinding: Object.fromEntries(canonical.decision.findings.map((item) => [item.finding_ref, item.reason])), objective: `Resolve accepted CODE-REVIEW findings: ${fixIds.join(", ")}` });
|
|
171
|
-
return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, instruction: "
|
|
205
|
+
return { ok: true, run_id: run.id, stage, outcome: "repair_required", repair, instruction: "The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. When it completes, invoke next.finish_command; this review cycle uses the decision already accepted by the runtime.", next: { finish_command: finishCommand(context, run.id, projectRoot, decisionFile) } };
|
|
172
206
|
}
|
|
173
207
|
repairs = reviewRepairWorks(context, project.id, run.id, root, cycle);
|
|
174
208
|
const incompleteRepairs = repairs.filter((work) => work.status !== "completed");
|
|
@@ -226,14 +260,29 @@ export async function finishVnextCodeReview(context, input) {
|
|
|
226
260
|
});
|
|
227
261
|
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, `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`) } } : {}) };
|
|
228
262
|
}
|
|
229
|
-
export function readCodeReviewDecision(file, accepted, explicit = false) {
|
|
263
|
+
export function readCodeReviewDecision(file, accepted, explicit = false, prepared) {
|
|
230
264
|
if (!accepted)
|
|
231
|
-
return
|
|
232
|
-
if (explicit && fs.existsSync(file) && JSON.stringify(
|
|
265
|
+
return prepared ?? prepareJsonFile(file, "CODE-REVIEW decision").value;
|
|
266
|
+
if ((prepared !== undefined || (explicit && fs.existsSync(file))) && JSON.stringify(prepared ?? prepareJsonFile(file, "CODE-REVIEW decision").value) !== JSON.stringify(accepted)) {
|
|
233
267
|
throw new AppError("review_decision_changed", "This review cycle already accepted a different decision", 2, { decision_file: file });
|
|
234
268
|
}
|
|
235
269
|
return accepted;
|
|
236
270
|
}
|
|
271
|
+
function disabledReviewDecision() {
|
|
272
|
+
return { schema_id: "dd-flow/code-review-decision@3", summary: "CODE-REVIEW is disabled by RUN configuration.", findings: [] };
|
|
273
|
+
}
|
|
274
|
+
/** Retained decisions remain authoritative after their caller file is gone. */
|
|
275
|
+
export function prepareCodeReviewDecision(runRoot, file) {
|
|
276
|
+
const root = path.join(runRoot, "06-code-review");
|
|
277
|
+
const context = readJson(path.join(root, "review-context.json"));
|
|
278
|
+
const source = path.resolve(file ?? path.join(root, "decision.json"));
|
|
279
|
+
if (context.accepted_decision && !fs.existsSync(source))
|
|
280
|
+
return { value: context.accepted_decision };
|
|
281
|
+
if (context.mode === "off" && !file && !fs.existsSync(source))
|
|
282
|
+
return { value: disabledReviewDecision() };
|
|
283
|
+
const prepared = prepareJsonFile(source, "CODE-REVIEW decision");
|
|
284
|
+
return { value: readCodeReviewDecision(source, context.accepted_decision, Boolean(file), prepared.value), text: prepared.text };
|
|
285
|
+
}
|
|
237
286
|
export function reviewGroups(home) {
|
|
238
287
|
const expected = new Set(baselineAspects);
|
|
239
288
|
const declared = [];
|
|
@@ -277,7 +326,7 @@ export function assertCodeReviewCoverage(groups, reviewers) {
|
|
|
277
326
|
throw new AppError("reviewer_coverage_invalid", "CODE-REVIEW requires every expected reviewer group", 2, { missing_groups: missing });
|
|
278
327
|
}
|
|
279
328
|
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.`; }
|
|
280
|
-
function orchestratorPrompt(context, input) { const template
|
|
329
|
+
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: ${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(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 post-repair aggregate gate fails, do not stop after `code_review_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use the returned repair command with the failed receipt, a relevant completed origin Work ID, and a concise repair objective; then end the Turn so the runner can dispatch the new repair. The repair command creates Work only; do not invoke its start_command in the coordinator Session. Do not edit invisibly in the root orchestrator.", `Accepted repair checks: ${JSON.stringify(checks)}`, "```json", JSON.stringify({ schema_id: "dd-flow/code-review-decision@3", summary: "Evidence-backed conclusion.", findings: [{ finding_ref: "WRK-001-review/FIND-001", disposition: "fix | defer | reject | duplicate", reason: "Why this classification is correct.", check_refs: ["CHK-CAUSAL-CHECK only when disposition is fix"], def_id: "DEF-0001 only for an allowed P2 deferral", duplicate_of: "canonical finding_ref only for duplicate" }] }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n"); }
|
|
281
330
|
function canonicalReviewEvidence(decision, reviewers) {
|
|
282
331
|
const known = new Set(canonicalCodeFindings(reviewers).map((item) => item.finding_ref));
|
|
283
332
|
const items = new Map(decision.findings.map((item) => [item.finding_ref, item]));
|
|
@@ -405,6 +454,7 @@ function effectiveMode(home, run) { const index = JSON.parse(run.index_json); if
|
|
|
405
454
|
function executionStopTarget(run) { return JSON.parse(run.index_json).execution_profile?.settings?.stop_target ?? "code_review_completed"; }
|
|
406
455
|
function changedPaths(workspaceRoot) { return workspaceChangedPaths(workspaceRoot); }
|
|
407
456
|
function sha256File(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|
|
457
|
+
function sha256Value(value) { return crypto.createHash("sha256").update(value).digest("hex"); }
|
|
408
458
|
function stageStatus(run, name) { try {
|
|
409
459
|
return JSON.parse(run.index_json).stage_runs?.find((item) => item.stage === name)?.status ?? null;
|
|
410
460
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
2
|
import { runWorkspaceBootstrap } from "./workspace-bootstrap.js";
|
|
4
3
|
import fs from "node:fs";
|
|
5
4
|
import path from "node:path";
|
|
@@ -7,20 +6,21 @@ import { AppError } from "../shared/errors.js";
|
|
|
7
6
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
8
7
|
import { aggregateCheckDeclarations, effectiveCheckDeclarations, finalCodeCheckDeclarations, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths } from "./code-checks.js";
|
|
9
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
10
|
-
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
9
|
+
import { appendFlowRunTimelineEvent, advanceFlowRun, attachFlowRunStage, prepareFlowRunStageAttachment, completeFlowRunStage, completeFlowRun, withStageSettlement, getFlowRunVariables, gitFacts } from "./runs.js";
|
|
11
10
|
import { validateSchema } from "./schema-validation.js";
|
|
12
11
|
import { flowCommand } from "./stage-pause.js";
|
|
13
12
|
import { validateVnextCodeHandoff } from "./vnext-plan.js";
|
|
14
13
|
import { requireVnextWorkspaceRoute } from "./vnext-workspace-policy.js";
|
|
15
14
|
import { readVnextSpecifyResult } from "./vnext-specify.js";
|
|
16
|
-
import { addWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
|
|
15
|
+
import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, codeWorkGraph, finishStageCoordinatorWork, refreshRunWorkProjection, validateWorkBatchFile, workStartCommand } from "./work-registry.js";
|
|
17
16
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
18
17
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
19
18
|
import { prepareVnextMergeRequest } from "./vnext-merge.js";
|
|
20
19
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
21
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
20
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
22
21
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
23
22
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
23
|
+
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
24
24
|
const stage = "code";
|
|
25
25
|
const stageDir = vnextStageDirectory("code");
|
|
26
26
|
/** A controller prepares slow, deterministic project tooling before it asks an
|
|
@@ -39,25 +39,28 @@ export async function prepareVnextCodeWorkspace(context, input) {
|
|
|
39
39
|
fs.mkdirSync(root, { recursive: true });
|
|
40
40
|
return ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress, { force: input.stage !== "code-review" });
|
|
41
41
|
}
|
|
42
|
-
export
|
|
42
|
+
export function prepareVnextCodeStart(context, input) {
|
|
43
43
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
44
44
|
const project = requireProjectByRoot(context, projectRoot);
|
|
45
45
|
const run = requireRun(context, project.id, input.runId);
|
|
46
|
-
|
|
46
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
47
47
|
const home = requireHome(run);
|
|
48
48
|
requireVnextWorkspaceRoute({ projectRoot, runId: input.runId, runHome: home, workspaceRoot: run.workspace_root, stage });
|
|
49
49
|
const existingRoot = path.join(home, stageDir);
|
|
50
50
|
const existingPrompt = path.join(existingRoot, "stage-prompt.md");
|
|
51
51
|
const existingStage = stageStatus(run, stage);
|
|
52
52
|
const sourceRepairAttempt = isMergeSourceRepairAttempt(run);
|
|
53
|
+
const rootWork = requireRootWork(context, project.id, input.runId);
|
|
54
|
+
if (input.externalContext)
|
|
55
|
+
prepareExternalStageContext({ stageRoot: existingRoot, loaded: input.externalContext });
|
|
56
|
+
prepareOutputFile(existingPrompt);
|
|
57
|
+
prepareOutputFile(path.join(existingRoot, "fanout.json"));
|
|
58
|
+
const common = { projectRoot, project, run, home, root: existingRoot, promptPath: existingPrompt, rootWork, sourceRepairAttempt };
|
|
53
59
|
if (existingStage && fs.existsSync(existingPrompt)) {
|
|
54
|
-
|
|
55
|
-
const prompt = fs.readFileSync(existingPrompt, "utf8");
|
|
56
|
-
const rootWork = requireRootWork(context, project.id, input.runId);
|
|
57
|
-
bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath: existingPrompt, resultPath: path.join(existingRoot, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
58
|
-
const orchestration = readFanoutDescriptor(existingRoot) ?? writeFanoutDescriptor(existingRoot, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
59
|
-
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: existingStage, prompt_path: existingPrompt, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: `${flowCommand(context)} work ls --run ${run.id} --ready --project-root ${JSON.stringify(projectRoot)} --json`, finish_command: finishCommand(context, input.runId, projectRoot, path.join(existingRoot, "code-verification.json")) } };
|
|
60
|
+
return { ...common, resumed: true, existingStage, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(existingRoot) };
|
|
60
61
|
}
|
|
62
|
+
if (!sourceRepairAttempt)
|
|
63
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
61
64
|
const batch = path.join(home, "03-plan", "code-work-batch.json");
|
|
62
65
|
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
63
66
|
projectRoot,
|
|
@@ -77,12 +80,25 @@ export async function startVnextCode(context, input) {
|
|
|
77
80
|
runRoot: home
|
|
78
81
|
});
|
|
79
82
|
validateWorkBatchFile(batch);
|
|
80
|
-
const
|
|
83
|
+
const template = fs.readFileSync(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
|
|
84
|
+
const variables = getFlowRunVariables(context, { projectRoot, runId: run.id });
|
|
85
|
+
return { ...common, resumed: false, template, capacity: variables.variables?.["runtime.subagent.available_slots"] };
|
|
86
|
+
}
|
|
87
|
+
export async function startVnextCode(context, input, prepared = prepareVnextCodeStart(context, input)) {
|
|
88
|
+
const { projectRoot, project, run, root, promptPath, rootWork, sourceRepairAttempt } = prepared;
|
|
89
|
+
assertRunMutationAllowed(context, project.id, run.id);
|
|
90
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
91
|
+
if (prepared.resumed) {
|
|
92
|
+
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
93
|
+
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: `${flowCommand(context)} work ls --run ${run.id} --ready --project-root ${JSON.stringify(projectRoot)} --json`, finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) } };
|
|
96
|
+
}
|
|
81
97
|
fs.mkdirSync(root, { recursive: true });
|
|
82
98
|
const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness });
|
|
99
|
+
// Graph rendering may issue managed child commands; it belongs to execution.
|
|
100
|
+
const graph = codeWorkGraph(context, project.id, run.id);
|
|
101
|
+
const prompt = coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph });
|
|
86
102
|
fs.writeFileSync(promptPath, prompt);
|
|
87
103
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
88
104
|
const binding = bindStageCoordinatorWork(context, {
|
|
@@ -103,7 +119,6 @@ export async function startVnextCode(context, input) {
|
|
|
103
119
|
dataSchemaId: "dd-flow/stage-report@1"
|
|
104
120
|
});
|
|
105
121
|
}
|
|
106
|
-
const graph = codeWorkGraph(context, project.id, input.runId);
|
|
107
122
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true });
|
|
108
123
|
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
|
|
109
124
|
return {
|
|
@@ -159,7 +174,8 @@ export async function finishVnextCode(context, input) {
|
|
|
159
174
|
throw new AppError("obligation_coverage_incomplete", "CODE graph does not cover every accepted obligation", 2, { missing });
|
|
160
175
|
}
|
|
161
176
|
// Reject a malformed semantic receipt before running the expensive gate.
|
|
162
|
-
const verification = verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
177
|
+
const verification = input.verification ?? verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
178
|
+
validateSchema({ schemaName: "code-verification", file: input.verificationFile, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
163
179
|
if (verification.verdict === "blocked") {
|
|
164
180
|
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, instruction: "CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again." };
|
|
165
181
|
}
|
|
@@ -173,9 +189,10 @@ export async function finishVnextCode(context, input) {
|
|
|
173
189
|
originWorkIds: works.filter((work) => work.status === "completed").map((work) => work.work_id),
|
|
174
190
|
semanticUnresolved: verification.unresolved.length ? verification.unresolved : [verification.summary],
|
|
175
191
|
verificationPath: input.verificationFile,
|
|
192
|
+
verification,
|
|
176
193
|
objective: verification.summary
|
|
177
194
|
});
|
|
178
|
-
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted.
|
|
195
|
+
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction: "The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke this same stage finish command again." };
|
|
179
196
|
}
|
|
180
197
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
181
198
|
if (input.retryCheckId) {
|
|
@@ -292,21 +309,41 @@ export async function finishVnextCode(context, input) {
|
|
|
292
309
|
...(next === "start_code_review" ? { next: { kind: "start_stage", stage: "code-review", command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage code-review --project-root ${JSON.stringify(projectRoot)} --json`) } } : next === "start_merge" ? { merge_request: mergeRequest, next: { kind: "start_stage", stage: "merge", command: managedLifecycleCommand(context, `${flowCommand(context)} stage start ${run.id} --stage merge --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`) } } : {})
|
|
293
310
|
};
|
|
294
311
|
}
|
|
295
|
-
export function
|
|
312
|
+
export function prepareVnextCodeRepair(context, input) {
|
|
296
313
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
297
314
|
const project = requireProjectByRoot(context, projectRoot);
|
|
298
315
|
const run = requireRun(context, project.id, input.runId);
|
|
299
316
|
const home = requireHome(run);
|
|
300
|
-
if (!input.objective.trim())
|
|
301
|
-
throw new AppError("validation", "Repair objective must not be empty", 2);
|
|
302
317
|
if (input.reviewCycle) {
|
|
303
318
|
const previous = codeWorks(context, project.id, run.id).find((work) => packet(work)?.review_cycle === input.reviewCycle);
|
|
304
319
|
if (previous)
|
|
305
|
-
return {
|
|
320
|
+
return { kind: "replay", project, run, previous };
|
|
306
321
|
}
|
|
322
|
+
if (typeof input.objective !== "string" || !input.objective.trim())
|
|
323
|
+
throw new AppError("validation", "Repair objective must not be empty", 2);
|
|
324
|
+
if (input.semanticUnresolved !== undefined && (!Array.isArray(input.semanticUnresolved) || input.semanticUnresolved.some(value => typeof value !== "string" || !value.trim())))
|
|
325
|
+
throw new AppError("validation", "Semantic unresolved items must be nonempty strings", 2);
|
|
326
|
+
if (input.reviewFindings !== undefined && !Array.isArray(input.reviewFindings))
|
|
327
|
+
throw new AppError("validation", "Review findings must be an array", 2);
|
|
328
|
+
if (input.reviewChecks !== undefined && !Array.isArray(input.reviewChecks))
|
|
329
|
+
throw new AppError("validation", "Review checks must be an array", 2);
|
|
330
|
+
if (!Array.isArray(input.originWorkIds) || !input.originWorkIds.length || input.originWorkIds.some(id => typeof id !== "string" || !id.trim()))
|
|
331
|
+
throw new AppError("validation", "Repair requires at least one origin Work", 2);
|
|
307
332
|
if (!input.checkReceiptId && !input.reviewFindings?.length && !input.semanticUnresolved?.length) {
|
|
308
333
|
throw new AppError("validation", "Repair needs failed check evidence, a CODE-REVIEW finding, or an unresolved CODE verification", 2);
|
|
309
334
|
}
|
|
335
|
+
if ([Boolean(input.checkReceiptId), Boolean(input.reviewFindings?.length), Boolean(input.semanticUnresolved?.length)].filter(Boolean).length !== 1)
|
|
336
|
+
throw new AppError("validation", "Repair requires exactly one evidence source", 2);
|
|
337
|
+
if (input.semanticUnresolved?.length) {
|
|
338
|
+
if (!input.verificationPath)
|
|
339
|
+
throw new AppError("usage", "Semantic repair requires a verification file", 2);
|
|
340
|
+
const verification = input.verification ?? readVerification(context, { file: input.verificationPath, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
341
|
+
if (input.verification !== undefined)
|
|
342
|
+
validateSchema({ schemaName: "code-verification", file: input.verificationPath, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
343
|
+
const unresolved = verification.unresolved.length ? verification.unresolved : [verification.summary];
|
|
344
|
+
if (verification.verdict !== "needs_repair" || input.semanticUnresolved.some(value => !unresolved.includes(value)))
|
|
345
|
+
throw new AppError("validation", "Semantic repair must reference unresolved items from a needs_repair verification", 2);
|
|
346
|
+
}
|
|
310
347
|
const receipt = input.checkReceiptId ? context.db.get("SELECT id, declaration_id, run_id, command, status, receipt_path, stdout_path, stderr_path FROM check_receipts WHERE project_id = ? AND id = ?", [
|
|
311
348
|
project.id,
|
|
312
349
|
input.checkReceiptId
|
|
@@ -387,23 +424,23 @@ export function addVnextCodeRepair(context, input) {
|
|
|
387
424
|
result_schema: "dd-flow/code-work-result@3",
|
|
388
425
|
...(input.reviewCycle ? { review_cycle: input.reviewCycle } : {})
|
|
389
426
|
};
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
427
|
+
const parentWorkId = requireRootWork(context, project.id, run.id).work_id;
|
|
428
|
+
const items = prepareWorkBatch(context, { parentWorkId, value: { works: [repair] } });
|
|
429
|
+
return { kind: "create", project, run, input, parentWorkId, items, key, receipt };
|
|
430
|
+
}
|
|
431
|
+
export function addVnextCodeRepair(context, input, prepared = prepareVnextCodeRepair(context, input)) {
|
|
432
|
+
const { project, run } = prepared;
|
|
433
|
+
if (prepared.kind === "replay")
|
|
434
|
+
return { ok: true, run_id: run.id, repair_work_id: prepared.previous.work_id, start_command: workStartCommand(context, prepared.previous), reused: true };
|
|
435
|
+
const { parentWorkId, items, key, receipt } = prepared;
|
|
436
|
+
const registered = addWorkBatch(context, { parentWorkId, prepared: items });
|
|
400
437
|
const id = String(registered.work_ids[key]);
|
|
401
438
|
const work = context.db.get("SELECT * FROM works WHERE work_id = ?", [id]);
|
|
402
439
|
appendFlowRunTimelineEvent(context, project.id, run.id, {
|
|
403
440
|
type: "code_repair_created",
|
|
404
441
|
work_id: id,
|
|
405
|
-
origin_work_ids: input.originWorkIds,
|
|
406
|
-
...(receipt ? { check_receipt_id: receipt.id } : input.reviewFindings?.length ? { review_finding_ids: input.reviewFindings.map((finding) => finding.finding_ref) } : { semantic_unresolved: input.semanticUnresolved })
|
|
442
|
+
origin_work_ids: prepared.input.originWorkIds,
|
|
443
|
+
...(receipt ? { check_receipt_id: receipt.id } : prepared.input.reviewFindings?.length ? { review_finding_ids: prepared.input.reviewFindings.map((finding) => finding.finding_ref) } : { semantic_unresolved: prepared.input.semanticUnresolved })
|
|
407
444
|
});
|
|
408
445
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
409
446
|
return { ok: true, run_id: run.id, repair_work_id: id, start_command: workStartCommand(context, work) };
|
|
@@ -439,10 +476,7 @@ function receiptRepairPaths(workspaceRoot, receipt) {
|
|
|
439
476
|
return [...paths].sort();
|
|
440
477
|
}
|
|
441
478
|
function coordinatorPrompt(context, input) {
|
|
442
|
-
const graph
|
|
443
|
-
const variables = getFlowRunVariables(context, { projectRoot: input.projectRoot, runId: input.run.id });
|
|
444
|
-
const capacity = variables.variables?.["runtime.subagent.available_slots"];
|
|
445
|
-
const template = fs.readFileSync(path.join(input.run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
|
|
479
|
+
const { graph, capacity, template } = input;
|
|
446
480
|
return [
|
|
447
481
|
"<stage_identity>",
|
|
448
482
|
`- RUN: ${input.run.id}`,
|
|
@@ -464,8 +498,8 @@ function coordinatorPrompt(context, input) {
|
|
|
464
498
|
"</code_graph>",
|
|
465
499
|
"",
|
|
466
500
|
"<execution_commands>",
|
|
467
|
-
"
|
|
468
|
-
"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.
|
|
501
|
+
"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: " + `${flowCommand(context)} work ls --run ${input.run.id} --ready --project-root ${JSON.stringify(input.projectRoot)} --json`,
|
|
502
|
+
"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. Its repair_command only creates the repair Work; use it with the required origin Work IDs, then end the Turn. Do not invoke that new 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.",
|
|
469
503
|
`A repairable engine, harness, or environment failure is not a user question. Record it without finishing CODE: ${flowCommand(context)} stage block ${input.run.id} --stage code --work ${input.rootWork.work_id} --kind <engine|harness|environment> --code <stable-code> --summary-stdin --retryable --project-root ${JSON.stringify(input.projectRoot)} --json. Repair it externally, then run the exact unblock_command returned by dd-flow and continue this same stage.`,
|
|
470
504
|
`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"))}`,
|
|
471
505
|
"If the aggregate gate fails, do not stop after `code_gate_failed`: that rejected finish does not create a repair Work. In the same coordinator Turn, use its returned repair command with the relevant completed origin Work IDs and a concise repair objective, then stop so the runner can dispatch the newly declared repair. Do not edit invisibly in the root orchestrator.",
|
|
@@ -658,10 +692,9 @@ export function finishCommand(context, runId, projectRoot, verificationFile, ret
|
|
|
658
692
|
return managedLifecycleCommand(context, `${flowCommand(context)} stage finish ${runId} --stage code --verification-file ${JSON.stringify(verificationFile)} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl${retry}`);
|
|
659
693
|
}
|
|
660
694
|
function readVerification(context, input) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
return data;
|
|
695
|
+
return prepareJsonSchema(input.file, "CODE verification", {
|
|
696
|
+
schemaName: "code-verification", projectRoot: input.projectRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.runRoot
|
|
697
|
+
}).value;
|
|
665
698
|
}
|
|
666
699
|
function verificationForFinish(context, input) {
|
|
667
700
|
return readVerification(context, input);
|
|
@@ -59,8 +59,10 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
59
59
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
60
60
|
const project = requireProjectByRoot(context, projectRoot);
|
|
61
61
|
const run = context.db.get("SELECT run_root FROM runs WHERE project_id = ? AND id = ?", [project.id, input.runId]);
|
|
62
|
-
if (!run
|
|
63
|
-
throw new AppError("not_found", "RUN
|
|
62
|
+
if (!run)
|
|
63
|
+
throw new AppError("not_found", "RUN is not registered", 1, { run_id: input.runId });
|
|
64
|
+
if (!run.run_root)
|
|
65
|
+
throw new AppError("runtime_artifact_missing", "RUN artifact root is unavailable", 1, { run_id: input.runId });
|
|
64
66
|
const stageRoot = path.join(run.run_root, vnextStageDirectory(input.stage));
|
|
65
67
|
const descriptor = readFanoutDescriptor(stageRoot);
|
|
66
68
|
if (!descriptor)
|
|
@@ -93,15 +95,10 @@ export function getVnextFanoutStatus(context, input) {
|
|
|
93
95
|
* Sessions need a provider execution identity: without one we report ambiguity
|
|
94
96
|
* instead of applying a late event to the newest Work attempt. */
|
|
95
97
|
export async function reconcileVnextFanout(context, input) {
|
|
98
|
+
const value = prepareNativeObservations(input.observations);
|
|
96
99
|
const status = getVnextFanoutStatus(context, input);
|
|
97
100
|
if (!status.orchestration)
|
|
98
101
|
return { ok: true, issues: [], reconciled: [] };
|
|
99
|
-
const value = input.observations;
|
|
100
|
-
if (!value || typeof value.harness_id !== "string" || !value.harness_id || typeof value.parent_session_id !== "string" || !Array.isArray(value.children)
|
|
101
|
-
|| value.children.some(child => !child || typeof child.session_id !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(String(child.status))))
|
|
102
|
-
throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
|
|
103
|
-
if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
|
|
104
|
-
throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
|
|
105
102
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
106
103
|
const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
|
|
107
104
|
const issues = [], reconciled = [];
|
|
@@ -157,13 +154,17 @@ export async function reconcileVnextFanout(context, input) {
|
|
|
157
154
|
* evidence for an earlier Work, while an unbound terminal child is retained
|
|
158
155
|
* as an inconsistency instead of being rediscovered in a later review wave.
|
|
159
156
|
*/
|
|
160
|
-
export function
|
|
161
|
-
const value =
|
|
162
|
-
if (!value || typeof value.harness_id !== "string" || !value.harness_id || typeof value.parent_session_id !== "string" || !Array.isArray(value.children)
|
|
163
|
-
|| value.children.some(child => !child || typeof child.session_id !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(
|
|
157
|
+
export function prepareNativeObservations(observations) {
|
|
158
|
+
const value = observations;
|
|
159
|
+
if (!value || typeof value.harness_id !== "string" || !value.harness_id.trim() || typeof value.parent_session_id !== "string" || !value.parent_session_id.trim() || !Array.isArray(value.children)
|
|
160
|
+
|| value.children.some(child => !child || typeof child.session_id !== "string" || !child.session_id.trim() || typeof child.status !== "string" || !["running", "unknown", "completed", "failed", "cancelled"].includes(child.status)))
|
|
164
161
|
throw new AppError("usage", "Expected native observations: harness_id, parent_session_id and children[{session_id,status}]", 2);
|
|
165
162
|
if (new Set(value.children.map(child => child.session_id)).size !== value.children.length)
|
|
166
163
|
throw new AppError("usage", "Native observations must contain each child Session exactly once", 2);
|
|
164
|
+
return { harness_id: value.harness_id, parent_session_id: value.parent_session_id, children: value.children };
|
|
165
|
+
}
|
|
166
|
+
export function observeVnextNativeChildren(context, input) {
|
|
167
|
+
const value = prepareNativeObservations(input.observations);
|
|
167
168
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
168
169
|
const parent = storageSessionId({ harness_id: value.harness_id, session_id: value.parent_session_id });
|
|
169
170
|
const issues = [], history = [], pending = [];
|