@deksden-com/dd-flow-cli 0.8.0-beta.135 → 0.9.0-beta.0

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +31 -4
  3. package/dist/build-info.json +10 -10
  4. package/dist/cli/help.js +15 -63
  5. package/dist/cli/run-cli.js +55 -26
  6. package/dist/domain/stage-catalog.js +2 -2
  7. package/dist/domain/validation.js +1 -1
  8. package/dist/schemas/agent-profile.schema.json +17 -0
  9. package/dist/schemas/code-review-decision.schema.json +5 -3
  10. package/dist/schemas/code-work-batch.schema.json +3 -3
  11. package/dist/schemas/compatibility.schema.json +32 -0
  12. package/dist/schemas/flow-contract.schema.json +3 -2
  13. package/dist/schemas/merge-result.schema.json +15 -0
  14. package/dist/schemas/protocol-plan.schema.json +1 -1
  15. package/dist/schemas/stage-start-response.schema.json +4 -2
  16. package/dist/schemas/status-report.schema.json +76 -0
  17. package/dist/schemas/vnext-protocol-plan.schema.json +4 -4
  18. package/dist/services/cli-operation-classifier.js +1 -1
  19. package/dist/services/code-checks.js +125 -208
  20. package/dist/services/eval-snapshots.js +10 -1
  21. package/dist/services/harness-adapter.js +59 -0
  22. package/dist/services/hooks.js +87 -237
  23. package/dist/services/ids.js +18 -1
  24. package/dist/services/lifecycle-command.js +288 -0
  25. package/dist/services/merge-server.js +124 -0
  26. package/dist/services/prompts.js +16 -10
  27. package/dist/services/runs.js +7 -2
  28. package/dist/services/sessions.js +18 -27
  29. package/dist/services/stage-pause.js +13 -0
  30. package/dist/services/vnext-code-review.js +71 -20
  31. package/dist/services/vnext-code.js +131 -34
  32. package/dist/services/vnext-execution-profile.js +6 -3
  33. package/dist/services/vnext-merge.js +330 -0
  34. package/dist/services/vnext-plan-review.js +2 -2
  35. package/dist/services/vnext-plan.js +22 -38
  36. package/dist/services/vnext-specify.js +5 -2
  37. package/dist/services/vnext-workspace-policy.js +8 -2
  38. package/dist/services/work-registry.js +85 -34
  39. package/dist/storage/database.js +66 -0
  40. package/package.json +2 -1
@@ -0,0 +1,330 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync, spawnSync } from "node:child_process";
4
+ import { vnextStageDirectory } from "../domain/stage-catalog.js";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { resolveProjectRoot } from "../storage/paths.js";
7
+ import { requireProjectByRoot } from "./projects.js";
8
+ import { nextMergeRequestId } from "./ids.js";
9
+ import { readProjectConfig } from "./config.js";
10
+ import { appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRun, completeFlowRunStage, gitFacts } from "./runs.js";
11
+ import { flowCommand, stagePauseCommandTemplate } from "./stage-pause.js";
12
+ import { validateSchema } from "./schema-validation.js";
13
+ import { writeStageReport } from "./stage-report-renderer.js";
14
+ import { bindStageCoordinatorWork, createChildWork, finishFanInWork, finishWork, refreshRunWorkProjection, startStageCoordinatorWork } from "./work-registry.js";
15
+ import { checkReceipts, effectiveCheckDeclarations, readCodeCheckProfile, runCodeChecks, unchangedFinalGateFailures, workspaceFingerprint } from "./code-checks.js";
16
+ import { applyExternalStageContext } from "./stage-context.js";
17
+ const stage = "merge";
18
+ const stageDir = vnextStageDirectory(stage);
19
+ export function isVnextMergeRun(context, input) {
20
+ const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
21
+ return Boolean(context.db.get("SELECT 1 FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
22
+ }
23
+ /** Terminal CODE/CODE-REVIEW calls this once; it freezes source and creates the child MERGE Work. */
24
+ export function ensureVnextMergeRequest(context, input) {
25
+ const projectRoot = resolveProjectRoot(input.projectRoot);
26
+ const project = requireProjectByRoot(context, projectRoot);
27
+ const run = requireRun(context, project.id, input.runId);
28
+ const existing = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at LIMIT 1", [project.id, run.id]);
29
+ if (existing)
30
+ return requestView(context, existing);
31
+ const settings = executionSettings(run);
32
+ const route = settings.merge_mode ?? "same_session";
33
+ const home = requireHome(run);
34
+ const protocols = protocolIds(home);
35
+ const sourceBranch = gitValue(run.workspace_root, ["branch", "--show-current"]);
36
+ if (!sourceBranch)
37
+ throw new AppError("merge_source_invalid", "MERGE requires a named source branch", 1, { workspace_root: run.workspace_root });
38
+ commitPendingSource(run.workspace_root, run.id, ignoredGitPaths(context, project.id));
39
+ const sourceCommit = gitValue(run.workspace_root, ["rev-parse", "HEAD"]);
40
+ const targetBranch = workspaceRoute(home).integration_branch;
41
+ if (!sourceCommit || !targetBranch)
42
+ throw new AppError("merge_source_invalid", "MERGE could not freeze source commit or target branch", 1, { source_commit: sourceCommit, target_branch: targetBranch });
43
+ const targetWorkspace = projectRoot;
44
+ const enqueueTarget = gitValue(targetWorkspace, ["rev-parse", targetBranch], true);
45
+ const semantic = planChecks(run.workspace_root, protocols);
46
+ validateMergeAcceptance(run.workspace_root, protocols, semantic);
47
+ const effective = effectiveCheckDeclarations(run.workspace_root, semantic, ["merge"]);
48
+ if (run.workspace_root !== targetWorkspace && effective.length === 0)
49
+ throw new AppError("merge_gate_missing", "A real source/target integration requires at least one semantic or policy merge check", 2, { run_id: run.id, protocol_ids: protocols });
50
+ const root = requireRootWork(context, project.id, run.id);
51
+ let child;
52
+ let requestId;
53
+ const now = context.now();
54
+ context.db.exec("BEGIN IMMEDIATE");
55
+ try {
56
+ const raced = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ?", [project.id, run.id]);
57
+ if (raced) {
58
+ context.db.exec("COMMIT");
59
+ return requestView(context, raced);
60
+ }
61
+ child = createChildWork(context, { parentWorkId: root.work_id, slug: "merge", task: `Integrate frozen source ${sourceCommit} into ${targetBranch}, resolve material conflicts, and preserve accepted behavior.`, launchPolicy: route === "server" ? "fresh_agent_required" : "reuse_allowed", resultSchema: "dd-flow/merge-result@1", payload: { kind: "merge", checks: semantic, protocol_ids: protocols } });
62
+ requestId = nextMergeRequestId(context);
63
+ context.db.run("INSERT INTO merge_requests (merge_request_id, project_id, run_id, executor_work_id, protocol_ids_json, source_workspace, source_branch, source_commit, target_workspace, target_branch, enqueue_target_head, execution_target_head, integration_commit, execution_route, status, dispatch_owner, dispatch_lease_token, dispatch_lease_expires_at, lock_acquired_at, checkpoint, profile_hash, adapter_receipt_json, result_json, last_error_json, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, 'queued', NULL, NULL, NULL, NULL, 'queued', ?, NULL, NULL, NULL, ?, ?, NULL)", [requestId, project.id, run.id, child.work_id, JSON.stringify(protocols), run.workspace_root, sourceBranch, sourceCommit, targetWorkspace, targetBranch, enqueueTarget, route, readCodeCheckProfile(targetWorkspace).hash, now, now]);
64
+ context.db.exec("COMMIT");
65
+ }
66
+ catch (error) {
67
+ context.db.exec("ROLLBACK");
68
+ throw error;
69
+ }
70
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_request_queued", merge_request_id: requestId, work_id: child.work_id, route, source_commit: sourceCommit, target_branch: targetBranch });
71
+ return requestView(context, requireRequest(context, requestId));
72
+ }
73
+ export async function startVnextMerge(context, input) {
74
+ const projectRoot = resolveProjectRoot(input.projectRoot);
75
+ const project = requireProjectByRoot(context, projectRoot);
76
+ const run = requireRun(context, project.id, input.runId);
77
+ const home = requireHome(run);
78
+ const request = requestForRun(context, project.id, run.id);
79
+ if (request.status === "completed")
80
+ return requestView(context, request);
81
+ const root = path.join(home, stageDir);
82
+ const workRoot = path.join(root, "works", request.executor_work_id);
83
+ fs.mkdirSync(workRoot, { recursive: true });
84
+ const resultPath = path.join(workRoot, "result.json");
85
+ const promptPath = path.join(workRoot, "prompt.md");
86
+ if (["active", "action_required"].includes(request.status)) {
87
+ const pendingWork = context.db.get("SELECT status FROM works WHERE work_id = ?", [request.executor_work_id]);
88
+ const binding = pendingWork?.status === "created"
89
+ ? startStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) })
90
+ : bindStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
91
+ const prompt = fs.existsSync(promptPath) ? fs.readFileSync(promptPath, "utf8") : mergePrompt(context, { run, request, resultPath, projectRoot });
92
+ if (!fs.existsSync(promptPath))
93
+ fs.writeFileSync(promptPath, prompt);
94
+ const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
95
+ ensureMergeStageAttached(context, run, projectRoot);
96
+ return startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, externalContext);
97
+ }
98
+ await waitForTurn(context, request, input.progress);
99
+ const current = requireRequest(context, request.merge_request_id);
100
+ const targetHead = gitValue(current.target_workspace, ["rev-parse", current.target_branch], true);
101
+ const checkedOutTarget = gitValue(current.target_workspace, ["branch", "--show-current"], true);
102
+ if (checkedOutTarget !== current.target_branch)
103
+ throw new AppError("merge_target_branch_mismatch", "Integration workspace is not on the configured target branch", 1, { expected: current.target_branch, actual: checkedOutTarget, target_workspace: current.target_workspace });
104
+ const targetStatus = meaningfulStatus(current.target_workspace, ignoredGitPaths(context, project.id));
105
+ if (targetStatus.length)
106
+ throw new AppError("merge_target_dirty", "Integration workspace must be clean before MERGE starts", 1, { target_workspace: current.target_workspace, status: targetStatus });
107
+ const claimed = context.db.run("UPDATE merge_requests SET status = 'active', execution_target_head = ?, lock_acquired_at = ?, checkpoint = 'baseline_locked', updated_at = ? WHERE merge_request_id = ? AND status IN ('queued','dispatching','action_required')", [targetHead, context.now(), context.now(), current.merge_request_id]);
108
+ if (claimed.changes !== 1 && requireRequest(context, current.merge_request_id).status !== "active")
109
+ throw new AppError("merge_claim_conflict", "MERGE request could not acquire the integration lane", 1, { merge_request_id: current.merge_request_id });
110
+ const started = startStageCoordinatorWork(context, { workId: current.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
111
+ const prompt = mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot });
112
+ fs.writeFileSync(promptPath, prompt);
113
+ const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
114
+ 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]);
115
+ attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
116
+ 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 });
117
+ return startPacket(context, run, current, started, promptPath, resultPath, prompt, projectRoot, externalContext);
118
+ }
119
+ function startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, externalContext) {
120
+ 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: effectiveMergeChecks(run, request), next: { apply_command: applyCommand(context, request, projectRoot), finish_command: finishCommand(context, run.id, request, projectRoot) } };
121
+ }
122
+ export function applyVnextMerge(context, input) {
123
+ const existing = requireRequest(context, input.requestId);
124
+ const receiptFile = applyReceiptPath(context, existing);
125
+ if (existing.executor_work_id === input.workId && ["apply_recorded", "integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(existing.checkpoint) && fs.existsSync(receiptFile))
126
+ return JSON.parse(fs.readFileSync(receiptFile, "utf8"));
127
+ const request = requireOwnedActiveRequest(context, input);
128
+ assertExecutionBaseline(request);
129
+ input.progress?.(`applying ${request.source_commit} to ${request.target_branch}`);
130
+ let outcome = "applied_clean";
131
+ let conflicts = [];
132
+ if (request.source_commit === request.execution_target_head)
133
+ outcome = "no_op";
134
+ else {
135
+ const result = spawnSync("git", ["merge", "--no-ff", "--no-commit", request.source_commit], { cwd: request.target_workspace, encoding: "utf8" });
136
+ conflicts = unmerged(request.target_workspace);
137
+ if (result.status !== 0 && conflicts.length === 0)
138
+ return recovery(context, request, "merge_apply_failed", { stdout: result.stdout, stderr: result.stderr, status: result.status });
139
+ if (conflicts.length)
140
+ outcome = "conflicts";
141
+ }
142
+ const receipt = { schema_id: "dd-flow/merge-apply-receipt@1", merge_request_id: request.merge_request_id, work_id: request.executor_work_id, source_commit: request.source_commit, target_baseline: request.execution_target_head, outcome, conflicts, at: context.now() };
143
+ fs.mkdirSync(path.dirname(receiptFile), { recursive: true });
144
+ fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
145
+ 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]);
146
+ return { ok: true, ...receipt };
147
+ }
148
+ export async function finishVnextMerge(context, input) {
149
+ const projectRoot = resolveProjectRoot(input.projectRoot);
150
+ const project = requireProjectByRoot(context, projectRoot);
151
+ const run = requireRun(context, project.id, input.runId);
152
+ const existing = requireRequest(context, input.requestId);
153
+ if (existing.project_id !== project.id || existing.run_id !== run.id || existing.executor_work_id !== input.workId)
154
+ throw new AppError("merge_request_mismatch", "MRG/Work/RUN/project identity does not match", 2);
155
+ if (existing.status === "completed") {
156
+ recoverCompletedMergeRun(context, run, projectRoot, existing);
157
+ 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" };
158
+ }
159
+ let request = requireOwnedActiveRequest(context, input);
160
+ if (request.run_id !== run.id)
161
+ throw new AppError("merge_request_mismatch", "MRG does not belong to RUN", 2);
162
+ if (request.checkpoint === "baseline_locked" || !fs.existsSync(applyReceiptPath(context, request)))
163
+ 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 });
164
+ if (unmerged(request.target_workspace).length)
165
+ throw new AppError("merge_conflicts_unresolved", "Resolve every unmerged path before finishing MERGE", 2, { paths: unmerged(request.target_workspace) });
166
+ const resultPath = path.join(requireHome(run), stageDir, "works", request.executor_work_id, "result.json");
167
+ validateSchema({ schemaName: "merge-result", file: resultPath, projectRoot: request.target_workspace, ddFlowHome: context.ddFlowHome, runId: run.id });
168
+ const semantic = JSON.parse(fs.readFileSync(resultPath, "utf8"));
169
+ if (semantic.outcome !== "completed")
170
+ throw new AppError("merge_semantic_blocked", "A blocked semantic result cannot complete MERGE", 2, { result_path: resultPath });
171
+ if (!["integration_committed", "bootstrap_ready", "checks_passed", "delivered", "finalized"].includes(request.checkpoint)) {
172
+ commitIntegration(request.target_workspace, run.id, ignoredGitPaths(context, project.id));
173
+ const commit = gitValue(request.target_workspace, ["rev-parse", "HEAD"]);
174
+ context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [commit, context.now(), request.merge_request_id]);
175
+ request = requireRequest(context, request.merge_request_id);
176
+ }
177
+ else if (meaningfulStatus(request.target_workspace, ignoredGitPaths(context, project.id)).length) {
178
+ commitIntegration(request.target_workspace, `${run.id}-fix`, ignoredGitPaths(context, project.id));
179
+ context.db.run("UPDATE merge_requests SET integration_commit = ?, checkpoint = 'integration_committed', updated_at = ? WHERE merge_request_id = ?", [gitValue(request.target_workspace, ["rev-parse", "HEAD"]), context.now(), request.merge_request_id]);
180
+ request = requireRequest(context, request.merge_request_id);
181
+ }
182
+ if (request.checkpoint === "integration_committed") {
183
+ input.progress?.("bootstrapping integrated target");
184
+ runBootstrap(run, request.target_workspace);
185
+ context.db.run("UPDATE merge_requests SET checkpoint = 'bootstrap_ready', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
186
+ request = requireRequest(context, request.merge_request_id);
187
+ }
188
+ const checks = effectiveMergeChecks(run, request);
189
+ const unchanged = unchangedFinalGateFailures(context, { projectId: project.id, runId: run.id, workspaceRoot: request.target_workspace, declarations: checks });
190
+ if (unchanged.length)
191
+ throw new AppError("merge_check_repair_required", "MERGE checks already failed for the unchanged integrated tree", 2, { failures: unchanged });
192
+ const receipts = request.checkpoint === "checks_passed" || request.checkpoint === "delivered" || request.checkpoint === "finalized" ? checkReceipts(context, { projectId: project.id, runId: run.id, workId: request.executor_work_id }).filter((receipt) => receipt.status === "passed" && receipt.before_fingerprint === workspaceFingerprint(request.target_workspace)) : await runCodeChecks(context, { projectId: project.id, runId: run.id, runHome: requireHome(run), workspaceRoot: request.target_workspace, workId: request.executor_work_id, artifactDir: path.join(stageDir, "works", request.executor_work_id), scope: "aggregate", checks, ...(input.progress ? { progress: input.progress } : {}) });
193
+ const failed = receipts.filter((receipt) => receipt.status !== "passed");
194
+ if (failed.length) {
195
+ context.db.run("UPDATE merge_requests SET status = 'action_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code: "merge_gate_failed", failures: failed }), context.now(), request.merge_request_id]);
196
+ throw new AppError("merge_gate_failed", "Integrated target checks failed; repair in the same MERGE Work and retry finish", 2, { failures: failed });
197
+ }
198
+ const requiredRefs = mergeAcceptanceRefs(run.workspace_root, JSON.parse(request.protocol_ids_json));
199
+ const passedRefs = new Set(receipts.flatMap((receipt) => receipt.check_refs));
200
+ const missingRefs = requiredRefs.filter((ref) => !passedRefs.has(ref));
201
+ if (missingRefs.length)
202
+ throw new AppError("merge_acceptance_unproven", "Current MERGE receipts do not cover every merge acceptance reference", 2, { missing_check_refs: missingRefs });
203
+ context.db.run("UPDATE merge_requests SET checkpoint = 'checks_passed', status = 'active', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
204
+ request = requireRequest(context, request.merge_request_id);
205
+ verifyLocalDelivery(request);
206
+ context.db.run("UPDATE merge_requests SET checkpoint = 'delivered', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
207
+ request = requireRequest(context, request.merge_request_id);
208
+ performConfiguredCleanup(run, request);
209
+ await finishWork(context, request.executor_work_id, fs.readFileSync(resultPath, "utf8"));
210
+ const now = context.now();
211
+ context.db.run("UPDATE merge_requests SET status = 'completed', checkpoint = 'finalized', result_json = ?, completed_at = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify(semantic), now, now, request.merge_request_id]);
212
+ const report = mergeReport(context, run, requireRequest(context, request.merge_request_id), semantic, receipts);
213
+ const root = path.join(requireHome(run), stageDir);
214
+ writeStageReport(root, report);
215
+ completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@2", report: "stage-report.md", stageReport: "stage-report.html" });
216
+ const rootWork = requireRootWork(context, project.id, run.id);
217
+ finishFanInWork(context, rootWork.work_id, JSON.stringify(report));
218
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "merge_completed", nextAction: undefined });
219
+ refreshRunWorkProjection(context, project.id, run.id);
220
+ appendFlowRunTimelineEvent(context, project.id, run.id, { type: "merge_completed", merge_request_id: request.merge_request_id, integration_commit: requireRequest(context, request.merge_request_id).integration_commit });
221
+ return { ok: true, run_id: run.id, stage, outcome: "completed", merge_request_id: request.merge_request_id, work_id: request.executor_work_id, integration_commit: requireRequest(context, request.merge_request_id).integration_commit, report_path: path.join(root, "stage-report.json"), next_action: "merge_completed" };
222
+ }
223
+ export function getVnextMergeRequest(context, input) { const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)); const request = input.requestId ? requireRequest(context, input.requestId) : input.runId ? requestForRun(context, project.id, input.runId) : null; if (!request || request.project_id !== project.id)
224
+ throw new AppError("not_found", "MERGE request is not registered", 1); return requestView(context, request); }
225
+ function ensureMergeStageAttached(context, run, projectRoot) {
226
+ const index = JSON.parse(run.index_json);
227
+ if (!index.stage_runs?.some((entry) => entry.stage === stage))
228
+ attachFlowRunStage(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
229
+ }
230
+ function recoverCompletedMergeRun(context, run, projectRoot, request) {
231
+ const root = path.join(requireHome(run), stageDir);
232
+ const reportPath = path.join(root, "stage-report.json");
233
+ if (!fs.existsSync(reportPath))
234
+ throw new AppError("merge_report_missing", "Completed MERGE request has no stage report", 1, { merge_request_id: request.merge_request_id, report_path: reportPath });
235
+ ensureMergeStageAttached(context, run, projectRoot);
236
+ completeFlowRunStage(context, { projectRoot, runId: run.id, stage, status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@2", report: "stage-report.md", stageReport: "stage-report.html" });
237
+ const rootWork = findRootWork(context, run.project_id, run.id);
238
+ const rootStatus = rootWork.status;
239
+ if (rootStatus === "running")
240
+ finishFanInWork(context, rootWork.work_id, fs.readFileSync(reportPath, "utf8"));
241
+ const runStatus = context.db.get("SELECT status FROM runs WHERE project_id = ? AND id = ?", [run.project_id, run.id])?.status;
242
+ if (runStatus !== "done")
243
+ completeFlowRun(context, { projectRoot, runId: run.id, status: "done", verdict: "merge_completed", nextAction: undefined });
244
+ refreshRunWorkProjection(context, run.project_id, run.id);
245
+ }
246
+ export function routeVnextMergeRequest(context, input) { const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)); const request = requireRequest(context, input.requestId); if (request.project_id !== project.id || request.status !== "queued")
247
+ throw new AppError("invalid_merge_state", "Only a queued request without an active dispatch may change route", 2, { status: request.status }); context.db.exec("BEGIN IMMEDIATE"); try {
248
+ context.db.run("UPDATE merge_requests SET execution_route = ?, last_error_json = ?, updated_at = ? WHERE merge_request_id = ? AND status = 'queued'", [input.mode, JSON.stringify({ route_reason: input.reason }), context.now(), request.merge_request_id]);
249
+ context.db.run("UPDATE works SET launch_policy = ?, updated_at = ? WHERE work_id = ? AND status = 'created'", [input.mode === "server" ? "fresh_agent_required" : "reuse_allowed", context.now(), request.executor_work_id]);
250
+ context.db.exec("COMMIT");
251
+ }
252
+ catch (error) {
253
+ context.db.exec("ROLLBACK");
254
+ throw error;
255
+ } refreshRunWorkProjection(context, request.project_id, request.run_id); return requestView(context, requireRequest(context, request.merge_request_id)); }
256
+ function mergePrompt(context, input) { const pause = `${flowCommand(context)} stage pause ${input.run.id} --stage merge --work ${input.request.executor_work_id} --project-root ${JSON.stringify(input.projectRoot)} --question-stdin --json`; return ["<stage_identity>", `- RUN: ${input.run.id}`, `- MERGE request: ${input.request.merge_request_id}`, `- Work: ${input.request.executor_work_id}`, "- stage: merge", "</stage_identity>", "", "<trusted_runtime_context>", `- integration workspace: ${input.request.target_workspace}`, `- source workspace: ${input.request.source_workspace}`, `- frozen source commit: ${input.request.source_commit}`, `- target branch: ${input.request.target_branch}`, `- execution target baseline: ${input.request.execution_target_head}`, `- queue route: ${input.request.execution_route}`, `- delivery: ${JSON.stringify(executionSettings(input.run).merge_delivery)}`, `- cleanup: ${JSON.stringify(executionSettings(input.run).merge_cleanup)}`, "These facts and the acquired project integration lane were established by dd-flow. Do not repeat discovery and do not run git merge/rebase/squash yourself.", "</trusted_runtime_context>", "", "<effective_merge_gate>", ...effectiveMergeChecks(input.run, input.request).map((check) => `- ${check.canonical_ref ?? check.id}: ${check.command} — ${check.purpose}`), "</effective_merge_gate>", "", "<execution_contract>", `1. Run this exact standalone command first: ${applyCommand(context, input.request, input.projectRoot)}`, "2. If it reports conflicts, resolve only the actual integration conflicts in the integration workspace. Do not repeat merge apply.", `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 check fails, inspect only the returned receipt/logs, repair the integrated target in this same Work, update the semantic result, and repeat the same finish command. Do not create a repair Work or rerun independent review.", "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"); }
257
+ 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, 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" } }; }
258
+ function effectiveMergeChecks(run, request) { return effectiveCheckDeclarations(request.target_workspace, planChecks(run.workspace_root, JSON.parse(request.protocol_ids_json)), ["merge"]); }
259
+ function planChecks(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
260
+ return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.checks ?? []).filter((check) => check.run_at === "merge").map((check) => ({ ...check, canonical_ref: `${protocol}/${check.id}` })); }); }
261
+ function validateMergeAcceptance(workspace, protocols, checks) { const known = new Set(checks.map((check) => check.canonical_ref)); const missing = mergeAcceptanceRefs(workspace, protocols).filter((ref) => !known.has(ref)); if (missing.length)
262
+ throw new AppError("merge_acceptance_invalid", "A merge acceptance criterion references a non-merge check", 2, { check_refs: missing }); }
263
+ function mergeAcceptanceRefs(workspace, protocols) { return protocols.flatMap((protocol) => { const file = path.join(workspace, ".memory-bank", "protocol", protocol, "plan.json"); if (!fs.existsSync(file))
264
+ return []; const plan = JSON.parse(fs.readFileSync(file, "utf8")); return (plan.acceptance ?? []).filter((entry) => entry.gate === "merge").flatMap((entry) => (entry.check_refs ?? []).map((ref) => `${protocol}/${ref}`)); }); }
265
+ function protocolIds(home) { const root = path.join(home, "03-plan"); if (!fs.existsSync(root))
266
+ return []; return fs.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("PRT-")).map((entry) => entry.name).sort(); }
267
+ function workspaceRoute(home) { const file = path.join(home, "02-protocolize", "workspace-route.json"); const value = JSON.parse(fs.readFileSync(file, "utf8")); if (!value.policy?.integration_branch)
268
+ throw new AppError("workspace_route_missing", "MERGE requires the frozen integration branch", 1, { file }); return { integration_branch: value.policy.integration_branch }; }
269
+ function executionSettings(run) { return JSON.parse(run.index_json).execution_profile?.settings ?? {}; }
270
+ function runBootstrap(run, cwd) { const command = executionSettings(run).code_bootstrap?.command; if (!command)
271
+ return; const result = spawnSync("/bin/sh", ["-lc", command], { cwd, encoding: "utf8" }); if (result.status !== 0)
272
+ throw new AppError("merge_bootstrap_failed", "Integrated target bootstrap failed", 2, { command, stdout: result.stdout, stderr: result.stderr, status: result.status }); }
273
+ function verifyLocalDelivery(request) { const head = gitValue(request.target_workspace, ["rev-parse", request.target_branch]); if (!request.integration_commit || head !== request.integration_commit)
274
+ throw new AppError("merge_delivery_unproven", "Local integration branch does not resolve to the accepted integration commit", 1, { target_branch: request.target_branch, expected: request.integration_commit, actual: head }); }
275
+ function cleanupReceiptPath(run) { return path.join(requireHome(run), stageDir, "cleanup-receipt.json"); }
276
+ function performConfiguredCleanup(run, request) {
277
+ const policy = executionSettings(run).merge_cleanup?.source ?? "retain";
278
+ const receipt = cleanupReceiptPath(run);
279
+ let action = "retained";
280
+ if (policy === "delete_after_success" && path.resolve(request.source_workspace) !== path.resolve(request.target_workspace)) {
281
+ git(request.target_workspace, ["worktree", "remove", request.source_workspace]);
282
+ git(request.target_workspace, ["branch", "-d", request.source_branch]);
283
+ action = "deleted";
284
+ }
285
+ fs.writeFileSync(receipt, `${JSON.stringify({ schema_id: "dd-flow/merge-cleanup-receipt@1", merge_request_id: request.merge_request_id, policy, action, source_workspace: request.source_workspace, source_branch: request.source_branch, at: new Date().toISOString() }, null, 2)}\n`);
286
+ }
287
+ function waitForTurn(context, request, progress) { return new Promise((resolve) => { const poll = () => { const current = requireRequest(context, request.merge_request_id); const ahead = queueAhead(context, current); const active = context.db.get("SELECT merge_request_id FROM merge_requests WHERE project_id = ? AND status IN ('active','action_required','recovery_required') AND merge_request_id <> ? LIMIT 1", [current.project_id, current.merge_request_id]); if (ahead === 0 && !active)
288
+ return resolve(); progress?.(`MERGE ${current.merge_request_id} is waiting: ${ahead} request(s) ahead; next update in 15 seconds`); setTimeout(poll, 15_000); }; poll(); }); }
289
+ function queueAhead(context, request) { return context.db.get("SELECT COUNT(*) AS count FROM merge_requests WHERE project_id = ? AND status NOT IN ('completed','failed','cancelled') AND (created_at < ? OR (created_at = ? AND merge_request_id < ?))", [request.project_id, request.created_at, request.created_at, request.merge_request_id])?.count ?? 0; }
290
+ function queueStatus(context, request) { return { position: queueAhead(context, request) + 1, requests_ahead: queueAhead(context, request), status: request.status, route: request.execution_route }; }
291
+ function requireOwnedActiveRequest(context, input) { const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)); const request = requireRequest(context, input.requestId); if (request.project_id !== project.id || request.executor_work_id !== input.workId || !["active", "action_required"].includes(request.status))
292
+ throw new AppError("invalid_merge_state", "MRG/Work/project is not the active integration owner", 2, { merge_request_id: input.requestId, work_id: input.workId, status: request.status }); return request; }
293
+ function assertExecutionBaseline(request) { const head = gitValue(request.target_workspace, ["rev-parse", "HEAD"], true); if (request.checkpoint === "baseline_locked" && head !== request.execution_target_head)
294
+ throw new AppError("merge_target_changed", "Integration target changed after the lane baseline was locked", 1, { expected: request.execution_target_head, actual: head }); }
295
+ function recovery(context, request, code, detail) { context.db.run("UPDATE merge_requests SET status = 'recovery_required', last_error_json = ?, updated_at = ? WHERE merge_request_id = ?", [JSON.stringify({ code, detail }), context.now(), request.merge_request_id]); throw new AppError("merge_recovery_required", "MERGE mutation could not be reconciled automatically", 1, { merge_request_id: request.merge_request_id, code, detail }); }
296
+ function commitPendingSource(cwd, runId, ignored) { const paths = meaningfulStatus(cwd, ignored).map(statusPath); if (!paths.length)
297
+ return; git(cwd, ["add", "-A", "--", ...paths]); git(cwd, ["commit", "-m", `chore(${runId.toLowerCase()}): freeze accepted source`]); }
298
+ function commitIntegration(cwd, runId, ignored) { const mergeHead = fs.existsSync(path.join(gitDirectory(cwd), "MERGE_HEAD")); const paths = meaningfulStatus(cwd, ignored).map(statusPath); if (!mergeHead && !paths.length)
299
+ return; if (paths.length)
300
+ git(cwd, ["add", "-A", "--", ...paths]); git(cwd, ["commit", "-m", `merge(${runId.toLowerCase()}): integrate accepted source`]); }
301
+ function ignoredGitPaths(context, projectId) { return [readProjectConfig(context, projectId).dashboard.markdown_path.replaceAll("\\", "/")]; }
302
+ function meaningfulStatus(cwd, ignored) { const result = spawnSync("git", ["status", "--porcelain=v1", "--untracked-files=all"], { cwd, encoding: "utf8" }); if (result.status !== 0)
303
+ return []; return result.stdout.split(/\r?\n/).filter(Boolean).filter((line) => !ignored.includes(statusPath(line))); }
304
+ function statusPath(line) { const value = line.slice(3); const renamed = value.includes(" -> ") ? value.slice(value.lastIndexOf(" -> ") + 4) : value; return renamed.replace(/^"|"$/g, ""); }
305
+ function unmerged(cwd) { const value = gitValue(cwd, ["diff", "--name-only", "--diff-filter=U"], true); return value.split(/\r?\n/).filter(Boolean); }
306
+ function gitDirectory(cwd) { const value = gitValue(cwd, ["rev-parse", "--git-dir"]); return path.resolve(cwd, value); }
307
+ function git(cwd, args) { try {
308
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
309
+ }
310
+ catch (error) {
311
+ throw new AppError("git_operation_failed", `git ${args[0]} failed`, 1, { cwd, args, cause: String(error) });
312
+ } }
313
+ function gitValue(cwd, args, allowFailure = false) { const result = spawnSync("git", args, { cwd, encoding: "utf8" }); if (result.status !== 0 && !allowFailure)
314
+ throw new AppError("git_operation_failed", `git ${args[0]} failed`, 1, { cwd, args, stderr: result.stderr }); return result.status === 0 ? result.stdout.trim() : ""; }
315
+ function applyReceiptPath(context, request) { const run = requireRun(context, request.project_id, request.run_id); return path.join(requireHome(run), stageDir, "works", request.executor_work_id, "apply-receipt.json"); }
316
+ function requestForRun(context, projectId, runId) { const request = context.db.get("SELECT * FROM merge_requests WHERE project_id = ? AND run_id = ? ORDER BY created_at LIMIT 1", [projectId, runId]); if (!request)
317
+ throw new AppError("merge_request_missing", "MERGE request was not materialized by the prior terminal stage", 1, { run_id: runId }); return request; }
318
+ function requireRequest(context, id) { const request = context.db.get("SELECT * FROM merge_requests WHERE merge_request_id = ?", [id]); if (!request)
319
+ throw new AppError("not_found", "MERGE request is not registered", 1, { merge_request_id: id }); return request; }
320
+ function requestView(context, request) { return { ok: true, merge_request_id: request.merge_request_id, run_id: request.run_id, work_id: request.executor_work_id, protocols: JSON.parse(request.protocol_ids_json), source: { workspace: request.source_workspace, branch: request.source_branch, commit: request.source_commit }, target: { workspace: request.target_workspace, branch: request.target_branch, enqueue_head: request.enqueue_target_head, execution_head: request.execution_target_head, integration_commit: request.integration_commit }, route: request.execution_route, status: request.status, checkpoint: request.checkpoint, queue: queueStatus(context, request), created_at: request.created_at, completed_at: request.completed_at }; }
321
+ function requireRun(context, projectId, runId) { const run = context.db.get("SELECT id, project_id, project_root, workspace_root, run_home_path, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]); if (!run)
322
+ throw new AppError("not_found", "RUN is not registered", 1); return run; }
323
+ function requireHome(run) { if (!run.run_home_path)
324
+ throw new AppError("runtime_missing", "RUN workspace is unavailable", 1); return run.run_home_path; }
325
+ function findRootWork(context, projectId, runId) { const work = context.db.get("SELECT * FROM works WHERE project_id = ? AND run_id = ? AND parent_work_id IS NULL ORDER BY created_at LIMIT 1", [projectId, runId]); if (!work)
326
+ throw new AppError("runtime_missing", "vNext RUN has no root Work", 1); return work; }
327
+ function requireRootWork(context, projectId, runId) { const work = findRootWork(context, projectId, runId); if (work.status !== "running")
328
+ throw new AppError("runtime_missing", "vNext RUN has no running root Work", 1); return work; }
329
+ function applyCommand(context, request, projectRoot) { return `${flowCommand(context)} merge apply ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`; }
330
+ function finishCommand(context, runId, request, projectRoot) { return `${flowCommand(context)} stage finish ${runId} --stage merge --request ${request.merge_request_id} --work ${request.executor_work_id} --project-root ${JSON.stringify(projectRoot)} --json --progress-jsonl`; }
@@ -258,7 +258,7 @@ function orchestratorPrompt(context, input) {
258
258
  const decision = path.join(input.root, "decision.json");
259
259
  const revision = currentPlanRevision(input.home, input.run.workspace_root);
260
260
  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");
261
- 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 requests capacity, run exactly one concurrent fan-out of ${capacityProbeFanoutSize} probes. This measures the harness limit; it is not a task to obtain ${capacityProbeFanoutSize} successful probes. Start #01…#${capacityProbeFanoutSize} once, all together, using all-settled handling so one rejection does not hide the other outcomes. A rejected launch is expected evidence. Never retry, replace, or add a probe. Each started probe calls no tools, reads no files, creates no children, waits ${capacityProbeHoldSeconds} seconds, then returns exactly AGENT-NN. For cleanup, wait at most ${capacityProbeDeadlineSeconds} seconds from the first launch, terminate every unfinished probe, then release every finished probe session that the harness permits. Only after that cleanup record the number of launches that started successfully, not the number of replacement attempts or late completions: ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<successful-initial-launches>")}. Capacity probes are not Works and are never registered.`, "After dispatch, launch at most the measured capacity at once. If more independent reviewer Works remain, wait for the current wave to settle, then start the unchanged queued Works in the next wave. A reviewer launch rejected before it starts is not review evidence: do not create a replacement; wait for a running wave to settle and start that same queued Work. Each reviewer must be a genuinely fresh harness child Session. The lifecycle adapter binds that observed Session; 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; do not let finished disposable workers occupy slots before the next wave.", "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 assigned to a concrete Work write_scope and ordered before its consumer; required_read alone is not ownership.", "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 | blocked | 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");
261
+ 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 requests capacity, run exactly one concurrent fan-out of ${capacityProbeFanoutSize} probes. This measures the harness limit; it is not a task to obtain ${capacityProbeFanoutSize} successful probes. Start #01…#${capacityProbeFanoutSize} once, all together, using all-settled handling so one rejection does not hide the other outcomes. A rejected launch is expected evidence. Never retry, replace, or add a probe. Each started probe calls no tools, reads no files, creates no children, waits ${capacityProbeHoldSeconds} seconds, then returns exactly AGENT-NN. For cleanup, wait at most ${capacityProbeDeadlineSeconds} seconds from the first launch, terminate every unfinished probe, then release every finished probe session that the harness permits. Only after that cleanup record the number of launches that started successfully, not the number of replacement attempts or late completions: ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<successful-initial-launches>")}. Capacity probes are not Works and are never registered.`, "After dispatch, launch at most the measured capacity at once. If more independent reviewer Works remain, wait for the current wave to settle, then start the unchanged queued Works in the next wave. A reviewer launch rejected before it starts is not review evidence: do not create a replacement; wait for a running wave to settle and start that same queued Work. Each reviewer must be a genuinely fresh harness child Session. The lifecycle adapter binds that observed Session; 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; do not let finished disposable workers occupy slots before the next wave.", "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 | blocked | 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");
262
262
  }
263
263
  function reviewGroups(home, workspaceRoot) {
264
264
  const root = path.join(home, "03-plan");
@@ -279,7 +279,7 @@ function reviewGroups(home, workspaceRoot) {
279
279
  if (dependencyGroup && dependencyGroup !== key)
280
280
  dependencies.add(dependencyGroup);
281
281
  }
282
- groups.push({ key, protocol_id: protocolId, aspect_ids: group.aspect_ids, depends_on: [...dependencies].sort(), task: `Review PLAN group ${key}: ${group.aspect_ids.join(", ")}. Read run://${path.basename(home)}/03-plan/${protocolId}/aspect-map.json and the referenced plan. Try to falsify ambiguous behavior and missing verification. For every selected check that starts fixture/reset, service, or client processes, verify that they share the intended data/configuration world and that any runtime entrypoint requiring change is in a concrete owner Work write_scope, not merely required_read. Do not edit plan or product files. Return one aspect entry for every assigned aspect, with exact evidence references and concise findings. Finish with the JSON contract supplied in your work prompt.` });
282
+ groups.push({ key, protocol_id: protocolId, aspect_ids: group.aspect_ids, depends_on: [...dependencies].sort(), task: `Review PLAN group ${key}: ${group.aspect_ids.join(", ")}. Read run://${path.basename(home)}/03-plan/${protocolId}/aspect-map.json and the referenced plan. Try to falsify ambiguous behavior and missing verification. For every selected check that starts fixture/reset, service, or client processes, verify that they share the intended data/configuration world and that any runtime entrypoint requiring change is explicit in a Work task and verification and ordered before its consumer. planned_write_areas are coordination hints, not ownership. Do not edit plan or product files. Return one aspect entry for every assigned aspect, with exact evidence references and concise findings. Finish with the JSON contract supplied in your work prompt.` });
283
283
  }
284
284
  }
285
285
  return groups;
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { AppError } from "../shared/errors.js";
5
- import { validateCheckPlacement, validateCodeCheckCommands } from "./code-checks.js";
5
+ import { validateCheckDeclaration, validateCheckPlacement, validateCodeCheckCommands } from "./code-checks.js";
6
6
  import { requireProjectByRoot } from "./projects.js";
7
7
  import { resolveProjectRoot } from "../storage/paths.js";
8
8
  import { advanceFlowRun, appendFlowRunTimelineEvent, attachFlowRunStage, completeFlowRunStage, getFlowRunVariables, gitFacts } from "./runs.js";
@@ -81,14 +81,14 @@ export function startVnextPlan(context, input) {
81
81
  : "- Reviewer capacity is not measured yet. PLAN must not probe or launch reviewers; PLAN-REVIEW will measure it once if review is enabled.";
82
82
  const reviewGroupingRule = "Group only semantically compatible applicable aspects, preserving real trust, irreversible, high-risk and hard-dependency boundaries. Prefer the fewest groups that retain independent review value, normally one review wave. Put two or three compatible aspects in a group; do not create one group per aspect merely for convenience. A later PLAN-REVIEW dispatch measures current capacity once and schedules these semantic groups into waves; do not invent a capacity value here.";
83
83
  const checkProfile = path.join(run.workspace_root, ".memory-bank", "spec", "engineering", "code-check-profile.json");
84
- const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(gitFacts(run.workspace_root))}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(fs.existsSync(checkProfile) ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", fs.readFileSync(checkProfile, "utf8").trim(), "</code_check_policy>", ""] : []), "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", "Use protocol-plan@5. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Existing @check aliases use the profile and do not repeat a definition. Items and acceptance entries use check_refs only; never duplicate command declarations.", "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an honestly limited external/manual proof. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each plan item must name concrete existing source/test paths in required_read and write_scope. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, assign that path to the Work that owns the change in write_scope; do not leave it merely in required_read or assume another Work will repair it. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision 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", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
84
+ const prompt = ["<stage_identity>", `- RUN: ${run.id}`, `- Work: ${planWorkId}`, "- stage: plan", "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility or permission discovery.", `- Project root: ${projectRoot}`, `- Workspace: ${run.workspace_root}`, `- Stage workspace: ${root}`, `- Git: ${JSON.stringify(gitFacts(run.workspace_root))}`, capacityContext, "</trusted_runtime_context>", "", "<workspace_contract>", `- route: ${workspaceRoute.route}`, `- feature branch: ${workspaceRoute.feature_branch ?? "not applicable"}`, `- base commit: ${workspaceRoute.base_ref ?? "not applicable"}`, `- write workspace: ${run.workspace_root}`, "The CLI has verified this frozen route. All project reads and writes for PLAN and later CODE happen in the write workspace; project root is only the stable runtime identity for lifecycle commands. Do not create, switch, merge or delete branches/worktrees.", "Keep the task runner's current cwd. Use the absolute paths in this packet instead of trying to set the provisioned workspace as a tool workdir.", "</workspace_contract>", "", "<accepted_inputs>", `- ${path.join(home, "01-specify", "specify.json")}`, `- ${path.join(home, "02-protocolize", "protocolize-result.json")}`, ...protocols.map((id) => `- ${path.join(run.workspace_root, ".memory-bank", "protocol", id, "summary.md")}`), "</accepted_inputs>", "", ...(fs.existsSync(checkProfile) ? ["<code_check_policy>", "You, not the CLI, select evidence for every accepted requirement and acceptance criterion. The profile only lists reusable aliases, mandatory project policy gates and guarded raw command prefixes. Inspect relevant package/test manifests before choosing a check. Do not classify checks by weight and do not omit a needed check because it looks expensive.", fs.readFileSync(checkProfile, "utf8").trim(), "</code_check_policy>", ""] : []), "<artifacts>", "The CLI has already materialized every artifact below as a partially filled draft. Edit these files in place; do not create replacements elsewhere.", "Prefilled and CLI-owned plan fields: schema_id, plan_id, protocol_id, initial revision and source_refs.", "Prefilled and CLI-owned aspect-map fields: schema_id, protocol_id, plan_id, plan revision, catalog_ref and every catalog aspect_id.", "You own the remaining semantic fields. Empty or missing semantic values are intentional draft markers and must be completed before validation.", ...planPaths.map((value) => `- partially filled plan: ${value}`), ...mapPaths.map((value) => `- partially filled aspect map: ${value}`), "</artifacts>", "", "<output_contract>", "Complete every named plan and aspect map in place. Do not create or edit code-work-batch.json: dd-flow derives it after validation.", "The CLI owns schema_id, plan_id, protocol_id, revision and source_refs. Preserve them exactly.", "Use protocol-plan@6. Its top-level checks[] is the single check catalog. Every check has id, command, purpose, run_at and availability. available means executable now. planned means one named P* Work first creates a NEW @check/... alias: planned therefore always needs provided_by and the exact alias definition. Every semantic @check alias, including an existing one, repeats its exact accepted profile command in definition so later stages can detect drift. Items and acceptance entries use check_refs only; never duplicate command declarations.", "For each R-* and AC-*, choose an actually relevant proof: an existing focused test, a new planned alias plus its provider Work, a project policy gate, or an honestly limited external/manual proof. Every plan item needs at least one check_ref. The CLI validates ids, provider ordering, materialization and guarded command policy; it never chooses a check for you. A provider Work may verify itself with the alias it has just created. A consumer must depend on that provider.", "Each plan item must name concrete existing source/test paths in required_read. planned_write_areas is optional: use stable component directories or files only when they help coordinate parallel Work; it is never a write allowlist. Reference every owned R-* and AC-* in one or more items; every AC-* needs an observable acceptance proof.", "For every selected check, inspect its command's launch path and the runtime entrypoints it starts. The fixture/reset process, service process and client process must observe one intended environment and data world. If a required runtime entrypoint needs a code change, make that change explicit in the Work task and its verification. Use planned_write_areas only to advertise likely concurrent overlap; do not treat it as ownership or assume another Work will repair an omitted change. If an independent infrastructure Work is clearer, plan that Work explicitly and order consumers after it.", reviewGroupingRule, "Complete compact contract and schema paths:", `- protocol plan schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "vnext-protocol-plan.schema.json")}`, `- aspect map schema: ${path.join(run.workspace_root, ".memory-bank", "dd-flow", "schemas", "plan-aspect-map.schema.json")}`, "Minimal valid protocol-plan shape:", "```json", JSON.stringify(planExample(protocols[0]), null, 2), "```", "Minimal valid aspect-map shape:", "```json", JSON.stringify(aspectMapExample(protocols[0]), null, 2), "```", "</output_contract>", "", "<execution_commands>", "PLAN never launches independent reviewers or registers CODE Work.", "If PLAN needs a material user decision 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", pauseCommandTemplate, "```", "Ask the returned user_message, stop, and resume this same PLAN Work with the exact returned command.", "Validate both partially filled drafts after completing their semantic fields:", ...validationCommands.map((command) => `- ${command}`), "Finish PLAN only after all questions are resolved and both validation commands pass:", finishCommand, "The response returns the only PLAN-REVIEW start command. Follow it; do not start CODE directly.", "</execution_commands>", "", "<stage_instructions>", template, "</stage_instructions>", ""].join("\n");
85
85
  const artifactMaterialization = { status: "materialized", completeness: "partially_filled", plan_paths: planPaths, aspect_map_paths: mapPaths, cli_owned_plan_fields: ["schema_id", "plan_id", "protocol_id", "revision", "source_refs"], cli_owned_aspect_map_fields: ["schema_id", "protocol_id", "plan_id", "plan_revision", "catalog_ref", "aspects[].aspect_id"], validation_commands: validationCommands };
86
86
  const promptPath = path.join(root, "stage-prompt.md");
87
87
  fs.writeFileSync(promptPath, prompt);
88
88
  const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
89
89
  fs.writeFileSync(path.join(root, "work-context.json"), JSON.stringify({ schema_id: "dd-flow/work-context@1", system: { run_id: run.id, work_id: planWorkId, stage: "plan" }, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root }, artifacts: artifactMaterialization, input: { protocols, owned_obligations: Object.fromEntries(owned) } }, null, 2));
90
90
  const binding = bindStageCoordinatorWork(context, { workId: planWorkId, hookEventId: input.hookEventId, stage: "plan", promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
91
- attachFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@5" });
91
+ attachFlowRunStage(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" });
92
92
  const workSessionId = String(binding.work_session_id);
93
93
  const sessionId = String(binding.session_id);
94
94
  refreshRunWorkProjection(context, project.id, run.id);
@@ -154,6 +154,8 @@ export function validateVnextPlanArtifacts(context, input) {
154
154
  assertPlanIdentity(value, planIdentity(input.home, input.runId, protocolId, ownership.get(protocolId) ?? []), file);
155
155
  validatePlanSemantics(file, new Set(ownership.get(protocolId) ?? []), obligations);
156
156
  validateCodeCheckCommands(workspaceRoot, value.checks.filter((check) => check.availability === "available").map((check) => check.command));
157
+ for (const check of value.checks.filter((item) => item.availability === "available"))
158
+ validateCheckDeclaration(workspaceRoot, check);
157
159
  // Placement is a PLAN-time rule. A Work may legitimately materialize a
158
160
  // new aggregate alias later, so rechecking against the changed profile
159
161
  // at CODE entry would retroactively invalidate an accepted plan.
@@ -208,7 +210,7 @@ export function validateVnextCodeHandoff(context, input) {
208
210
  });
209
211
  }
210
212
  const planTask = "Produce accepted plan.json and aspect-map.json artifacts.";
211
- function planExample(protocolId) { return { schema_id: "dd-flow/protocol-plan@5", plan_id: "PLAN-001", protocol_id: protocolId, revision: 1, title: "Example", summary: "A compact executable plan.", source_refs: [{ kind: "specify", id: "SPECIFY", path: "run://RUN-000/01-specify/specify.json", requirement_ids: ["R-001", "AC-001"] }], goal: { outcome: "Deliver the accepted behavior.", constraints: ["Keep the accepted scope."], non_goals: [] }, assessment: { scope_breadth: { level: "narrow", surfaces: ["one surface"], reason: "One vertical slice." }, solution_novelty: { level: "established", surfaces: ["existing pattern"], reason: "Reuse project practice." }, solution_uncertainty: { level: "low", surfaces: ["known behavior"], reason: "No open technical question." }, failure_impact: { level: "low", surfaces: ["local feature"], reason: "Reversible local change." }, selected_depth: "compact_plan", depth_trigger: "none" }, decisions: [], document_updates: [], checks: [{ id: "CHK-P1-TEST", command: "pnpm test", purpose: "Proves the changed behavior.", run_at: "work", availability: "available" }], items: [{ id: "P1", title: "Implement behavior", summary: "Change the owning surface.", details: "Follow the accepted requirement and project conventions.", depends_on: [], requirement_refs: ["R-001", "AC-001"], semantic_spine: { user_outcome: "The requested behavior is available.", component_responsibility: "Own the behavior.", must_preserve: ["Existing behavior."], non_goals: [], acceptance_contribution: "Makes AC-001 observable." }, execution_context: { required_read: ["apps/api/src/example.ts"], discovery_boundary: ["Related tests only."], write_scope: ["apps/api/src/example.ts"], stop_conditions: ["Stop if accepted scope conflicts with current truth."] }, verification: { check_refs: ["CHK-P1-TEST"] } }], acceptance: [{ criterion_id: "AC-001", plan_item_ids: ["P1"], changed_surfaces: ["apps/api/src/example.ts"], path: "Exercise the accepted user path.", environment: "Local test environment.", fixtures: [], cleanup: "No persistent fixture.", check_refs: ["CHK-P1-TEST"], expected_evidence: ["Focused check passes."], proof_limits: ["Manual production evidence is not claimed."], gate: "work" }] }; }
213
+ function planExample(protocolId) { return { schema_id: "dd-flow/protocol-plan@6", plan_id: "PLAN-001", protocol_id: protocolId, revision: 1, title: "Example", summary: "A compact executable plan.", source_refs: [{ kind: "specify", id: "SPECIFY", path: "run://RUN-000/01-specify/specify.json", requirement_ids: ["R-001", "AC-001"] }], goal: { outcome: "Deliver the accepted behavior.", constraints: ["Keep the accepted scope."], non_goals: [] }, assessment: { scope_breadth: { level: "narrow", surfaces: ["one surface"], reason: "One vertical slice." }, solution_novelty: { level: "established", surfaces: ["existing pattern"], reason: "Reuse project practice." }, solution_uncertainty: { level: "low", surfaces: ["known behavior"], reason: "No open technical question." }, failure_impact: { level: "low", surfaces: ["local feature"], reason: "Reversible local change." }, selected_depth: "compact_plan", depth_trigger: "none" }, decisions: [], document_updates: [], checks: [{ id: "CHK-P1-TEST", command: "pnpm test", purpose: "Proves the changed behavior.", run_at: "work", availability: "available" }], items: [{ id: "P1", title: "Implement behavior", summary: "Change the owning surface.", details: "Follow the accepted requirement and project conventions.", depends_on: [], requirement_refs: ["R-001", "AC-001"], semantic_spine: { user_outcome: "The requested behavior is available.", component_responsibility: "Own the behavior.", must_preserve: ["Existing behavior."], non_goals: [], acceptance_contribution: "Makes AC-001 observable." }, execution_context: { required_read: ["apps/api/src/example.ts"], discovery_boundary: ["Related tests only."], planned_write_areas: ["apps/api/src/"], stop_conditions: ["Stop if accepted scope conflicts with current truth."] }, verification: { check_refs: ["CHK-P1-TEST"] } }], acceptance: [{ criterion_id: "AC-001", plan_item_ids: ["P1"], changed_surfaces: ["apps/api/src/example.ts"], path: "Exercise the accepted user path.", environment: "Local test environment.", fixtures: [], cleanup: "No persistent fixture.", check_refs: ["CHK-P1-TEST"], expected_evidence: ["Focused check passes."], proof_limits: ["Manual production evidence is not claimed."], gate: "work" }] }; }
212
214
  function aspectMapExample(protocolId) { return { $schema: "plan-aspect-map.schema.json", schema_id: "dd-flow/plan-aspect-map@3", protocol_id: protocolId, plan_id: "PLAN-001", plan_revision: 1, catalog_ref: { path: ".memory-bank/dd-flow/mb-sdlc/plan-aspects/aspects" }, routing: { initial_state: "orchestrator_local", selected_route: "local_compact", reason: "One genuinely small semantic unit.", groups: [] }, review_groups: [], aspects: [{ aspect_id: "example_aspect", applicability: "not_applicable", reason: "Only an example; use the supplied real catalog.", planned_artifact_refs: [] }] }; }
213
215
  function requireRun(context, root, id) { const project = requireProjectByRoot(context, root); const run = context.db.get("SELECT id, project_id, workspace_root, run_home_path FROM runs WHERE project_id = ? AND id = ?", [project.id, id]); if (!run)
214
216
  throw new AppError("not_found", "RUN is not registered", 1); return run; }
@@ -304,7 +306,7 @@ function protocolOwnership(home, protocols) {
304
306
  function planIdentity(home, runId, protocolId, owned) {
305
307
  const specify = path.join(home, "01-specify", "specify.json");
306
308
  return {
307
- schema_id: "dd-flow/protocol-plan@5",
309
+ schema_id: "dd-flow/protocol-plan@6",
308
310
  plan_id: `PLAN-${protocolId.slice(4)}`,
309
311
  protocol_id: protocolId,
310
312
  revision: 1,
@@ -377,11 +379,11 @@ function projectCodeWorkBatch(input) {
377
379
  const documentPaths = documentUpdates.map((entry) => entry.path);
378
380
  const existingDocumentPaths = documentUpdates.filter((entry) => entry.action === "update").map((entry) => entry.path);
379
381
  return ({
380
- schema_id: "dd-flow/code-work-packet@4",
382
+ schema_id: "dd-flow/code-work-packet@5",
381
383
  key: workKey,
382
384
  launch_policy: "fresh_agent_required",
383
385
  source: { plan_id: value.plan_id, protocol_id: protocolId, plan_item_id: item.id, revision: value.revision, sha256: checksum(file) },
384
- task: renderCodeTask(item, value.checks),
386
+ task: renderCodeTask(item, value.checks, value.acceptance),
385
387
  semantic_spine: item.semantic_spine,
386
388
  requirements: item.requirement_refs.map((id) => ({ id, statement: obligationMap.get(id) ?? "" })),
387
389
  acceptance: value.acceptance.filter((entry) => entry.plan_item_ids.includes(item.id)).map(({ plan_item_ids: _ids, changed_surfaces: _surfaces, ...entry }) => entry),
@@ -401,8 +403,11 @@ function projectCodeWorkBatch(input) {
401
403
  : [])
402
404
  ])],
403
405
  discovery_boundary: item.execution_context.discovery_boundary,
404
- write_scope: [...new Set([...item.execution_context.write_scope, ...documentPaths])],
405
- checks: value.checks.filter((check) => item.verification.check_refs.includes(check.id)),
406
+ planned_write_areas: [...new Set([...item.execution_context.planned_write_areas, ...documentPaths])],
407
+ // A criterion belongs to every listed Work. Its proof must travel with
408
+ // those Work packets as well as item-local verification; otherwise CODE
409
+ // silently loses readiness/code checks declared only by acceptance.
410
+ checks: value.checks.filter((check) => itemCheckRefs(item, value.acceptance).includes(check.id)),
406
411
  provides_checks: value.checks.filter((check) => check.availability === "planned" && check.provided_by === item.id),
407
412
  stop_conditions: item.execution_context.stop_conditions,
408
413
  depends_on: item.depends_on.map((dependency) => `${protocolId}:${dependency}`),
@@ -429,39 +434,24 @@ function projectCodeWorkBatch(input) {
429
434
  }
430
435
  }
431
436
  const ordered = [...works].sort((a, b) => a.key.localeCompare(b.key));
432
- return { schema_id: "dd-flow/code-work-batch@4", sources: input.plans.map(({ protocolId, file, value }) => ({ plan_id: value.plan_id, protocol_id: protocolId, revision: value.revision, sha256: checksum(file) })), works: ordered };
437
+ return { schema_id: "dd-flow/code-work-batch@5", sources: input.plans.map(({ protocolId, file, value }) => ({ plan_id: value.plan_id, protocol_id: protocolId, revision: value.revision, sha256: checksum(file) })), works: ordered };
433
438
  }
434
- function renderCodeTask(item, catalog) { const checks = catalog.filter((check) => item.verification.check_refs.includes(check.id)); return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Checks: ${checks.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n"); }
439
+ function itemCheckRefs(item, acceptance) {
440
+ return [...new Set([...item.verification.check_refs, ...acceptance.filter((entry) => entry.plan_item_ids.includes(item.id)).flatMap((entry) => entry.check_refs)])];
441
+ }
442
+ function renderCodeTask(item, catalog, acceptance = []) { const checks = catalog.filter((check) => itemCheckRefs(item, acceptance).includes(check.id)); return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Checks: ${checks.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n"); }
435
443
  function validateProjectedPaths(batch, workspaceRoot, home, runId) {
436
- const byKey = new Map(batch.works.map((work) => [work.key, work]));
437
- const ancestors = (key, seen = new Set()) => { for (const dependency of byKey.get(key)?.depends_on ?? [])
438
- if (!seen.has(dependency)) {
439
- seen.add(dependency);
440
- ancestors(dependency, seen);
441
- } return seen; };
442
444
  for (const work of batch.works) {
443
- for (const write of work.write_scope)
444
- assertProjectPath(write, workspaceRoot, "write_scope");
445
+ for (const write of work.planned_write_areas)
446
+ assertProjectPath(write, workspaceRoot, "planned_write_areas");
445
447
  for (const read of work.required_read) {
446
448
  const existing = resolvePortablePath(read, workspaceRoot, home, runId);
447
449
  if (existing && fs.existsSync(existing))
448
450
  continue;
449
- const producers = [...ancestors(work.key)].map((key) => byKey.get(key)).filter((candidate) => candidate.write_scope.includes(read));
450
- if (!producers.length)
451
- throw new AppError("validation", "CODE read path must exist at entry or be written by an ordered predecessor", 2, { work: work.key, read_path: read });
451
+ throw new AppError("validation", "CODE required_read path must exist at Work entry", 2, { work: work.key, read_path: read });
452
452
  }
453
453
  }
454
- for (let index = 0; index < batch.works.length; index += 1)
455
- for (const other of batch.works.slice(index + 1)) {
456
- const work = batch.works[index];
457
- for (const pathValue of work.write_scope.filter((value) => other.write_scope.some((otherPath) => pathsOverlap(value, otherPath)))) {
458
- const ordered = ancestors(work.key).has(other.key) || ancestors(other.key).has(work.key);
459
- if (!ordered)
460
- throw new AppError("validation", "Several CODE Works write the same path without graph ordering", 2, { path: pathValue, works: [work.key, other.key] });
461
- }
462
- }
463
454
  }
464
- function pathsOverlap(left, right) { return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`); }
465
455
  function acceptedObligationMap(home) {
466
456
  const specify = readVnextSpecifyResult(path.join(home, "01-specify", "specify.json"));
467
457
  const effective = new Map([...specify.requirements, ...specify.acceptance_criteria].map((item) => [item.id, item.statement]));
@@ -513,12 +503,6 @@ function validatePlanSemantics(file, ownedRefs, acceptedRefs) {
513
503
  throw new AppError("planned_check_requires_alias", "A planned check must declare a new @check/... alias", 2, { file, check_id: check.id, command: check.command });
514
504
  if (check.availability === "planned" && !check.definition)
515
505
  throw new AppError("planned_check_definition_missing", "A planned check must declare its exact alias definition", 2, { file, check_id: check.id });
516
- if (check.availability === "planned") {
517
- const provider = items.find((item) => item.id === check.provided_by);
518
- if (!(provider.execution_context.write_scope ?? []).some((scope) => scope === ".memory-bank/spec/engineering/code-check-profile.json")) {
519
- throw new AppError("planned_check_provider_scope_missing", "The planned check provider must own code-check-profile.json", 2, { file, check_id: check.id, provider: check.provided_by });
520
- }
521
- }
522
506
  checks.set(check.id, check);
523
507
  }
524
508
  const ancestors = (itemId, seen = new Set()) => {