@deksden-com/dd-flow-cli 0.9.0-beta.122 → 0.9.0-beta.124
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 +18 -0
- package/README.md +23 -0
- package/dist/build-info.json +4 -4
- package/dist/cli/run-cli.js +3 -0
- package/dist/domain/stage-catalog.js +1 -1
- package/dist/harness-runtime/lib/codex-capacity-policy.d.mts +10 -0
- package/dist/harness-runtime/lib/codex-capacity-policy.mjs +89 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +100 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +117 -21
- package/dist/harness-runtime/lib/dd-agy.mjs +13 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +58 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +16 -26
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +19 -12
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +42 -10
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +57 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +25 -23
- package/dist/harness-runtime/lib/dd-zcode.mjs +24 -14
- package/dist/harness-runtime/lib/driver-recovery.mjs +12 -2
- package/dist/harness-runtime/lib/managed-daemon.d.mts +2 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +100 -31
- package/dist/harness-runtime/lib/model-observations.mjs +16 -9
- package/dist/harness-runtime/lib/tool-observations.d.mts +1 -0
- package/dist/harness-runtime/lib/tool-observations.mjs +52 -11
- package/dist/schemas/flow-run.schema.json +1 -0
- package/dist/schemas/plan-review-decision.schema.json +3 -3
- package/dist/services/code-checks.js +14 -18
- package/dist/services/codex-capacity.js +251 -31
- package/dist/services/controller-fanout.js +1 -0
- package/dist/services/eval-snapshots.js +13 -8
- package/dist/services/external-work-launch.js +36 -11
- package/dist/services/harness-adapter.js +21 -15
- package/dist/services/managed-processes.js +14 -6
- package/dist/services/merge-server.js +35 -12
- package/dist/services/review-copy.js +21 -0
- package/dist/services/review-deferral.js +23 -0
- package/dist/services/run-controller-adapter.js +33 -8
- package/dist/services/run-controller-recovery.js +26 -2
- package/dist/services/run-controller.js +91 -24
- package/dist/services/run-observations.js +7 -2
- package/dist/services/run-recovery-runtime.js +49 -3
- package/dist/services/runs.js +1 -1
- package/dist/services/stage-report-renderer.js +25 -2
- package/dist/services/verification-matrix.js +57 -8
- package/dist/services/vnext-code-review.js +22 -8
- package/dist/services/vnext-merge.js +20 -2
- package/dist/services/vnext-plan-review.js +87 -19
- package/dist/services/work-registry.js +11 -2
- package/package.json +1 -1
|
@@ -23,6 +23,8 @@ import { getStageFanoutCapacity, readFanoutDescriptor, subagentCapacityKey, writ
|
|
|
23
23
|
import { stageGraphScope } from "./stage-work-graph.js";
|
|
24
24
|
import { stageLifecycleInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
25
25
|
import { prepareRunVerificationMatrix, acceptVerificationMatrix, commitVerificationMatrixStart, resumeVerificationMatrix, verificationMatrixInstruction, latestRunVerificationMatrix } from "./verification-matrix.js";
|
|
26
|
+
import { reviewerSourceInputs, reviewerSourceInstruction } from "./review-copy.js";
|
|
27
|
+
import { reviewDefEvidence } from "./review-deferral.js";
|
|
26
28
|
const stage = "plan-review";
|
|
27
29
|
const stageDir = vnextStageDirectory(stage);
|
|
28
30
|
const reviewTask = "Independently review the accepted PLAN and decide whether CODE may open.";
|
|
@@ -55,14 +57,18 @@ export function prepareVnextPlanReviewStart(context, input) {
|
|
|
55
57
|
assertReviewWorkspace(run.workspace_root, protocolIds);
|
|
56
58
|
const groups = reviewGroups(home, run.workspace_root);
|
|
57
59
|
const effective = resolveMode(requested, groups, home, run.workspace_root);
|
|
60
|
+
const reviewerInputs = effective === "off" ? [] : reviewerSourceInputs(run.workspace_root, groups.flatMap(group => group.aspect_ids));
|
|
61
|
+
if (effective !== "off")
|
|
62
|
+
for (const group of groups)
|
|
63
|
+
group.task += `\n${reviewerSourceInstruction(run.workspace_root, group.aspect_ids)}`;
|
|
58
64
|
const existing = context.db.get("SELECT work_id, status FROM works WHERE project_id = ? AND run_id = ? AND task = ? ORDER BY created_at DESC LIMIT 1", [project.id, run.id, reviewTask]);
|
|
59
65
|
const batchChecksum = checksum(batch), planChecksum = planSetChecksum(home, run.workspace_root);
|
|
60
66
|
if (input.externalContext)
|
|
61
67
|
prepareExternalStageContext({ stageRoot: root, loaded: input.externalContext });
|
|
62
68
|
prepareOutputFile(path.join(root, "stage-prompt.md"));
|
|
63
|
-
const attachment = existing?.status === "running" ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@
|
|
69
|
+
const attachment = existing?.status === "running" ? null : prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" });
|
|
64
70
|
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 };
|
|
71
|
+
const active = { ...common, requested, protocolIds, groups, effective, workspaceRoute, batchChecksum, planChecksum, matrix, reviewerInputs };
|
|
66
72
|
if (existing?.status === "running") {
|
|
67
73
|
const orchestration = readFanoutDescriptor(root);
|
|
68
74
|
if (!orchestration)
|
|
@@ -112,7 +118,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
112
118
|
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 } : {}) };
|
|
113
119
|
}
|
|
114
120
|
ensureWorkRegistry(context);
|
|
115
|
-
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@
|
|
121
|
+
prepareFlowRunStageAttachment(context, { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" }, true);
|
|
116
122
|
fs.mkdirSync(root, { recursive: true });
|
|
117
123
|
const now = context.now(), rootWork = prepared.rootWork;
|
|
118
124
|
if (prepared.state === "off") {
|
|
@@ -120,7 +126,7 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
120
126
|
fs.writeFileSync(promptPath, "PLAN-REVIEW is disabled by the frozen RUN configuration.\n");
|
|
121
127
|
const externalContext = applyExternalStageContext({ stageRoot: root, promptPath, ...(input.externalContext ? { loaded: input.externalContext } : {}) });
|
|
122
128
|
bindStageCoordinatorWork(context, { workId: rootWork, hookEventId: input.hookEventId, stage, promptPath, resultPath: path.join(root, "stage-report.json"), ...(input.contextSha256 ? { contextSha256: input.contextSha256 } : {}) });
|
|
123
|
-
const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@
|
|
129
|
+
const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" };
|
|
124
130
|
const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
|
|
125
131
|
commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
|
|
126
132
|
const reportGit = gitFacts(run.workspace_root);
|
|
@@ -156,14 +162,14 @@ export function startVnextPlanReview(context, input, prepared = prepareVnextPlan
|
|
|
156
162
|
fs.rmSync(promptPath, { force: true });
|
|
157
163
|
throw error;
|
|
158
164
|
}
|
|
159
|
-
const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@
|
|
165
|
+
const attachmentInput = { projectRoot, runId: run.id, stage, dir: stageDir, status: "running", dataSchemaId: "dd-flow/plan-review-decision@4" };
|
|
160
166
|
const attachment = prepareFlowRunStageAttachment(context, attachmentInput);
|
|
161
167
|
commitVerificationMatrixStart(context, prepared.matrix, () => attachFlowRunStage(context, attachmentInput, attachment));
|
|
162
168
|
markReviewPending(home, groups);
|
|
163
169
|
const workSessionId = String(binding.work_session_id);
|
|
164
170
|
const sessionId = String(binding.session_id);
|
|
165
171
|
const orchestration = writeFanoutDescriptor(root, { stage, parent_work_id: workId, dispatch: "plan-review", capacity_required: true, scope: stageGraphScope(context, project.id, run.id, stage) });
|
|
166
|
-
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
|
|
172
|
+
writeJson(path.join(root, "work-context.json"), { schema_id: "dd-flow/plan-review-context@1", reviewer_inputs: prepared.reviewerInputs, read_only_input: { parent_work_id: workId, version: 3 }, system: { run_id: run.id, work_id: workId, stage, requested_mode: requested, effective_mode: effective, plan_sources: readJson(prepared.batch).sources.map(source => ({ ...source, content_sha256: planContentHash(readJson(path.join(run.workspace_root, ".memory-bank", "protocol", source.protocol_id, "plan.json"))) })), plan_revision: prepared.revision, plan_checksum: planChecksum, batch_checksum: batchChecksum }, groups, workspace: { project_root: projectRoot, workspace_root: run.workspace_root, stage_root: root, plan_root: planRoot } });
|
|
167
173
|
refreshRunWorkProjection(context, project.id, run.id);
|
|
168
174
|
appendFlowRunTimelineEvent(context, project.id, run.id, { type: "work_session_started", stage, work_id: workId, id: workSessionId, session_id: sessionId });
|
|
169
175
|
return { ok: true, run_id: run.id, stage, outcome: "review_required", work_id: workId, id: workSessionId, prompt_path: promptPath, worker_prompt_markdown: fs.readFileSync(promptPath, "utf8"), ...(externalContext ? { external_context: externalContext } : {}), orchestration, plan_review: { requested_mode: requested, effective_mode: effective, policy_source: policySource(run), groups }, next: { dispatch_command: dispatchCommand(context, run.id, projectRoot), finish_command: finishCommand(context, run.id, projectRoot, path.join(root, "decision.json")) } };
|
|
@@ -255,7 +261,6 @@ function finishPlanReviewOwned(context, input) {
|
|
|
255
261
|
if (!Number.isInteger(reviewedPlanRevision) || reviewedPlanRevision < 1)
|
|
256
262
|
throw new AppError("runtime_missing", "PLAN-REVIEW starting revision is unavailable", 1);
|
|
257
263
|
const planRevision = currentPlanRevision(home, run.workspace_root);
|
|
258
|
-
const currentPlanChecksum = planSetChecksum(home, run.workspace_root);
|
|
259
264
|
if (decision.outcome === "blocked") {
|
|
260
265
|
throw new AppError("stage_pause_required", "A user or decision blocker must pause the current PLAN-REVIEW Work with `dd-flow stage pause`; do not finish the stage as blocked", 2, { run_id: run.id, stage, work_id: parent.work_id });
|
|
261
266
|
}
|
|
@@ -282,7 +287,6 @@ function finishPlanReviewOwned(context, input) {
|
|
|
282
287
|
// plan uncertainty prevents it from approving its aspect. That is evidence
|
|
283
288
|
// for the coordinator to classify and fix in this one review pass, not an
|
|
284
289
|
// infrastructure failure or a reason to start another review cycle.
|
|
285
|
-
const needsCorrection = latestChildren.some((child) => ["needs_changes", "blocked"].includes(reviewerVerdict(child.result)));
|
|
286
290
|
if (latestChildren.some((child) => child.status !== "completed"))
|
|
287
291
|
throw new AppError("blocked", "PLAN-REVIEW cannot accept incomplete reviewer evidence", 2);
|
|
288
292
|
const canonicalFindings = canonicalReviewerFindings(latestChildren);
|
|
@@ -293,19 +297,23 @@ function finishPlanReviewOwned(context, input) {
|
|
|
293
297
|
const unknownDecisions = decisionFindingIds.filter((id) => !allFindingIds.includes(id));
|
|
294
298
|
if (new Set(allFindingIds).size !== allFindingIds.length || decidedFindingIds.size !== decisionFindingIds.length || unknownDecisions.length || materialFindingIds.some((id) => !decidedFindingIds.has(id)))
|
|
295
299
|
throw new AppError("review_evidence_invalid", "Reviewer findings and coordinator decisions must use unique known canonical references, with every material finding decided", 2, { material_finding_ids: materialFindingIds, decided_finding_ids: decisionFindingIds, unknown_decisions: unknownDecisions });
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
300
|
+
const defEvidence = validatePlanFindingDecisions(decision.finding_decisions, allFindingIds, run.workspace_root);
|
|
301
|
+
const acceptedMatrix = latestRunVerificationMatrix(context, project.id, run.id, "plan", "output");
|
|
302
|
+
const planSources = acceptedMatrix ? acceptedMatrix.matrix.sources.filter(source => source.role.startsWith("plan:")).map(source => {
|
|
303
|
+
const packet = acceptedMatrix.packets.find(packet => packet.file === path.join(source.root === "run" ? home : run.workspace_root, source.path) && packet.sha256 === source.sha256);
|
|
304
|
+
if (!packet)
|
|
305
|
+
throw new AppError("review_input_contract_missing", "Accepted PLAN source lacks its immutable publication bytes", 1);
|
|
306
|
+
const value = JSON.parse(packet.bytes.toString("utf8"));
|
|
307
|
+
return { plan_id: String(value.plan_id), protocol_id: String(value.protocol_id), revision: Number(value.revision), sha256: source.sha256, content_sha256: planContentHash(value) };
|
|
308
|
+
}) : contextFile.system?.plan_sources ?? [];
|
|
309
|
+
if (checksum(path.join(planRoot, "stage-report.json")) !== readIndex(run).stage_runs?.find(entry => entry.stage === "plan")?.data_sha256)
|
|
310
|
+
throw new AppError("read_only_input_changed", "Accepted PLAN report changed during review", 2);
|
|
311
|
+
const acceptedPlan = readJson(path.join(planRoot, "stage-report.json"));
|
|
312
|
+
validatePlanCorrection({ decision, reviewedPlanRevision: reviewedPlanRevision, sources: planSources, maps: acceptedPlan.semantic.aspect_map_checksums, workspaceRoot: run.workspace_root, home });
|
|
305
313
|
const prepared = prepareVnextPlanArtifacts(context, { projectRoot, workspaceRoot: run.workspace_root, runId: run.id, home, protocols: protocolIdsForReview(home) });
|
|
306
314
|
if (prepared.failures.length)
|
|
307
315
|
throw new AppError("validation", "Corrected PLAN artifacts have validation errors", 2, { errors: prepared.failures, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
308
|
-
if (
|
|
316
|
+
if (decision.correction.status === "applied") {
|
|
309
317
|
// Regeneration above is authoritative. A valid PLAN correction need not
|
|
310
318
|
// change the derived CODE tasks (for example, a clarified explanation).
|
|
311
319
|
if (decision.correction.changed_paths.some((item) => item.endsWith("code-work-batch.json")))
|
|
@@ -325,6 +333,7 @@ function finishPlanReviewOwned(context, input) {
|
|
|
325
333
|
const now = context.now();
|
|
326
334
|
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") });
|
|
327
335
|
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 });
|
|
336
|
+
Object.assign(report.semantic, { def_evidence: defEvidence });
|
|
328
337
|
applyReviewResults(home, latestChildren, decision);
|
|
329
338
|
if (matrix)
|
|
330
339
|
Object.assign(report.semantic, { verification_matrix: matrix.metadata });
|
|
@@ -355,7 +364,7 @@ function orchestratorPrompt(context, input) {
|
|
|
355
364
|
const reviewerLaunch = capacity.source === "external_policy"
|
|
356
365
|
? `After dispatch, return at the Work-graph boundary. The shared runtime launches the queued reviewer Works as separate external Sessions, at most ${capacity.available_slots} in parallel under the RUN's frozen profiles. Do not launch native children, create provider roots, run a capacity probe, or record this external limit as native capacity. Reviewers are read-only leaf workers. Continue the semantic decision only after their Work receipts settle; do not substitute a missing result or relaunch a settled reviewer.`
|
|
357
366
|
: "After dispatch, stop at the Work-graph boundary. The shared controller supplies the selected adapter's native tool, exact parameters, child task, and wait contract; do not choose a provider tool from this stage prompt or substitute a shell command. It launches at most the qualified capacity at once and starts unchanged queued Works only after the current wave settles. A launch rejected before it starts is not review evidence: do not create a replacement. Each reviewer must be a genuinely fresh harness child Session; the lifecycle adapter binds that observed Session, so do not bind or supply a Session ID manually. Reviewers are read-only and must not create children. As soon as a reviewer result is accepted, release that reviewer Session when the harness permits.";
|
|
358
|
-
return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch reports qualified_capacity_required, stop. The external harness controller qualifies the selected profile outside this RUN and records the resulting integer with ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<qualified-native-child-count>")}. PLAN-REVIEW never launches a capacity probe.`, reviewerLaunch, "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be explicit in one Work's task and verification and ordered before its consumer. planned_write_areas may advertise likely overlap, but do not treat them as ownership; required_read alone is not a delivery plan.", "If the final decision needs user input with no reasonable default, write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one 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.
|
|
367
|
+
return ["<stage_identity>", `- RUN: ${input.run.id}`, `- Work: ${input.workId}`, "- Stage: plan-review", `- Mode: ${input.effective}`, "</stage_identity>", "", "<trusted_runtime_context>", "These facts were collected by dd-flow. Trust them; do not repeat CLI, Git, compatibility, permission or schema discovery.", `- Project root: ${input.projectRoot}`, `- Stage workspace: ${input.root}`, `- PLAN revision: ${revision}`, `- PLAN report checksum: ${input.planChecksum}`, `- Generated CODE batch checksum: ${input.batchChecksum}`, "</trusted_runtime_context>", "", workspaceContract, "", "<review_groups>", ...input.groups.map((group) => `- ${group.key}: ${group.aspect_ids.join(", ")}`), "</review_groups>", "", "<execution_commands>", `Dispatch fresh reviewers: ${dispatchCommand(context, input.run.id, input.projectRoot)}`, `If dispatch reports qualified_capacity_required, stop. The external harness controller qualifies the selected profile outside this RUN and records the resulting integer with ${capacityRecordCommand(context, input.run.id, input.projectRoot, "<qualified-native-child-count>")}. PLAN-REVIEW never launches a capacity probe.`, reviewerLaunch, "Review the execution environment of every selected check as part of its proof: a reset/fixture process, service process and client process must share the intended data and configuration world. A runtime entrypoint that can break that invariant must be explicit in one Work's task and verification and ordered before its consumer. planned_write_areas may advertise likely overlap, but do not treat them as ownership; required_read alone is not a delivery plan.", "If the final decision needs user input with no reasonable default, write the question packet to the named text file, then run the separate standalone lifecycle command below. Do not combine file creation and dd-flow in one 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. Accepted fixes need an actual authored delta. Rejected/duplicate findings need no artificial correction. Coordinator-only correction is allowed. Increment only the changed protocol PLAN's own revision; an aspect-map-only correction needs no fake PLAN revision. changed_paths must list exactly the changed workspace-relative PLAN paths and RUN/<aspect-map-path>. 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@4", outcome: "accepted | failed | cancelled", summary: "Concise evidence-backed final decision.", finding_decisions: [{ finding_ref: "WRK-001-review/FIND-001", decision: "accepted_fix | rejected | deferred_as_DEF | requires_user | duplicate", reason: "Why.", duplicate_of: "A distinct canonical finding_ref only for duplicate", def_id: "DEF-0001 only for deferred_as_DEF" }], 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");
|
|
359
368
|
}
|
|
360
369
|
function reviewGroups(home, workspaceRoot) {
|
|
361
370
|
const root = path.join(home, "03-plan");
|
|
@@ -433,6 +442,65 @@ function markReviewPending(_home, _groups) { }
|
|
|
433
442
|
/** Review evidence is immutable child Work output plus the stage decision. */
|
|
434
443
|
function applyReviewResults(_home, _children, _decision) { }
|
|
435
444
|
function canonicalReviewerFindings(children) { return children.flatMap((child) => (reviewerResult(child.result)?.aspects ?? []).flatMap((aspect) => aspect.findings.map((finding) => ({ finding_ref: `${child.work_id}/${finding.finding_id}`, finding })))); }
|
|
445
|
+
export function validatePlanFindingDecisions(items, knownRefs, workspaceRoot) {
|
|
446
|
+
const known = new Set(knownRefs), decisions = new Map(items.map(item => [item.finding_ref, item]));
|
|
447
|
+
if (decisions.size !== items.length || items.some(item => !item.finding_ref || !known.has(item.finding_ref)))
|
|
448
|
+
throw new AppError("review_evidence_invalid", "PLAN decisions require unique known finding references", 2);
|
|
449
|
+
if (items.some(item => item.decision === "requires_user"))
|
|
450
|
+
throw new AppError("stage_pause_required", "Resolve the user question with stage pause/resume before accepting PLAN-REVIEW", 2);
|
|
451
|
+
for (const item of items.filter(item => item.decision === "duplicate")) {
|
|
452
|
+
let cursor = item;
|
|
453
|
+
const seen = new Set();
|
|
454
|
+
while (cursor?.decision === "duplicate") {
|
|
455
|
+
if (!cursor.finding_ref || seen.has(cursor.finding_ref) || !cursor.duplicate_of || !known.has(cursor.duplicate_of) || cursor.duplicate_of === cursor.finding_ref)
|
|
456
|
+
throw new AppError("review_evidence_invalid", "PLAN duplicates must form an acyclic chain of distinct known findings", 2);
|
|
457
|
+
seen.add(cursor.finding_ref);
|
|
458
|
+
cursor = decisions.get(cursor.duplicate_of);
|
|
459
|
+
}
|
|
460
|
+
if (!cursor)
|
|
461
|
+
throw new AppError("review_evidence_invalid", "A PLAN duplicate must end at a final decided finding", 2);
|
|
462
|
+
}
|
|
463
|
+
return items.filter(item => item.decision === "deferred_as_DEF").map(item => reviewDefEvidence(workspaceRoot, item.def_id));
|
|
464
|
+
}
|
|
465
|
+
function planContentHash(plan) {
|
|
466
|
+
const content = { ...plan };
|
|
467
|
+
delete content.revision;
|
|
468
|
+
return crypto.createHash("sha256").update(JSON.stringify(content, (_key, value) => value && typeof value === "object" && !Array.isArray(value) ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b))) : value)).digest("hex");
|
|
469
|
+
}
|
|
470
|
+
export function validatePlanCorrection(input) {
|
|
471
|
+
const changed = [];
|
|
472
|
+
if (!input.sources.length || !input.maps?.length)
|
|
473
|
+
throw new AppError("review_input_contract_missing", "PLAN correction needs its accepted per-protocol sources", 1);
|
|
474
|
+
const invalid = (message) => { throw new AppError("validation", message, 2, { phase: "prepare", effect: "no_effect", recoverable: true }); };
|
|
475
|
+
for (const source of input.sources) {
|
|
476
|
+
const relative = `.memory-bank/protocol/${source.protocol_id}/plan.json`;
|
|
477
|
+
const file = assertPortableArtifactRef(relative, { workspaceRoot: input.workspaceRoot, runHome: input.home, runId: path.basename(input.home) });
|
|
478
|
+
const current = readJson(file);
|
|
479
|
+
if (current.plan_id !== source.plan_id || current.protocol_id !== source.protocol_id)
|
|
480
|
+
invalid("PLAN correction cannot change protocol identity");
|
|
481
|
+
if (checksum(file) !== source.sha256) {
|
|
482
|
+
if (!Number.isSafeInteger(current.revision) || Number(current.revision) <= source.revision || planContentHash(current) === source.content_sha256)
|
|
483
|
+
invalid("A changed PLAN must advance its own revision and contain a real authored change");
|
|
484
|
+
changed.push(relative);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (const map of input.maps) {
|
|
488
|
+
const file = assertPortableArtifactRef(`run://${path.basename(input.home)}/${map.path}`, { workspaceRoot: input.workspaceRoot, runHome: input.home, runId: path.basename(input.home) });
|
|
489
|
+
if (checksum(file) !== map.sha256)
|
|
490
|
+
changed.push(`RUN/${map.path}`);
|
|
491
|
+
}
|
|
492
|
+
const correction = input.decision.correction;
|
|
493
|
+
const actual = [...new Set(changed)].sort();
|
|
494
|
+
const named = correction.changed_paths.map(ref => ref.startsWith(`run://${path.basename(input.home)}/`) ? `RUN/${ref.slice(`run://${path.basename(input.home)}/`.length)}` : ref).sort();
|
|
495
|
+
if (new Set(named).size !== named.length || JSON.stringify(actual) !== JSON.stringify(named))
|
|
496
|
+
invalid("Correction changed_paths must exactly name the changed PLAN/aspect-map artifacts");
|
|
497
|
+
if (actual.length) {
|
|
498
|
+
if (correction.status !== "applied" || correction.previous_plan_revision !== input.reviewedPlanRevision)
|
|
499
|
+
invalid("An authored correction requires an applied receipt bound to the reviewed revision");
|
|
500
|
+
}
|
|
501
|
+
else if (correction.status !== "not_required" || input.decision.finding_decisions.some(item => item.decision === "accepted_fix"))
|
|
502
|
+
invalid("An accepted fix needs a real authored correction; unchanged rejected findings need not_required");
|
|
503
|
+
}
|
|
436
504
|
function resolveMode(requested, groups, home, projectRoot) {
|
|
437
505
|
if (requested !== "auto")
|
|
438
506
|
return requested;
|
|
@@ -11,7 +11,7 @@ import { findRecentMatchingHookEvent, claimStageLifecycleHookEvent, claimStageSt
|
|
|
11
11
|
import { validateSchema } from "./schema-validation.js";
|
|
12
12
|
import { resolveProjectRoot, resolveRunReferences } from "../storage/paths.js";
|
|
13
13
|
import { refreshRunSessionProjection } from "./run-projection.js";
|
|
14
|
-
import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
14
|
+
import { ignoredWorkspacePaths, normalizeWorkspacePath, readCodeCheckProfile, runCodeChecks, validateCheckDeclaration, validateFailedCheckRetry, workspaceFingerprint } from "./code-checks.js";
|
|
15
15
|
import { nextWorkId, nextWorkIds } from "./ids.js";
|
|
16
16
|
import { appendFlowRunTimelineEvent } from "./runs.js";
|
|
17
17
|
import { resolveStageTransition } from "./execution-policy.js";
|
|
@@ -994,7 +994,11 @@ function assertSharedReviewInput(context, work, run, generation = recoveryGuard(
|
|
|
994
994
|
if (matrix) {
|
|
995
995
|
add("run", matrix.metadata.json.path, matrix.metadata.json.sha256);
|
|
996
996
|
add("run", matrix.metadata.markdown.path, matrix.metadata.markdown.sha256);
|
|
997
|
+
for (const source of matrix.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)))
|
|
998
|
+
add(source.root, source.path, source.sha256);
|
|
997
999
|
}
|
|
1000
|
+
for (const item of value.reviewer_inputs ?? [])
|
|
1001
|
+
add("workspace", item.path, item.sha256);
|
|
998
1002
|
const runInputs = [...expected].map(([key, sha]) => { const [root, ...parts] = key.split(":"); return { root: root, path: parts.join(":"), sha256: sha }; }).sort((a, b) => `${a.root}:${a.path}`.localeCompare(`${b.root}:${b.path}`));
|
|
999
1003
|
for (const item of runInputs) {
|
|
1000
1004
|
const target = reviewInputPath(item.root === "run" ? home : run.workspace_root, item.path);
|
|
@@ -1064,6 +1068,10 @@ async function settle(context, id, status, result, progress, retry) {
|
|
|
1064
1068
|
// profile. Check it even when the Work has only raw focused checks, so a
|
|
1065
1069
|
// worker cannot complete after downgrading the frozen project contract.
|
|
1066
1070
|
readCodeCheckProfile(run.workspace_root);
|
|
1071
|
+
// A provider promises aliases even when their execution belongs to a
|
|
1072
|
+
// later aggregate gate. Validate materialization, without running them.
|
|
1073
|
+
for (const check of packet.provides_checks)
|
|
1074
|
+
validateCheckDeclaration(run.workspace_root, check);
|
|
1067
1075
|
coordinationDrift = plannedAreaDrift(packet, result);
|
|
1068
1076
|
const artifactDir = path.relative(requireRunHome(run), path.dirname(link.result_path));
|
|
1069
1077
|
const requiredChecks = packet.repair?.verification_check_refs?.length
|
|
@@ -1248,7 +1256,8 @@ function renderWorkerPrompt(context, work, run, dependencies, commands) {
|
|
|
1248
1256
|
const kind = parsePayload(work)?.kind;
|
|
1249
1257
|
const matrixStage = kind === "plan-review" || kind === "code-review" ? kind : codePacket(work) ? "code" : null;
|
|
1250
1258
|
const matrix = matrixStage ? latestRunVerificationMatrix(context, work.project_id, work.run_id, matrixStage, kind === "plan-review" || kind === "code-review" ? "input" : undefined) : null;
|
|
1251
|
-
|
|
1259
|
+
const sources = matrix?.matrix.sources.filter(source => ["specify", "protocolize", "protocolize_report"].includes(source.role)) ?? [];
|
|
1260
|
+
return ["<accepted_decision_precedence>", `Read ${path.join(requireRunHome(run), "01-specify", "specify.json")} and later accepted RUN decisions relevant to this Work.`, ...sources.map(source => `Accepted ${source.role}: ${path.join(source.root === "run" ? requireRunHome(run) : run.workspace_root, source.path)} (sha256 ${source.sha256}). PROTOCOLIZE includes the effective amendments; preserve SPECIFY non-goals.`), acceptedDecisionPrecedence, "</accepted_decision_precedence>", "", body, verificationMatrixInstruction(matrix?.metadata ?? null, requireRunHome(run), `assigned Work ${work.work_id}`)].join("\n");
|
|
1252
1261
|
}
|
|
1253
1262
|
function workerLifecycleCommands(context, work, run) {
|
|
1254
1263
|
const resultInputPath = path.join(requireRunHome(run), "works", work.work_id, "result-input.json");
|