@deksden-com/dd-flow-cli 0.9.0-beta.120 → 0.9.0-beta.122
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 +9 -0
- package/dist/build-info.json +5 -5
- package/dist/cli/hook-ingress.js +4 -3
- package/dist/cli/native-hook-ingress.js +34 -2
- package/dist/harness-runtime/bin/dd-agy.mjs +6 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +16 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +12 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +60 -8
- package/dist/harness-runtime/lib/dd-zcode.mjs +28 -13
- package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
- package/dist/harness-runtime/lib/lifecycle-command.d.mts +20 -0
- package/dist/harness-runtime/lib/lifecycle-command.mjs +218 -0
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +10 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +19 -0
- package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +36 -5
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/services/code-checks.js +40 -0
- package/dist/services/codex-hook-delivery.js +6 -2
- package/dist/services/hooks.js +51 -59
- package/dist/services/lifecycle-command.js +1 -383
- package/dist/services/lifecycle-contract.js +1 -30
- package/dist/services/lifecycle-invocations.js +34 -13
- package/dist/services/managed-processes.js +42 -6
- package/dist/services/run-control.js +7 -1
- package/dist/services/runs.js +60 -47
- package/dist/services/runtime-scope-stop.js +23 -2
- package/dist/services/stage-report-renderer.js +19 -0
- package/dist/services/verification-matrix.js +325 -0
- package/dist/services/vnext-code-review.js +25 -8
- package/dist/services/vnext-code.js +38 -11
- package/dist/services/vnext-merge.js +30 -7
- package/dist/services/vnext-plan-review.js +22 -6
- package/dist/services/vnext-plan.js +45 -17
- package/dist/services/work-registry.js +13 -1
- package/dist/storage/database.js +43 -9
- package/package.json +4 -1
|
@@ -25,6 +25,7 @@ import { applyExternalStageContext, prepareExternalStageContext } from "./stage-
|
|
|
25
25
|
import { readFanoutDescriptor, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
26
26
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
27
27
|
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
28
|
+
import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, publishVerificationMatrix, latestRunVerificationMatrix } from "./verification-matrix.js";
|
|
28
29
|
const stage = "code";
|
|
29
30
|
const stageDir = vnextStageDirectory("code");
|
|
30
31
|
/** A controller prepares slow, deterministic project tooling before it asks an
|
|
@@ -63,8 +64,7 @@ export function prepareVnextCodeStart(context, input) {
|
|
|
63
64
|
if (existingStage && fs.existsSync(existingPrompt)) {
|
|
64
65
|
return { ...common, resumed: true, existingStage, prompt: fs.readFileSync(existingPrompt, "utf8"), orchestration: readFanoutDescriptor(existingRoot) };
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
67
|
+
const attachment = sourceRepairAttempt ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" });
|
|
68
68
|
const batch = path.join(home, "03-plan", "code-work-batch.json");
|
|
69
69
|
const handoffFailures = validateVnextCodeHandoff(context, {
|
|
70
70
|
projectRoot,
|
|
@@ -86,23 +86,27 @@ export function prepareVnextCodeStart(context, input) {
|
|
|
86
86
|
validateWorkBatchFile(batch);
|
|
87
87
|
const template = fs.readFileSync(path.join(run.workspace_root, ".memory-bank", "dd-flow", "vnext", "code.md"), "utf8");
|
|
88
88
|
const variables = getFlowRunVariables(context, { projectRoot, runId: run.id });
|
|
89
|
-
|
|
89
|
+
const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", completeness: "declared", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
|
|
90
|
+
return { ...common, resumed: false, template, capacity: variables.variables?.["runtime.subagent.available_slots"], matrix };
|
|
90
91
|
}
|
|
91
92
|
export async function startVnextCode(context, input, prepared = prepareVnextCodeStart(context, input)) {
|
|
92
93
|
const { projectRoot, project, run, root, promptPath, rootWork, sourceRepairAttempt } = prepared;
|
|
93
94
|
assertRunMutationAllowed(context, project.id, run.id);
|
|
94
95
|
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
95
96
|
if (prepared.resumed) {
|
|
97
|
+
resumeVerificationMatrix(context, project.id, run.id, stage);
|
|
96
98
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
97
99
|
bindStageCoordinatorWork(context, { workId: rootWork.work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
98
100
|
const orchestration = prepared.orchestration ?? writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
99
101
|
return { ok: true, resumed: true, run_id: run.id, stage, stage_status: prepared.existingStage, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, graph: codeWorkGraph(context, project.id, run.id), next: { refresh_graph_command: runtimeCommand(context, ["work", "ls", "--run", run.id, "--ready", "--project-root", projectRoot, "--json"]), finish_command: finishCommand(context, input.runId, projectRoot, path.join(root, "code-verification.json")) } };
|
|
100
102
|
}
|
|
103
|
+
if (!sourceRepairAttempt)
|
|
104
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@1" }, true);
|
|
101
105
|
fs.mkdirSync(root, { recursive: true });
|
|
102
106
|
const readiness = await ensureCodeWorkspaceReady(context, project.id, run, root, [], input.progress);
|
|
103
107
|
// Graph rendering may issue managed child commands; it belongs to execution.
|
|
104
108
|
const graph = codeWorkGraph(context, project.id, run.id);
|
|
105
|
-
const prompt = `${coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph })}\n${stageLifecycleInstruction}\n`;
|
|
109
|
+
const prompt = `${coordinatorPrompt(context, { projectRoot, run, root, rootWork, readiness, template: prepared.template, capacity: prepared.capacity, graph })}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, prepared.home, "implementation and code-verification.json")}\n`;
|
|
106
110
|
fs.writeFileSync(promptPath, prompt);
|
|
107
111
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
108
112
|
const binding = bindStageCoordinatorWork(context, {
|
|
@@ -114,15 +118,19 @@ export async function startVnextCode(context, input, prepared = prepareVnextCode
|
|
|
114
118
|
...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {})
|
|
115
119
|
});
|
|
116
120
|
if (!sourceRepairAttempt) {
|
|
117
|
-
|
|
121
|
+
const attachmentInput = {
|
|
118
122
|
projectRoot,
|
|
119
123
|
runId: input.runId,
|
|
120
124
|
stage,
|
|
121
125
|
dir: stageDir,
|
|
122
126
|
status: "running",
|
|
123
127
|
dataSchemaId: "dd-flow/stage-report@1"
|
|
124
|
-
}
|
|
128
|
+
};
|
|
129
|
+
const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
|
|
130
|
+
commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
|
|
125
131
|
}
|
|
132
|
+
else
|
|
133
|
+
commitVerificationMatrixStart(context, prepared.matrix, () => undefined);
|
|
126
134
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: rootWork.work_id, dispatch: "none", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
127
135
|
appendFlowRunTimelineEvent(context, project.id, input.runId, { type: "code_started", work_id: rootWork.work_id, graph });
|
|
128
136
|
return {
|
|
@@ -180,8 +188,10 @@ export async function finishVnextCode(context, input) {
|
|
|
180
188
|
// Reject a malformed semantic receipt before running the expensive gate.
|
|
181
189
|
const verification = input.verification ?? verificationForFinish(context, { file: input.verificationFile, projectRoot: run.workspace_root, runId: run.id, runRoot: home });
|
|
182
190
|
validateSchema({ schemaName: "code-verification", file: input.verificationFile, data: verification, projectRoot: run.workspace_root, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
191
|
+
const retainedMatrix = latestRunVerificationMatrix(context, project.id, run.id, stage);
|
|
192
|
+
const retainedMatrixInstruction = verificationMatrixInstruction(retainedMatrix?.metadata ?? null, home, "the current semantic verification and repair");
|
|
183
193
|
if (verification.verdict === "blocked") {
|
|
184
|
-
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, instruction:
|
|
194
|
+
return { ok: true, run_id: run.id, stage, outcome: "blocked", verification, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `CODE remains running. Resolve the stated external or user-input blocker in this same coordinator session, update code-verification.json, then invoke this same stage finish command again.\n${retainedMatrixInstruction}` };
|
|
185
195
|
}
|
|
186
196
|
if (verification.verdict === "passed" && verification.unresolved.length > 0) {
|
|
187
197
|
throw new AppError("verification_contradictory", "A passed CODE verification cannot contain unresolved obligations", 2, { unresolved: verification.unresolved });
|
|
@@ -196,7 +206,7 @@ export async function finishVnextCode(context, input) {
|
|
|
196
206
|
verification,
|
|
197
207
|
objective: verification.summary
|
|
198
208
|
});
|
|
199
|
-
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, instruction:
|
|
209
|
+
return { ok: true, run_id: run.id, stage, outcome: "repair_required", verification, repair, ...(retainedMatrix ? { verification_matrix: retainedMatrix.metadata } : {}), instruction: `The semantic verification is not accepted. The repair Work is already registered: do not invoke its start_command in this coordinator Session. End this Turn so the controller can launch it in a fresh child Session. After that Work completes, update code-verification.json and invoke this same stage finish command again.\n${retainedMatrixInstruction}` };
|
|
200
210
|
}
|
|
201
211
|
const checks = finalCodeCheckDeclarations(run.workspace_root, works.flatMap((work) => packet(work)?.checks ?? []));
|
|
202
212
|
if (input.retryCheckId) {
|
|
@@ -209,7 +219,11 @@ export async function finishVnextCode(context, input) {
|
|
|
209
219
|
...(input.progress ? { progress: input.progress } : {})
|
|
210
220
|
});
|
|
211
221
|
const failed = receipts.filter((receipt) => receipt.status !== "passed");
|
|
222
|
+
const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: input.verificationFile });
|
|
223
|
+
const matrixInstruction = matrix ? verificationMatrixInstruction(matrix.metadata, home, "semantic verification in code-verification.json") : "";
|
|
212
224
|
if (failed.length) {
|
|
225
|
+
context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
|
|
226
|
+
publishVerificationMatrix(context, matrix);
|
|
213
227
|
const continuation = registerAggregateCodeRepair(context, { projectRoot, runId: run.id, stage, failures: failed, checks, generation });
|
|
214
228
|
throw new AppError("code_gate_failed", "CODE remains running because the aggregate project gate failed", 2, {
|
|
215
229
|
failures: failed,
|
|
@@ -217,15 +231,19 @@ export async function finishVnextCode(context, input) {
|
|
|
217
231
|
retry_after_workspace_change: true,
|
|
218
232
|
workspace_fingerprint: failed[0].workspace_fingerprint,
|
|
219
233
|
continuation,
|
|
220
|
-
|
|
234
|
+
...(matrix ? { verification_matrix: matrix.metadata, verification_matrix_instruction: matrixInstruction } : {}),
|
|
235
|
+
instruction: `Repair Work is already registered. End this coordinator Turn; the controller will launch a fresh child. After it completes, update semantic verification and finish the Stage.\n${matrixInstruction}`
|
|
221
236
|
});
|
|
222
237
|
}
|
|
223
238
|
const finalReceipts = receipts;
|
|
224
239
|
if (verification.verdict === "needs_checks") {
|
|
240
|
+
context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
|
|
241
|
+
publishVerificationMatrix(context, matrix);
|
|
225
242
|
return { ok: true, run_id: run.id, stage, outcome: "verification_required", verification,
|
|
243
|
+
...(matrix ? { verification_matrix: matrix.metadata, verification_matrix_instruction: matrixInstruction } : {}),
|
|
226
244
|
receipts: finalReceipts.map(receipt => ({ id: receipt.id, receipt_path: receipt.receipt_path, input_hash: receipt.input_hash, status: receipt.status })),
|
|
227
245
|
next: { kind: "verification_required", finish_command: finishCommand(context, run.id, projectRoot, input.verificationFile) },
|
|
228
|
-
instruction:
|
|
246
|
+
instruction: `Aggregate receipts are available. Keep CODE running. Inspect them, update code-verification.json to passed or needs_repair, then invoke only the issued successor finish_command. A material document change belongs to the registered fresh repair Work, not this coordinator Session.\n${matrixInstruction}` };
|
|
229
247
|
}
|
|
230
248
|
const projectedVerification = verificationProjection(works, finalReceipts, { historicalReceipts: finalReceipts, runId: run.id, runHome: home });
|
|
231
249
|
const unresolvedAcceptance = (projectedVerification.acceptance ?? []).filter((item) => item.status === "unresolved");
|
|
@@ -278,6 +296,8 @@ export async function finishVnextCode(context, input) {
|
|
|
278
296
|
let mergeRequest = null;
|
|
279
297
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
280
298
|
const prepareMerge = next === "start_merge" ? prepareVnextMergeRequest(context, { projectRoot, runId: run.id, acceptedPaths: changedPaths }) : null;
|
|
299
|
+
if (matrix)
|
|
300
|
+
Object.assign(report.semantic, { verification_matrix: matrix.metadata });
|
|
281
301
|
const preparedReport = prepareStageReport(report);
|
|
282
302
|
const completionInput = { 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", dataSha256: preparedReport.sha256, git: report.mechanical.git };
|
|
283
303
|
withStageSettlement(context, () => {
|
|
@@ -288,6 +308,7 @@ export async function finishVnextCode(context, input) {
|
|
|
288
308
|
}
|
|
289
309
|
mergeRequest = prepareMerge?.() ?? null;
|
|
290
310
|
const preparedCompletion = prepareFlowRunStageCompletion(context, completionInput);
|
|
311
|
+
acceptVerificationMatrix(context, matrix);
|
|
291
312
|
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
292
313
|
completeFlowRunStage(context, completionInput, preparedCompletion);
|
|
293
314
|
const rootWork = requireRootWork(context, project.id, run.id);
|
|
@@ -582,7 +603,13 @@ export function verificationProjection(works, receipts, options = {}) {
|
|
|
582
603
|
if (item.obligation_ref)
|
|
583
604
|
rawEvidence.set(item.obligation_ref, unique([...(rawEvidence.get(item.obligation_ref) ?? []), ...(item.refs ?? [])]));
|
|
584
605
|
}
|
|
585
|
-
const
|
|
606
|
+
const byCriterionGate = new Map();
|
|
607
|
+
for (const entry of packets.flatMap((value) => value.acceptance)) {
|
|
608
|
+
const key = JSON.stringify([entry.criterion_id, entry.gate]);
|
|
609
|
+
const previous = byCriterionGate.get(key);
|
|
610
|
+
byCriterionGate.set(key, { criterion_id: entry.criterion_id, gate: entry.gate, check_refs: unique([...(previous?.check_refs ?? []), ...entry.check_refs]) });
|
|
611
|
+
}
|
|
612
|
+
const acceptance = [...byCriterionGate.values()];
|
|
586
613
|
const historicalReceiptRefs = new Set((options.historicalReceipts ?? receipts).flatMap((receipt) => receiptReferences(receipt, options)));
|
|
587
614
|
const acceptanceEvidence = new Map(acceptance.map((item) => {
|
|
588
615
|
const reported = rawEvidence.get(item.criterion_id) ?? [];
|
|
@@ -23,6 +23,7 @@ import { ensureLaneWorkspace, prepareLaneWorkspace, heartbeatLaneLock, releaseLa
|
|
|
23
23
|
import { assertRunMutationAllowed } from "./run-recovery.js";
|
|
24
24
|
import { assertStageStartHookEvent } from "./hooks.js";
|
|
25
25
|
import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.js";
|
|
26
|
+
import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix, publishVerificationMatrix } from "./verification-matrix.js";
|
|
26
27
|
const stage = "merge";
|
|
27
28
|
const stageDir = vnextStageDirectory(stage);
|
|
28
29
|
export function isVnextMergeRun(context, input) {
|
|
@@ -133,8 +134,7 @@ export function prepareVnextMergeStart(context, input) {
|
|
|
133
134
|
if (!work || work.project_id !== project.id || work.run_id !== run.id || !["created", "running"].includes(work.status))
|
|
134
135
|
throw new AppError("runtime_artifact_invalid", "MERGE executor Work is missing or not startable", 1, { work_id: request.executor_work_id });
|
|
135
136
|
const index = JSON.parse(run.index_json);
|
|
136
|
-
|
|
137
|
-
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
|
|
137
|
+
const attachment = ["active", "action_required"].includes(request.status) && index.stage_runs?.some(entry => entry.stage === stage) ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" });
|
|
138
138
|
prepareLaneWorkspace(context, { projectRoot, lane: "merge", workspacePath: request.target_workspace, branch: request.target_branch });
|
|
139
139
|
const checks = effectiveMergeChecks(run, request), settings = executionSettings(run);
|
|
140
140
|
const existingPrompt = ["active", "action_required"].includes(request.status) && fs.existsSync(promptPath) ? fs.readFileSync(promptPath, "utf8") : null;
|
|
@@ -142,15 +142,21 @@ export function prepareVnextMergeStart(context, input) {
|
|
|
142
142
|
prepareOutputFile(promptPath);
|
|
143
143
|
if (input.externalContext)
|
|
144
144
|
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
145
|
-
|
|
145
|
+
const matrix = existingPrompt !== null ? latestRunVerificationMatrix(context, project.id, run.id, stage, "input") : prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
|
|
146
|
+
return { ...common, terminal: false, root, workRoot, resultPath, promptPath, checks, settings, existingPrompt, matrix };
|
|
146
147
|
}
|
|
147
148
|
export async function startVnextMerge(context, input, prepared = prepareVnextMergeStart(context, input)) {
|
|
148
149
|
const { projectRoot, project, run, request, generation } = prepared;
|
|
149
150
|
if (prepared.terminal)
|
|
150
151
|
return requestView(context, request);
|
|
151
152
|
const { root, workRoot, resultPath, promptPath, checks, settings, existingPrompt } = prepared;
|
|
153
|
+
if (existingPrompt !== null)
|
|
154
|
+
resumeVerificationMatrix(context, project.id, run.id, stage);
|
|
152
155
|
assertRunMutationAllowed(context, project.id, run.id, generation);
|
|
153
156
|
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
157
|
+
const alreadyAttached = ["active", "action_required"].includes(request.status) && (JSON.parse(requireRun(context, project.id, run.id).index_json).stage_runs ?? []).some((item) => item.stage === stage);
|
|
158
|
+
if (!alreadyAttached)
|
|
159
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" }, true);
|
|
154
160
|
fs.mkdirSync(workRoot, { recursive: true });
|
|
155
161
|
ensureLaneWorkspace(context, { projectRoot, lane: "merge", workspacePath: request.target_workspace, branch: request.target_branch });
|
|
156
162
|
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 } : {}) });
|
|
@@ -163,11 +169,13 @@ export async function startVnextMerge(context, input, prepared = prepareVnextMer
|
|
|
163
169
|
const binding = pendingWork?.status === "created"
|
|
164
170
|
? startStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) })
|
|
165
171
|
: bindStageCoordinatorWork(context, { workId: request.executor_work_id, hookEventId: input.hookEventId, stage, promptPath, resultPath, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
166
|
-
const prompt = existingPrompt ?? `${mergePrompt(context, { run, request, resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n`;
|
|
172
|
+
const prompt = existingPrompt ?? `${mergePrompt(context, { run, request, resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, run.run_root, "MERGE source/target verification and residual-risk assessment")}\n`;
|
|
167
173
|
if (existingPrompt === null)
|
|
168
174
|
fs.writeFileSync(promptPath, prompt);
|
|
169
175
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
170
|
-
|
|
176
|
+
const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" };
|
|
177
|
+
const attachment = alreadyAttached ? null : prepareFlowRunStageAttachment(context, attachmentInput);
|
|
178
|
+
commitVerificationMatrixStart(context, prepared.matrix, () => attachment ? attachFlowRunStage(context, attachmentInput, attachment) : undefined);
|
|
171
179
|
return startPacket(context, run, request, binding, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
|
|
172
180
|
}
|
|
173
181
|
const current = requireRequest(context, request.merge_request_id);
|
|
@@ -182,11 +190,13 @@ export async function startVnextMerge(context, input, prepared = prepareVnextMer
|
|
|
182
190
|
if (claimed.changes !== 1 && requireRequest(context, current.merge_request_id).status !== "active")
|
|
183
191
|
throw new AppError("merge_claim_conflict", "MERGE request could not acquire the integration lane", 1, { merge_request_id: current.merge_request_id });
|
|
184
192
|
const started = startStageCoordinatorWork(context, { workId: current.executor_work_id, hookEventId: input.hookEventId, stage, projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
185
|
-
const prompt = `${mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n`;
|
|
193
|
+
const prompt = `${mergePrompt(context, { run, request: requireRequest(context, current.merge_request_id), resultPath, projectRoot }, checks, settings)}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, run.run_root, "MERGE source/target verification and residual-risk assessment")}\n`;
|
|
186
194
|
fs.writeFileSync(promptPath, prompt);
|
|
187
195
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
188
196
|
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]);
|
|
189
|
-
|
|
197
|
+
const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/stage-report@2" };
|
|
198
|
+
const attachment = alreadyAttached ? null : prepareFlowRunStageAttachment(context, attachmentInput);
|
|
199
|
+
commitVerificationMatrixStart(context, prepared.matrix, () => attachment ? attachFlowRunStage(context, attachmentInput, attachment) : undefined);
|
|
190
200
|
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 });
|
|
191
201
|
return startPacket(context, run, current, started, promptPath, resultPath, prompt, projectRoot, checks, externalContext);
|
|
192
202
|
}
|
|
@@ -323,6 +333,11 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
|
|
|
323
333
|
verifyLocalDelivery(request);
|
|
324
334
|
context.db.run("UPDATE merge_requests SET checkpoint = 'delivered', updated_at = ? WHERE merge_request_id = ?", [context.now(), request.merge_request_id]);
|
|
325
335
|
request = requireRequest(context, request.merge_request_id);
|
|
336
|
+
const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", receipts, semanticFile: prepared.resultPath });
|
|
337
|
+
// Retain exact bounded sources before deleting the feature checkout. A
|
|
338
|
+
// crash here recovers presentation from packets, never repeats the checks.
|
|
339
|
+
context.db.writeTransaction(() => acceptVerificationMatrix(context, matrix));
|
|
340
|
+
publishVerificationMatrix(context, matrix);
|
|
326
341
|
performConfiguredCleanup(run, request);
|
|
327
342
|
const acceptedResult = prepared.result.text;
|
|
328
343
|
const executor = context.db.get("SELECT status, result FROM works WHERE work_id = ?", [request.executor_work_id]);
|
|
@@ -338,6 +353,8 @@ export async function finishVnextMerge(context, input, prepared = prepareVnextMe
|
|
|
338
353
|
const root = path.join(requireHome(run), stageDir);
|
|
339
354
|
const retainedReport = completedMergeReport(context, run, root);
|
|
340
355
|
const report = retainedReport ?? mergeReport(context, run, requireRequest(context, request.merge_request_id), semantic, receipts);
|
|
356
|
+
if (matrix)
|
|
357
|
+
Object.assign(report.semantic, { verification_matrix: matrix.metadata });
|
|
341
358
|
if (!retainedReport)
|
|
342
359
|
commitMergeStageReport(context, run, projectRoot, root, report);
|
|
343
360
|
const rootWork = requireRootWork(context, project.id, run.id);
|
|
@@ -409,10 +426,16 @@ function completedMergeReport(context, run, root) {
|
|
|
409
426
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
410
427
|
}
|
|
411
428
|
function commitMergeStageReport(context, run, projectRoot, root, report) {
|
|
429
|
+
const matrix = latestRunVerificationMatrix(context, run.project_id, run.id, stage, "output");
|
|
430
|
+
if (matrix)
|
|
431
|
+
Object.assign(report.semantic, { verification_matrix: matrix.metadata });
|
|
432
|
+
if (JSON.parse(run.index_json).verification_matrix_contract && !matrix)
|
|
433
|
+
throw new AppError("verification_matrix_packet_missing", "Completed marked MERGE lacks its retained final matrix", 1, { run_id: run.id });
|
|
412
434
|
const preparedReport = prepareStageReport(report);
|
|
413
435
|
const completion = { 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", dataSha256: preparedReport.sha256 };
|
|
414
436
|
const preparedCompletion = prepareFlowRunStageCompletion(context, completion);
|
|
415
437
|
withStageSettlement(context, () => {
|
|
438
|
+
acceptVerificationMatrix(context, matrix);
|
|
416
439
|
acceptStageReport(context, { projectId: run.project_id, runId: run.id, stage, root, prepared: preparedReport });
|
|
417
440
|
completeFlowRunStage(context, completion, preparedCompletion);
|
|
418
441
|
});
|
|
@@ -22,6 +22,7 @@ import { prepareJsonSchema, prepareOutputFile } from "../cli/input-preparation.j
|
|
|
22
22
|
import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writeFanoutDescriptor } from "./vnext-fanout.js";
|
|
23
23
|
import { stageGraphScope } from "./stage-work-graph.js";
|
|
24
24
|
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
25
|
+
import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
|
|
25
26
|
const stage = "plan-review";
|
|
26
27
|
const stageDir = vnextStageDirectory(stage);
|
|
27
28
|
const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
|
|
@@ -59,14 +60,15 @@ export function prepareVnextPlanReviewStart(context, input) {
|
|
|
59
60
|
if (input.externalContext)
|
|
60
61
|
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
61
62
|
prepareOutputFile(path.join(root, "stage-prompt.md"));
|
|
62
|
-
const
|
|
63
|
+
const attachment = existing?.status === "running" ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
64
|
+
const matrix = existing?.status === "running" ? latestRunVerificationMatrix(context, project.id, run.id, stage, "input") : prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "input", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
|
|
65
|
+
const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum, matrix };
|
|
63
66
|
if (existing?.status === "running") {
|
|
64
67
|
const orchestration = readFanoutDescriptor(root);
|
|
65
68
|
if (!orchestration)
|
|
66
69
|
prepareOutputFile(path.join(root, "fanout.json"));
|
|
67
70
|
return { ...active, state: "resumed", workId: existing.work_id, prompt: fs.readFileSync(path.join(root, "stage-prompt.md"), "utf8"), orchestration };
|
|
68
71
|
}
|
|
69
|
-
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" });
|
|
70
72
|
const rootWork = rootWorkId(context, project.id, run.id);
|
|
71
73
|
if (effective === "off") {
|
|
72
74
|
const failures = validateVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIds });
|
|
@@ -85,7 +87,8 @@ export function prepareVnextPlanReviewStart(context, input) {
|
|
|
85
87
|
}
|
|
86
88
|
for (const name of ["stage-report.json", "stage-report.md", "stage-report.html"])
|
|
87
89
|
prepareOutputFile(path.join(root, name));
|
|
88
|
-
|
|
90
|
+
const outputMatrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", ...(attachment ? { stageAttempt: attachment.attempt } : {}) });
|
|
91
|
+
return { ...active, state: "off", rootWork, codeItems, outputMatrix };
|
|
89
92
|
}
|
|
90
93
|
const template = read(path.join(projectRoot, ".memory-bank", "dd-flow", "vnext", "plan-review.md"));
|
|
91
94
|
const revision = currentPlanRevision(home, run.workspace_root);
|
|
@@ -103,11 +106,13 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
103
106
|
}
|
|
104
107
|
const { requested, effective, groups, batchChecksum, planChecksum, workspaceRoute } = prepared;
|
|
105
108
|
if (prepared.state === "resumed") {
|
|
109
|
+
resumeVerificationMatrix(context, project.id, run.id, stage);
|
|
106
110
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
107
111
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
108
112
|
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 } : {}) };
|
|
109
113
|
}
|
|
110
114
|
ensureWorkRegistry(context);
|
|
115
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" }, true);
|
|
111
116
|
fs.mkdirSync(root, { recursive: true });
|
|
112
117
|
const now = context.now(), rootWork = prepared.rootWork;
|
|
113
118
|
if (prepared.state === "off") {
|
|
@@ -115,12 +120,17 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
115
120
|
fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
|
|
116
121
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
117
122
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
118
|
-
|
|
123
|
+
const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" };
|
|
124
|
+
const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
|
|
125
|
+
commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
|
|
119
126
|
const reportGit = gitFacts(run.workspace_root);
|
|
120
127
|
const nextCommand = codeCommand(context, run.id, projectRoot);
|
|
121
128
|
return withStageSettlement(context, () => {
|
|
122
129
|
const registered = addWorkBatch(context, { parentWorkId: rootWork, prepared: prepared.codeItems, stage: "code" });
|
|
123
130
|
const report = reportFor(context, { run, mode: effective, requested, outcome: "off", groups: [], batchChecksum, planChecksum, code: registered, now, projectRoot, flow: flowCommand(context), git: reportGit, nextCommand });
|
|
131
|
+
acceptVerificationMatrix(context, prepared.outputMatrix);
|
|
132
|
+
if (prepared.outputMatrix)
|
|
133
|
+
Object.assign(report.semantic, { verification_matrix: prepared.outputMatrix.metadata });
|
|
124
134
|
const preparedReport = prepareStageReport(report);
|
|
125
135
|
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
126
136
|
const completionInput = { 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", dataSha256: preparedReport.sha256, git: reportGit };
|
|
@@ -133,7 +143,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
133
143
|
const workId = nextWorkId(context, project.id, "plan-review");
|
|
134
144
|
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]);
|
|
135
145
|
const pauseCommand = stagePauseCommand(context, { runId: run.id, stage, workId, projectRoot });
|
|
136
|
-
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 })}\n${stageLifecycleInstruction}\n`;
|
|
146
|
+
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 })}\n${stageLifecycleInstruction}\n${verificationMatrixInstruction(prepared.matrix?.metadata ?? null, home, "reviewing PLAN coverage, fixtures and proof limits")}\n`;
|
|
137
147
|
const promptPath = path.join(root, "stage-prompt.md");
|
|
138
148
|
fs.writeFileSync(promptPath, prompt);
|
|
139
149
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
@@ -146,7 +156,9 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
146
156
|
fs.rmSync(promptPath, { force: true });
|
|
147
157
|
throw error;
|
|
148
158
|
}
|
|
149
|
-
|
|
159
|
+
const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@3" };
|
|
160
|
+
const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
|
|
161
|
+
commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
|
|
150
162
|
markReviewPending(home, groups);
|
|
151
163
|
const workSessionId = String(binding.work_session_id);
|
|
152
164
|
const sessionId = String(binding.session_id);
|
|
@@ -307,13 +319,17 @@ function finishPlanReviewOwned(context, input) {
|
|
|
307
319
|
const codeItems = prepareWorkBatch(context, { parentWorkId: codeParentId, file: batch, stage: "code" });
|
|
308
320
|
const reportGit = gitFacts(run.workspace_root);
|
|
309
321
|
const nextCommand = codeCommand(context, run.id, projectRoot);
|
|
322
|
+
const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage, role: "output", semanticFile: decisionPath });
|
|
310
323
|
return withStageSettlement(context, () => {
|
|
311
324
|
const registered = addWorkBatch(context, { parentWorkId: codeParentId, prepared: codeItems, stage: "code" });
|
|
312
325
|
const now = context.now();
|
|
313
326
|
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") });
|
|
314
327
|
const report = reportFor(context, { run, mode: contextFile.system?.effective_mode ?? "standard", requested: contextFile.system?.requested_mode ?? "auto", outcome: "accepted", groups, batchChecksum: contextFile.system?.batch_checksum ?? finalBatchChecksum, planChecksum: contextFile.system?.plan_checksum ?? finalPlanChecksum, finalBatchChecksum, finalPlanChecksum, initialPlanRevision: reviewedPlanRevision, finalPlanRevision: planRevision, code: registered, now, projectRoot, decision, children: latestChildren, flow: flowCommand(context), git: reportGit, nextCommand });
|
|
315
328
|
applyReviewResults(home, latestChildren, decision);
|
|
329
|
+
if (matrix)
|
|
330
|
+
Object.assign(report.semantic, { verification_matrix: matrix.metadata });
|
|
316
331
|
const preparedReport = prepareStageReport(report);
|
|
332
|
+
acceptVerificationMatrix(context, matrix);
|
|
317
333
|
acceptStagePacket(context, { projectId: project.id, runId: run.id, stage, prepared: decisionReceipt });
|
|
318
334
|
acceptStageReport(context, { projectId: project.id, runId: run.id, stage, root, prepared: preparedReport });
|
|
319
335
|
const completionInput = { 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", dataSha256: preparedReport.sha256, git: reportGit };
|
|
@@ -21,6 +21,7 @@ import { acceptStageReport, prepareStageReport } from "./stage-report-renderer.j
|
|
|
21
21
|
import { applyExternalStageContext, prepareExternalStageContext } from "./stage-context.js";
|
|
22
22
|
import { subagentCapacityKey } from "./vnext-fanout.js";
|
|
23
23
|
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
24
|
+
import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, verificationMatrixInstruction } from "./verification-matrix.js";
|
|
24
25
|
export function isVnextPlanRun(context, input) {
|
|
25
26
|
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
26
27
|
return Boolean(context.db.get("SELECT id FROM runs WHERE project_id = ? AND id = ? AND flow_kind = 'vnext_protocolize'", [project.id, input.runId]));
|
|
@@ -41,7 +42,7 @@ export function prepareVnextPlanStart(context, input) {
|
|
|
41
42
|
const { profile: codeCheckProfile } = readCodeCheckProfile(run.workspace_root);
|
|
42
43
|
if (context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running'", [project.id, run.id, planTask]))
|
|
43
44
|
throw new AppError("invalid_work_state", "PLAN already has a running Work", 1, { run_id: run.id });
|
|
44
|
-
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" });
|
|
45
|
+
const attachment = prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" });
|
|
45
46
|
const root = path.join(home, vnextStageDirectory("plan"));
|
|
46
47
|
if (input.externalContext)
|
|
47
48
|
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
@@ -58,12 +59,16 @@ export function prepareVnextPlanStart(context, input) {
|
|
|
58
59
|
const git = gitFacts(run.workspace_root);
|
|
59
60
|
for (const file of [...planPaths, ...mapPaths, ...["stage-prompt.md", "work-context.json", "fixture-root.prompt.md"].map(name => path.join(root, name))])
|
|
60
61
|
prepareOutputFile(file);
|
|
61
|
-
|
|
62
|
+
const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage: "plan", role: "input", stageAttempt: attachment.attempt });
|
|
63
|
+
return { projectRoot, project, run, home, root, rootWork, workspaceRoute, protocols, template, codeCheckProfile, planPaths, mapPaths, owned, identities, aspects, runVariables, mergeRequired, git, matrix };
|
|
62
64
|
}
|
|
63
65
|
export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(context, input)) {
|
|
64
66
|
const { projectRoot, project, run, root, rootWork, workspaceRoute, protocols, template, codeCheckProfile, planPaths, mapPaths, owned, identities, aspects, runVariables, mergeRequired, git, home } = prepared;
|
|
65
67
|
ensureWorkRegistry(context);
|
|
66
68
|
assertStageStartHookEvent(context, { projectId: project.id, eventKey: input.hookEventId, runId: run.id, stage: "plan", projectRoot, ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
69
|
+
if (context.db.get("SELECT 1 FROM works WHERE project_id = ? AND run_id = ? AND task = ? AND status = 'running'", [project.id, run.id, planTask]))
|
|
70
|
+
throw new AppError("invalid_work_state", "PLAN already has a running Work", 1, { run_id: run.id });
|
|
71
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" }, true);
|
|
67
72
|
fs.mkdirSync(root, { recursive: true });
|
|
68
73
|
const now = context.now();
|
|
69
74
|
// Imported stage fixtures have a synthetic running root with no prior agent.
|
|
@@ -113,9 +118,13 @@ export function startVnextPlan(context, input, prepared = prepareVnextPlanStart(
|
|
|
113
118
|
: `PLAN-REVIEW mode is ${planReviewMode}: include review groups only for genuinely applicable aspects; an empty set is allowed.`;
|
|
114
119
|
fs.writeFileSync(promptPath, `${prompt}\n<review_mode_rule>${reviewModeRule}</review_mode_rule>\n<verification_rule>Every flow check must be an autonomous executable command. Do not declare external/manual proof or a human review as a check, evidence substitute, DEF, or gate. The only user interaction supported by this flow is an explicit stage pause for a material unanswered question.</verification_rule>\n${stageLifecycleInstruction}\n`);
|
|
115
120
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
121
|
+
if (prepared.matrix)
|
|
122
|
+
fs.appendFileSync(promptPath, verificationMatrixInstruction(prepared.matrix.metadata, home, "designing the source PLAN and its tests"));
|
|
116
123
|
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));
|
|
117
124
|
const binding = bindStageCoordinatorWork(context, { workId: planWorkId, hookEventId: input.hookEventId, stage: "plan", promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
118
|
-
|
|
125
|
+
const attachmentInput = { projectRoot, runId: run.id, stage: "plan", dir: "03-plan", status: "running", dataSchemaId: "dd-flow/protocol-plan@6" };
|
|
126
|
+
const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
|
|
127
|
+
commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
|
|
119
128
|
const workSessionId = String(binding.work_session_id);
|
|
120
129
|
const sessionId = String(binding.session_id);
|
|
121
130
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
@@ -169,6 +178,9 @@ function finishVnextPlanOwned(context, input) {
|
|
|
169
178
|
plan_checksums: planFiles.map((file) => ({ path: path.relative(run.workspace_root, file).split(path.sep).join("/"), sha256: checksum(file) })),
|
|
170
179
|
aspect_map_checksums: mapFiles.map((file) => ({ path: path.relative(home, file).split(path.sep).join("/"), sha256: checksum(file) }))
|
|
171
180
|
});
|
|
181
|
+
const matrix = prepareRunVerificationMatrix(context, { projectId: project.id, runId: run.id, stage: "plan", role: "output" });
|
|
182
|
+
if (matrix)
|
|
183
|
+
Object.assign(report.semantic, { verification_matrix: matrix.metadata });
|
|
172
184
|
const preparedReport = prepareStageReport(report);
|
|
173
185
|
validateSchema({ schemaName: "stage-report", file: path.join(root, "stage-report.json"), data: preparedReport.normalized, projectRoot, ddFlowHome: context.ddFlowHome, runId: run.id, runRoot: home });
|
|
174
186
|
const completionInput = { projectRoot, runId: run.id, stage: "plan", status: "done", data: "stage-report.json", dataSchemaId: "dd-flow/stage-report@1", report: "stage-report.md", stageReport: "stage-report.html", dataSha256: preparedReport.sha256, git: report.mechanical.git };
|
|
@@ -179,6 +191,7 @@ function finishVnextPlanOwned(context, input) {
|
|
|
179
191
|
throw new AppError("invalid_work_state", "PLAN Work changed during finish preparation", 1);
|
|
180
192
|
context.db.run("UPDATE works SET status = 'completed', result = ?, completed_at = ?, updated_at = ? WHERE work_id = ?", ["PLAN accepted; awaiting PLAN-REVIEW", now, now, work.work_id]);
|
|
181
193
|
context.db.run("UPDATE work_sessions SET status = 'completed', result_path = ?, updated_at = ?, completed_at = ? WHERE id = ?", [path.join(root, "stage-report.json"), now, now, workSession.id]);
|
|
194
|
+
acceptVerificationMatrix(context, matrix);
|
|
182
195
|
acceptStageReport(context, { projectId: project.id, runId: run.id, stage: "plan", root, prepared: preparedReport });
|
|
183
196
|
completeFlowRunStage(context, completionInput, preparedCompletion);
|
|
184
197
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
@@ -201,6 +214,7 @@ export function prepareVnextPlanArtifacts(context, input) {
|
|
|
201
214
|
const batch = path.join(input.home, "03-plan", "code-work-batch.json");
|
|
202
215
|
const ownership = protocolOwnership(input.home, input.protocols);
|
|
203
216
|
const obligations = acceptedObligations(input.home);
|
|
217
|
+
const mergeEnding = runEndsAtMerge(context, input.projectRoot, input.runId);
|
|
204
218
|
const frozenDocumentBaselines = input.publishBatch === false && fs.existsSync(batch)
|
|
205
219
|
? new Map(readJson(batch).works?.flatMap((work) => typeof work.key === "string" ? (work.document_updates ?? []).flatMap((update) => typeof update.path === "string" && (typeof update.baseline_sha256 === "string" || update.baseline_sha256 === null)
|
|
206
220
|
? [[`${work.key}\u0000${update.path}`, update.baseline_sha256]]
|
|
@@ -214,7 +228,7 @@ export function prepareVnextPlanArtifacts(context, input) {
|
|
|
214
228
|
try {
|
|
215
229
|
const { value } = prepareJsonSchema(file, "PLAN", { schemaName: "vnext-protocol-plan", projectRoot: workspaceRoot, ddFlowHome: context.ddFlowHome, runId: input.runId, runRoot: input.home });
|
|
216
230
|
assertPlanIdentity(value, planIdentity(input.home, input.runId, protocolId, ownership.get(protocolId) ?? []), file);
|
|
217
|
-
validatePlanSemantics(file, value, new Set(ownership.get(protocolId) ?? []), obligations);
|
|
231
|
+
validatePlanSemantics(file, value, new Set(ownership.get(protocolId) ?? []), obligations, mergeEnding);
|
|
218
232
|
validateCodeCheckCommands(workspaceRoot, value.checks.filter((check) => check.availability === "available").map((check) => check.command));
|
|
219
233
|
for (const check of value.checks.filter((item) => item.availability === "available"))
|
|
220
234
|
validateCheckDeclaration(workspaceRoot, check);
|
|
@@ -481,7 +495,7 @@ function normalizedAspectMapRefs(file, bytes, projectRoot, runHome, runId) {
|
|
|
481
495
|
aspect.planned_artifact_refs = aspect.planned_artifact_refs.map(normalize);
|
|
482
496
|
return changed ? `${JSON.stringify(map, null, 2)}\n` : bytes;
|
|
483
497
|
}
|
|
484
|
-
function projectCodeWorkBatch(input) {
|
|
498
|
+
export function projectCodeWorkBatch(input) {
|
|
485
499
|
const obligationMap = acceptedObligationMap(input.home);
|
|
486
500
|
const orientation = projectOrientation(input.workspaceRoot);
|
|
487
501
|
const works = input.plans.flatMap(({ protocolId, file, value }) => value.items.map((item) => {
|
|
@@ -516,10 +530,9 @@ function projectCodeWorkBatch(input) {
|
|
|
516
530
|
])],
|
|
517
531
|
discovery_boundary: item.execution_context.discovery_boundary,
|
|
518
532
|
planned_write_areas: [...new Set([...item.execution_context.planned_write_areas, ...documentPaths])],
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
checks: value.checks.filter((check) => itemCheckRefs(item, value.acceptance).includes(check.id)),
|
|
533
|
+
// CODE still needs acceptance-only aggregate declarations in its packets;
|
|
534
|
+
// work checks belong only to the item that explicitly verifies them.
|
|
535
|
+
checks: value.checks.filter((check) => itemCheckRefs(item, value.acceptance, value.checks).includes(check.id)),
|
|
523
536
|
provides_checks: value.checks.filter((check) => check.availability === "planned" && check.provided_by === item.id),
|
|
524
537
|
stop_conditions: item.execution_context.stop_conditions,
|
|
525
538
|
depends_on: item.depends_on.map((dependency) => `${protocolId}:${dependency}`),
|
|
@@ -548,10 +561,15 @@ function projectCodeWorkBatch(input) {
|
|
|
548
561
|
const ordered = [...works].sort((a, b) => a.key.localeCompare(b.key));
|
|
549
562
|
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 };
|
|
550
563
|
}
|
|
551
|
-
function itemCheckRefs(item, acceptance) {
|
|
552
|
-
|
|
564
|
+
function itemCheckRefs(item, acceptance, catalog) {
|
|
565
|
+
const aggregate = new Set(catalog.filter((check) => check.run_at !== "work").map((check) => check.id));
|
|
566
|
+
return [...new Set([...item.verification.check_refs, ...acceptance.filter((entry) => entry.plan_item_ids.includes(item.id)).flatMap((entry) => entry.check_refs.filter((ref) => aggregate.has(ref)))])];
|
|
567
|
+
}
|
|
568
|
+
function renderCodeTask(item, catalog, acceptance = []) {
|
|
569
|
+
const local = catalog.filter((check) => check.run_at === "work" && item.verification.check_refs.includes(check.id));
|
|
570
|
+
const aggregate = catalog.filter((check) => check.run_at !== "work" && itemCheckRefs(item, acceptance, catalog).includes(check.id));
|
|
571
|
+
return [item.title, item.summary, item.details, `Preserve: ${item.semantic_spine.must_preserve.join("; ")}`, `Work checks: ${local.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Later aggregate checks (CODE/MERGE; do not run in this Work): ${aggregate.map((check) => `${check.id}:${check.command}`).join("; ")}`, `Stop: ${item.execution_context.stop_conditions.join("; ")}`].join("\n\n");
|
|
553
572
|
}
|
|
554
|
-
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"); }
|
|
555
573
|
function validateProjectedPaths(batch, workspaceRoot, home, runId) {
|
|
556
574
|
for (const work of batch.works) {
|
|
557
575
|
for (const write of work.planned_write_areas)
|
|
@@ -595,7 +613,7 @@ function resolvePortablePath(value, workspaceRoot, home, runId) {
|
|
|
595
613
|
}
|
|
596
614
|
function writeJson(file, value) { const temporary = `${file}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(temporary, file); }
|
|
597
615
|
function runRef(runId, runHome, file) { return `run://${runId}/${path.relative(runHome, file).split(path.sep).join("/")}`; }
|
|
598
|
-
function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
|
|
616
|
+
function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs, mergeEnding) {
|
|
599
617
|
const items = plan.items ?? [];
|
|
600
618
|
const ids = new Set(items.map((item) => item.id).filter((id) => Boolean(id)));
|
|
601
619
|
if (ids.size !== items.length)
|
|
@@ -608,6 +626,8 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
|
|
|
608
626
|
for (const check of plan.checks ?? []) {
|
|
609
627
|
if (checks.has(check.id))
|
|
610
628
|
throw new AppError("validation", "PLAN check ids must be unique", 2, { file, check_id: check.id });
|
|
629
|
+
if (mergeEnding && check.run_at === "release")
|
|
630
|
+
throw new AppError("check_gate_unreachable", "A RUN ending at MERGE cannot execute RELEASE checks", 2, { file, check_id: check.id });
|
|
611
631
|
if (check.availability === "planned" && (!check.provided_by || !ids.has(check.provided_by)))
|
|
612
632
|
throw new AppError("planned_check_provider_missing", "A planned check must name a PLAN item that provides it", 2, { file, check_id: check.id, provided_by: check.provided_by ?? null });
|
|
613
633
|
if (check.availability === "planned" && !check.command.startsWith("@check/"))
|
|
@@ -616,6 +636,8 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
|
|
|
616
636
|
throw new AppError("planned_check_definition_missing", "A planned check must declare its exact alias definition", 2, { file, check_id: check.id });
|
|
617
637
|
checks.set(check.id, check);
|
|
618
638
|
}
|
|
639
|
+
const localChecks = new Set(items.flatMap((item) => item.verification.check_refs));
|
|
640
|
+
const acceptanceIds = new Set();
|
|
619
641
|
const ancestors = (itemId, seen = new Set()) => {
|
|
620
642
|
for (const dependency of items.find((item) => item.id === itemId)?.depends_on ?? [])
|
|
621
643
|
if (!seen.has(dependency)) {
|
|
@@ -639,7 +661,7 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
|
|
|
639
661
|
const check = checks.get(checkId);
|
|
640
662
|
if (!check)
|
|
641
663
|
throw new AppError("check_reference_unknown", "PLAN item references an unknown check", 2, { file, item: item.id, check_id: checkId });
|
|
642
|
-
if (check.availability === "planned" && check.provided_by !== item.id && !ancestors(item.id).has(check.provided_by))
|
|
664
|
+
if (check.run_at === "work" && check.availability === "planned" && check.provided_by !== item.id && !ancestors(item.id).has(check.provided_by))
|
|
643
665
|
throw new AppError("check_consumer_not_ordered_after_provider", "A Work may consume a planned check only after its provider", 2, { file, item: item.id, check_id: checkId, provider: check.provided_by });
|
|
644
666
|
}
|
|
645
667
|
}
|
|
@@ -655,6 +677,11 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
|
|
|
655
677
|
for (const acceptance of plan.acceptance ?? []) {
|
|
656
678
|
if (!acceptance.criterion_id || !acceptance.criterion_id.startsWith("AC-") || !ownedRefs.has(acceptance.criterion_id))
|
|
657
679
|
throw new AppError("validation", "PLAN acceptance references an AC-* owned by this protocol", 2, { file, criterion_id: acceptance.criterion_id });
|
|
680
|
+
if (acceptanceIds.has(acceptance.criterion_id))
|
|
681
|
+
throw new AppError("duplicate_plan_criterion", "PLAN acceptance criterion ids must be unique within a protocol", 2, { file, criterion_id: acceptance.criterion_id });
|
|
682
|
+
acceptanceIds.add(acceptance.criterion_id);
|
|
683
|
+
if (mergeEnding && acceptance.gate === "release")
|
|
684
|
+
throw new AppError("check_gate_unreachable", "A RUN ending at MERGE cannot prove RELEASE acceptance", 2, { file, criterion_id: acceptance.criterion_id });
|
|
658
685
|
for (const id of acceptance.plan_item_ids ?? [])
|
|
659
686
|
if (!ids.has(id))
|
|
660
687
|
throw new AppError("validation", "PLAN acceptance references an unknown item", 2, { file, criterion_id: acceptance.criterion_id, plan_item_id: id });
|
|
@@ -662,9 +689,10 @@ function validatePlanSemantics(file, plan, ownedRefs, acceptedRefs) {
|
|
|
662
689
|
const check = checks.get(id);
|
|
663
690
|
if (!check)
|
|
664
691
|
throw new AppError("check_reference_unknown", "PLAN acceptance references an unknown check", 2, { file, criterion_id: acceptance.criterion_id, check_id: id });
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
692
|
+
if ((acceptance.gate !== "release" && check.run_at === "release") || (!["merge", "release"].includes(acceptance.gate) && check.run_at === "merge"))
|
|
693
|
+
throw new AppError("check_gate_unreachable", "PLAN acceptance references a check after its gate", 2, { file, criterion_id: acceptance.criterion_id, check_id: id, gate: acceptance.gate, run_at: check.run_at });
|
|
694
|
+
if (check.run_at === "work" && !localChecks.has(id))
|
|
695
|
+
throw new AppError("work_check_unassigned", "A work check referenced by acceptance needs an explicit verifying PLAN item", 2, { file, criterion_id: acceptance.criterion_id, check_id: id });
|
|
668
696
|
}
|
|
669
697
|
}
|
|
670
698
|
for (const obligation of ownedRefs)
|