@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,16 +10,17 @@ import { resolveProjectRoot } from "../storage/paths.js";
|
|
|
10
10
|
import { requireProjectByRoot } from "./projects.js";
|
|
11
11
|
import { nextMergeRequestId } from "./ids.js";
|
|
12
12
|
import { readProjectConfig } from "./config.js";
|
|
13
|
-
import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareVnextMergeSourceRepairAttempt } from "./runs.js";
|
|
13
|
+
import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts, prepareFlowRunStageAttachment, prepareVnextMergeSourceRepairAttempt } from "./runs.js";
|
|
14
14
|
import { flowCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
15
|
-
import { validateSchema } from "./schema-validation.js";
|
|
16
15
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
17
16
|
import { bindStageCoordinatorWork, createChildWork, failWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
|
|
18
17
|
import { addVnextCodeRepair } from "./vnext-code.js";
|
|
19
18
|
import { checkReceiptEvidenceValid, checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceChangedPaths, workspaceFingerprint } from "./code-checks.js";
|
|
20
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
21
|
-
import { ensureLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
|
|
19
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
20
|
+
import { ensureLaneWorkspace, prepareLaneWorkspace, heartbeatLaneLock, releaseLaneLock, waitAcquireLaneLock } from "./lanes.js";
|
|
22
21
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
22
|
+
import { assertStageStartHookEvent } from "./hooks.js";
|
|
23
|
+
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
23
24
|
const stage = "merge";
|
|
24
25
|
const stageDir = vnextStageDirectory(stage);
|
|
25
26
|
export function isVnextMergeRun(context, input) {
|
|
@@ -108,20 +109,43 @@ export function prepareVnextMergeRequest(context, input) {
|
|
|
108
109
|
return requestView(context, requireRequest(context, requestId));
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
|
-
export
|
|
112
|
+
export function prepareVnextMergeStart(context, input) {
|
|
112
113
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
113
114
|
const project = requireProjectByRoot(context, projectRoot);
|
|
114
115
|
const run = requireRun(context, project.id, input.runId);
|
|
115
116
|
const home = requireHome(run);
|
|
116
117
|
const request = requestForRun(context, project.id, run.id);
|
|
117
118
|
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
119
|
+
const common = { projectRoot, project, run, request, generation };
|
|
118
120
|
if (request.status === "completed")
|
|
121
|
+
return { ...common, terminal: true };
|
|
122
|
+
if (!["queued", "dispatching", "active", "action_required"].includes(request.status))
|
|
123
|
+
throw new AppError("merge_request_state_invalid", "MERGE request cannot be started in its current state", 1, { status: request.status, merge_request_id: request.merge_request_id });
|
|
124
|
+
const root = path.join(home, stageDir), workRoot = path.join(root, "works", request.executor_work_id);
|
|
125
|
+
const resultPath = path.join(workRoot, "result.json"), promptPath = path.join(workRoot, "prompt.md");
|
|
126
|
+
const work = context.db.get("SELECT status, project_id, run_id FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
127
|
+
if (!work || work.project_id !== project.id || work.run_id !== run.id || !["created", "running"].includes(work.status))
|
|
128
|
+
throw new AppError("runtime_artifact_invalid", "MERGE executor Work is missing or not startable", 1, { work_id: request.executor_work_id });
|
|
129
|
+
const index = JSON.parse(run.index_json);
|
|
130
|
+
if (!index.stage_runs?.some(entry => entry.stage === stage))
|
|
131
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
|
|
132
|
+
prepareLaneWorkspace(context, { projectRoot, lane: "merge", workspacePath: request.target_workspace, branch: request.target_branch });
|
|
133
|
+
const checks = effectiveMergeChecks(run, request), settings = executionSettings(run);
|
|
134
|
+
const existingPrompt = ["active", "action_required"].includes(request.status) && fs.existsSync(promptPath) ? fs.readFileSync(promptPath, "utf8") : null;
|
|
135
|
+
if (existingPrompt === null || input.externalContext)
|
|
136
|
+
prepareOutputFile(promptPath);
|
|
137
|
+
if (input.externalContext)
|
|
138
|
+
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
139
|
+
return { ...common, terminal: false, root, workRoot, resultPath, promptPath, checks, settings, existingPrompt };
|
|
140
|
+
}
|
|
141
|
+
export async function startVnextMerge(context, input, prepared = prepareVnextMergeStart(context, input)) {
|
|
142
|
+
const { projectRoot, project, run, request, generation } = prepared;
|
|
143
|
+
if (prepared.terminal)
|
|
119
144
|
return requestView(context, request);
|
|
120
|
-
const root
|
|
121
|
-
|
|
145
|
+
const { root, workRoot, resultPath, promptPath, checks, settings, existingPrompt } = prepared;
|
|
146
|
+
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
147
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
122
148
|
fs.mkdirSync(workRoot, { recursive: true });
|
|
123
|
-
const resultPath = path.join(workRoot, "result.json");
|
|
124
|
-
const promptPath = path.join(workRoot, "prompt.md");
|
|
125
149
|
ensureLaneWorkspace(context, { projectRoot, lane: "merge", workspacePath: request.target_workspace, branch: request.target_branch });
|
|
126
150
|
const lane = await waitAcquireLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, timeoutSeconds: 3600, pollIntervalSeconds: 15, ttlSeconds: 7200, reason: `MERGE ${request.merge_request_id}`, ...(input.progress ? { progress: input.progress } : {}) });
|
|
127
151
|
if (!lane.acquired)
|
|
@@ -133,12 +157,12 @@ export async function startVnextMerge(context, input) {
|
|
|
133
157
|
const binding = pendingWork?.status === "created"
|
|
134
158
|
? startStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) })
|
|
135
159
|
: bindStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
136
|
-
const prompt =
|
|
137
|
-
if (
|
|
160
|
+
const prompt = existingPrompt ?? mergePrompt(context, { run, request, resultPath, projectRoot }, checks, settings);
|
|
161
|
+
if (existingPrompt === null)
|
|
138
162
|
fs.writeFileSync(promptPath, prompt);
|
|
139
163
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
140
164
|
ensureMergeStageAttached(context, run, projectRoot);
|
|
141
|
-
return startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, externalContext);
|
|
165
|
+
return startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
|
|
142
166
|
}
|
|
143
167
|
const current = requireRequest(context, request.merge_request_id);
|
|
144
168
|
const targetHead = gitValue(current.target_workspace, ["rev-parse", current.target_branch], true);
|
|
@@ -152,13 +176,13 @@ export async function startVnextMerge(context, input) {
|
|
|
152
176
|
if (claimed.changes !== 1 && requireRequest(context, current.merge_request_id).status !== "active")
|
|
153
177
|
throw new AppError("merge_claim_conflict", "MERGE request could not acquire the integration lane", 1, { merge_request_id: current.merge_request_id });
|
|
154
178
|
const started = startStageCoordinatorWork(context, { workId: current.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
155
|
-
const prompt = mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot });
|
|
179
|
+
const prompt = mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot }, checks, settings);
|
|
156
180
|
fs.writeFileSync(promptPath, prompt);
|
|
157
181
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
158
182
|
context.db.run("UPDATE work_sessions SET prompt_path = ?, result_path = ?, updated_at = ? WHERE work_id = ? AND status = 'running'", [promptPath, resultPath, context.now(), current.executor_work_id]);
|
|
159
183
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
|
|
160
184
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_started", merge_request_id: current.merge_request_id, work_id: current.executor_work_id, target_head: targetHead });
|
|
161
|
-
return startPacket(context, run, current, started, promptPath, resultPath, prompt, projectRoot, externalContext);
|
|
185
|
+
return startPacket(context, run, current, started, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
|
|
162
186
|
}
|
|
163
187
|
catch (error) {
|
|
164
188
|
const latest = requireRequest(context, request.merge_request_id);
|
|
@@ -172,8 +196,8 @@ export async function startVnextMerge(context, input) {
|
|
|
172
196
|
throw error;
|
|
173
197
|
}
|
|
174
198
|
}
|
|
175
|
-
function startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, externalContext) {
|
|
176
|
-
return { ok: true, run_id: run.id, stage, merge_request_id: request.merge_request_id, work_id: request.executor_work_id, session_binding: binding.session_binding ?? binding, prompt_path: promptPath, result_path: resultPath, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), queue: queueStatus(context, request), effective_checks:
|
|
199
|
+
function startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, checks, externalContext) {
|
|
200
|
+
return { ok: true, run_id: run.id, stage, merge_request_id: request.merge_request_id, work_id: request.executor_work_id, session_binding: binding.session_binding ?? binding, prompt_path: promptPath, result_path: resultPath, worker_prompt_markdown: prompt, ...(externalContext ? { external_context: externalContext } : {}), queue: queueStatus(context, request), effective_checks: checks, next: { apply_command: applyCommand(context, request, projectRoot), finish_command: finishCommand(context, run.id, request, projectRoot) } };
|
|
177
201
|
}
|
|
178
202
|
export function applyVnextMerge(context, input) {
|
|
179
203
|
const existing = requireRequest(context, input.requestId);
|
|
@@ -207,7 +231,7 @@ export function applyVnextMerge(context, input) {
|
|
|
207
231
|
context.db.run("UPDATE merge_requests SET checkpoint = 'apply_recorded', result_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ apply_receipt: receiptFile }), context.now(), request.merge_request_id]);
|
|
208
232
|
return { ok: true, ...receipt };
|
|
209
233
|
}
|
|
210
|
-
export
|
|
234
|
+
export function prepareVnextMergeFinish(context, input) {
|
|
211
235
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
212
236
|
const project = requireProjectByRoot(context, projectRoot);
|
|
213
237
|
const run = requireRun(context, project.id, input.runId);
|
|
@@ -215,24 +239,35 @@ export async function finishVnextMerge(context, input) {
|
|
|
215
239
|
const generation = assertRunMutationAllowed(context, project.id, run.id);
|
|
216
240
|
if (existing.project_id !== project.id || existing.run_id !== run.id || existing.executor_work_id !== input.workId)
|
|
217
241
|
throw new AppError("merge_request_mismatch", "MRG/Work/RUN/project identity does not match", 2);
|
|
218
|
-
if (existing.status === "completed")
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return { ok: true, resumed: true, run_id: run.id, stage, outcome: "completed", merge_request_id: existing.merge_request_id, work_id: existing.executor_work_id, integration_commit: existing.integration_commit, report_path: path.join(requireHome(run), stageDir, "stage-report.json"), next_action: "merge_completed" };
|
|
222
|
-
}
|
|
223
|
-
let request = requireOwnedActiveRequest(context, input);
|
|
224
|
-
heartbeatLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, ttlSeconds: 7200 });
|
|
242
|
+
if (existing.status === "completed")
|
|
243
|
+
return { projectRoot, project, run, existing, generation, terminal: true };
|
|
244
|
+
const request = requireOwnedActiveRequest(context, input);
|
|
225
245
|
if (request.run_id !== run.id)
|
|
226
246
|
throw new AppError("merge_request_mismatch", "MRG does not belong to RUN", 2);
|
|
227
247
|
if (request.checkpoint === "baseline_locked" || !fs.existsSync(applyReceiptPath(context, request)))
|
|
228
248
|
throw new AppError("merge_apply_required", "Run the exact merge apply command before MERGE finish", 2, { merge_request_id: request.merge_request_id, checkpoint: request.checkpoint });
|
|
229
|
-
|
|
230
|
-
|
|
249
|
+
const conflicts = unmerged(request.target_workspace);
|
|
250
|
+
if (conflicts.length)
|
|
251
|
+
throw new AppError("merge_conflicts_unresolved", "Resolve every unmerged path before finishing MERGE", 2, { paths: conflicts });
|
|
231
252
|
const resultPath = path.join(requireHome(run), stageDir, "works", request.executor_work_id, "result.json");
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (semantic.outcome !== "completed")
|
|
253
|
+
const result = prepareJsonSchema(resultPath, "MERGE result", { schemaName: "merge-result", projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: requireHome(run) });
|
|
254
|
+
if (result.value.outcome !== "completed")
|
|
235
255
|
throw new AppError("merge_semantic_blocked", "A blocked semantic result cannot complete MERGE", 2, { result_path: resultPath });
|
|
256
|
+
const gate = readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id);
|
|
257
|
+
if (input.retryCheckId)
|
|
258
|
+
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: gate.checks, reason: input.retryReason });
|
|
259
|
+
return { projectRoot, project, run, existing, generation, terminal: false, request, resultPath, result, gate };
|
|
260
|
+
}
|
|
261
|
+
export async function finishVnextMerge(context, input, prepared = prepareVnextMergeFinish(context, input)) {
|
|
262
|
+
const { projectRoot, project, run, existing, generation } = prepared;
|
|
263
|
+
if (prepared.terminal) {
|
|
264
|
+
recoverCompletedMergeRun(context, run, projectRoot, existing);
|
|
265
|
+
releaseMergeLane(context, projectRoot, existing, `MERGE ${existing.merge_request_id} completion recovered`);
|
|
266
|
+
return { ok: true, resumed: true, run_id: run.id, stage, outcome: "completed", merge_request_id: existing.merge_request_id, work_id: existing.executor_work_id, integration_commit: existing.integration_commit, report_path: path.join(requireHome(run), stageDir, "stage-report.json"), next_action: "merge_completed" };
|
|
267
|
+
}
|
|
268
|
+
let request = prepared.request;
|
|
269
|
+
heartbeatLaneLock(context, { projectRoot, lane: "merge", workerId: request.executor_work_id, workspacePath: request.target_workspace, ttlSeconds: 7200 });
|
|
270
|
+
const semantic = prepared.result.value;
|
|
236
271
|
if (request.checkpoint === "apply_recorded") {
|
|
237
272
|
input.progress?.("bootstrapping integrated target");
|
|
238
273
|
await runBootstrap(context, run, request.target_workspace, request.merge_request_id, input.progress);
|
|
@@ -240,11 +275,9 @@ export async function finishVnextMerge(context, input) {
|
|
|
240
275
|
context.db.run("UPDATE merge_requests SET checkpoint = 'bootstrap_ready', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
|
|
241
276
|
request = requireRequest(context, request.merge_request_id);
|
|
242
277
|
}
|
|
243
|
-
const gate =
|
|
244
|
-
if (input.retryCheckId)
|
|
245
|
-
validateFailedCheckRetry(context, { projectId: project.id, runId: run.id, receiptId: input.retryCheckId, declarations: checks, reason: input.retryReason });
|
|
278
|
+
const gate = prepared.gate, checks = gate.checks;
|
|
279
|
+
if (input.retryCheckId)
|
|
246
280
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "check_retry_requested", check_receipt_id: input.retryCheckId, stage, reason: input.retryReason ?? "operator retry" });
|
|
247
|
-
}
|
|
248
281
|
const acceptancePath = path.join(requireHome(run), stageDir, "merge-gate-acceptance.json");
|
|
249
282
|
const recovered = readMergeGateAcceptance(context, acceptancePath, request, gate);
|
|
250
283
|
const resumingGate = ["checks_passed", "integration_committed", "delivered"].includes(request.checkpoint);
|
|
@@ -285,7 +318,7 @@ export async function finishVnextMerge(context, input) {
|
|
|
285
318
|
context.db.run("UPDATE merge_requests SET checkpoint = 'delivered', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
|
|
286
319
|
request = requireRequest(context, request.merge_request_id);
|
|
287
320
|
performConfiguredCleanup(run, request);
|
|
288
|
-
const acceptedResult =
|
|
321
|
+
const acceptedResult = prepared.result.text;
|
|
289
322
|
const executor = context.db.get("SELECT status, result FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
290
323
|
if (!executor)
|
|
291
324
|
return recovery(context, request, "merge_executor_missing", { work_id: request.executor_work_id });
|
|
@@ -396,7 +429,7 @@ export async function repairVnextMerge(context, input) {
|
|
|
396
429
|
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 });
|
|
397
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, `${flowCommand(context)} stage start ${request.run_id} --stage code --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`) } };
|
|
398
431
|
}
|
|
399
|
-
function mergePrompt(context, input) { const pause = managedLifecycleCommand(context, `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`); const repair = `${flowCommand(context)} merge repair ${input.request.merge_request_id} --project-root ${JSON.stringify(input.projectRoot)} --json`; return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(
|
|
432
|
+
function mergePrompt(context, input, checks, settings) { const pause = managedLifecycleCommand(context, `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`); const repair = `${flowCommand(context)} merge repair ${input.request.merge_request_id} --project-root ${JSON.stringify(input.projectRoot)} --json`; return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(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 use the returned finish command with --retry-check <receipt-id> --reason "<what was restored>". 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 with the exact heredoc below, ask the returned user_message, then use the exact resume command returned by CLI:", "```sh", stagePauseCommandTemplate(pause), "```", "</execution_contract>", ""].join("\n"); }
|
|
400
433
|
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" } }; }
|
|
401
434
|
function effectiveMergeChecks(run, request) { return readFrozenMergeGate(path.join(requireHome(run), stageDir, "merge-gate.json"), request.merge_request_id).checks; }
|
|
402
435
|
function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
-
import { withStageSettlement } from "./runs.js";
|
|
3
|
+
import { withStageSettlement, prepareFlowRunStageAttachment } from "./runs.js";
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
7
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
8
8
|
import { requireProjectByRoot } from "./projects.js";
|
|
9
9
|
import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, getFlowRunVariables, gitFacts, setRuntimeRunVariable } from "./runs.js";
|
|
10
|
-
import { addWorkBatch, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunWorkProjection, workStartCommand } from "./work-registry.js";
|
|
10
|
+
import { addWorkBatch, prepareWorkBatch, bindStageCoordinatorWork, ensureWorkRegistry, refreshRunWorkProjection, workStartCommand } from "./work-registry.js";
|
|
11
11
|
import { validateSchema } from "./schema-validation.js";
|
|
12
12
|
import { flowCommand, stagePauseCommand, stagePauseCommandTemplate } from "./stage-pause.js";
|
|
13
13
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
@@ -17,7 +17,8 @@ import { nextWorkId } from "./ids.js";
|
|
|
17
17
|
import { vnextStageDirectory } from "../domain/stage-catalog.js";
|
|
18
18
|
import { writeStageReport } from "./stage-report-renderer.js";
|
|
19
19
|
import { assertPortableArtifactRef } from "./portable-refs.js";
|
|
20
|
-
import { applyExternalStageContext } from "./stage-context.js";
|
|
20
|
+
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
21
|
+
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
21
22
|
import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
22
23
|
const stage = "plan-review";
|
|
23
24
|
const stageDir = vnextStageDirectory(stage);
|
|
@@ -26,54 +27,95 @@ export function isVnextPlanReviewRun(context, input) {
|
|
|
26
27
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
27
28
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
28
29
|
}
|
|
29
|
-
export function
|
|
30
|
-
ensureWorkRegistry(context);
|
|
30
|
+
export function prepareVnextPlanReviewStart(context, input) {
|
|
31
31
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
32
32
|
const project = requireProjectByRoot(context, projectRoot);
|
|
33
33
|
const run = requireRun(context, projectRoot, input.runId);
|
|
34
34
|
const home = requireHome(run);
|
|
35
|
-
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
36
|
-
const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
|
|
37
35
|
const planRoot = path.join(home, "03-plan");
|
|
38
36
|
const root = path.join(home, stageDir);
|
|
39
37
|
const batch = path.join(planRoot, "code-work-batch.json");
|
|
38
|
+
const common = { projectRoot, project, run, home, planRoot, root, batch };
|
|
39
|
+
const priorStage = readIndex(run).stage_runs?.find((entry) => entry.stage === stage);
|
|
40
|
+
if (priorStage?.status === "done") {
|
|
41
|
+
const report = readJson(path.join(root, "stage-report.json"));
|
|
42
|
+
const outcome = report.semantic?.plan_review?.outcome;
|
|
43
|
+
if (outcome === "off" || outcome === "accepted")
|
|
44
|
+
return { ...common, state: "terminal", report, outcome };
|
|
45
|
+
throw new AppError("invalid_work_state", "PLAN-REVIEW is already terminal; corrections are applied in the same review Work and are not re-reviewed automatically", 2, { run_id: run.id, outcome });
|
|
46
|
+
}
|
|
47
|
+
const workspaceRoute = requireVnextWorkspaceRoute({ projectRoot, runId: run.id, runHome: home, workspaceRoot: run.workspace_root, stage: "plan-review" });
|
|
40
48
|
if (!fs.existsSync(path.join(planRoot, "stage-report.json")) || !fs.existsSync(batch))
|
|
41
|
-
throw new AppError("
|
|
49
|
+
throw new AppError("runtime_artifact_missing", "PLAN-REVIEW requires the accepted PLAN report and proposed CODE batch", 1);
|
|
42
50
|
const requested = requestedMode(run);
|
|
43
51
|
const protocolIds = protocolIdsForReview(home);
|
|
44
52
|
assertReviewWorkspace(run.workspace_root, protocolIds);
|
|
45
53
|
const groups = reviewGroups(home, run.workspace_root);
|
|
46
54
|
const effective = resolveMode(requested, groups, home, run.workspace_root);
|
|
47
55
|
const existing = context.db.get("SELECT work_id, status FROM works WHERE project_id = ? AND run_id = ? AND task = ? ORDER BY created_at DESC LIMIT 1", [project.id, run.id, reviewTask]);
|
|
56
|
+
const batchChecksum = checksum(batch), planChecksum = planSetChecksum(home, run.workspace_root);
|
|
57
|
+
if (input.externalContext)
|
|
58
|
+
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
59
|
+
prepareOutputFile(path.join(root, "stage-prompt.md"));
|
|
60
|
+
const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum };
|
|
48
61
|
if (existing?.status === "running") {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const priorStage = readIndex(run).stage_runs?.find((entry) => entry.stage === stage);
|
|
54
|
-
if (priorStage?.status === "done") {
|
|
55
|
-
const report = readJson(path.join(root, "stage-report.json"));
|
|
56
|
-
const outcome = report.semantic?.plan_review?.outcome;
|
|
57
|
-
if (outcome === "off" || outcome === "accepted")
|
|
58
|
-
return { ok: true, run_id: run.id, stage, outcome: outcome === "off" ? "review_off" : "accepted", ...(report.semantic?.code ? { code: report.semantic.code } : {}), next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) }, idempotent: true };
|
|
59
|
-
throw new AppError("invalid_work_state", "PLAN-REVIEW is already terminal; corrections are applied in the same review Work and are not re-reviewed automatically", 2, { run_id: run.id, outcome });
|
|
62
|
+
const orchestration = readFanoutDescriptor(root);
|
|
63
|
+
if (!orchestration)
|
|
64
|
+
prepareOutputFile(path.join(root, "fanout.json"));
|
|
65
|
+
return { ...active, state: "resumed", workId: existing.work_id, prompt: fs.readFileSync(path.join(root, "stage-prompt.md"), "utf8"), orchestration };
|
|
60
66
|
}
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
const batchChecksum = checksum(batch);
|
|
64
|
-
const planChecksum = planSetChecksum(home, run.workspace_root);
|
|
67
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
68
|
+
const rootWork = rootWorkId(context, project.id, run.id);
|
|
65
69
|
if (effective === "off") {
|
|
66
70
|
const failures = validateVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIds });
|
|
67
71
|
if (failures.length)
|
|
68
|
-
throw new AppError("
|
|
69
|
-
|
|
72
|
+
throw new AppError("runtime_artifact_invalid", "Accepted PLAN artifacts have validation errors", 1, { errors: failures });
|
|
73
|
+
// This is retained runtime data, not a newly supplied --file argument.
|
|
74
|
+
const codeBatch = readJson(batch);
|
|
75
|
+
let codeItems;
|
|
76
|
+
try {
|
|
77
|
+
codeItems = prepareWorkBatch(context, { parentWorkId: rootWork, value: codeBatch });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof AppError && error.exitCode === 2)
|
|
81
|
+
throw new AppError("runtime_artifact_invalid", "Accepted CODE batch cannot be registered", 1, { file: batch, cause_code: error.code });
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
for (const name of ["stage-report.json", "stage-report.md", "stage-report.html"])
|
|
85
|
+
prepareOutputFile(path.join(root, name));
|
|
86
|
+
return { ...active, state: "off", rootWork, codeItems };
|
|
87
|
+
}
|
|
88
|
+
const template = read(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "plan-review.md"));
|
|
89
|
+
const revision = currentPlanRevision(home, run.workspace_root);
|
|
90
|
+
const capacity = getStageFanoutCapacity(context, { projectRoot, runId: run.id, stage });
|
|
91
|
+
for (const name of ["work-context.json", "fanout.json"])
|
|
92
|
+
prepareOutputFile(path.join(root, name));
|
|
93
|
+
return { ...active, state: "fresh", rootWork, template, revision, capacity };
|
|
94
|
+
}
|
|
95
|
+
export function startVnextPlanReview(context, input, prepared = prepareVnextPlanReviewStart(context, input)) {
|
|
96
|
+
const { projectRoot, project, run, home, root, planRoot } = prepared;
|
|
97
|
+
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
98
|
+
if (prepared.state === "terminal") {
|
|
99
|
+
const { report, outcome } = prepared;
|
|
100
|
+
return { ok: true, run_id: run.id, stage, outcome: outcome === "off" ? "review_off" : "accepted", ...(report.semantic?.code ? { code: report.semantic.code } : {}), next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) }, idempotent: true };
|
|
101
|
+
}
|
|
102
|
+
const { requested, effective, groups, batchChecksum, planChecksum, workspaceRoute } = prepared;
|
|
103
|
+
if (prepared.state === "resumed") {
|
|
104
|
+
const promptPath = path.join(root, "stage-prompt.md");
|
|
105
|
+
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
106
|
+
return { ...preparedExisting(context, { projectRoot, projectId: project.id, run, root, workId: prepared.workId, hookEventId: input.hookEventId, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}), requested, effective, groups, batchChecksum, planChecksum, orchestration: prepared.orchestration }), ...(externalContext ? { external_context: externalContext } : {}) };
|
|
107
|
+
}
|
|
108
|
+
ensureWorkRegistry(context);
|
|
109
|
+
fs.mkdirSync(root, { recursive: true });
|
|
110
|
+
const now = context.now(), rootWork = prepared.rootWork;
|
|
111
|
+
if (prepared.state === "off") {
|
|
70
112
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
71
113
|
fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
|
|
72
114
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
73
115
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
74
116
|
attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
75
117
|
return withStageSettlement(context, () => {
|
|
76
|
-
const registered =
|
|
118
|
+
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems });
|
|
77
119
|
const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context) });
|
|
78
120
|
writeReport(root, report);
|
|
79
121
|
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" });
|
|
@@ -82,11 +124,10 @@ export function startVnextPlanReview(context, input) {
|
|
|
82
124
|
return { ok: true, run_id: run.id, stage, outcome: "review_off", ...(externalContext ? { external_context: externalContext } : {}), plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), reason: "No applicable independent review group." }, next: { kind: "start_stage", stage: "code", command: codeCommand(context, run.id, projectRoot) } };
|
|
83
125
|
});
|
|
84
126
|
}
|
|
85
|
-
const rootWork = rootWorkId(context, project.id, run.id);
|
|
86
127
|
const workId = nextWorkId(context, project.id, "plan-review");
|
|
87
128
|
context.db.run("INSERT INTO works (work_id, project_id, run_id, parent_work_id, task, launch_policy, result_schema, depends_on_json, status, result, started_at, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, 'reuse_allowed', NULL, '[]', 'running', NULL, ?, ?, ?, NULL)", [workId, project.id, run.id, rootWork, reviewTask, now, now, now]);
|
|
88
129
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId, projectRoot });
|
|
89
|
-
const prompt = orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand) });
|
|
130
|
+
const prompt = orchestratorPrompt(context, { projectRoot, run, home, root, workId, requested, effective, groups, batchChecksum, planChecksum, workspaceRoute, pauseCommand, pauseCommandTemplate: stagePauseCommandTemplate(pauseCommand), template: prepared.template, revision: prepared.revision, capacity: prepared.capacity });
|
|
90
131
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
91
132
|
fs.writeFileSync(promptPath, prompt);
|
|
92
133
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
@@ -104,7 +145,7 @@ export function startVnextPlanReview(context, input) {
|
|
|
104
145
|
const workSessionId = String(binding.work_session_id);
|
|
105
146
|
const sessionId = String(binding.session_id);
|
|
106
147
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true });
|
|
107
|
-
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:
|
|
148
|
+
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 } });
|
|
108
149
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
109
150
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
|
|
110
151
|
return { ok: true, run_id: run.id, stage, outcome: "review_required", work_id: workId, id: workSessionId, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), groups }, next: { dispatch_command: dispatchCommand(context, run.id, projectRoot), finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
@@ -187,7 +228,8 @@ function finishPlanReviewOwned(context, input) {
|
|
|
187
228
|
if (!parent || parent.status !== "running")
|
|
188
229
|
throw new AppError("invalid_work_state", "PLAN-REVIEW has no running parent Work", 2);
|
|
189
230
|
const decisionPath = path.resolve(input.decisionFile ?? path.join(root, "decision.json"));
|
|
190
|
-
const decision = readDecision(context, projectRoot, run.id, home, decisionPath);
|
|
231
|
+
const decision = input.decision ?? readDecision(context, projectRoot, run.id, home, decisionPath);
|
|
232
|
+
validateSchema({ schemaName: "plan-review-decision", file: decisionPath, data: decision, projectRoot, runId: run.id, runRoot: home, ddFlowHome: context.ddFlowHome });
|
|
191
233
|
const children = context.db.all("SELECT work_id, task, status, result, created_at FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id = ?", [project.id, run.id, parent.work_id]);
|
|
192
234
|
const contextFile = readJson(path.join(root, "work-context.json"));
|
|
193
235
|
const groups = contextFile.groups ?? [];
|
|
@@ -200,7 +242,7 @@ function finishPlanReviewOwned(context, input) {
|
|
|
200
242
|
throw new AppError("stage_pause_required", "A user or decision blocker must pause the current PLAN-REVIEW Work with `dd-flow stage pause`; do not finish the stage as blocked", 2, { run_id: run.id, stage, work_id: parent.work_id });
|
|
201
243
|
}
|
|
202
244
|
if (["failed", "cancelled"].includes(decision.outcome)) {
|
|
203
|
-
return withStageSettlement(context, () => { preserveDecisionReceipt(root,
|
|
245
|
+
return withStageSettlement(context, () => { preserveDecisionReceipt(root, decision, input.decisionText); return finishTerminalReview(context, { projectRoot, projectId: project.id, run, root, planRoot, batch, parentWorkId: parent.work_id, children, contextFile, decision }); });
|
|
204
246
|
}
|
|
205
247
|
if (children.some((child) => child.status === "created" || child.status === "running"))
|
|
206
248
|
throw new AppError("worker_jobs_incomplete", "PLAN-REVIEW finish requires all reviewer Works to settle", 1, { works: children.map((child) => ({ work_id: child.work_id, status: child.status })) });
|
|
@@ -247,7 +289,7 @@ function finishPlanReviewOwned(context, input) {
|
|
|
247
289
|
const finalBatchChecksum = prepared.batchChecksum;
|
|
248
290
|
return withStageSettlement(context, () => {
|
|
249
291
|
prepared.publish();
|
|
250
|
-
preserveDecisionReceipt(root,
|
|
292
|
+
preserveDecisionReceipt(root, decision, input.decisionText);
|
|
251
293
|
const registered = registerCode(context, project.id, run.id, batch);
|
|
252
294
|
const now = context.now();
|
|
253
295
|
settleReviewParent(context, { projectRoot, projectId: project.id, runId: run.id, parentWorkId: parent.work_id, status: "completed", result: decision.summary, reason: "plan_review_completed", resultPath: path.join(root, "stage-report.json") });
|
|
@@ -264,18 +306,16 @@ function finishPlanReviewOwned(context, input) {
|
|
|
264
306
|
function preparedExisting(context, input) {
|
|
265
307
|
const promptPath = path.join(input.root, "stage-prompt.md");
|
|
266
308
|
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 } : {}) });
|
|
267
|
-
const orchestration =
|
|
309
|
+
const orchestration = input.orchestration ?? writeFanoutDescriptor(input.root, { stage, parent_work_id: input.workId, dispatch: "plan-review", capacity_required: true });
|
|
268
310
|
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")) } };
|
|
269
311
|
}
|
|
270
312
|
function orchestratorPrompt(context, input) {
|
|
271
|
-
const template
|
|
313
|
+
const { template, revision, capacity } = input;
|
|
272
314
|
const decision = path.join(input.root, "decision.json");
|
|
273
|
-
const revision = currentPlanRevision(input.home, input.run.workspace_root);
|
|
274
315
|
const workspaceContract = ["<workspace_contract>", `- route: ${input.workspaceRoute.route}`, `- feature branch: ${input.workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${input.workspaceRoute.base_ref ?? "not applicable"}`, `- read/write workspace: ${input.run.workspace_root}`, "The CLI verified this frozen route. All plan and correction writes belong in the named workspace; project root remains only the stable lifecycle identity. Do not create, switch, merge or delete branches/worktrees.", "</workspace_contract>"].join("\n");
|
|
275
|
-
const capacity = getStageFanoutCapacity(context, { projectRoot: input.projectRoot, runId: input.run.id, stage });
|
|
276
316
|
const reviewerLaunch = capacity.source === "external_policy"
|
|
277
317
|
? `After dispatch, return at the Work-graph boundary. The shared runtime launches the queued reviewer Works as separate external Sessions, at most ${capacity.available_slots} in parallel under the RUN's frozen profiles. Do not launch native children, create provider roots, run a capacity probe, or record this external limit as native capacity. Reviewers are read-only leaf workers. Continue the semantic decision only after their Work receipts settle; do not substitute a missing result or relaunch a settled reviewer.`
|
|
278
|
-
: "After dispatch,
|
|
318
|
+
: "After dispatch, stop at the Work-graph boundary. The shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract; do not choose a provider tool from this stage prompt or substitute a shell command. It launches at most the qualified capacity at once and starts unchanged queued Works only after the current wave settles. A launch rejected before it starts is not review evidence: do not create a replacement. Each reviewer must be a genuinely fresh harness child Session; the lifecycle adapter binds that observed Session, so do not bind or supply a Session ID manually. Reviewers are read-only and must not create children. As soon as a reviewer result is accepted, release that reviewer Session when the harness permits.";
|
|
279
319
|
return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch reports qualified_capacity_required, stop. The external harness controller qualifies the selected profile outside this RUN and records the resulting integer with ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<qualified-native-child-count>")}. PLAN-REVIEW never launches a capacity probe.`, reviewerLaunch, "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be explicit in one Work's task and verification and ordered before its consumer. planned_write_areas may advertise likely overlap, but do not treat them as ownership; required_read alone is not a delivery plan.", "If the final decision needs user input with no reasonable default, run this exact one-command heredoc, replacing only its placeholder body. The heredoc is the permitted stdin form; do not use cat, a pipe, a temporary file or a second shell command:", "```sh", input.pauseCommandTemplate, "```", "Ask the returned user_message, stop, then resume this same PLAN-REVIEW Work. Do not write decision.json or finish first.", `When all reviewer results are complete and every user question is resolved, classify every material finding, fix accepted findings in this same PLAN-REVIEW Work, then write ${decision} and finish: ${finishCommand(context, input.run.id, input.projectRoot, decision)}`, "Reviewer findings use local FIND-NNN ids. dd-flow exposes each finding to this coordinator as WRK-.../FIND-NNN; use that canonical finding_ref in the decision.", "A completed reviewer result with needs_changes or blocked is evidence, not the stage outcome. Classify its material findings and apply accepted fixes in this one review pass; do not start a second review automatically. Only a missing, malformed or unfinished reviewer result blocks the stage. For an accepted correction, increment PLAN revision and update only plan.json and the relevant aspect map. Do not edit or list code-work-batch.json: the CLI validates final PLAN and regenerates it. If no material correction is needed, set correction.status=not_required. The CLI checks mechanical handoff coherence; it does not prove semantic correctness.", "```json", JSON.stringify({ schema_id: "dd-flow/plan-review-decision@3", outcome: "accepted | failed | cancelled", summary: "Concise evidence-backed final decision.", finding_decisions: [{ finding_ref: "WRK-001-review/FIND-001", decision: "accepted_fix | rejected | deferred_as_DEF | requires_user | duplicate", reason: "Why." }], correction: { status: "not_required | applied", previous_plan_revision: revision, changed_paths: [], summary: "No material correction was needed, or summarize the applied correction." } }, null, 2), "```", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
|
|
280
320
|
}
|
|
281
321
|
function reviewGroups(home, workspaceRoot) {
|
|
@@ -393,10 +433,13 @@ catch {
|
|
|
393
433
|
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)
|
|
394
434
|
throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return root.work_id; }
|
|
395
435
|
function registerCode(context, projectId, runId, batch) { return addWorkBatch(context, { parentWorkId: rootWorkId(context, projectId, runId), file: batch }); }
|
|
396
|
-
function readDecision(context, projectRoot, runId, runRoot, file) {
|
|
397
|
-
function preserveDecisionReceipt(root,
|
|
436
|
+
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
|
+
function preserveDecisionReceipt(root, decision, preparedText) {
|
|
398
438
|
const receipt = path.join(root, "decision.receipt.json");
|
|
399
|
-
|
|
439
|
+
// The CLI has already parsed and validated this decision during prepare.
|
|
440
|
+
// Persist that immutable value rather than re-opening a caller-controlled
|
|
441
|
+
// filename after lifecycle admission.
|
|
442
|
+
const bytes = Buffer.from(preparedText ?? `${JSON.stringify(decision, null, 2)}\n`);
|
|
400
443
|
if (fs.existsSync(receipt)) {
|
|
401
444
|
if (!bytes.equals(fs.readFileSync(receipt)))
|
|
402
445
|
throw new AppError("decision_receipt_conflict", "The immutable PLAN-REVIEW decision receipt already contains a different payload", 2, { receipt });
|
|
@@ -516,12 +559,12 @@ function requireRun(context, projectRoot, runId) { const project = requireProjec
|
|
|
516
559
|
function requireHome(run) { if (!run.run_root)
|
|
517
560
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1); return run.run_root; }
|
|
518
561
|
function read(file) { if (!fs.existsSync(file))
|
|
519
|
-
throw new AppError("
|
|
562
|
+
throw new AppError("runtime_artifact_missing", "Required vNext prompt is missing", 1, { file }); return fs.readFileSync(file, "utf8"); }
|
|
520
563
|
function readJson(file) { try {
|
|
521
564
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
522
565
|
}
|
|
523
566
|
catch {
|
|
524
|
-
throw new AppError("
|
|
567
|
+
throw new AppError("runtime_artifact_invalid", "Expected valid retained JSON artifact", 1, { file });
|
|
525
568
|
} }
|
|
526
569
|
function writeJson(file, value) { const temporary = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
|
|
527
570
|
function checksum(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|